Add INSTALL.ps1
This commit is contained in:
+530
@@ -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-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAgEA0MKbtecz0btBv91Rtsaiov4UYVJTtkM7FVqosiZz6ZeqoXj5wl9b
|
||||
Zics60o6kpMM3EeOkLTP39oJWiRxra/9PjqvKT+4TDlIH3R9cBZo8n4u17le7wLLTk/0P6
|
||||
iTj05d3TR1tBObRMuMAbFJM1b6YyTfV/kf5NY8t9IwvB2dUMCX7podvdtFjC5T4YM1qarC
|
||||
1XaTKrNaj0YU2+IUKAfyDoA1gL45Q8K0lL1OukAq9eTlZiVPWdI5fAjy5z9EKiFxRUqZhE
|
||||
hT4sJKGrRYBqYwjdAlwTXxbmdg6B57oarC5KwLb3f5ysVVXwpgsBqd5mB7FnyKPgaGAa7P
|
||||
dE+183EbemmiWIO173JBDNArZhfy2LHW7VZkhkUBgZxW6rq3ugU1qDj+J9nmQucpyjUmMe
|
||||
rKfFsBUcL4m5vvugEs8R8NiYUG13e8HkiieF4/+G/SRTgFfg8qQrXmz8AOXKzEdTQcfA5I
|
||||
T8ps/fid/5KSogtQDJDUsLp8UcCOTG3tmpJjk1uxKjXkCY8hpJnvAHuiHZYsXS9icI+A5g
|
||||
G0qMWL27rMdsQvKwiNleV/0sHjpTzwCqXuAUB06G9c1AxN5pS8yhyvl2qSZdy31Sqh5Ur5
|
||||
RRPQh3tnDNucFboP1e/fKHbwf7jEb/2k5B3Z2cUYHwSKvfIZZpcO+8XRcBQWZS8fyRwjiA
|
||||
EAAAdQ6y/Dcusvw3IAAAAHc3NoLXJzYQAAAgEA0MKbtecz0btBv91Rtsaiov4UYVJTtkM7
|
||||
FVqosiZz6ZeqoXj5wl9bZics60o6kpMM3EeOkLTP39oJWiRxra/9PjqvKT+4TDlIH3R9cB
|
||||
Zo8n4u17le7wLLTk/0P6iTj05d3TR1tBObRMuMAbFJM1b6YyTfV/kf5NY8t9IwvB2dUMCX
|
||||
7podvdtFjC5T4YM1qarC1XaTKrNaj0YU2+IUKAfyDoA1gL45Q8K0lL1OukAq9eTlZiVPWd
|
||||
I5fAjy5z9EKiFxRUqZhEhT4sJKGrRYBqYwjdAlwTXxbmdg6B57oarC5KwLb3f5ysVVXwpg
|
||||
sBqd5mB7FnyKPgaGAa7PdE+183EbemmiWIO173JBDNArZhfy2LHW7VZkhkUBgZxW6rq3ug
|
||||
U1qDj+J9nmQucpyjUmMerKfFsBUcL4m5vvugEs8R8NiYUG13e8HkiieF4/+G/SRTgFfg8q
|
||||
QrXmz8AOXKzEdTQcfA5IT8ps/fid/5KSogtQDJDUsLp8UcCOTG3tmpJjk1uxKjXkCY8hpJ
|
||||
nvAHuiHZYsXS9icI+A5gG0qMWL27rMdsQvKwiNleV/0sHjpTzwCqXuAUB06G9c1AxN5pS8
|
||||
yhyvl2qSZdy31Sqh5Ur5RRPQh3tnDNucFboP1e/fKHbwf7jEb/2k5B3Z2cUYHwSKvfIZZp
|
||||
cO+8XRcBQWZS8fyRwjiAEAAAADAQABAAACAFZBwuLjWBb1v5IOWYAjDPo576PSx4IMv3Hw
|
||||
Vrndh5FiOH+lo9U7X2GTGE1UC2Wa2vp9mpuSCj5dMfYMDuiMSiAXUV7C1FyyYmmU0Wup5s
|
||||
0jdClwj5hEWErQYISZG/dfkwsebO/uFf7T99KPNUbATo7+okYQSqxcFRSDBd4EgobmPSC6
|
||||
j0VuP4tPbRtGArtLMlvPNbm0B9whQeckv91Wgx6YvQKoFrM31TOMEOaGMvoNDPgqvGHJqj
|
||||
Tk4bDJBFpAHTRbQZlV5UtRqhrkn2aH7pH6Ck+OAWbz0ie2yLChBQxFRihVs2GkLcUqXY8G
|
||||
QG37OYCuDtTYDzDM0S0m7nBbMNWgtJ82LdTT6VQMEVgff10q25kbLRmKJbhezUyyXSBfrk
|
||||
i2644Udqnyqtai/JY9ubkiREJQSVBjd6VsG2iVPw3a0xoVdyazo9pwS6i44bjbH7qBbcJv
|
||||
rTwpcfrOs8hBtVtGVIndm8iEvBdG4rwislS1w8cIJrw3VgAUhnULMUBmqP+kz0gRT7c/JG
|
||||
5W6tPxy5nP3YchcOgdCOB5XKK/tmEsa5g1hpSTeEkqz5wAwEu1yCsiXBemoIDvxqHcFk6X
|
||||
BWnJMpB6OQxVSzieet1lsWXTX+8AXfQjk1CvuO+Hmd/oRjDj2EACBw/bBjhdA/lELPqMKo
|
||||
tfVYBmmJ6TwH1ivenNAAABAQCjMDP5ZW2DfmsswEyg6MEuuJSiEIZkc4hzShqIFlQGbILw
|
||||
vOGfrcM2a4PBLT6u0fA8blklbFjVL7BUbIwykbftE9U1R6jMacrdubLB/4QRkD7Yxoa5DB
|
||||
91ayK4SguI+0eFw8wDJSTmhxC4NHuVg+I4gRNCwGGdgf+9zP4g0lpA3el0LVDm4rOP1Jc+
|
||||
2ZtTGA8f5vhfe98kZCIoWNIvc1e2DYkRvMMbvWMdCf65cfML9g+Ql7/xFMqcvLeyeSZVVN
|
||||
FgBfup6EhjDHfVZ93R71+g1Rmph/dmvmbYpYdMrvyIQDy6ZhWrkcoyKYREHEDUtDmIh5dn
|
||||
nyNBGXgBmnCG2sp1AAABAQD+ww8qrc66ivWwI7A935axxptpXRRl/cYW2p7PFbU59gMnbH
|
||||
Ddw3AwsKqNIElE2mJlxeNm1++sSVI0cQlBi54Cwzcs0J5ic8NpCUmhm7GSZt4B8Yz3PXNV
|
||||
UsXFsBpKeU977XBS1RW3JODh3AizjOLGFOLk5IQS8rhlNRlJ/MfN9gmngTPs+JVuupvONf
|
||||
1WwBB6Q/pNZakH9HyTi8eX4e9jzj97pZBZlzq7LF9UeALSS0rZWiwjjUzD5kk72c+JYQ+7
|
||||
lmnQR5xwk9A+TUzN834XK2MsZmdmzaigkhXIpyRy+7TnY7s/0Hr+myRH8jZWxFSRpvJyVA
|
||||
knoPQ4wJOtw17TAAABAQDRxlHbrHanwHO9jjBsEONYtHn73qA6+CzPoo0shCDTivgXFAa3
|
||||
31Z8iF6q7shXbwebsPly4vD15g8gD8OYHrETb/v9RgJHXMsil0z7UpdE66FMTeqH4Tmjq/
|
||||
qTySjqjHKBK3D1xt9kgz6sO2fztUpoIb9e5Jy8LIx04uGQV6KSeRbglnaFvBuv3RxtlEI8
|
||||
3/OyA9I01TfLopFjAgQBP/bNFLAhy+ocbjBwl2MZGx+fCSLQbWPi/DiFOam+a0l7vQTFTS
|
||||
1S68vyqMQ2c/xGqz96nzoWs3pox9K7fBWdqbR/EFn1QmZEa7qXDFfMQfLvQGXC/hA/Wrpa
|
||||
SwU0SMrbJgFbAAAAE2FkbWluaXN0cmF0b3JAVzJLMjUBAgMEBQYH
|
||||
-----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 ""
|
||||
Reference in New Issue
Block a user