From 8460bb734035ebf77654272b339811ed4d611c65 Mon Sep 17 00:00:00 2001 From: maniac Date: Mon, 8 Jun 2026 13:43:47 +0200 Subject: [PATCH] Add INSTALL.ps1 --- INSTALL.ps1 | 530 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 INSTALL.ps1 diff --git a/INSTALL.ps1 b/INSTALL.ps1 new file mode 100644 index 0000000..e13b28c --- /dev/null +++ b/INSTALL.ps1 @@ -0,0 +1,530 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Lexware DB Mirror – Vollstaendiges Installations-Script + Richtet die gesamte Replikations-Infrastruktur ein (Windows + Linux). + +.NOTES + Ausfuehren als Administrator: + powershell -ExecutionPolicy Bypass -File "Lexware-DB-Mirror-INSTALL.ps1" + + Was dieses Script tut: + 1. Verzeichnisstruktur auf Windows anlegen + 2. SSH-Key schreiben + Berechtigungen setzen + 3. Alle Windows-Scripts schreiben (push-loop, push-dump, hba-watcher) + 4. PostgreSQL: replication.conf (wal_level=logical) anlegen + Neustart + 5. PostgreSQL-User lxreplica + lxdump anlegen + 6. Publications auf allen 8 Datenbanken anlegen + 7. Scheduled Tasks registrieren und starten + 8. Firewall-Regel fuer Port 15432 pruefen + 9. Linux: webhook.py deployen + starten + 10. Linux: restore-latest.sh (mit Replikations-Guard) deployen + 11. Linux: PostgreSQL max_logical_replication_workers erhoehen + 12. Linux: Subscriptions anlegen +#> + +Set-StrictMode -Off +$ErrorActionPreference = "Continue" +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) + +function Step([int]$n, [string]$text) { + Write-Host "" + Write-Host "[$n] $text" -ForegroundColor Yellow +} +function OK([string]$text) { Write-Host " OK: $text" -ForegroundColor Green } +function WARN([string]$text) { Write-Host " WARN: $text" -ForegroundColor Red } + +Write-Host "" +Write-Host "================================================================" -ForegroundColor Cyan +Write-Host " Lexware DB Mirror - Installation" -ForegroundColor Cyan +Write-Host "================================================================" -ForegroundColor Cyan + +# ================================================================ +# KONFIGURATION – hier anpassen falls sich etwas aendert +# ================================================================ +$baseDir = "C:\lexware-db-connect" +$pgBin = "C:\Program Files\Lexware\PostgreSql\17\Bin" +$dataDir = "C:\ProgramData\Lexware\LexwarePG\Data\current" +$hbaFile = "$dataDir\pg_hba.conf" +$logFile = "C:\Users\Administrator\Desktop\LexWare-DB-Mirror.log" +$sshKey = "$baseDir\ssh\id_rsa" +$linuxIp = "192.168.115.113" +$linuxHost = "root@$linuxIp" +$linuxDir = "/opt/lexware-dumps" +$pgPort = "15432" +$pgAdmin = "altertillattbruker" +$databases = @("f1","f2","lexkonto","lexkk","rk","lxoffice","lx","lxcatalog") + +# ================================================================ +# SCHRITT 1: Verzeichnisse +# ================================================================ +Step 1 "Verzeichnisse anlegen" +foreach ($d in @("$baseDir\ssh","$baseDir\dumps","$baseDir\linux-connect")) { + New-Item -ItemType Directory -Force -Path $d | Out-Null +} +OK $baseDir + +# ================================================================ +# SCHRITT 2: SSH-Key +# ================================================================ +Step 2 "SSH-Key einrichten" + +$privateKey = @' +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- +'@ +$publicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDQwpu15zPRu0G/3VG2xqKi/hRhUlO2QzsVWqiyJnPpl6qhePnCX1tmJyzrSjqSkwzcR46QtM/f2glaJHGtr/0+Oq8pP7hMOUgfdH1wFmjyfi7XuV7vAstOT/Q/qJOPTl3dNHW0E5tEy4wBsUkzVvpjJN9X+R/k1jy30jC8HZ1QwJfumh2920WMLlPhgzWpqsLVdpMqs1qPRhTb4hQoB/IOgDWAvjlDwrSUvU66QCr15OVmJU9Z0jl8CPLnP0QqIXFFSpmESFPiwkoatFgGpjCN0CXBNfFuZ2DoHnuhqsLkrAtvd/nKxVVfCmCwGp3mYHsWfIo+BoYBrs90T7XzcRt6aaJYg7XvckEM0CtmF/LYsdbtVmSGRQGBnFbqure6BTWoOP4n2eZC5ynKNSYx6sp8WwFRwvibm++6ASzxHw2JhQbXd7weSKJ4Xj/4b9JFOAV+DypCtebPwA5crMR1NBx8DkhPymz9+J3/kpKiC1AMkNSwunxRwI5Mbe2akmOTW7EqNeQJjyGkme8Ae6IdlixdL2Jwj4DmAbSoxYvbusx2xC8rCI2V5X/SweOlPPAKpe4BQHTob1zUDE3mlLzKHK+XapJl3LfVKqHlSvlFE9CHe2cM25wVug/V798odvB/uMRv/aTkHdnZxRgfBIq98hlmlw77xdFwFBZlLx/JHCOIAQ== administrator@W2K25" + +[System.IO.File]::WriteAllText("$baseDir\ssh\id_rsa", $privateKey, $utf8NoBom) +[System.IO.File]::WriteAllText("$baseDir\ssh\id_rsa.pub", $publicKey, $utf8NoBom) +icacls "$baseDir\ssh\id_rsa" /inheritance:r /grant "NT AUTHORITY\SYSTEM:(F)" /grant "VORDEFINIERT\Administratoren:(F)" 2>&1 | Out-Null +OK "SSH-Key + Berechtigungen" + +# ================================================================ +# SCHRITT 3: Windows-Scripts schreiben +# ================================================================ +Step 3 "Windows-Scripts schreiben" + +[System.IO.File]::WriteAllText("$baseDir\hba-watcher.ps1", @' +$pgCtl = "C:\Program Files\Lexware\PostgreSql\17\Bin\pg_ctl.exe" +$dataDir = "C:\ProgramData\Lexware\LexwarePG\Data\current" +$hbaFile = "$dataDir\pg_hba.conf" +$ruleAll = "hostssl all lxreplica 192.168.115.113/32 scram-sha-256" +$ruleRep = "hostssl replication lxreplica 192.168.115.113/32 scram-sha-256" +while ($true) { + try { + $lines = [System.IO.File]::ReadAllLines($hbaFile) + $existing = $lines | Where-Object { $_ -match "lxreplica" } + $ok = ($existing.Count -eq 2) -and ($existing[0] -eq $ruleAll) -and ($existing[1] -eq $ruleRep) + if (-not $ok) { + $base = $lines | Where-Object { $_ -notmatch "lxreplica" } + $newLines = [System.Collections.Generic.List[string]]::new() + foreach ($line in $base) { + if ($line -match "192\.168\.115\.0/24") { $newLines.Add($ruleAll); $newLines.Add($ruleRep) } + $newLines.Add($line) + } + [System.IO.File]::WriteAllLines($hbaFile, $newLines, [System.Text.Encoding]::ASCII) + & $pgCtl reload -D $dataDir 2>&1 | Out-Null + } + } catch {} + Start-Sleep -Seconds 10 +} +'@, $utf8NoBom) + +[System.IO.File]::WriteAllText("$baseDir\push-dump.ps1", @' +$pgDump = "C:\Program Files\Lexware\PostgreSql\17\Bin\pg_dump.exe" +$pgCtl = "C:\Program Files\Lexware\PostgreSql\17\Bin\pg_ctl.exe" +$dataDir = "C:\ProgramData\Lexware\LexwarePG\Data\current" +$hbaFile = "$dataDir\pg_hba.conf" +$dumpDir = "C:\lexware-db-connect\dumps" +$linuxHost = "root@192.168.115.113" +$linuxDir = "/opt/lexware-dumps" +$sshKey = "C:\lexware-db-connect\ssh\id_rsa" +$logFile = "C:\Users\Administrator\Desktop\LexWare-DB-Mirror.log" +$lockFile = "C:\lexware-db-connect\push-dump.lock" +$maxLines = 500 +$databases = @("f1","f2","lexkonto","lexkk","rk","lxoffice","lx","lxcatalog") +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) +function Write-Log { param([string]$Message,[string]$Level="INFO") + $entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $Message`n" + [System.IO.File]::AppendAllText($logFile,$entry,$utf8NoBom) + $c=[System.IO.File]::ReadAllLines($logFile); if($c.Count -gt $maxLines){[System.IO.File]::WriteAllLines($logFile,($c|Select-Object -Last $maxLines),$utf8NoBom)} +} +if(Test-Path $lockFile){$age=(Get-Date)-(Get-Item $lockFile).LastWriteTime;if($age.TotalMinutes -lt 10){exit 0};Remove-Item $lockFile -Force} +New-Item -ItemType File -Path $lockFile -Force | Out-Null +try { + New-Item -ItemType Directory -Force -Path $dumpDir | Out-Null + $ts=$( Get-Date -Format "yyyy-MM-dd_HH-mm"); $sw=[System.Diagnostics.Stopwatch]::StartNew() + Write-Log "===== Sync-Lauf gestartet ($ts) =====" + $tr="host all lxdump 127.0.0.1/32 trust" + $lines=Get-Content $hbaFile + if($lines -notmatch "lxdump"){ + $nl=[System.Collections.Generic.List[string]]::new();$ins=$false + foreach($l in $lines){if(-not $ins -and $l -match "127\.0\.0\.1/32"){$nl.Add($tr);$ins=$true};$nl.Add($l)} + [System.IO.File]::WriteAllLines($hbaFile,$nl,[System.Text.Encoding]::ASCII) + & $pgCtl reload -D $dataDir 2>&1|Out-Null; Start-Sleep -Seconds 2 + } + $env:PGSSLMODE="disable";$env:PGPASSWORD="";$env:PGCLIENTENCODING="LATIN1" + $sql=@' +DO $$BEGIN IF NOT EXISTS(SELECT 1 FROM pg_roles WHERE rolname='lxdump') THEN CREATE USER lxdump WITH SUPERUSER LOGIN; END IF; END$$; +'@ + & "C:\Program Files\Lexware\PostgreSql\17\Bin\psql.exe" -h 127.0.0.1 -p 15432 -U altertillattbruker -d postgres -c $sql 2>&1|Out-Null + $errs=@();$ok=0 + foreach($db in $databases){ + $f="$dumpDir\${db}_${ts}.dump" + & $pgDump -h 127.0.0.1 -p 15432 -U lxdump -d $db -Fc -f $f 2>&1|Out-Null + if($LASTEXITCODE -ne 0){Write-Log " $db -> DUMP FEHLGESCHLAGEN" "ERROR";$errs+=$db;continue} + $sz=if(Test-Path $f){"{0:N1} MB" -f ((Get-Item $f).Length/1MB)}else{"?"} + & scp -i $sshKey -o StrictHostKeyChecking=no $f "${linuxHost}:${linuxDir}/${db}_${ts}.dump" 2>&1|Out-Null + if($LASTEXITCODE -eq 0){ + try{Invoke-WebRequest -Uri "http://192.168.115.113:9055/restore?db=$db" -Method POST -UseBasicParsing -TimeoutSec 5|Out-Null}catch{} + Write-Log " $db -> OK ($sz)";$ok++ + }else{Write-Log " $db -> SCP FEHLGESCHLAGEN ($sz)" "ERROR";$errs+=$db} + Remove-Item $f -Force + } + $cl=Get-Content $hbaFile|Where-Object{$_ -notmatch "^host\s+all\s+all\s+127\.0\.0\.1/32\s+trust"} + [System.IO.File]::WriteAllLines($hbaFile,$cl,[System.Text.Encoding]::ASCII) + & $pgCtl reload -D $dataDir 2>&1|Out-Null + & ssh -i $sshKey -o StrictHostKeyChecking=no root@192.168.115.113 "for db in f1 f2 lexkonto lexkk rk lxoffice lx lxcatalog; do ls -t /opt/lexware-dumps/\${db}_*.dump 2>/dev/null|tail -n +6|xargs -r rm -f; done" 2>&1|Out-Null + $sw.Stop();$el="{0:mm\:ss}" -f $sw.Elapsed + if($errs.Count -gt 0){Write-Log " Ergebnis: $ok/$($databases.Count) OK | Fehler: $($errs -join ', ') | Dauer: $el" "WARN"} + else{Write-Log " Ergebnis: $ok/$($databases.Count) OK | Alle synchronisiert | Dauer: $el"} + Write-Log "===== Sync-Lauf beendet =====" +} finally { + $fl=Get-Content $hbaFile -ErrorAction SilentlyContinue|Where-Object{$_ -notmatch "lxdump"} + if($fl){[System.IO.File]::WriteAllLines($hbaFile,$fl,[System.Text.Encoding]::ASCII);& $pgCtl reload -D $dataDir 2>&1|Out-Null} + Remove-Item $lockFile -Force -ErrorAction SilentlyContinue +} +'@, $utf8NoBom) + +[System.IO.File]::WriteAllText("$baseDir\push-loop.ps1", @' +$script=$( "C:\lexware-db-connect\push-dump.ps1") +$logFile="C:\Users\Administrator\Desktop\LexWare-DB-Mirror.log" +$utf8NoBom=[System.Text.UTF8Encoding]::new($false) +$wait=120 +function WL([string]$m){[System.IO.File]::AppendAllText($logFile,"[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [LOOP] $m`n",$utf8NoBom)} +WL "Loop gestartet (Interval: ${wait}s nach Lauf-Ende)" +while($true){ + $ok=$false + try{ + $r=& ssh -i "C:\lexware-db-connect\ssh\id_rsa" -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@192.168.115.113 "sudo -u postgres psql -d f1 -tAc 'SELECT COUNT(*) FROM pg_stat_subscription WHERE received_lsn IS NOT NULL;' 2>/dev/null" + $ok=([int]($r.Trim()) -ge 4) + }catch{} + if($ok){Start-Sleep -Seconds $wait;continue} + WL "Replikation inaktiv - starte Dump-Fallback" + $sw=[System.Diagnostics.Stopwatch]::StartNew() + & powershell.exe -NonInteractive -ExecutionPolicy Bypass -File $script + $sw.Stop();$el=[int]$sw.Elapsed.TotalSeconds + $sl=[Math]::Max(10,$wait-$el) + WL "Lauf beendet nach ${el}s - naechster Start in ${sl}s" + Start-Sleep -Seconds $sl +} +'@, $utf8NoBom) + +OK "hba-watcher.ps1, push-dump.ps1, push-loop.ps1" + +# ================================================================ +# SCHRITT 4: PostgreSQL replication.conf + Neustart +# ================================================================ +Step 4 "PostgreSQL WAL-Level auf logical setzen" +$replConf = "$dataDir\conf.d\replication.conf" +[System.IO.File]::WriteAllText($replConf, "wal_level = logical`nmax_replication_slots = 30`nmax_wal_senders = 10`n", [System.Text.Encoding]::ASCII) +Write-Host " Starte PostgreSQL neu (Lexware ~5 Sek nicht erreichbar)..." -ForegroundColor Gray +& "$pgBin\pg_ctl.exe" restart -D $dataDir -w 2>&1 | Out-Null +Start-Sleep -Seconds 6 +OK "wal_level = logical in conf.d\replication.conf" + +# ================================================================ +# SCHRITT 5: Datenbankbenutzer + Publications +# ================================================================ +Step 5 "Datenbankbenutzer und Publications anlegen" + +$lines = Get-Content $hbaFile +$newLines = [System.Collections.Generic.List[string]]::new(); $ins = $false +foreach ($line in $lines) { + if (-not $ins -and $line -match "127\.0\.0\.1/32") { $newLines.Add("host all all 127.0.0.1/32 trust"); $ins = $true } + $newLines.Add($line) +} +[System.IO.File]::WriteAllLines($hbaFile, $newLines, [System.Text.Encoding]::ASCII) +& "$pgBin\pg_ctl.exe" reload -D $dataDir 2>&1 | Out-Null; Start-Sleep -Seconds 2 +$env:PGSSLMODE = "disable" + +& "$pgBin\psql.exe" -h 127.0.0.1 -p $pgPort -U $pgAdmin -d postgres -c "DO `$`$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_roles WHERE rolname='lxreplica') THEN CREATE USER lxreplica WITH REPLICATION LOGIN PASSWORD 'LxRepl2026!'; END IF; END`$`$;" 2>&1 | Out-Null +& "$pgBin\psql.exe" -h 127.0.0.1 -p $pgPort -U $pgAdmin -d postgres -c "DO `$`$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_roles WHERE rolname='lxdump') THEN CREATE USER lxdump WITH SUPERUSER LOGIN; END IF; END`$`$;" 2>&1 | Out-Null + +foreach ($db in $databases) { + $r = & "$pgBin\psql.exe" -h 127.0.0.1 -p $pgPort -U $pgAdmin -d $db -c "CREATE PUBLICATION lx_pub FOR ALL TABLES;" 2>&1 + Write-Host (" {0,-12} {1}" -f $db, (if ($r -match "already exists") {"bereits vorhanden"} else {"Publication angelegt"})) -ForegroundColor Green +} + +$cl = Get-Content $hbaFile | Where-Object { $_ -notmatch "^host\s+all\s+all\s+127\.0\.0\.1/32\s+trust" } +[System.IO.File]::WriteAllLines($hbaFile, $cl, [System.Text.Encoding]::ASCII) +& "$pgBin\pg_ctl.exe" reload -D $dataDir 2>&1 | Out-Null +OK "lxreplica, lxdump, Publications" + +# ================================================================ +# SCHRITT 6: Scheduled Tasks +# ================================================================ +Step 6 "Scheduled Tasks registrieren" +$principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest +$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 0) -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew + +foreach ($task in @("LexwarePG-HBA-Watcher","LexwarePG-Push-Loop")) { + Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue +} + +$a1 = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$baseDir\hba-watcher.ps1`"" +Register-ScheduledTask -TaskName "LexwarePG-HBA-Watcher" -Action $a1 -Trigger (New-ScheduledTaskTrigger -AtStartup) -Settings $settings -Principal $principal | Out-Null +Start-ScheduledTask -TaskName "LexwarePG-HBA-Watcher" + +$a2 = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$baseDir\push-loop.ps1`"" +Register-ScheduledTask -TaskName "LexwarePG-Push-Loop" -Action $a2 -Trigger (New-ScheduledTaskTrigger -AtStartup) -Settings $settings -Principal $principal | Out-Null +Start-ScheduledTask -TaskName "LexwarePG-Push-Loop" +OK "LexwarePG-HBA-Watcher + LexwarePG-Push-Loop gestartet" + +# ================================================================ +# SCHRITT 7: Firewall +# ================================================================ +Step 7 "Firewall Port 15432" +if (-not (Get-NetFirewallRule -DisplayName "*15432*" -ErrorAction SilentlyContinue)) { + New-NetFirewallRule -DisplayName "PostgreSQL Lexware 15432" -Direction Inbound -Protocol TCP -LocalPort 15432 -Action Allow | Out-Null + OK "Regel neu angelegt" +} else { OK "Bereits vorhanden" } + +# ================================================================ +# SCHRITT 8: Linux – webhook.py deployen +# ================================================================ +Step 8 "Linux: webhook.py deployen" + +$webhookPy = @' +#!/usr/bin/env python3 +import http.server, subprocess, threading, urllib.parse, logging, sys, os +PORT = 9055 +RESTORE_SCRIPT = "/opt/lexware-dumps/restore-latest.sh" +LOG_FILE = "/var/log/lexware-restore.log" +VALID_DBS = {"f1","f2","lexkonto","lexkk","rk","lxoffice","lx","lxcatalog"} +logging.basicConfig(level=logging.INFO, format="%(asctime)s [webhook] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler(sys.stdout)]) +def run_restore(db): + env = os.environ.copy() + cmd = [RESTORE_SCRIPT] + if db: env["RESTORE_DB"] = db + logging.info(f"Starte Restore: db={db or 'alle'}") + result = subprocess.run(cmd, env=env, capture_output=True, text=True) + if result.stdout: + for line in result.stdout.strip().splitlines(): logging.info(line) + if result.returncode not in (0,1): logging.error(f"Restore exit {result.returncode}") +class H(http.server.BaseHTTPRequestHandler): + def log_message(self, fmt, *args): logging.info(f"{self.address_string()} {fmt%args}") + def do_POST(self): + p=urllib.parse.urlparse(self.path); q=urllib.parse.parse_qs(p.query) + if p.path!="/restore": self._r(404,"Not found"); return + db=q.get("db",[None])[0] + if db and db not in VALID_DBS: self._r(400,f"Unbekannte DB: {db}"); return + self._r(200,f"OK: db={db or 'alle'}") + threading.Thread(target=run_restore,args=(db,),daemon=True).start() + def do_GET(self): + if self.path=="/health": self._r(200,"OK") + else: self._r(404,"Not found") + def _r(self,code,body): + d=body.encode(); self.send_response(code) + self.send_header("Content-Type","text/plain"); self.send_header("Content-Length",str(len(d))); self.end_headers(); self.wfile.write(d) +if __name__=="__main__": + s=http.server.ThreadingHTTPServer(("0.0.0.0",PORT),H) + logging.info(f"Webhook-Server laeuft auf Port {PORT}") + s.serve_forever() +'@ + +$tmpWebhook = [System.IO.Path]::GetTempFileName() + ".py" +[System.IO.File]::WriteAllText($tmpWebhook, $webhookPy, $utf8NoBom) +& scp -i $sshKey -o StrictHostKeyChecking=no $tmpWebhook "${linuxHost}:${linuxDir}/webhook.py" 2>&1 | Out-Null +Remove-Item $tmpWebhook -Force -ErrorAction SilentlyContinue + +# Webhook als Hintergrundprozess starten (falls nicht schon laufend) +& ssh -i $sshKey -o StrictHostKeyChecking=no $linuxHost "pkill -f 'webhook.py' 2>/dev/null; sleep 1; nohup python3 ${linuxDir}/webhook.py >> /var/log/lexware-restore.log 2>&1 &" 2>&1 | Out-Null +OK "webhook.py deployed und gestartet" + +# ================================================================ +# SCHRITT 9: Linux – restore-latest.sh deployen +# ================================================================ +Step 9 "Linux: restore-latest.sh deployen" + +$restoreSh = @' +#!/bin/bash +DUMP_DIR="/opt/lexware-dumps" +STAMP_DIR="${DUMP_DIR}/.last_restore" +LOG_FILE="/var/log/lexware-restore.log" +DATABASES="${RESTORE_DB:-f1 f2 lexkonto lexkk rk lxoffice lx lxcatalog}" +KEEP_DUMPS=5 +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; } +psql_c() { sudo -u postgres psql -v ON_ERROR_STOP=1 -c "$1" 2>>"$LOG_FILE"; } +exec 9>/var/lock/lexware-restore.lock +if ! flock -n 9; then log "SKIP Restore laeuft bereits"; exit 0; fi +mkdir -p "$STAMP_DIR" + +restore_one() { + local db="$1" dump="$2" shadow="${1}_shadow" + psql_c "DROP DATABASE IF EXISTS \"${shadow}\";" 2>/dev/null + if ! psql_c "CREATE DATABASE \"${shadow}\" ENCODING 'UTF8' LC_COLLATE 'de_DE.UTF-8' LC_CTYPE 'de_DE.UTF-8' TEMPLATE template0;"; then + log "ERROR $db Shadow anlegen fehlgeschlagen"; return 1; fi + local tmplog=$(mktemp) + sudo -u postgres bash -c "PGCLIENTENCODING=LATIN1 pg_restore -d '${shadow}' '${dump}'" 2>"$tmplog" + local rc=$?; cat "$tmplog" >> "$LOG_FILE" + if [[ $rc -eq 1 ]]; then + local errs=$(grep "^pg_restore: error:" "$tmplog" | grep -v "encoding\|COPY failed\|does not exist.*role" | wc -l) + rm -f "$tmplog" + if [[ $errs -gt 0 ]]; then log "ERROR $db $errs kritische Fehler"; psql_c "DROP DATABASE IF EXISTS \"${shadow}\";" 2>/dev/null; return 1; fi + log "WARN $db Restore mit harmlosen Warnungen" + elif [[ $rc -gt 1 ]]; then rm -f "$tmplog"; log "ERROR $db exit $rc"; psql_c "DROP DATABASE IF EXISTS \"${shadow}\";" 2>/dev/null; return 1 + else rm -f "$tmplog"; fi + log "SWAP $db" + psql_c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${db}' AND pid<>pg_backend_pid();" 2>/dev/null + sleep 0.2 + if ! psql_c "ALTER DATABASE \"${db}\" RENAME TO \"${db}_old\";"; then log "ERROR $db Rename fehlgeschlagen"; return 1; fi + if ! psql_c "ALTER DATABASE \"${shadow}\" RENAME TO \"${db}\";"; then log "ERROR $db Shadow-Rename fehlgeschlagen"; return 1; fi + ( psql_c "DROP DATABASE IF EXISTS \"${db}_old\";" 2>/dev/null ) & + return 0 +} + +for db in $DATABASES; do + latest=$(ls -1 "${DUMP_DIR}/${db}_"*.dump 2>/dev/null | sort | tail -n 1) + [[ -z "$latest" ]] && { log "SKIP $db kein Dump"; continue; } + stamp_file="${STAMP_DIR}/${db}"; last_restore="" + [[ -f "$stamp_file" ]] && last_restore=$(cat "$stamp_file") + [[ "$latest" == "$last_restore" ]] && { log "SKIP $db bereits eingespielt"; continue; } + + # Logical Replication Guard: ueberspringen wenn WAL aktiv + repl=$(sudo -u postgres psql -d "$db" -tAc "SELECT COUNT(*) FROM pg_stat_subscription WHERE subname='lx_sub_${db}' AND received_lsn IS NOT NULL;" 2>/dev/null | tr -d ' \t\n') + if [ "${repl:-0}" -gt "0" ]; then log "SKIP $db Logical Replication aktiv"; continue; fi + + log "START $db $latest" + if restore_one "$db" "$latest"; then echo "$latest" > "$stamp_file"; log "OK $db"; fi +done + +for db in $DATABASES; do + ls -t "${DUMP_DIR}/${db}_"*.dump 2>/dev/null | tail -n +$((KEEP_DUMPS+1)) | xargs -r rm -f +done +'@ + +$tmpRestore = [System.IO.Path]::GetTempFileName() + ".sh" +[System.IO.File]::WriteAllText($tmpRestore, $restoreSh, $utf8NoBom) +& scp -i $sshKey -o StrictHostKeyChecking=no $tmpRestore "${linuxHost}:${linuxDir}/restore-latest.sh" 2>&1 | Out-Null +& ssh -i $sshKey -o StrictHostKeyChecking=no $linuxHost "chmod +x ${linuxDir}/restore-latest.sh" 2>&1 | Out-Null +Remove-Item $tmpRestore -Force -ErrorAction SilentlyContinue +OK "restore-latest.sh deployed" + +# ================================================================ +# SCHRITT 10: Linux – PostgreSQL Worker-Config +# ================================================================ +Step 10 "Linux: PostgreSQL max_logical_replication_workers" +& ssh -i $sshKey -o StrictHostKeyChecking=no $linuxHost @" +mkdir -p /etc/postgresql/18/main/conf.d +echo 'max_logical_replication_workers = 16' > /etc/postgresql/18/main/conf.d/replication.conf +echo 'max_worker_processes = 32' >> /etc/postgresql/18/main/conf.d/replication.conf +systemctl restart postgresql +sleep 5 +"@ 2>&1 | Out-Null +OK "max_logical_replication_workers = 16" + +# ================================================================ +# SCHRITT 11: Linux – Subscriptions anlegen +# ================================================================ +Step 11 "Linux: Subscriptions anlegen" + +$createSubsSh = @' +#!/bin/bash +WINHOST="192.168.115.111" +echo "Droppe alte Subscriptions..." +for db in f1 f2 lexkonto lexkk rk lxoffice lx lxcatalog; do + sudo -u postgres psql -d "$db" -c "ALTER SUBSCRIPTION lx_sub_${db} DISABLE;" 2>/dev/null + sudo -u postgres psql -d "$db" -c "ALTER SUBSCRIPTION lx_sub_${db} SET (slot_name=NONE);" 2>/dev/null + sudo -u postgres psql -d "$db" -c "DROP SUBSCRIPTION IF EXISTS lx_sub_${db};" 2>/dev/null +done +echo "Lege Subscriptions neu an (copy_data=false)..." +for db in f1 f2 lexkonto lexkk rk lxoffice lx lxcatalog; do + echo "=== $db ===" + sudo -u postgres psql -d "$db" <&1 | Out-Null +Remove-Item $tmpSubs -Force -ErrorAction SilentlyContinue + +Write-Host " Fuehre Subscriptions-Script auf Linux aus..." -ForegroundColor Gray +$subsResult = & ssh -i $sshKey -o StrictHostKeyChecking=no $linuxHost "bash ${linuxDir}/create_subs.sh 2>&1" +$subsResult | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + +# ================================================================ +# ABSCHLUSS + PRUEFUNG +# ================================================================ +Write-Host "" +Write-Host "================================================================" -ForegroundColor Cyan +Write-Host " Pruefung" -ForegroundColor Cyan +Write-Host "================================================================" -ForegroundColor Cyan + +Start-Sleep -Seconds 12 + +# HBA-Regeln +$hbaCheck = Get-Content $hbaFile | Select-String "lxreplica" +Write-Host (" pg_hba.conf lxreplica-Regeln: {0}" -f $hbaCheck.Count) -ForegroundColor (if ($hbaCheck.Count -eq 2) {"Green"} else {"Red"}) + +# Tasks +Get-ScheduledTask | Where-Object { $_.TaskName -like "LexwarePG*" } | ForEach-Object { + Write-Host (" Task {0,-30} {1}" -f $_.TaskName, $_.State) -ForegroundColor Green +} + +# Replikation +Start-Sleep -Seconds 15 +$replCheck = & ssh -i $sshKey -o StrictHostKeyChecking=no -o ConnectTimeout=10 $linuxHost ` + "sudo -u postgres psql -d f1 -tAc 'SELECT COUNT(*) FROM pg_stat_subscription WHERE received_lsn IS NOT NULL;' 2>/dev/null" 2>&1 +$replCount = [int]($replCheck.Trim()) +Write-Host (" Aktive Subscriptions mit WAL: {0}/8" -f $replCount) -ForegroundColor (if ($replCount -ge 7) {"Green"} else {"Yellow"}) + +# Webhook +$webhookOk = & curl.exe -s --max-time 5 "http://${linuxIp}:9055/health" 2>$null +Write-Host (" Webhook /health: {0}" -f $webhookOk) -ForegroundColor (if ($webhookOk -eq "OK") {"Green"} else {"Red"}) + +Write-Host "" +Write-Host "================================================================" -ForegroundColor Cyan +Write-Host " Fertig!" -ForegroundColor Cyan +Write-Host " Log: $logFile" -ForegroundColor White +Write-Host " Doku: C:\Users\Administrator\Desktop\Lexware-DB-Mirror-Dokumentation.md" -ForegroundColor White +Write-Host "================================================================" -ForegroundColor Cyan +Write-Host ""