setup.sh, update.sh und uninstall.sh
Drei Betriebsskripte fuer Linux, im Auslieferungspaket neben den Programmen. setup.sh richtet eine Anlage vollstaendig ein: PostgreSQL auf Wunsch mit (apt/dnf/yum/zypper/pacman), Dienstkonto, Verschluesselungsschluessel, Schema, erster Administrator, gehaertetes Repository, gehaertete systemd-Einheit. Jeder Schritt vermerkt, was er angelegt hat; bricht der Lauf ab, wird genau das zurueckgebaut und nichts sonst. Eine bestehende Installation wird nicht ueberschrieben — dafuer gibt es update.sh, und der Unterschied ist, dass ein Update vorher sichert. update.sh haelt die Reihenfolge ein, um die es geht: sichern, anhalten, tauschen, migrieren, starten, pruefen. Kommt der Dienst danach nicht hoch, holt es die vorige Fassung zurueck. Ohne pg_dump wird gar nicht erst begonnen — ohne Sicherung gibt es nach einer misslungenen Migration keinen Weg zurueck. Repository und Verschluesselungsschluessel bleiben unberuehrt. uninstall.sh entfernt standardmaessig NUR Dienst und Programme. Datenbank, Repository und Konfiguration bleiben liegen; jede dieser Loeschungen verlangt ein woertlich getipptes Bestaetigungswort an einem Terminal. Ein Deinstallationsskript, das nebenbei die Backups mitnimmt, vernichtet genau das, wofuer jemand jahrelang Speicher bezahlt hat. Gegen Debian 12 im Container gefahren — Installation, Anmeldung, Repository eingetragen, Loeschschutz gemessen (advisory auf overlayfs, richtig), echter Sicherungslauf, Update mit unveraendertem Bestand, beide Abbauarten. Drei Fehler dabei gefunden und behoben, alle derselben Art: - "tr </dev/urandom | head -c 32" und "psql | grep -q": Der frueh geschlossene Pipe schickt dem Schreiber SIGPIPE, und mit "set -o pipefail" bricht das Skript mitten in der Einrichtung ab, ohne erkennbaren Grund. - update.sh las die laufende Fassung mit "grep -o" aus der Antwort von /health/ready. Die enthaelt gar kein Versionsfeld; grep endet mit 1, und das Skript nahm ein GELUNGENES Update wieder zurueck — wegen einer Zeile, die nur der Ausgabe dient. Dazu: SYNCOVA_ADMIN_PASSWORD stand in der Doku und existiert nicht — das Passwort kommt ueber die Standardeingabe. Und "syncova-repo break-lock" fehlte zwar nicht mehr, aber der Test auf die Uebereinstimmung der drei Skripte ist neu: Drei Skripte, die sich ueber den Installationsort uneinig sind, ergeben eine Anlage, die sich nicht mehr entfernen laesst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e1fd31e9c8
commit
0c5a106a83
17
CHANGELOG.md
17
CHANGELOG.md
@ -65,6 +65,23 @@ nicht als freigegeben:
|
|||||||
- **Der SSH-Zugriffsweg auf Proxmox-Knoten.** Die Fingerabdruckprüfung ist
|
- **Der SSH-Zugriffsweg auf Proxmox-Knoten.** Die Fingerabdruckprüfung ist
|
||||||
getestet, eine echte Verbindung gab es nie.
|
getestet, eine echte Verbindung gab es nie.
|
||||||
|
|
||||||
|
### Einrichtung, Aktualisierung, Entfernung
|
||||||
|
|
||||||
|
Das Linux-Paket bringt drei Skripte mit:
|
||||||
|
|
||||||
|
- **`setup.sh`** richtet eine Anlage vollständig ein — PostgreSQL auf Wunsch
|
||||||
|
mit, Dienstkonto, Schlüssel, Schema, erster Administrator, gehärtetes
|
||||||
|
Repository, systemd-Einheit. Bricht ein Schritt ab, wird zurückgebaut, was
|
||||||
|
dieser Lauf angelegt hat; Vorgefundenes bleibt unangetastet.
|
||||||
|
- **`update.sh`** sichert **zuerst** Datenbank und Konfiguration, hält den
|
||||||
|
Dienst an, tauscht die Programme, migriert mit der neuen Fassung und startet.
|
||||||
|
Kommt der Dienst danach nicht hoch, holt es die vorige Fassung zurück.
|
||||||
|
Repository und Verschlüsselungsschlüssel werden nie angefasst.
|
||||||
|
- **`uninstall.sh`** entfernt standardmäßig **nur** Dienst und Programme.
|
||||||
|
Datenbank, Repository und Konfiguration bleiben liegen; jede dieser drei
|
||||||
|
Löschungen verlangt ein wörtlich getipptes Bestätigungswort an einem
|
||||||
|
Terminal.
|
||||||
|
|
||||||
### Eingefrorene Verträge
|
### Eingefrorene Verträge
|
||||||
|
|
||||||
Ab dieser Fassung sind API (102 Endpunkte), Migrationen, Backup-Format und
|
Ab dieser Fassung sind API (102 Endpunkte), Migrationen, Backup-Format und
|
||||||
|
|||||||
@ -340,11 +340,13 @@ Erzeugt je Zielplattform einen Verzeichnisbaum und ein Archiv — Programme, Obe
|
|||||||
|
|
||||||
| Paket | Inhalt |
|
| Paket | Inhalt |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `linux-amd64`, `linux-arm64` | vollständiger Server, Oberfläche, Agent, alle Werkzeuge |
|
| `linux-amd64`, `linux-arm64` | vollständiger Server, Oberfläche, Agent, alle Werkzeuge, `setup.sh`/`update.sh`/`uninstall.sh` |
|
||||||
| `windows-amd64` | Agent und `syncova-repo` |
|
| `windows-amd64` | Agent und `syncova-repo` |
|
||||||
|
|
||||||
macOS und ein vollständiger Windows-Server werden bewusst nicht ausgeliefert: Eine Plattform ohne Betriebskonzept weckt Erwartungen, die niemand einlöst.
|
macOS und ein vollständiger Windows-Server werden bewusst nicht ausgeliefert: Eine Plattform ohne Betriebskonzept weckt Erwartungen, die niemand einlöst.
|
||||||
|
|
||||||
|
Installieren: `sudo ./setup.sh` aus dem entpackten Paket. Aktualisieren: `sudo ./update.sh` — es sichert vorher und nimmt sich zurück, wenn der Dienst danach nicht hochkommt. Entfernen: `sudo ./uninstall.sh` — Datenbank, Repository und Konfiguration bleiben liegen, sofern man nicht ausdrücklich etwas anderes verlangt.
|
||||||
|
|
||||||
**Release veröffentlichen und das System durchtesten:** [docs/release-howto.md](docs/release-howto.md) — vier Stufen vom Rauchtest bis zum offenen Proxmox-Meilenstein, jeweils mit Gegenprobe.
|
**Release veröffentlichen und das System durchtesten:** [docs/release-howto.md](docs/release-howto.md) — vier Stufen vom Rauchtest bis zum offenen Proxmox-Meilenstein, jeweils mit Gegenprobe.
|
||||||
|
|
||||||
Für den Einstieg: [Installation](docs/installation.md) · [Wiederherstellung im Ernstfall](docs/recovery-runbook.md) · [Sicherheitsleitfaden](docs/security-guide.md) · [API](docs/api.md) · [Störungen](docs/troubleshooting.md)
|
Für den Einstieg: [Installation](docs/installation.md) · [Wiederherstellung im Ernstfall](docs/recovery-runbook.md) · [Sicherheitsleitfaden](docs/security-guide.md) · [API](docs/api.md) · [Störungen](docs/troubleshooting.md)
|
||||||
|
|||||||
@ -17,6 +17,33 @@ wiederherstellt. Sie beschreibt den Serverteil; die Agenten stehen in
|
|||||||
Datenträgerausfall nähme sonst Original und Sicherung gemeinsam mit. Das ist
|
Datenträgerausfall nähme sonst Original und Sicherung gemeinsam mit. Das ist
|
||||||
keine Feinheit der Einrichtung, sondern der Zweck der Übung.
|
keine Feinheit der Einrichtung, sondern der Zweck der Übung.
|
||||||
|
|
||||||
|
## Der kurze Weg: setup.sh
|
||||||
|
|
||||||
|
Das Paket bringt ein Einrichtungsskript mit. Es geht genau die Schritte dieser
|
||||||
|
Anleitung, und was es angelegt hat, baut es bei einem Abbruch wieder zurück.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar -xzf syncova-<version>-linux-amd64.tar.gz
|
||||||
|
cd syncova-<version>-linux-amd64
|
||||||
|
sudo ./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Es fragt nach Datenbank, Repository und dem ersten Administrator, installiert
|
||||||
|
PostgreSQL auf Wunsch mit, erzeugt den Verschlüsselungsschlüssel und richtet den
|
||||||
|
Dienst ein. Für einen unbeaufsichtigten Lauf: `sudo ./setup.sh --unbeaufsichtigt`
|
||||||
|
mit den Werten aus der Umgebung (`./setup.sh --hilfe` zeigt sie).
|
||||||
|
|
||||||
|
Später aktualisieren: `sudo ./update.sh` — es sichert vorher Datenbank und
|
||||||
|
Konfiguration und nimmt sich selbst zurück, wenn der Dienst danach nicht
|
||||||
|
hochkommt. Entfernen: `sudo ./uninstall.sh` — es lässt Datenbank, Repository
|
||||||
|
und Konfiguration liegen, sofern man nicht ausdrücklich etwas anderes verlangt.
|
||||||
|
|
||||||
|
**Eine bestehende Installation überschreibt `setup.sh` nicht.** Es bricht ab und
|
||||||
|
verweist auf `update.sh`; der Unterschied ist, dass ein Update vorher sichert.
|
||||||
|
|
||||||
|
Der Rest dieses Dokuments beschreibt dieselben Schritte von Hand — für alle, die
|
||||||
|
wissen wollen, was das Skript tut, oder die davon abweichen müssen.
|
||||||
|
|
||||||
## 1. Paket auspacken
|
## 1. Paket auspacken
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -117,7 +144,17 @@ davor, dass eine neue Fassung auf ein altes Schema schreibt.
|
|||||||
## 6. Ersten Administrator anlegen
|
## 6. Ersten Administrator anlegen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SYNCOVA_ADMIN_PASSWORD='<passwort>' ./bin/syncova-admin create-admin --username admin
|
./bin/syncova-admin create-admin --username admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Passwort wird abgefragt und wiederholt — es steht **nicht** in einer
|
||||||
|
Umgebungsvariablen und nicht in den Aufrufparametern. Beides landete sonst in
|
||||||
|
der Prozessliste und in der Shell-Historie.
|
||||||
|
|
||||||
|
In einem Skript geht es über die Standardeingabe:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '%s\n%s\n' "$PASSWORD" "$PASSWORD" | ./bin/syncova-admin create-admin --username admin
|
||||||
```
|
```
|
||||||
|
|
||||||
**Es gibt kein Standardkonto im Programm.** Ein ausgeliefertes Kennwort wäre
|
**Es gibt kein Standardkonto im Programm.** Ein ausgeliefertes Kennwort wäre
|
||||||
|
|||||||
@ -173,7 +173,7 @@ set -a; . /tmp/syncova.env; set +a
|
|||||||
# 4. Schema und erster Administrator
|
# 4. Schema und erster Administrator
|
||||||
./bin/syncova-migrate up
|
./bin/syncova-migrate up
|
||||||
./bin/syncova-migrate status
|
./bin/syncova-migrate status
|
||||||
SYNCOVA_ADMIN_PASSWORD='<starkes-passwort>' ./bin/syncova-admin create-admin --username admin
|
./bin/syncova-admin create-admin --username admin # fragt das Passwort ab
|
||||||
|
|
||||||
# 5. Starten
|
# 5. Starten
|
||||||
./bin/syncova-api &
|
./bin/syncova-api &
|
||||||
|
|||||||
@ -137,6 +137,13 @@ for platformEntry in "${targetPlatforms[@]}"; do
|
|||||||
|
|
||||||
cp -R docs/. "${packageRoot}/docs/"
|
cp -R docs/. "${packageRoot}/docs/"
|
||||||
cp README.md CHANGELOG.md "${packageRoot}/"
|
cp README.md CHANGELOG.md "${packageRoot}/"
|
||||||
|
|
||||||
|
# Die Betriebsskripte gehoeren neben die Programme, nicht in einen
|
||||||
|
# Unterordner: Wer ein Paket auspackt, soll setup.sh sehen, ohne zu suchen.
|
||||||
|
# Fuer Windows entfallen sie — dort gibt es weder systemd noch apt.
|
||||||
|
if [[ "${targetOS}" != "windows" ]]; then
|
||||||
|
install -m 0755 scripts/setup.sh scripts/update.sh scripts/uninstall.sh "${packageRoot}/"
|
||||||
|
fi
|
||||||
cp deployment/docker-compose.yml "${packageRoot}/deployment/" 2>/dev/null || true
|
cp deployment/docker-compose.yml "${packageRoot}/deployment/" 2>/dev/null || true
|
||||||
cp deployment/syncova-agent.service "${packageRoot}/deployment/" 2>/dev/null || true
|
cp deployment/syncova-agent.service "${packageRoot}/deployment/" 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ package scripts
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -138,3 +139,89 @@ func readReleaseScript(testInstance *testing.T) string {
|
|||||||
|
|
||||||
return string(scriptContent)
|
return string(scriptContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestOperationScriptsAgreeOnPaths haelt die drei Betriebsskripte deckungsgleich.
|
||||||
|
//
|
||||||
|
// setup.sh, update.sh und uninstall.sh teilen die Orte, an denen die Anlage
|
||||||
|
// liegt. Waeren sie sich uneinig, entstuende der schlimmste Fall dieser drei
|
||||||
|
// Skripte: eine Installation, die sich nicht mehr entfernen laesst, weil das
|
||||||
|
// Deinstallationsskript woanders sucht — oder ein Update, das neben die
|
||||||
|
// vorhandene Fassung installiert.
|
||||||
|
//
|
||||||
|
// Geprueft wird die **Schnittmenge**: Wo zwei Skripte dieselbe Konstante
|
||||||
|
// deklarieren, muessen die Werte uebereinstimmen. Eine Konstante nur deshalb
|
||||||
|
// mitzufuehren, damit dieser Test etwas zu vergleichen hat, waere verkehrt.
|
||||||
|
func TestOperationScriptsAgreeOnPaths(testInstance *testing.T) {
|
||||||
|
scriptNames := []string{"setup.sh", "update.sh", "uninstall.sh"}
|
||||||
|
|
||||||
|
// valuesByConstant sammelt je Konstante die Werte samt Herkunft.
|
||||||
|
valuesByConstant := make(map[string]map[string]string)
|
||||||
|
|
||||||
|
constantPattern := regexp.MustCompile(`^readonly\s+(\w+)="([^"]*)"`)
|
||||||
|
|
||||||
|
for _, scriptName := range scriptNames {
|
||||||
|
scriptContent, readError := os.ReadFile(filepath.Join(repositoryRoot, "scripts", scriptName))
|
||||||
|
if readError != nil {
|
||||||
|
testInstance.Fatalf("%s ließ sich nicht lesen: %v", scriptName, readError)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, currentLine := range strings.Split(string(scriptContent), "\n") {
|
||||||
|
matchedGroups := constantPattern.FindStringSubmatch(strings.TrimSpace(currentLine))
|
||||||
|
if matchedGroups == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
constantName, constantValue := matchedGroups[1], matchedGroups[2]
|
||||||
|
|
||||||
|
// Farben und dergleichen sind Ausgabe, kein Ort.
|
||||||
|
if strings.HasPrefix(constantName, "color") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if valuesByConstant[constantName] == nil {
|
||||||
|
valuesByConstant[constantName] = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesByConstant[constantName][scriptName] = constantValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(valuesByConstant) == 0 {
|
||||||
|
testInstance.Fatal("in den Betriebsskripten wurde keine einzige Konstante gefunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
for constantName, valuesByScript := range valuesByConstant {
|
||||||
|
if len(valuesByScript) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var referenceValue, referenceScript string
|
||||||
|
|
||||||
|
for scriptName, constantValue := range valuesByScript {
|
||||||
|
if referenceScript == "" {
|
||||||
|
referenceValue, referenceScript = constantValue, scriptName
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if constantValue != referenceValue {
|
||||||
|
testInstance.Errorf("%s ist uneinheitlich:\n %s: %q\n %s: %q\n"+
|
||||||
|
" Drei Skripte, die sich über den Installationsort uneinig sind, ergeben eine\n"+
|
||||||
|
" Anlage, die sich nicht mehr entfernen lässt.",
|
||||||
|
constantName, referenceScript, referenceValue, scriptName, constantValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOperationScriptsAreShipped prueft, dass die Skripte im Paket landen.
|
||||||
|
func TestOperationScriptsAreShipped(testInstance *testing.T) {
|
||||||
|
buildScript := readReleaseScript(testInstance)
|
||||||
|
|
||||||
|
for _, scriptName := range []string{"setup.sh", "update.sh", "uninstall.sh"} {
|
||||||
|
if !strings.Contains(buildScript, scriptName) {
|
||||||
|
testInstance.Errorf("%s wird nicht ausgeliefert — dann steht ein Betreiber "+
|
||||||
|
"mit einem Paket da und ohne den Weg hinein", scriptName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
821
scripts/setup.sh
Executable file
821
scripts/setup.sh
Executable file
@ -0,0 +1,821 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Richtet Syncova Backups auf einem Linux-Server ein.
|
||||||
|
#
|
||||||
|
# Der Ablauf folgt docs/installation.md. Das Skript nimmt niemandem eine
|
||||||
|
# Entscheidung ab, die er selbst treffen muss — es fragt, erklaert, und legt
|
||||||
|
# nichts an, was es nicht auch wieder zuruecknehmen koennte.
|
||||||
|
#
|
||||||
|
# ./setup.sh fragt nach
|
||||||
|
# ./setup.sh --unbeaufsichtigt liest alles aus der Umgebung
|
||||||
|
# ./setup.sh --hilfe zeigt alle Schalter
|
||||||
|
#
|
||||||
|
# Bei einem Abbruch wird zurueckgebaut, was dieser Lauf angelegt hat. Eine halb
|
||||||
|
# eingerichtete Anlage sieht arbeitsfaehig aus und scheitert beim ersten Lauf.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Feste Orte
|
||||||
|
#
|
||||||
|
# Sie stehen gleichlautend in update.sh und uninstall.sh; ein Test haelt die
|
||||||
|
# drei Skripte deckungsgleich. Drei Skripte, die sich ueber den Installationsort
|
||||||
|
# uneinig sind, ergeben eine Anlage, die sich nicht mehr entfernen laesst.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# installationRoot ist das Programmverzeichnis.
|
||||||
|
readonly installationRoot="/opt/syncova"
|
||||||
|
# configurationDirectory enthaelt die Umgebungsdatei.
|
||||||
|
readonly configurationDirectory="/etc/syncova"
|
||||||
|
# configurationFile ist die Umgebungsdatei des Dienstes.
|
||||||
|
readonly configurationFile="${configurationDirectory}/syncova.env"
|
||||||
|
# serviceName ist die systemd-Einheit.
|
||||||
|
readonly serviceName="syncova-api"
|
||||||
|
# serviceUnitFile ist deren Datei.
|
||||||
|
readonly serviceUnitFile="/etc/systemd/system/${serviceName}.service"
|
||||||
|
# serviceAccount ist das Dienstkonto.
|
||||||
|
readonly serviceAccount="syncova"
|
||||||
|
# defaultRepositoryPath ist der vorgeschlagene Ort des Repositorys.
|
||||||
|
readonly defaultRepositoryPath="/srv/syncova-repository"
|
||||||
|
# backupDirectory nimmt Sicherungen von Datenbank und Konfiguration auf.
|
||||||
|
readonly backupDirectory="/var/backups/syncova"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ausgabe
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
readonly colorReset=$'\033[0m'
|
||||||
|
readonly colorBold=$'\033[1m'
|
||||||
|
readonly colorRed=$'\033[31m'
|
||||||
|
readonly colorGreen=$'\033[32m'
|
||||||
|
readonly colorYellow=$'\033[33m'
|
||||||
|
else
|
||||||
|
readonly colorReset='' colorBold='' colorRed='' colorGreen='' colorYellow=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# writeStep meldet einen Arbeitsschritt.
|
||||||
|
writeStep() { printf '%s==>%s %s\n' "${colorBold}" "${colorReset}" "$1"; }
|
||||||
|
|
||||||
|
# writeDetail meldet eine Einzelheit.
|
||||||
|
writeDetail() { printf ' %s\n' "$1"; }
|
||||||
|
|
||||||
|
# writeSuccess meldet einen gelungenen Schritt.
|
||||||
|
writeSuccess() { printf ' %s✓%s %s\n' "${colorGreen}" "${colorReset}" "$1"; }
|
||||||
|
|
||||||
|
# writeWarning meldet etwas, das Beachtung verlangt.
|
||||||
|
writeWarning() { printf ' %s!%s %s\n' "${colorYellow}" "${colorReset}" "$1" >&2; }
|
||||||
|
|
||||||
|
# abortWithMessage bricht mit einer Begruendung ab.
|
||||||
|
abortWithMessage() {
|
||||||
|
printf '\n%sAbbruch:%s %s\n' "${colorRed}${colorBold}" "${colorReset}" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Rueckbau bei Abbruch
|
||||||
|
#
|
||||||
|
# Jeder Schritt vermerkt, was er angelegt hat. Bricht das Skript ab, wird genau
|
||||||
|
# das wieder entfernt — und nichts sonst. Eine vorgefundene Datenbank oder ein
|
||||||
|
# vorgefundenes Repository bleiben unangetastet, auch wenn der Lauf scheitert.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# createdArtifacts sammelt die Dinge, die dieser Lauf angelegt hat.
|
||||||
|
createdArtifacts=()
|
||||||
|
|
||||||
|
# noteCreated vermerkt ein angelegtes Objekt fuer den Rueckbau.
|
||||||
|
noteCreated() { createdArtifacts+=("$1"); }
|
||||||
|
|
||||||
|
# rollbackOnFailure baut zurueck, was dieser Lauf angelegt hat.
|
||||||
|
rollbackOnFailure() {
|
||||||
|
local exitCode=$?
|
||||||
|
|
||||||
|
if [[ ${exitCode} -eq 0 ]] || [[ ${#createdArtifacts[@]} -eq 0 ]]; then
|
||||||
|
exit "${exitCode}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n%sDer Lauf wird zurueckgebaut.%s\n' "${colorYellow}${colorBold}" "${colorReset}" >&2
|
||||||
|
|
||||||
|
# Rueckwaerts: zuerst der Dienst, zuletzt die Datenbank.
|
||||||
|
local artifactIndex
|
||||||
|
for (( artifactIndex=${#createdArtifacts[@]}-1; artifactIndex>=0; artifactIndex-- )); do
|
||||||
|
local artifact="${createdArtifacts[artifactIndex]}"
|
||||||
|
local artifactKind="${artifact%%:*}"
|
||||||
|
local artifactValue="${artifact#*:}"
|
||||||
|
|
||||||
|
case "${artifactKind}" in
|
||||||
|
service)
|
||||||
|
systemctl stop "${serviceName}" 2>/dev/null || true
|
||||||
|
systemctl disable "${serviceName}" 2>/dev/null || true
|
||||||
|
rm -f "${serviceUnitFile}"
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
|
writeDetail "Dienst entfernt"
|
||||||
|
;;
|
||||||
|
directory)
|
||||||
|
rm -rf "${artifactValue}"
|
||||||
|
writeDetail "Verzeichnis entfernt: ${artifactValue}"
|
||||||
|
;;
|
||||||
|
file)
|
||||||
|
rm -f "${artifactValue}"
|
||||||
|
writeDetail "Datei entfernt: ${artifactValue}"
|
||||||
|
;;
|
||||||
|
account)
|
||||||
|
userdel "${artifactValue}" 2>/dev/null || true
|
||||||
|
writeDetail "Dienstkonto entfernt: ${artifactValue}"
|
||||||
|
;;
|
||||||
|
database)
|
||||||
|
# Nur eine Datenbank, die dieser Lauf angelegt hat.
|
||||||
|
runAsPostgres "DROP DATABASE IF EXISTS ${artifactValue}" || true
|
||||||
|
writeDetail "Datenbank entfernt: ${artifactValue}"
|
||||||
|
;;
|
||||||
|
dbuser)
|
||||||
|
runAsPostgres "DROP ROLE IF EXISTS ${artifactValue}" || true
|
||||||
|
writeDetail "Datenbankkonto entfernt: ${artifactValue}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\n%sEs wurde nichts angetastet, was vorher schon da war.%s\n' \
|
||||||
|
"${colorYellow}" "${colorReset}" >&2
|
||||||
|
|
||||||
|
exit "${exitCode}"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap rollbackOnFailure EXIT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Schalter
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# unattendedMode laeuft ohne Rueckfragen.
|
||||||
|
unattendedMode="nein"
|
||||||
|
# packageDirectory ist das entpackte Auslieferungspaket.
|
||||||
|
packageDirectory=""
|
||||||
|
# repositoryPath ist der Ort des Repositorys.
|
||||||
|
repositoryPath="${SYNCOVA_SETUP_REPOSITORY_PATH:-${defaultRepositoryPath}}"
|
||||||
|
# databaseMode ist "lokal" oder "vorhanden".
|
||||||
|
databaseMode="${SYNCOVA_SETUP_DATABASE_MODE:-}"
|
||||||
|
# databaseHost, -Port, -Name, -User, -Password beschreiben die Datenbank.
|
||||||
|
databaseHost="${SYNCOVA_SETUP_DB_HOST:-127.0.0.1}"
|
||||||
|
databasePort="${SYNCOVA_SETUP_DB_PORT:-5432}"
|
||||||
|
databaseName="${SYNCOVA_SETUP_DB_NAME:-syncova}"
|
||||||
|
databaseUser="${SYNCOVA_SETUP_DB_USER:-syncova}"
|
||||||
|
databasePassword="${SYNCOVA_SETUP_DB_PASSWORD:-}"
|
||||||
|
databaseSSLMode="${SYNCOVA_SETUP_DB_SSLMODE:-}"
|
||||||
|
# listenAddress ist die Adresse des Dienstes.
|
||||||
|
listenAddress="${SYNCOVA_SETUP_LISTEN_ADDRESS:-127.0.0.1:8080}"
|
||||||
|
# administratorName ist der erste Administrator.
|
||||||
|
administratorName="${SYNCOVA_SETUP_ADMIN_USERNAME:-admin}"
|
||||||
|
# administratorPassword ist dessen Passwort.
|
||||||
|
administratorPassword="${SYNCOVA_SETUP_ADMIN_PASSWORD:-}"
|
||||||
|
# createHardenedRepository legt ein gehaertetes Repository an.
|
||||||
|
createHardenedRepository="${SYNCOVA_SETUP_HARDENED:-ja}"
|
||||||
|
# encryptionKeys ist ein vorgegebener Schluessel; leer erzeugt einen neuen.
|
||||||
|
encryptionKeys="${SYNCOVA_SETUP_ENCRYPTION_KEYS:-}"
|
||||||
|
|
||||||
|
showUsage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Verwendung:
|
||||||
|
setup.sh [--paket <verzeichnis>] [--unbeaufsichtigt] [weitere Schalter]
|
||||||
|
|
||||||
|
Schalter:
|
||||||
|
--paket <verzeichnis> Entpacktes Auslieferungspaket (Vorgabe: Verzeichnis des Skripts)
|
||||||
|
--repository <pfad> Ort des Repositorys (Vorgabe: /srv/syncova-repository)
|
||||||
|
--adresse <host:port> Lauschadresse (Vorgabe: 127.0.0.1:8080)
|
||||||
|
--admin <name> Anmeldename des ersten Administrators (Vorgabe: admin)
|
||||||
|
--ohne-haertung Repository ohne Aufbewahrungsschutz anlegen
|
||||||
|
--unbeaufsichtigt Keine Rueckfragen; alle Werte aus der Umgebung
|
||||||
|
--hilfe Diese Uebersicht
|
||||||
|
|
||||||
|
Umgebung fuer den unbeaufsichtigten Lauf:
|
||||||
|
SYNCOVA_SETUP_DATABASE_MODE lokal | vorhanden
|
||||||
|
SYNCOVA_SETUP_DB_HOST/_PORT/_NAME/_USER/_PASSWORD/_SSLMODE
|
||||||
|
SYNCOVA_SETUP_ADMIN_USERNAME Anmeldename (Vorgabe: admin)
|
||||||
|
SYNCOVA_SETUP_ADMIN_PASSWORD Passwort des ersten Administrators
|
||||||
|
SYNCOVA_SETUP_ENCRYPTION_KEYS Vorhandener Schluessel; leer erzeugt einen neuen
|
||||||
|
SYNCOVA_SETUP_REPOSITORY_PATH Ort des Repositorys
|
||||||
|
SYNCOVA_SETUP_LISTEN_ADDRESS Lauschadresse
|
||||||
|
|
||||||
|
Ist bereits eine Installation vorhanden, bricht das Skript ab und verweist auf
|
||||||
|
update.sh. Eine bestehende Anlage zu ueberschreiben ist kein Setup.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--paket) packageDirectory="${2:-}"; shift 2 ;;
|
||||||
|
--repository) repositoryPath="${2:-}"; shift 2 ;;
|
||||||
|
--adresse) listenAddress="${2:-}"; shift 2 ;;
|
||||||
|
--admin) administratorName="${2:-}"; shift 2 ;;
|
||||||
|
--ohne-haertung) createHardenedRepository="nein"; shift ;;
|
||||||
|
--unbeaufsichtigt) unattendedMode="ja"; shift ;;
|
||||||
|
--hilfe|-h|--help) showUsage; trap - EXIT; exit 0 ;;
|
||||||
|
*) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# askQuestion stellt eine Frage mit Vorgabewert.
|
||||||
|
askQuestion() {
|
||||||
|
local questionText="$1" defaultValue="${2:-}" answerVariable="$3" enteredValue=""
|
||||||
|
|
||||||
|
if [[ "${unattendedMode}" == "ja" ]]; then
|
||||||
|
printf -v "${answerVariable}" '%s' "${defaultValue}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${defaultValue}" ]]; then
|
||||||
|
read -r -p " ${questionText} [${defaultValue}]: " enteredValue
|
||||||
|
else
|
||||||
|
read -r -p " ${questionText}: " enteredValue
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf -v "${answerVariable}" '%s' "${enteredValue:-${defaultValue}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# askPassword liest ein Passwort verdeckt und laesst es wiederholen.
|
||||||
|
askPassword() {
|
||||||
|
local questionText="$1" answerVariable="$2" firstEntry="" secondEntry=""
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
read -r -s -p " ${questionText}: " firstEntry; printf '\n'
|
||||||
|
read -r -s -p " ${questionText} wiederholen: " secondEntry; printf '\n'
|
||||||
|
|
||||||
|
if [[ -z "${firstEntry}" ]]; then
|
||||||
|
writeWarning "Ein leeres Passwort wird nicht angenommen."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${firstEntry}" != "${secondEntry}" ]]; then
|
||||||
|
writeWarning "Die beiden Eingaben stimmen nicht ueberein."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf -v "${answerVariable}" '%s' "${firstEntry}"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# confirmQuestion stellt eine Ja/Nein-Frage.
|
||||||
|
confirmQuestion() {
|
||||||
|
local questionText="$1" defaultAnswer="${2:-n}" enteredValue=""
|
||||||
|
|
||||||
|
if [[ "${unattendedMode}" == "ja" ]]; then
|
||||||
|
[[ "${defaultAnswer}" == "j" ]]
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r -p " ${questionText} [$( [[ "${defaultAnswer}" == "j" ]] && echo 'J/n' || echo 'j/N' )]: " enteredValue
|
||||||
|
enteredValue="${enteredValue:-${defaultAnswer}}"
|
||||||
|
|
||||||
|
[[ "${enteredValue}" =~ ^[jJyY] ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# generatePassword erzeugt ein zufaelliges Passwort.
|
||||||
|
generatePassword() {
|
||||||
|
# Aus /dev/urandom und nicht aus $RANDOM: Letzteres ist vorhersagbar und
|
||||||
|
# hat 15 Bit Zufall — fuer ein Datenbankpasswort zu wenig.
|
||||||
|
#
|
||||||
|
# Die Reihenfolge der Pipe ist wesentlich: Erst wird eine **begrenzte**
|
||||||
|
# Menge gelesen, am Ende steht "cut". Mit "tr < /dev/urandom | head -c 32"
|
||||||
|
# schliesst head den Pipe nach 32 Zeichen, tr bekommt SIGPIPE, und mit
|
||||||
|
# "set -o pipefail" bricht das Skript ab — mitten in der Einrichtung und
|
||||||
|
# ohne erkennbaren Grund.
|
||||||
|
head -c 512 /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | cut -c1-32
|
||||||
|
}
|
||||||
|
|
||||||
|
# runAsPostgres fuehrt SQL als Datenbankverwalter aus.
|
||||||
|
runAsPostgres() {
|
||||||
|
su - postgres -c "psql -v ON_ERROR_STOP=1 -c \"$1\"" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# queryAsPostgres liefert das Ergebnis einer Abfrage als Text.
|
||||||
|
#
|
||||||
|
# Bewusst ohne Pipe zu grep: "psql | grep -q" schliesst den Pipe nach dem ersten
|
||||||
|
# Treffer, psql bekommt SIGPIPE, und mit "set -o pipefail" bricht das ganze
|
||||||
|
# Skript ab — mitten in der Einrichtung, ohne erkennbaren Grund.
|
||||||
|
queryAsPostgres() {
|
||||||
|
su - postgres -c "psql -tAc \"$1\"" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# detectPackageManager ermittelt das Paketwerkzeug der Distribution.
|
||||||
|
detectPackageManager() {
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo "apt"; return; fi
|
||||||
|
if command -v dnf >/dev/null 2>&1; then echo "dnf"; return; fi
|
||||||
|
if command -v yum >/dev/null 2>&1; then echo "yum"; return; fi
|
||||||
|
if command -v zypper >/dev/null 2>&1; then echo "zypper"; return; fi
|
||||||
|
if command -v pacman >/dev/null 2>&1; then echo "pacman"; return; fi
|
||||||
|
echo "unbekannt"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Voraussetzungen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n%sSyncova Backups — Einrichtung%s\n\n' "${colorBold}" "${colorReset}"
|
||||||
|
|
||||||
|
writeStep "Voraussetzungen"
|
||||||
|
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
abortWithMessage "Dieses Skript braucht root-Rechte (sudo ./setup.sh)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||||
|
abortWithMessage "Der Syncova-Server wird ausschliesslich fuer Linux ausgeliefert."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v systemctl >/dev/null 2>&1; then
|
||||||
|
abortWithMessage "Es wurde kein systemd gefunden. Der Dienst laesst sich sonst nicht einrichten."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Eine bestehende Installation wird nicht ueberschrieben. Dafuer gibt es
|
||||||
|
# update.sh — und der Unterschied ist wesentlich: Ein Update sichert vorher.
|
||||||
|
if [[ -e "${installationRoot}" ]] || [[ -f "${configurationFile}" ]] || [[ -f "${serviceUnitFile}" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
writeWarning "Es ist bereits eine Installation vorhanden:"
|
||||||
|
[[ -e "${installationRoot}" ]] && writeDetail " ${installationRoot}"
|
||||||
|
[[ -f "${configurationFile}" ]] && writeDetail " ${configurationFile}"
|
||||||
|
[[ -f "${serviceUnitFile}" ]] && writeDetail " ${serviceUnitFile}"
|
||||||
|
printf '\n'
|
||||||
|
abortWithMessage "Zum Aktualisieren: ./update.sh — es sichert vorher Datenbank und Konfiguration."
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeSuccess "root, Linux, systemd"
|
||||||
|
|
||||||
|
# Das Paket finden.
|
||||||
|
if [[ -z "${packageDirectory}" ]]; then
|
||||||
|
packageDirectory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${packageDirectory}/bin/syncova-api" ]]; then
|
||||||
|
abortWithMessage "Unter ${packageDirectory} liegt kein Auslieferungspaket (bin/syncova-api fehlt).
|
||||||
|
Geben Sie es mit --paket <verzeichnis> an."
|
||||||
|
fi
|
||||||
|
|
||||||
|
packageVersion="$("${packageDirectory}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')"
|
||||||
|
writeSuccess "Paket gefunden: ${packageVersion}"
|
||||||
|
|
||||||
|
# Pruefsummen kontrollieren, sofern das Paket welche mitbringt.
|
||||||
|
if [[ -f "${packageDirectory}/SHA256SUMS" ]] && command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
if ( cd "${packageDirectory}" && sha256sum --quiet -c SHA256SUMS >/dev/null 2>&1 ); then
|
||||||
|
writeSuccess "Pruefsummen des Pakets stimmen"
|
||||||
|
else
|
||||||
|
writeWarning "Die Pruefsummen des Pakets stimmen nicht. Wurde es vollstaendig uebertragen?"
|
||||||
|
confirmQuestion "Trotzdem fortfahren?" "n" || abortWithMessage "abgebrochen"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Datenbank
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Datenbank"
|
||||||
|
|
||||||
|
if [[ -z "${databaseMode}" ]]; then
|
||||||
|
if [[ "${unattendedMode}" == "ja" ]]; then
|
||||||
|
databaseMode="lokal"
|
||||||
|
else
|
||||||
|
writeDetail "PostgreSQL 17 oder neuer wird gebraucht."
|
||||||
|
writeDetail ""
|
||||||
|
writeDetail " 1) auf diesem Server installieren"
|
||||||
|
writeDetail " 2) eine vorhandene Datenbank verwenden"
|
||||||
|
writeDetail ""
|
||||||
|
|
||||||
|
local_choice=""
|
||||||
|
askQuestion "Auswahl" "1" local_choice
|
||||||
|
[[ "${local_choice}" == "2" ]] && databaseMode="vorhanden" || databaseMode="lokal"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${databaseMode}" == "lokal" ]]; then
|
||||||
|
if command -v psql >/dev/null 2>&1 && systemctl is-active --quiet postgresql 2>/dev/null; then
|
||||||
|
writeSuccess "PostgreSQL laeuft bereits"
|
||||||
|
else
|
||||||
|
packageManager="$(detectPackageManager)"
|
||||||
|
writeDetail "PostgreSQL wird installiert (${packageManager})"
|
||||||
|
|
||||||
|
case "${packageManager}" in
|
||||||
|
apt)
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq postgresql postgresql-contrib >/dev/null
|
||||||
|
;;
|
||||||
|
dnf|yum)
|
||||||
|
"${packageManager}" install -y -q postgresql-server postgresql-contrib >/dev/null
|
||||||
|
# Auf Red-Hat-Systemen muss der Datenbankbereich zuerst angelegt werden.
|
||||||
|
[[ -f /var/lib/pgsql/data/PG_VERSION ]] || postgresql-setup --initdb >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
zypper)
|
||||||
|
zypper --non-interactive install -y postgresql-server postgresql-contrib >/dev/null
|
||||||
|
;;
|
||||||
|
pacman)
|
||||||
|
pacman -Sy --noconfirm postgresql >/dev/null
|
||||||
|
[[ -f /var/lib/postgres/data/PG_VERSION ]] || \
|
||||||
|
su - postgres -c "initdb -D /var/lib/postgres/data" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
abortWithMessage "Die Distribution wurde nicht erkannt. Installieren Sie PostgreSQL von Hand
|
||||||
|
und starten Sie das Skript mit SYNCOVA_SETUP_DATABASE_MODE=vorhanden erneut."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
systemctl enable --now postgresql >/dev/null 2>&1 || \
|
||||||
|
abortWithMessage "PostgreSQL liess sich nicht starten. Pruefen Sie 'systemctl status postgresql'."
|
||||||
|
|
||||||
|
writeSuccess "PostgreSQL installiert und gestartet"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Eine lokale Datenbank spricht ueber den Unix-Socket; TLS ist dort ohne
|
||||||
|
# Belang und wuerde den Verbindungsaufbau nur scheitern lassen.
|
||||||
|
databaseHost="127.0.0.1"
|
||||||
|
databaseSSLMode="${databaseSSLMode:-disable}"
|
||||||
|
|
||||||
|
if [[ -z "${databasePassword}" ]]; then
|
||||||
|
databasePassword="$(generatePassword)" # secretscan:erlaubt: erzeugter Zufallswert, kein hinterlegtes Geheimnis
|
||||||
|
writeDetail "Ein Datenbankpasswort wurde erzeugt."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rolle und Datenbank anlegen — beides nur, wenn es sie nicht schon gibt.
|
||||||
|
existingRole="$(queryAsPostgres "SELECT 1 FROM pg_roles WHERE rolname='${databaseUser}'")"
|
||||||
|
|
||||||
|
if [[ "${existingRole}" == "1" ]]; then
|
||||||
|
writeDetail "Das Datenbankkonto ${databaseUser} ist bereits vorhanden; das Passwort wird gesetzt."
|
||||||
|
runAsPostgres "ALTER ROLE ${databaseUser} WITH LOGIN PASSWORD '${databasePassword}'" || \
|
||||||
|
abortWithMessage "Das Datenbankpasswort liess sich nicht setzen."
|
||||||
|
else
|
||||||
|
runAsPostgres "CREATE ROLE ${databaseUser} WITH LOGIN PASSWORD '${databasePassword}'" || \
|
||||||
|
abortWithMessage "Das Datenbankkonto liess sich nicht anlegen."
|
||||||
|
noteCreated "dbuser:${databaseUser}"
|
||||||
|
writeSuccess "Datenbankkonto ${databaseUser} angelegt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
existingDatabase="$(queryAsPostgres "SELECT 1 FROM pg_database WHERE datname='${databaseName}'")"
|
||||||
|
|
||||||
|
if [[ "${existingDatabase}" == "1" ]]; then
|
||||||
|
writeDetail "Die Datenbank ${databaseName} ist bereits vorhanden und wird verwendet."
|
||||||
|
else
|
||||||
|
su - postgres -c "createdb -O ${databaseUser} ${databaseName}" >/dev/null 2>&1 || \
|
||||||
|
abortWithMessage "Die Datenbank liess sich nicht anlegen."
|
||||||
|
noteCreated "database:${databaseName}"
|
||||||
|
writeSuccess "Datenbank ${databaseName} angelegt"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
askQuestion "Datenbankserver" "${databaseHost}" databaseHost
|
||||||
|
askQuestion "Port" "${databasePort}" databasePort
|
||||||
|
askQuestion "Datenbankname" "${databaseName}" databaseName
|
||||||
|
askQuestion "Datenbankkonto" "${databaseUser}" databaseUser
|
||||||
|
|
||||||
|
if [[ -z "${databasePassword}" ]]; then
|
||||||
|
askPassword "Passwort des Datenbankkontos" databasePassword
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bei einer entfernten Datenbank ist Verschluesselung der Standard: Ohne sie
|
||||||
|
# ginge das Passwort im Klartext durch das Netz.
|
||||||
|
databaseSSLMode="${databaseSSLMode:-require}"
|
||||||
|
|
||||||
|
writeDetail "Die Verbindung wird geprueft."
|
||||||
|
|
||||||
|
if ! PGPASSWORD="${databasePassword}" psql -h "${databaseHost}" -p "${databasePort}" \
|
||||||
|
-U "${databaseUser}" -d "${databaseName}" -tAc "SELECT 1" >/dev/null 2>&1; then
|
||||||
|
abortWithMessage "Die Datenbank ist nicht erreichbar oder die Anmeldung schlug fehl.
|
||||||
|
Geprueft wurde: ${databaseUser}@${databaseHost}:${databasePort}/${databaseName}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeSuccess "Datenbank erreichbar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Dienstkonto und Verzeichnisse
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Dienstkonto und Verzeichnisse"
|
||||||
|
|
||||||
|
if id "${serviceAccount}" >/dev/null 2>&1; then
|
||||||
|
writeDetail "Das Dienstkonto ${serviceAccount} ist bereits vorhanden."
|
||||||
|
else
|
||||||
|
useradd --system --no-create-home --shell /usr/sbin/nologin "${serviceAccount}" 2>/dev/null || \
|
||||||
|
useradd --system --no-create-home --shell /sbin/nologin "${serviceAccount}" || \
|
||||||
|
abortWithMessage "Das Dienstkonto liess sich nicht anlegen."
|
||||||
|
noteCreated "account:${serviceAccount}"
|
||||||
|
writeSuccess "Dienstkonto ${serviceAccount} angelegt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0755 "${installationRoot}"
|
||||||
|
noteCreated "directory:${installationRoot}"
|
||||||
|
|
||||||
|
install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${configurationDirectory}"
|
||||||
|
noteCreated "directory:${configurationDirectory}"
|
||||||
|
|
||||||
|
install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${backupDirectory}"
|
||||||
|
|
||||||
|
cp -R "${packageDirectory}/bin" "${installationRoot}/"
|
||||||
|
[[ -d "${packageDirectory}/web" ]] && cp -R "${packageDirectory}/web" "${installationRoot}/"
|
||||||
|
[[ -d "${packageDirectory}/migrations" ]] && cp -R "${packageDirectory}/migrations" "${installationRoot}/"
|
||||||
|
[[ -d "${packageDirectory}/docs" ]] && cp -R "${packageDirectory}/docs" "${installationRoot}/"
|
||||||
|
[[ -d "${packageDirectory}/deployment" ]] && cp -R "${packageDirectory}/deployment" "${installationRoot}/"
|
||||||
|
chmod -R 0755 "${installationRoot}/bin"
|
||||||
|
|
||||||
|
# Die Version wird festgehalten. update.sh und uninstall.sh lesen sie; ohne sie
|
||||||
|
# muesste man ein Binary aufrufen, um zu wissen, was installiert ist.
|
||||||
|
printf '%s\n' "${packageVersion}" > "${installationRoot}/VERSION"
|
||||||
|
|
||||||
|
writeSuccess "Programme unter ${installationRoot}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Verschluesselungsschluessel
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Verschluesselungsschluessel"
|
||||||
|
|
||||||
|
if [[ -z "${encryptionKeys}" ]]; then
|
||||||
|
# "|| true", damit die eigene Fehlermeldung unten greift: Findet grep
|
||||||
|
# nichts, endet es mit 1, und "set -e" braeche vorher ab — mit einer
|
||||||
|
# Meldung, die niemandem sagt, was fehlt.
|
||||||
|
generatedKeyLine="$("${installationRoot}/bin/syncova-admin" generate-key 2>/dev/null \
|
||||||
|
| grep '^SYNCOVA_ENCRYPTION_KEYS=' || true)"
|
||||||
|
encryptionKeys="${generatedKeyLine#SYNCOVA_ENCRYPTION_KEYS=}"
|
||||||
|
|
||||||
|
[[ -n "${encryptionKeys}" ]] || abortWithMessage "Der Schluessel liess sich nicht erzeugen."
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
printf ' %s┌─────────────────────────────────────────────────────────────────┐%s\n' "${colorYellow}${colorBold}" "${colorReset}"
|
||||||
|
printf ' %s│ DIESER SCHLUESSEL EXISTIERT NUR HIER │%s\n' "${colorYellow}${colorBold}" "${colorReset}"
|
||||||
|
printf ' %s└─────────────────────────────────────────────────────────────────┘%s\n' "${colorYellow}${colorBold}" "${colorReset}"
|
||||||
|
printf '\n'
|
||||||
|
printf ' SYNCOVA_ENCRYPTION_KEYS=%s\n' "${encryptionKeys}"
|
||||||
|
printf '\n'
|
||||||
|
writeDetail "Ohne ihn sind MFA-Geheimnisse, die Zugangsdaten der"
|
||||||
|
writeDetail "Virtualisierungsverbuende und die Datenschluessel der Repositories"
|
||||||
|
writeDetail "verloren — und damit die verschluesselten Backups."
|
||||||
|
writeDetail ""
|
||||||
|
writeDetail "Er gehoert an einen Ort ausserhalb dieser Anlage: Passwortspeicher"
|
||||||
|
writeDetail "oder Tresor. In ein Backup, das Syncova erzeugt, gehoert er"
|
||||||
|
writeDetail "ausdruecklich nicht — man braeuchte ihn, um an ihn heranzukommen."
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [[ "${unattendedMode}" != "ja" ]]; then
|
||||||
|
acknowledgement=""
|
||||||
|
while [[ "${acknowledgement}" != "gesichert" ]]; do
|
||||||
|
read -r -p " Tippen Sie 'gesichert', wenn der Schluessel anderswo liegt: " acknowledgement
|
||||||
|
done
|
||||||
|
else
|
||||||
|
writeWarning "Unbeaufsichtigter Lauf: Sichern Sie den Schluessel aus ${configurationFile}."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
writeSuccess "Ein vorgegebener Schluessel wird uebernommen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. Konfiguration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Konfiguration"
|
||||||
|
|
||||||
|
# Erst mit strengen Rechten anlegen, dann fuellen. Andersherum stuende das
|
||||||
|
# Passwort fuer einen Augenblick weltlesbar auf der Platte.
|
||||||
|
install -m 0600 -o "${serviceAccount}" -g "${serviceAccount}" /dev/null "${configurationFile}"
|
||||||
|
noteCreated "file:${configurationFile}"
|
||||||
|
|
||||||
|
cat > "${configurationFile}" <<CONFIG
|
||||||
|
# Konfiguration von Syncova Backups.
|
||||||
|
#
|
||||||
|
# Angelegt von setup.sh am $(date -u '+%Y-%m-%d %H:%M:%S UTC').
|
||||||
|
# Diese Datei enthaelt Geheimnisse: root, Modus 0600.
|
||||||
|
|
||||||
|
SYNCOVA_ENV=production
|
||||||
|
|
||||||
|
SYNCOVA_DB_HOST=${databaseHost}
|
||||||
|
SYNCOVA_DB_PORT=${databasePort}
|
||||||
|
SYNCOVA_DB_NAME=${databaseName}
|
||||||
|
SYNCOVA_DB_USER=${databaseUser}
|
||||||
|
SYNCOVA_DB_PASSWORD=${databasePassword}
|
||||||
|
SYNCOVA_DB_SSLMODE=${databaseSSLMode}
|
||||||
|
|
||||||
|
# Ohne diesen Schluessel sind die abgelegten Geheimnisse verloren.
|
||||||
|
SYNCOVA_ENCRYPTION_KEYS=${encryptionKeys}
|
||||||
|
SYNCOVA_ENCRYPTION_CURRENT_KEY=v1
|
||||||
|
|
||||||
|
SYNCOVA_HTTP_LISTEN_ADDRESS=${listenAddress}
|
||||||
|
SYNCOVA_LOG_LEVEL=info
|
||||||
|
SYNCOVA_LOG_FORMAT=json
|
||||||
|
|
||||||
|
# TLS im Dienst selbst. Nur eines von beiden zu setzen verweigert den Start —
|
||||||
|
# sonst liefe der Dienst im Klartext, obwohl Verschluesselung eingerichtet
|
||||||
|
# aussieht. Hinter einem Reverse Proxy bleiben beide leer und die Lauschadresse
|
||||||
|
# steht auf 127.0.0.1.
|
||||||
|
# SYNCOVA_HTTP_TLS_CERT_FILE=
|
||||||
|
# SYNCOVA_HTTP_TLS_KEY_FILE=
|
||||||
|
|
||||||
|
# Begrenzt Wiederherstellungsziele. Ohne diese Angabe kann jeder mit dem Recht
|
||||||
|
# restores.execute in jedes beschreibbare Verzeichnis schreiben.
|
||||||
|
# SYNCOVA_RESTORE_ALLOWED_ROOTS=/srv/restore
|
||||||
|
|
||||||
|
# Zweiter Faktor fuer Konten mit Benutzer-, Rollen- oder Sicherheitsrechten.
|
||||||
|
# Zuerst einrichten, dann einschalten — sonst sperren Sie sich aus.
|
||||||
|
# SYNCOVA_AUTH_REQUIRE_MFA_FOR_PRIVILEGED_USERS=true
|
||||||
|
CONFIG
|
||||||
|
|
||||||
|
chmod 0600 "${configurationFile}"
|
||||||
|
chown "${serviceAccount}:${serviceAccount}" "${configurationFile}"
|
||||||
|
|
||||||
|
writeSuccess "${configurationFile} (0600, ${serviceAccount})"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. Schema
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Datenbankschema"
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${configurationFile}"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
if ! "${installationRoot}/bin/syncova-migrate" up 2>&1 | tail -1; then
|
||||||
|
abortWithMessage "Die Migrationen liessen sich nicht anwenden."
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeSuccess "Schema angelegt"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7. Erster Administrator
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Erster Administrator"
|
||||||
|
|
||||||
|
if [[ -z "${administratorPassword}" ]]; then
|
||||||
|
if [[ "${unattendedMode}" == "ja" ]]; then
|
||||||
|
abortWithMessage "Im unbeaufsichtigten Lauf muss SYNCOVA_SETUP_ADMIN_PASSWORD gesetzt sein."
|
||||||
|
fi
|
||||||
|
|
||||||
|
askQuestion "Anmeldename" "${administratorName}" administratorName
|
||||||
|
askPassword "Passwort fuer ${administratorName}" administratorPassword
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Das Passwort geht ueber die Standardeingabe, nicht als Aufrufparameter:
|
||||||
|
# Parameter stehen in der Prozessliste und in der Shell-Historie. Das Programm
|
||||||
|
# fragt zweimal, deshalb zwei Zeilen.
|
||||||
|
if ! printf '%s\n%s\n' "${administratorPassword}" "${administratorPassword}" \
|
||||||
|
| "${installationRoot}/bin/syncova-admin" create-admin --username "${administratorName}" >/dev/null 2>&1; then
|
||||||
|
abortWithMessage "Der Administrator liess sich nicht anlegen.
|
||||||
|
Haeufigste Ursache: Das Passwort erfuellt die Mindestanforderung nicht
|
||||||
|
(12 Zeichen, mindestens 5 verschiedene, Buchstaben plus ein weiteres Zeichen)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeSuccess "Administrator ${administratorName} angelegt"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 8. Repository
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Repository"
|
||||||
|
|
||||||
|
if [[ "${unattendedMode}" != "ja" ]]; then
|
||||||
|
writeDetail "Das Repository gehoert nicht auf denselben Datentraeger wie die"
|
||||||
|
writeDetail "zu sichernden Daten. Ein Datentraegerausfall naehme sonst Original"
|
||||||
|
writeDetail "und Sicherung gemeinsam mit."
|
||||||
|
printf '\n'
|
||||||
|
askQuestion "Ort des Repositorys" "${repositoryPath}" repositoryPath
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "${repositoryPath}/format/repository.json" ]]; then
|
||||||
|
writeDetail "Unter ${repositoryPath} liegt bereits ein Repository; es wird verwendet."
|
||||||
|
else
|
||||||
|
install -d -m 0700 -o "${serviceAccount}" -g "${serviceAccount}" "${repositoryPath}"
|
||||||
|
|
||||||
|
hardenedFlag=()
|
||||||
|
[[ "${createHardenedRepository}" == "ja" ]] && hardenedFlag=(--hardened)
|
||||||
|
|
||||||
|
if ! su -s /bin/bash "${serviceAccount}" -c \
|
||||||
|
"'${installationRoot}/bin/syncova-repo' create --path '${repositoryPath}' --name 'Hauptziel' ${hardenedFlag[*]:-}" \
|
||||||
|
>/dev/null 2>&1; then
|
||||||
|
abortWithMessage "Das Repository liess sich nicht anlegen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
noteCreated "directory:${repositoryPath}"
|
||||||
|
writeSuccess "Repository unter ${repositoryPath}$( [[ "${createHardenedRepository}" == "ja" ]] && echo ' (gehaertet)' )"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 9. Dienst
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Dienst"
|
||||||
|
|
||||||
|
cat > "${serviceUnitFile}" <<UNIT
|
||||||
|
[Unit]
|
||||||
|
Description=Syncova Control Plane
|
||||||
|
Documentation=file://${installationRoot}/docs/installation.md
|
||||||
|
After=network-online.target postgresql.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${serviceAccount}
|
||||||
|
Group=${serviceAccount}
|
||||||
|
EnvironmentFile=${configurationFile}
|
||||||
|
ExecStart=${installationRoot}/bin/syncova-api
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# Haertung: Der Dienst braucht Netz und sein Repository, sonst nichts.
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
ReadWritePaths=${repositoryPath} ${backupDirectory}
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectKernelModules=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
RestrictSUIDSGID=yes
|
||||||
|
# AF_UNIX gehoert in die Liste: Mit systemd-resolved laeuft die Namensaufloesung
|
||||||
|
# ueber einen Unix-Socket. Ohne diesen Eintrag findet der Dienst seine Datenbank
|
||||||
|
# nicht und meldet einen Netzfehler, waehrend das Netz einwandfrei arbeitet.
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||||
|
MemoryDenyWriteExecute=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
UNIT
|
||||||
|
|
||||||
|
noteCreated "service:${serviceName}"
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "${serviceName}" >/dev/null 2>&1
|
||||||
|
systemctl start "${serviceName}"
|
||||||
|
|
||||||
|
writeDetail "Der Dienst wird geprueft."
|
||||||
|
|
||||||
|
serviceIsReady="nein"
|
||||||
|
for _attempt in $(seq 1 15); do
|
||||||
|
if curl -fsS "http://${listenAddress}/health/ready" >/dev/null 2>&1; then
|
||||||
|
serviceIsReady="ja"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${serviceIsReady}" != "ja" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
writeWarning "Der Dienst meldet sich nicht als betriebsbereit."
|
||||||
|
writeDetail " systemctl status ${serviceName}"
|
||||||
|
writeDetail " journalctl -u ${serviceName} -n 50 --no-pager"
|
||||||
|
abortWithMessage "Die Einrichtung wird zurueckgebaut."
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeSuccess "Dienst laeuft und ist betriebsbereit"
|
||||||
|
|
||||||
|
# Ab hier ist die Einrichtung gelungen; der Rueckbau entfaellt.
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Abschluss
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n%sDie Einrichtung ist abgeschlossen.%s\n\n' "${colorGreen}${colorBold}" "${colorReset}"
|
||||||
|
|
||||||
|
printf ' Version %s\n' "${packageVersion}"
|
||||||
|
printf ' Programme %s\n' "${installationRoot}"
|
||||||
|
printf ' Konfiguration %s\n' "${configurationFile}"
|
||||||
|
printf ' Repository %s\n' "${repositoryPath}"
|
||||||
|
printf ' Dienst %s (systemctl status %s)\n' "${serviceName}" "${serviceName}"
|
||||||
|
printf ' Oberflaeche %s/web — braucht einen Webserver mit SPA-Fallback\n' "${installationRoot}"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
printf '%sWas jetzt zu tun bleibt:%s\n\n' "${colorBold}" "${colorReset}"
|
||||||
|
cat <<'NEXT'
|
||||||
|
1. Repository in der Control Plane eintragen und die Durchsetzungsstufe
|
||||||
|
MESSEN — sie wird gemessen, nicht behauptet:
|
||||||
|
|
||||||
|
curl -X POST http://<adresse>/api/v1/repositories \
|
||||||
|
-H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"name":"Hauptziel","location":"<repository>"}'
|
||||||
|
|
||||||
|
curl -X POST http://<adresse>/api/v1/repositories/<id>/enforcement/measure \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
|
||||||
|
2. Oberflaeche ausliefern (nginx, SPA-Fallback try_files $uri /index.html)
|
||||||
|
und TLS einrichten — oder den Dienst an 127.0.0.1 binden.
|
||||||
|
|
||||||
|
3. Zweiten Faktor einrichten, mindestens fuer Konten mit Loeschrecht.
|
||||||
|
|
||||||
|
4. Benachrichtigungsweg einrichten. Ohne ihn erfaehrt niemand von einem
|
||||||
|
Ausfall.
|
||||||
|
|
||||||
|
5. Eine Wiederherstellung durchfuehren, bevor Sie eine brauchen. Erst danach
|
||||||
|
steigt ein Wiederherstellungspunkt auf "recoverable" — alles davor ist ein
|
||||||
|
Indiz, kein Nachweis.
|
||||||
|
NEXT
|
||||||
|
|
||||||
|
printf '\n Ausfuehrlich: %s/docs/installation.md\n' "${installationRoot}"
|
||||||
|
printf ' Im Ernstfall: %s/docs/recovery-runbook.md\n\n' "${installationRoot}"
|
||||||
459
scripts/uninstall.sh
Executable file
459
scripts/uninstall.sh
Executable file
@ -0,0 +1,459 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Entfernt Syncova Backups von einem Server.
|
||||||
|
#
|
||||||
|
# **Standardmaessig bleiben die Daten liegen.** Entfernt werden Dienst und
|
||||||
|
# Programme; Datenbank, Repository und Konfiguration bleiben, wo sie sind.
|
||||||
|
#
|
||||||
|
# Der Grund ist der Gegenstand dieser Software: Ein Deinstallationsskript, das
|
||||||
|
# nebenbei die Backups mitnimmt, vernichtet genau das, wofuer jemand jahrelang
|
||||||
|
# Speicher bezahlt hat — und zwar in dem Moment, in dem er am wenigsten damit
|
||||||
|
# rechnet.
|
||||||
|
#
|
||||||
|
# ./uninstall.sh Dienst und Programme
|
||||||
|
# ./uninstall.sh --mit-konfiguration zusaetzlich /etc/syncova
|
||||||
|
# ./uninstall.sh --mit-datenbank zusaetzlich die Datenbank
|
||||||
|
# ./uninstall.sh --mit-repository zusaetzlich das Repository
|
||||||
|
# ./uninstall.sh --alles alle drei
|
||||||
|
#
|
||||||
|
# Jeder der drei Schalter verlangt eine getippte Bestaetigung. Ein Kennzeichen
|
||||||
|
# in einem Skript reicht nicht aus.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Feste Orte — gleichlautend in setup.sh und update.sh
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
readonly installationRoot="/opt/syncova"
|
||||||
|
readonly configurationDirectory="/etc/syncova"
|
||||||
|
readonly configurationFile="${configurationDirectory}/syncova.env"
|
||||||
|
readonly serviceName="syncova-api"
|
||||||
|
readonly serviceUnitFile="/etc/systemd/system/${serviceName}.service"
|
||||||
|
readonly serviceAccount="syncova"
|
||||||
|
readonly backupDirectory="/var/backups/syncova"
|
||||||
|
readonly previousReleaseRoot="${installationRoot}.vorherige"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ausgabe
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
readonly colorReset=$'\033[0m'
|
||||||
|
readonly colorBold=$'\033[1m'
|
||||||
|
readonly colorRed=$'\033[31m'
|
||||||
|
readonly colorGreen=$'\033[32m'
|
||||||
|
readonly colorYellow=$'\033[33m'
|
||||||
|
else
|
||||||
|
readonly colorReset='' colorBold='' colorRed='' colorGreen='' colorYellow=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeStep() { printf '%s==>%s %s\n' "${colorBold}" "${colorReset}" "$1"; }
|
||||||
|
writeDetail() { printf ' %s\n' "$1"; }
|
||||||
|
writeSuccess() { printf ' %s✓%s %s\n' "${colorGreen}" "${colorReset}" "$1"; }
|
||||||
|
writeWarning() { printf ' %s!%s %s\n' "${colorYellow}" "${colorReset}" "$1" >&2; }
|
||||||
|
|
||||||
|
abortWithMessage() {
|
||||||
|
printf '\n%sAbbruch:%s %s\n' "${colorRed}${colorBold}" "${colorReset}" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Schalter
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
removeConfiguration="nein"
|
||||||
|
removeDatabase="nein"
|
||||||
|
removeRepository="nein"
|
||||||
|
removeServiceAccount="nein"
|
||||||
|
|
||||||
|
showUsage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Verwendung:
|
||||||
|
uninstall.sh [Schalter]
|
||||||
|
|
||||||
|
Ohne Schalter werden entfernt:
|
||||||
|
- der Dienst syncova-api
|
||||||
|
- die Programme unter /opt/syncova
|
||||||
|
|
||||||
|
Es bleiben liegen:
|
||||||
|
- die Datenbank mit Auftraegen, Laeufen und Wiederherstellungspunkten
|
||||||
|
- das Repository mit den Sicherungen selbst
|
||||||
|
- die Konfiguration samt Verschluesselungsschluessel
|
||||||
|
|
||||||
|
Zusaetzliche Schalter (jeder verlangt eine getippte Bestaetigung):
|
||||||
|
--mit-konfiguration Entfernt /etc/syncova — DARIN LIEGT DER SCHLUESSEL
|
||||||
|
--mit-datenbank Entfernt die Datenbank
|
||||||
|
--mit-repository Entfernt das Repository — DAS SIND DIE BACKUPS
|
||||||
|
--mit-dienstkonto Entfernt das Dienstkonto syncova
|
||||||
|
--alles Alle vier
|
||||||
|
--hilfe Diese Uebersicht
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--mit-konfiguration) removeConfiguration="ja"; shift ;;
|
||||||
|
--mit-datenbank) removeDatabase="ja"; shift ;;
|
||||||
|
--mit-repository) removeRepository="ja"; shift ;;
|
||||||
|
--mit-dienstkonto) removeServiceAccount="ja"; shift ;;
|
||||||
|
--alles)
|
||||||
|
removeConfiguration="ja"; removeDatabase="ja"
|
||||||
|
removeRepository="ja"; removeServiceAccount="ja"; shift ;;
|
||||||
|
--hilfe|-h|--help) showUsage; exit 0 ;;
|
||||||
|
*) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bestandsaufnahme
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n%sSyncova Backups — Deinstallation%s\n\n' "${colorBold}" "${colorReset}"
|
||||||
|
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
abortWithMessage "Dieses Skript braucht root-Rechte (sudo ./uninstall.sh)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeStep "Was ist vorhanden?"
|
||||||
|
|
||||||
|
foundAnything="nein"
|
||||||
|
|
||||||
|
installedVersion=""
|
||||||
|
if [[ -x "${installationRoot}/bin/syncova-api" ]]; then
|
||||||
|
installedVersion="$("${installationRoot}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')"
|
||||||
|
writeDetail "Programme ${installationRoot} (${installedVersion:-Version unbekannt})"
|
||||||
|
foundAnything="ja"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${serviceUnitFile}" ]]; then
|
||||||
|
serviceState="$(systemctl is-active "${serviceName}" 2>/dev/null || echo 'gestoppt')"
|
||||||
|
writeDetail "Dienst ${serviceName} (${serviceState})"
|
||||||
|
foundAnything="ja"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Die Konfiguration wird gelesen, um Datenbank und Repository zu finden. Sie
|
||||||
|
# enthaelt Geheimnisse; ausgegeben wird nichts davon.
|
||||||
|
databaseName="" ; databaseUser="" ; databaseHost="" ; databasePort=""
|
||||||
|
if [[ -f "${configurationFile}" ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${configurationFile}"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
databaseName="${SYNCOVA_DB_NAME:-}"
|
||||||
|
databaseUser="${SYNCOVA_DB_USER:-}"
|
||||||
|
databaseHost="${SYNCOVA_DB_HOST:-127.0.0.1}"
|
||||||
|
databasePort="${SYNCOVA_DB_PORT:-5432}"
|
||||||
|
|
||||||
|
writeDetail "Konfiguration ${configurationFile}"
|
||||||
|
foundAnything="ja"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Repositories aus der Datenbank ermitteln — es koennen mehrere sein, und ihr
|
||||||
|
# Ort steht nicht in der Konfiguration.
|
||||||
|
knownRepositoryPaths=()
|
||||||
|
if [[ -n "${databaseName}" ]] && command -v psql >/dev/null 2>&1; then
|
||||||
|
while IFS= read -r repositoryLocation; do
|
||||||
|
[[ -n "${repositoryLocation}" ]] && knownRepositoryPaths+=("${repositoryLocation}")
|
||||||
|
done < <(PGPASSWORD="${SYNCOVA_DB_PASSWORD:-}" psql -h "${databaseHost}" -p "${databasePort}" \
|
||||||
|
-U "${databaseUser}" -d "${databaseName}" -tAc \
|
||||||
|
"SELECT location FROM repositories ORDER BY name" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${databaseName}" ]]; then
|
||||||
|
backupCount=""
|
||||||
|
if command -v psql >/dev/null 2>&1; then
|
||||||
|
backupCount="$(PGPASSWORD="${SYNCOVA_DB_PASSWORD:-}" psql -h "${databaseHost}" -p "${databasePort}" \
|
||||||
|
-U "${databaseUser}" -d "${databaseName}" -tAc \
|
||||||
|
"SELECT COUNT(*) FROM backups WHERE deleted_at IS NULL" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeDetail "Datenbank ${databaseName}@${databaseHost}:${databasePort}${backupCount:+ (${backupCount} Wiederherstellungspunkte)}"
|
||||||
|
foundAnything="ja"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for repositoryLocation in "${knownRepositoryPaths[@]}"; do
|
||||||
|
repositorySize=""
|
||||||
|
[[ -d "${repositoryLocation}" ]] && repositorySize="$(du -sh "${repositoryLocation}" 2>/dev/null | awk '{print $1}')"
|
||||||
|
writeDetail "Repository ${repositoryLocation}${repositorySize:+ (${repositorySize})}"
|
||||||
|
foundAnything="ja"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -d "${backupDirectory}" ]]; then
|
||||||
|
writeDetail "Sicherungen ${backupDirectory} (Datenbankabzuege der Updates)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${foundAnything}" != "ja" ]]; then
|
||||||
|
printf '\n Es ist keine Installation zu finden.\n\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Was passiert
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Was entfernt wird"
|
||||||
|
|
||||||
|
writeDetail "Dienst und Programme"
|
||||||
|
|
||||||
|
[[ "${removeConfiguration}" == "ja" ]] && writeDetail "Konfiguration ${configurationDirectory}"
|
||||||
|
[[ "${removeDatabase}" == "ja" ]] && writeDetail "Datenbank ${databaseName}"
|
||||||
|
[[ "${removeRepository}" == "ja" ]] && writeDetail "Repositories (siehe oben)"
|
||||||
|
[[ "${removeServiceAccount}" == "ja" ]] && writeDetail "Dienstkonto ${serviceAccount}"
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Was liegen bleibt"
|
||||||
|
|
||||||
|
[[ "${removeConfiguration}" != "ja" ]] && writeDetail "Konfiguration ${configurationDirectory} — enthaelt den Verschluesselungsschluessel"
|
||||||
|
[[ "${removeDatabase}" != "ja" ]] && writeDetail "Datenbank ${databaseName:-(keine gefunden)}"
|
||||||
|
[[ "${removeRepository}" != "ja" ]] && {
|
||||||
|
if [[ ${#knownRepositoryPaths[@]} -eq 0 ]]; then
|
||||||
|
writeDetail "Repositories — keine gefunden"
|
||||||
|
else
|
||||||
|
for repositoryLocation in "${knownRepositoryPaths[@]}"; do
|
||||||
|
writeDetail "Repository ${repositoryLocation} — die Sicherungen selbst"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
[[ -d "${backupDirectory}" ]] && writeDetail "Sicherungen ${backupDirectory}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bestaetigungen
|
||||||
|
#
|
||||||
|
# Je gefaehrlicher, desto laenger das zu tippende Wort. Ein Kennzeichen in einem
|
||||||
|
# Skript soll nicht genuegen, um Backups zu vernichten.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# requireTypedConfirmation verlangt ein woertlich getipptes Wort.
|
||||||
|
requireTypedConfirmation() {
|
||||||
|
local confirmationWord="$1" questionText="$2" enteredValue=""
|
||||||
|
|
||||||
|
if [[ ! -t 0 ]]; then
|
||||||
|
abortWithMessage "Diese Handlung verlangt eine Bestaetigung an einem Terminal.
|
||||||
|
In einem Skript ist sie bewusst nicht moeglich."
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
printf ' %s%s%s\n' "${colorRed}${colorBold}" "${questionText}" "${colorReset}"
|
||||||
|
read -r -p " Tippen Sie '${confirmationWord}' zum Bestaetigen: " enteredValue
|
||||||
|
|
||||||
|
if [[ "${enteredValue}" != "${confirmationWord}" ]]; then
|
||||||
|
abortWithMessage "Nicht bestaetigt. Es wurde nichts entfernt."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${removeRepository}" == "ja" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
printf ' %s┌──────────────────────────────────────────────────────────────┐%s\n' "${colorRed}${colorBold}" "${colorReset}"
|
||||||
|
printf ' %s│ DAS REPOSITORY ENTHAELT DIE SICHERUNGEN SELBST │%s\n' "${colorRed}${colorBold}" "${colorReset}"
|
||||||
|
printf ' %s└──────────────────────────────────────────────────────────────┘%s\n' "${colorRed}${colorBold}" "${colorReset}"
|
||||||
|
printf '\n'
|
||||||
|
writeDetail "Nach dem Entfernen gibt es keinen Wiederherstellungspunkt mehr."
|
||||||
|
writeDetail "Kein Rueckweg, keine zweite Kopie, kein Papierkorb."
|
||||||
|
writeDetail ""
|
||||||
|
writeDetail "Ein gehaertetes Repository laesst sich nicht einfach loeschen; das"
|
||||||
|
writeDetail "Skript hebt dafuer den Aufbewahrungsschutz auf. Genau davor schuetzt"
|
||||||
|
writeDetail "dieser Schutz normalerweise."
|
||||||
|
|
||||||
|
requireTypedConfirmation "SICHERUNGEN VERNICHTEN" \
|
||||||
|
"Sollen die Sicherungen unwiederbringlich entfernt werden?"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${removeDatabase}" == "ja" ]]; then
|
||||||
|
requireTypedConfirmation "DATENBANK LOESCHEN" \
|
||||||
|
"Auftraege, Laeufe, Wiederherstellungspunkte und das Auditprotokoll gehen verloren."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${removeConfiguration}" == "ja" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
writeDetail "In ${configurationFile} steht der Verschluesselungsschluessel."
|
||||||
|
writeDetail "Ohne ihn sind verschluesselte Sicherungen nicht mehr lesbar — auch"
|
||||||
|
writeDetail "die, die anderswo liegen."
|
||||||
|
writeDetail ""
|
||||||
|
writeDetail "Sichern Sie ihn jetzt, falls noch nicht geschehen:"
|
||||||
|
writeDetail " grep SYNCOVA_ENCRYPTION_KEYS ${configurationFile}"
|
||||||
|
|
||||||
|
requireTypedConfirmation "SCHLUESSEL VERWERFEN" \
|
||||||
|
"Soll die Konfiguration samt Schluessel entfernt werden?"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${removeConfiguration}" != "ja" && "${removeDatabase}" != "ja" && \
|
||||||
|
"${removeRepository}" != "ja" ]]; then
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
printf '\n'
|
||||||
|
read -r -p " Dienst und Programme entfernen? [j/N]: " proceedAnswer
|
||||||
|
[[ "${proceedAnswer}" =~ ^[jJyY] ]] || { printf '\n Abgebrochen.\n\n'; exit 0; }
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Dienst
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Dienst"
|
||||||
|
|
||||||
|
if systemctl is-active --quiet "${serviceName}" 2>/dev/null; then
|
||||||
|
systemctl stop "${serviceName}"
|
||||||
|
writeSuccess "Dienst angehalten"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl is-enabled --quiet "${serviceName}" 2>/dev/null; then
|
||||||
|
systemctl disable "${serviceName}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${serviceUnitFile}" ]]; then
|
||||||
|
rm -f "${serviceUnitFile}"
|
||||||
|
systemctl daemon-reload
|
||||||
|
writeSuccess "Diensteinheit entfernt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Repositories
|
||||||
|
#
|
||||||
|
# Vor den Programmen, weil das Aufheben des Aufbewahrungsschutzes das Werkzeug
|
||||||
|
# aus dem Paket braucht.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "${removeRepository}" == "ja" ]] && [[ ${#knownRepositoryPaths[@]} -gt 0 ]]; then
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Repositories"
|
||||||
|
|
||||||
|
for repositoryLocation in "${knownRepositoryPaths[@]}"; do
|
||||||
|
[[ -d "${repositoryLocation}" ]] || continue
|
||||||
|
|
||||||
|
# Der Aufbewahrungsschutz muss Datei fuer Datei aufgehoben werden. Eine
|
||||||
|
# rekursive Aufhebungsfunktion gibt es im Produktivcode ausdruecklich
|
||||||
|
# nicht — sie waere genau das Werkzeug, das ein Angreifer sucht.
|
||||||
|
if command -v chattr >/dev/null 2>&1; then
|
||||||
|
find "${repositoryLocation}" -type f -exec chattr -i {} + 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if rm -rf "${repositoryLocation}" 2>/dev/null; then
|
||||||
|
writeSuccess "Entfernt: ${repositoryLocation}"
|
||||||
|
else
|
||||||
|
writeWarning "Nicht vollstaendig entfernt: ${repositoryLocation}"
|
||||||
|
writeDetail " Der Aufbewahrungsschutz greift noch. Von Hand:"
|
||||||
|
writeDetail " chattr -R -i '${repositoryLocation}' && rm -rf '${repositoryLocation}'"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Programme
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Programme"
|
||||||
|
|
||||||
|
for programRoot in "${installationRoot}" "${previousReleaseRoot}"; do
|
||||||
|
if [[ -d "${programRoot}" ]]; then
|
||||||
|
rm -rf "${programRoot}"
|
||||||
|
writeSuccess "Entfernt: ${programRoot}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Datenbank
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "${removeDatabase}" == "ja" ]] && [[ -n "${databaseName}" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Datenbank"
|
||||||
|
|
||||||
|
# Ohne Pipe zu grep: Der frueh geschlossene Pipe schickt psql ein SIGPIPE,
|
||||||
|
# und mit "set -o pipefail" braeche das Skript mitten im Abraeumen ab.
|
||||||
|
existingDatabase="$(su - postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='${databaseName}'\"" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ "${existingDatabase}" == "1" ]]; then
|
||||||
|
# WITH (FORCE) trennt bestehende Verbindungen. Ohne den Zusatz
|
||||||
|
# scheitert das Loeschen an einer einzigen offenen Sitzung.
|
||||||
|
if su - postgres -c "psql -v ON_ERROR_STOP=1 -c \"DROP DATABASE IF EXISTS ${databaseName} WITH (FORCE)\"" >/dev/null 2>&1; then
|
||||||
|
writeSuccess "Datenbank ${databaseName} entfernt"
|
||||||
|
else
|
||||||
|
writeWarning "Die Datenbank liess sich nicht entfernen."
|
||||||
|
writeDetail " Von Hand: sudo -u postgres dropdb --force ${databaseName}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${databaseUser}" ]]; then
|
||||||
|
# Kein "A && B || C": Scheitert B, liefe auch C — und der Betreiber
|
||||||
|
# bekaeme beide Meldungen zugleich.
|
||||||
|
if su - postgres -c "psql -c \"DROP ROLE IF EXISTS ${databaseUser}\"" >/dev/null 2>&1; then
|
||||||
|
writeSuccess "Datenbankkonto ${databaseUser} entfernt"
|
||||||
|
else
|
||||||
|
writeDetail "Das Datenbankkonto ${databaseUser} blieb (es gehoert womoeglich anderen Datenbanken)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
writeDetail "Die Datenbank ${databaseName} ist von hier aus nicht erreichbar."
|
||||||
|
writeDetail "Bei einer entfernten Datenbank entfernen Sie sie dort."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. Konfiguration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "${removeConfiguration}" == "ja" ]] && [[ -d "${configurationDirectory}" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Konfiguration"
|
||||||
|
|
||||||
|
rm -rf "${configurationDirectory}"
|
||||||
|
writeSuccess "Entfernt: ${configurationDirectory}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. Dienstkonto
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "${removeServiceAccount}" == "ja" ]] && id "${serviceAccount}" >/dev/null 2>&1; then
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Dienstkonto"
|
||||||
|
|
||||||
|
if userdel "${serviceAccount}" 2>/dev/null; then
|
||||||
|
writeSuccess "Dienstkonto ${serviceAccount} entfernt"
|
||||||
|
else
|
||||||
|
writeWarning "Das Dienstkonto liess sich nicht entfernen (gehoeren ihm noch Dateien?)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Abschluss
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n%sDie Deinstallation ist abgeschlossen.%s\n\n' "${colorGreen}${colorBold}" "${colorReset}"
|
||||||
|
|
||||||
|
remainingItems=()
|
||||||
|
[[ "${removeConfiguration}" != "ja" && -d "${configurationDirectory}" ]] && \
|
||||||
|
remainingItems+=("${configurationDirectory} — Konfiguration samt Verschluesselungsschluessel")
|
||||||
|
[[ "${removeDatabase}" != "ja" && -n "${databaseName}" ]] && \
|
||||||
|
remainingItems+=("Datenbank ${databaseName} auf ${databaseHost}:${databasePort}")
|
||||||
|
if [[ "${removeRepository}" != "ja" ]]; then
|
||||||
|
for repositoryLocation in "${knownRepositoryPaths[@]}"; do
|
||||||
|
[[ -d "${repositoryLocation}" ]] && remainingItems+=("${repositoryLocation} — die Sicherungen")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[[ -d "${backupDirectory}" ]] && remainingItems+=("${backupDirectory} — Datenbankabzuege frueherer Updates")
|
||||||
|
|
||||||
|
if [[ ${#remainingItems[@]} -gt 0 ]]; then
|
||||||
|
printf '%sEs bleibt liegen:%s\n\n' "${colorBold}" "${colorReset}"
|
||||||
|
for remainingItem in "${remainingItems[@]}"; do
|
||||||
|
printf ' %s\n' "${remainingItem}"
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
printf 'Das ist Absicht. Eine spaetere Neuinstallation findet diese Daten wieder;\n'
|
||||||
|
printf 'ein Skript, das sie nebenbei mitnimmt, vernichtet genau das, wofuer\n'
|
||||||
|
printf 'jemand jahrelang Speicher bezahlt hat.\n\n'
|
||||||
|
|
||||||
|
if [[ "${removeConfiguration}" != "ja" && -f "${configurationFile}" ]]; then
|
||||||
|
printf '%sSichern Sie den Verschluesselungsschluessel, bevor Sie den Server%s\n' \
|
||||||
|
"${colorYellow}${colorBold}" "${colorReset}"
|
||||||
|
printf '%sabraeumen — ohne ihn sind verschluesselte Sicherungen unlesbar:%s\n\n' \
|
||||||
|
"${colorYellow}${colorBold}" "${colorReset}"
|
||||||
|
printf ' grep SYNCOVA_ENCRYPTION_KEYS %s\n\n' "${configurationFile}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf 'Es wurde alles entfernt.\n\n'
|
||||||
|
fi
|
||||||
418
scripts/update.sh
Executable file
418
scripts/update.sh
Executable file
@ -0,0 +1,418 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Aktualisiert eine vorhandene Syncova-Installation.
|
||||||
|
#
|
||||||
|
# Die Reihenfolge ist nicht verhandelbar und der Grund fuer dieses Skript:
|
||||||
|
#
|
||||||
|
# sichern -> anhalten -> Programme tauschen -> migrieren -> starten -> pruefen
|
||||||
|
#
|
||||||
|
# Laeuft der Dienst noch, waehrend sich das Schema aendert, arbeitet er auf
|
||||||
|
# einem Stand, den er nicht kennt. Und ohne die Sicherung davor gibt es keinen
|
||||||
|
# Weg zurueck: Ein Rollback nimmt genau eine Migration zuruck, und Daten in
|
||||||
|
# Tabellen, die es vorher nicht gab, sind danach weg.
|
||||||
|
#
|
||||||
|
# Scheitert ein Schritt, wird die vorige Fassung zurueckgeholt.
|
||||||
|
#
|
||||||
|
# ./update.sh prueft, fragt, aktualisiert
|
||||||
|
# ./update.sh --pruefen sieht nur nach und aendert nichts
|
||||||
|
# ./update.sh --ja ohne Rueckfrage
|
||||||
|
#
|
||||||
|
# Das Repository wird nicht angefasst. Der Verschluesselungsschluessel auch
|
||||||
|
# nicht — er bleibt, wie er ist, sonst waeren alle Geheimnisse unlesbar.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Feste Orte — gleichlautend in setup.sh und uninstall.sh
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
readonly installationRoot="/opt/syncova"
|
||||||
|
readonly configurationDirectory="/etc/syncova"
|
||||||
|
readonly configurationFile="${configurationDirectory}/syncova.env"
|
||||||
|
readonly serviceName="syncova-api"
|
||||||
|
readonly backupDirectory="/var/backups/syncova"
|
||||||
|
|
||||||
|
# previousReleaseRoot nimmt die abgeloeste Fassung auf.
|
||||||
|
#
|
||||||
|
# Sie bleibt liegen, bis die neue nachweislich laeuft. Ein Update, das die alte
|
||||||
|
# Fassung sofort loescht, hat keinen Rueckweg.
|
||||||
|
readonly previousReleaseRoot="${installationRoot}.vorherige"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ausgabe
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
readonly colorReset=$'\033[0m'
|
||||||
|
readonly colorBold=$'\033[1m'
|
||||||
|
readonly colorRed=$'\033[31m'
|
||||||
|
readonly colorGreen=$'\033[32m'
|
||||||
|
readonly colorYellow=$'\033[33m'
|
||||||
|
else
|
||||||
|
readonly colorReset='' colorBold='' colorRed='' colorGreen='' colorYellow=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeStep() { printf '%s==>%s %s\n' "${colorBold}" "${colorReset}" "$1"; }
|
||||||
|
writeDetail() { printf ' %s\n' "$1"; }
|
||||||
|
writeSuccess() { printf ' %s✓%s %s\n' "${colorGreen}" "${colorReset}" "$1"; }
|
||||||
|
writeWarning() { printf ' %s!%s %s\n' "${colorYellow}" "${colorReset}" "$1" >&2; }
|
||||||
|
|
||||||
|
abortWithMessage() {
|
||||||
|
printf '\n%sAbbruch:%s %s\n' "${colorRed}${colorBold}" "${colorReset}" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Schalter
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
checkOnly="nein"
|
||||||
|
assumeYes="nein"
|
||||||
|
packageDirectory=""
|
||||||
|
|
||||||
|
showUsage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Verwendung:
|
||||||
|
update.sh [--paket <verzeichnis>] [--pruefen] [--ja]
|
||||||
|
|
||||||
|
Schalter:
|
||||||
|
--paket <verzeichnis> Entpacktes neues Paket (Vorgabe: Verzeichnis des Skripts)
|
||||||
|
--pruefen Nur nachsehen: installierte und neue Fassung vergleichen
|
||||||
|
--ja Ohne Rueckfrage aktualisieren
|
||||||
|
--hilfe Diese Uebersicht
|
||||||
|
|
||||||
|
Vor dem Tausch werden Datenbank und Konfiguration gesichert nach:
|
||||||
|
/var/backups/syncova
|
||||||
|
|
||||||
|
Nicht angetastet werden das Repository und der Verschluesselungsschluessel.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--paket) packageDirectory="${2:-}"; shift 2 ;;
|
||||||
|
--pruefen) checkOnly="ja"; shift ;;
|
||||||
|
--ja) assumeYes="ja"; shift ;;
|
||||||
|
--hilfe|-h|--help) showUsage; exit 0 ;;
|
||||||
|
*) showUsage >&2; abortWithMessage "unbekannter Schalter: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Was ist installiert?
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n%sSyncova Backups — Aktualisierung%s\n\n' "${colorBold}" "${colorReset}"
|
||||||
|
|
||||||
|
writeStep "Vorhandene Installation"
|
||||||
|
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
abortWithMessage "Dieses Skript braucht root-Rechte (sudo ./update.sh)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${installationRoot}/bin/syncova-api" ]]; then
|
||||||
|
abortWithMessage "Unter ${installationRoot} ist keine Installation zu finden.
|
||||||
|
Zum Einrichten: ./setup.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${configurationFile}" ]]; then
|
||||||
|
abortWithMessage "Die Konfiguration ${configurationFile} fehlt.
|
||||||
|
Ohne sie laesst sich weder sichern noch migrieren."
|
||||||
|
fi
|
||||||
|
|
||||||
|
installedVersion="$("${installationRoot}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')"
|
||||||
|
[[ -n "${installedVersion}" ]] || installedVersion="unbekannt"
|
||||||
|
|
||||||
|
writeSuccess "Installiert: ${installedVersion}"
|
||||||
|
|
||||||
|
# Der Schemastand gehoert zur Bestandsaufnahme: Steht er auf "unsauber", ist
|
||||||
|
# eine fruehere Migration abgebrochen, und ein Update darauf waere fahrlaessig.
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${configurationFile}"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
schemaState="$("${installationRoot}/bin/syncova-migrate" status 2>&1 | tail -2 | tr '\n' ' ')"
|
||||||
|
writeDetail "Schema: ${schemaState}"
|
||||||
|
|
||||||
|
if printf '%s' "${schemaState}" | grep -qi "unsauber\|dirty"; then
|
||||||
|
printf '\n'
|
||||||
|
writeWarning "Der Schemastand ist als unsauber vermerkt."
|
||||||
|
writeDetail "Eine fruehere Migration ist abgebrochen. Bringen Sie das Schema"
|
||||||
|
writeDetail "zuerst in Ordnung ('syncova-migrate force <n>' nach Ruecksprache),"
|
||||||
|
writeDetail "bevor Sie aktualisieren."
|
||||||
|
abortWithMessage "Ein Update auf einem unsauberen Schema wird nicht durchgefuehrt."
|
||||||
|
fi
|
||||||
|
|
||||||
|
serviceWasRunning="nein"
|
||||||
|
if systemctl is-active --quiet "${serviceName}" 2>/dev/null; then
|
||||||
|
serviceWasRunning="ja"
|
||||||
|
writeSuccess "Dienst laeuft"
|
||||||
|
else
|
||||||
|
writeDetail "Der Dienst laeuft derzeit nicht."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Was ist neu?
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Neues Paket"
|
||||||
|
|
||||||
|
if [[ -z "${packageDirectory}" ]]; then
|
||||||
|
packageDirectory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${packageDirectory}/bin/syncova-api" ]]; then
|
||||||
|
abortWithMessage "Unter ${packageDirectory} liegt kein Auslieferungspaket.
|
||||||
|
Geben Sie es mit --paket <verzeichnis> an."
|
||||||
|
fi
|
||||||
|
|
||||||
|
packageVersion="$("${packageDirectory}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}')"
|
||||||
|
writeSuccess "Neu: ${packageVersion}"
|
||||||
|
|
||||||
|
if [[ -f "${packageDirectory}/SHA256SUMS" ]] && command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
if ( cd "${packageDirectory}" && sha256sum --quiet -c SHA256SUMS >/dev/null 2>&1 ); then
|
||||||
|
writeSuccess "Pruefsummen stimmen"
|
||||||
|
else
|
||||||
|
abortWithMessage "Die Pruefsummen des neuen Pakets stimmen nicht.
|
||||||
|
Ein unvollstaendig uebertragenes Paket wird nicht eingespielt."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${installedVersion}" == "${packageVersion}" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
writeDetail "Installiert und neu sind dieselbe Fassung (${packageVersion})."
|
||||||
|
|
||||||
|
if [[ "${checkOnly}" == "ja" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${assumeYes}" != "ja" ]]; then
|
||||||
|
read -r -p " Trotzdem neu einspielen? [j/N]: " reinstallAnswer
|
||||||
|
[[ "${reinstallAnswer}" =~ ^[jJyY] ]] || { printf '\n Nichts zu tun.\n\n'; exit 0; }
|
||||||
|
else
|
||||||
|
printf '\n Nichts zu tun.\n\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${checkOnly}" == "ja" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
printf ' %s%s%s → %s%s%s\n\n' "${colorBold}" "${installedVersion}" "${colorReset}" \
|
||||||
|
"${colorBold}${colorGreen}" "${packageVersion}" "${colorReset}"
|
||||||
|
printf ' Zum Aktualisieren: %s\n\n' "$0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Rueckfrage
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "${assumeYes}" != "ja" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
printf ' %s%s%s → %s%s%s\n\n' "${colorBold}" "${installedVersion}" "${colorReset}" \
|
||||||
|
"${colorBold}${colorGreen}" "${packageVersion}" "${colorReset}"
|
||||||
|
writeDetail "Vor dem Tausch werden Datenbank und Konfiguration gesichert."
|
||||||
|
writeDetail "Repository und Verschluesselungsschluessel bleiben unberuehrt."
|
||||||
|
writeDetail "Der Dienst ist waehrend des Updates kurz nicht erreichbar."
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
read -r -p " Jetzt aktualisieren? [j/N]: " updateAnswer
|
||||||
|
[[ "${updateAnswer}" =~ ^[jJyY] ]] || { printf '\n Abgebrochen. Es wurde nichts geaendert.\n\n'; exit 0; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Sichern
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Sicherung vor dem Update"
|
||||||
|
|
||||||
|
install -d -m 0700 "${backupDirectory}"
|
||||||
|
|
||||||
|
backupTimestamp="$(date -u '+%Y%m%d-%H%M%S')"
|
||||||
|
backupPrefix="${backupDirectory}/${backupTimestamp}-vor-${packageVersion}"
|
||||||
|
|
||||||
|
# Die Konfiguration zuerst: Sie ist klein und enthaelt den Schluessel.
|
||||||
|
install -m 0600 "${configurationFile}" "${backupPrefix}.env"
|
||||||
|
writeSuccess "Konfiguration: ${backupPrefix}.env"
|
||||||
|
|
||||||
|
# Die Datenbank. Ohne pg_dump gibt es keinen Weg zurueck — deshalb bricht das
|
||||||
|
# Skript hier ab, statt "auf gut Glueck" weiterzumachen.
|
||||||
|
if ! command -v pg_dump >/dev/null 2>&1; then
|
||||||
|
printf '\n'
|
||||||
|
writeWarning "pg_dump ist nicht vorhanden."
|
||||||
|
writeDetail "Ohne Datenbanksicherung gibt es nach einer misslungenen Migration"
|
||||||
|
writeDetail "keinen Weg zurueck: Ein Rollback nimmt genau eine Migration"
|
||||||
|
writeDetail "zurueck, und Daten in neuen Tabellen sind danach weg."
|
||||||
|
printf '\n'
|
||||||
|
writeDetail "Installieren Sie postgresql-client, oder sichern Sie von Hand und"
|
||||||
|
writeDetail "starten Sie danach mit --ja erneut."
|
||||||
|
abortWithMessage "Ohne Datenbanksicherung wird nicht aktualisiert."
|
||||||
|
fi
|
||||||
|
|
||||||
|
databaseDumpFile="${backupPrefix}.dump"
|
||||||
|
|
||||||
|
if ! PGPASSWORD="${SYNCOVA_DB_PASSWORD:-}" pg_dump \
|
||||||
|
--host "${SYNCOVA_DB_HOST:-127.0.0.1}" \
|
||||||
|
--port "${SYNCOVA_DB_PORT:-5432}" \
|
||||||
|
--username "${SYNCOVA_DB_USER:-syncova}" \
|
||||||
|
--dbname "${SYNCOVA_DB_NAME:-syncova}" \
|
||||||
|
--format=custom --file "${databaseDumpFile}" 2>/tmp/syncova-pgdump.log; then
|
||||||
|
printf '\n'
|
||||||
|
writeWarning "Die Datenbanksicherung schlug fehl:"
|
||||||
|
head -5 /tmp/syncova-pgdump.log | sed 's/^/ /' >&2
|
||||||
|
rm -f /tmp/syncova-pgdump.log
|
||||||
|
abortWithMessage "Ohne Sicherung wird nicht aktualisiert."
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /tmp/syncova-pgdump.log
|
||||||
|
chmod 0600 "${databaseDumpFile}"
|
||||||
|
|
||||||
|
dumpSize="$(du -h "${databaseDumpFile}" | awk '{print $1}')"
|
||||||
|
writeSuccess "Datenbank: ${databaseDumpFile} (${dumpSize})"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. Tauschen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# updateFailed holt die vorige Fassung zurueck.
|
||||||
|
#
|
||||||
|
# Aufgerufen aus dem trap. Die Reihenfolge ist die Umkehrung des Updates: erst
|
||||||
|
# den Dienst anhalten, dann die Programme zuruecktauschen, dann starten.
|
||||||
|
updateFailed() {
|
||||||
|
local exitCode=$?
|
||||||
|
[[ ${exitCode} -eq 0 ]] && exit 0
|
||||||
|
|
||||||
|
printf '\n%sDas Update wird zurueckgenommen.%s\n' "${colorYellow}${colorBold}" "${colorReset}" >&2
|
||||||
|
|
||||||
|
systemctl stop "${serviceName}" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ -d "${previousReleaseRoot}" ]]; then
|
||||||
|
rm -rf "${installationRoot}"
|
||||||
|
mv "${previousReleaseRoot}" "${installationRoot}"
|
||||||
|
writeDetail "Die vorige Fassung ${installedVersion} liegt wieder an ihrem Platz."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${serviceWasRunning}" == "ja" ]]; then
|
||||||
|
systemctl start "${serviceName}" 2>/dev/null || \
|
||||||
|
writeWarning "Der Dienst liess sich nicht wieder starten: systemctl status ${serviceName}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n' >&2
|
||||||
|
writeDetail "Die Datenbank wurde NICHT zurueckgespielt — das geht nur von Hand:"
|
||||||
|
writeDetail ""
|
||||||
|
writeDetail " systemctl stop ${serviceName}"
|
||||||
|
writeDetail " pg_restore --clean --if-exists -d ${SYNCOVA_DB_NAME:-syncova} ${databaseDumpFile}"
|
||||||
|
writeDetail " systemctl start ${serviceName}"
|
||||||
|
writeDetail ""
|
||||||
|
writeDetail "Notwendig ist das nur, wenn die Migration bereits gelaufen war."
|
||||||
|
writeDetail "Der Schemastand sagt es: ${installationRoot}/bin/syncova-migrate status"
|
||||||
|
|
||||||
|
exit "${exitCode}"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap updateFailed EXIT
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Programme tauschen"
|
||||||
|
|
||||||
|
if [[ "${serviceWasRunning}" == "ja" ]]; then
|
||||||
|
systemctl stop "${serviceName}"
|
||||||
|
writeSuccess "Dienst angehalten"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "${previousReleaseRoot}"
|
||||||
|
mv "${installationRoot}" "${previousReleaseRoot}"
|
||||||
|
|
||||||
|
install -d -m 0755 "${installationRoot}"
|
||||||
|
cp -R "${packageDirectory}/bin" "${installationRoot}/"
|
||||||
|
[[ -d "${packageDirectory}/web" ]] && cp -R "${packageDirectory}/web" "${installationRoot}/"
|
||||||
|
[[ -d "${packageDirectory}/migrations" ]] && cp -R "${packageDirectory}/migrations" "${installationRoot}/"
|
||||||
|
[[ -d "${packageDirectory}/docs" ]] && cp -R "${packageDirectory}/docs" "${installationRoot}/"
|
||||||
|
[[ -d "${packageDirectory}/deployment" ]] && cp -R "${packageDirectory}/deployment" "${installationRoot}/"
|
||||||
|
chmod -R 0755 "${installationRoot}/bin"
|
||||||
|
|
||||||
|
printf '%s\n' "${packageVersion}" > "${installationRoot}/VERSION"
|
||||||
|
|
||||||
|
writeSuccess "Programme ausgetauscht"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. Migrieren
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Datenbankschema"
|
||||||
|
|
||||||
|
# Mit dem NEUEN Programm und bei angehaltenem Dienst. Beides gehoert zusammen:
|
||||||
|
# Die neue Fassung bringt ihre Migrationen mit, und ein laufender Dienst
|
||||||
|
# arbeitete waehrenddessen auf einem Stand, den er nicht kennt.
|
||||||
|
migrationOutput="$("${installationRoot}/bin/syncova-migrate" up 2>&1 | tail -1)"
|
||||||
|
writeDetail "${migrationOutput}"
|
||||||
|
|
||||||
|
if printf '%s' "${migrationOutput}" | grep -qi "error\|fehler"; then
|
||||||
|
abortWithMessage "Die Migration schlug fehl."
|
||||||
|
fi
|
||||||
|
|
||||||
|
writeSuccess "Schema aktuell"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7. Starten und pruefen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
writeStep "Dienst"
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl start "${serviceName}"
|
||||||
|
|
||||||
|
healthAddress="${SYNCOVA_HTTP_LISTEN_ADDRESS:-127.0.0.1:8080}"
|
||||||
|
serviceIsReady="nein"
|
||||||
|
|
||||||
|
for _attempt in $(seq 1 20); do
|
||||||
|
if curl -fsS "http://${healthAddress}/health/ready" >/dev/null 2>&1; then
|
||||||
|
serviceIsReady="ja"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${serviceIsReady}" != "ja" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
writeWarning "Der Dienst meldet sich nach dem Update nicht als betriebsbereit."
|
||||||
|
journalctl -u "${serviceName}" -n 15 --no-pager 2>/dev/null | sed 's/^/ /' >&2 || true
|
||||||
|
abortWithMessage "Das Update wird zurueckgenommen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Die laufende Fassung ist eine Beigabe, kein Pruefkriterium — sie wird aus dem
|
||||||
|
# Programm gelesen, nicht aus der Gesundheitsantwort.
|
||||||
|
#
|
||||||
|
# Der erste Anlauf holte sie mit "grep -o" aus der Antwort von /health/ready.
|
||||||
|
# Die enthaelt aber gar kein Versionsfeld; grep endet dann mit 1, und mit
|
||||||
|
# "set -o pipefail" nahm das Skript ein **gelungenes** Update wieder zurueck.
|
||||||
|
# Ein Rollback wegen einer Zeile, die nur der Ausgabe dient.
|
||||||
|
runningVersion="$("${installationRoot}/bin/syncova-api" --version 2>/dev/null | awk '{print $2}' || true)"
|
||||||
|
|
||||||
|
writeSuccess "Dienst laeuft und ist betriebsbereit${runningVersion:+ (${runningVersion})}"
|
||||||
|
|
||||||
|
# Ab hier ist das Update gelungen.
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Abschluss
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
printf '\n%sDie Aktualisierung ist abgeschlossen.%s\n\n' "${colorGreen}${colorBold}" "${colorReset}"
|
||||||
|
|
||||||
|
printf ' %s → %s\n\n' "${installedVersion}" "${packageVersion}"
|
||||||
|
printf ' Sicherung %s.dump\n' "${backupPrefix}"
|
||||||
|
printf ' Vorige Fassung %s\n' "${previousReleaseRoot}"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
writeDetail "Die vorige Fassung bleibt liegen, bis Sie sie entfernen:"
|
||||||
|
writeDetail " rm -rf ${previousReleaseRoot}"
|
||||||
|
printf '\n'
|
||||||
|
writeDetail "Sehen Sie vorher nach, ob die neue Fassung im Betrieb traegt —"
|
||||||
|
writeDetail "ein Update, das man am selben Tag entsorgt, hat keinen Rueckweg."
|
||||||
|
printf '\n'
|
||||||
Loading…
Reference in New Issue
Block a user