Add INSTALL.ps1

This commit is contained in:
2026-06-08 13:43:47 +02:00
parent 77965ed31a
commit 8460bb7340
+530
View File
@@ -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" <<EOF
CREATE SUBSCRIPTION lx_sub_${db}
CONNECTION 'host=${WINHOST} port=15432 user=lxreplica password=LxRepl2026! sslmode=require dbname=${db}'
PUBLICATION lx_pub
WITH (copy_data = false);
EOF
done
echo ""
echo "=== Status ==="
sudo -u postgres psql -d f1 -c "SELECT subname, subenabled FROM pg_subscription ORDER BY subname;"
'@
$tmpSubs = [System.IO.Path]::GetTempFileName() + ".sh"
[System.IO.File]::WriteAllText($tmpSubs, $createSubsSh, $utf8NoBom)
& scp -i $sshKey -o StrictHostKeyChecking=no $tmpSubs "${linuxHost}:${linuxDir}/create_subs.sh" 2>&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 ""