#!/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