# ============================================================== # GUARDiA ITSM Setup Script - Windows Server 2019/2022 # ============================================================== # Requirements: Windows Server OS, PowerShell 5.1+ # Usage: PowerShell -ExecutionPolicy Bypass -File setup_windows.ps1 # Test : .\setup_windows.ps1 -Test # Env vars (offline): # TOMCAT_VER = 9.0.98 (default) # TOMCAT_MIRROR = http://internal-mirror/... # OLLAMA_INSTALL = online|offline|skip # OLLAMA_BIN_PATH = path to OllamaSetup.exe # ============================================================== param( [switch]$Test = $false ) $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $GuardiaRoot = Split-Path -Parent $ScriptDir $LogFile = "C:\guardia_install.log" function Write-OK { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green } function Write-Warn { param($msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow } function Write-Fail { param($msg) Write-Host "[FAIL] $msg" -ForegroundColor Red; exit 1 } function Write-Info { param($msg) Write-Host " $msg" } Start-Transcript -Path $LogFile -Append | Out-Null Write-Host "==================================================" Write-Host " GUARDiA ITSM Setup - Windows Server" Write-Host " Start: $(Get-Date)" Write-Host "==================================================" # ============================================================== # TEST MODE # ============================================================== if ($Test) { Write-Host "=== Installation Verification Mode ===" $pass = 0; $fail = 0 function Check-Item { param($desc, [scriptblock]$cmd) try { & $cmd | Out-Null Write-OK $desc; $script:pass++ } catch { Write-Host "[FAIL] $desc" -ForegroundColor Red; $script:fail++ } } Check-Item "Python 3.11+" { $v = python --version 2>&1 if ($v -notmatch "3\.(1[1-9]|[2-9]\d)") { throw "Version mismatch: $v" } } Check-Item "Java 17 (OpenJDK)" { $v = java -version 2>&1 if ("$v" -notmatch "17|18|19|20|21") { throw "Java 17+ required" } } Check-Item "PostgreSQL ready" { pg_isready -q } Check-Item "Redis ping" { redis-cli ping } Check-Item "Tomcat 9 service" { $s = Get-Service "tomcat9" -ErrorAction Stop if ($s.Status -ne "Running") { throw "Status: $($s.Status)" } } Check-Item "Tomcat HTTP" { $r = Invoke-WebRequest "http://localhost:8080/" -UseBasicParsing -TimeoutSec 5 if ($r.StatusCode -ne 200) { throw "HTTP $($r.StatusCode)" } } Check-Item "Ollama API" { $r = Invoke-WebRequest "http://localhost:11434/api/version" -UseBasicParsing -TimeoutSec 5 if ($r.StatusCode -ne 200) { throw "Ollama not responding" } } Check-Item "Ollama model exists" { $out = ollama list 2>&1 if ("$out" -notmatch "\S") { throw "No models found" } } Check-Item "guardia-itsm service" { Get-Service "guardia-itsm" -ErrorAction Stop } Check-Item "GUARDiA HTTP" { $r = Invoke-WebRequest "http://localhost:8001/" -UseBasicParsing -TimeoutSec 10 if ($r.StatusCode -ne 200) { throw "HTTP $($r.StatusCode)" } } Check-Item "GUARDiA login API" { $body = '{"username":"admin","password":"1111"}' $r = Invoke-WebRequest "http://localhost:8001/api/auth/login" ` -Method POST -ContentType "application/json" -Body $body ` -UseBasicParsing -TimeoutSec 10 if ($r.StatusCode -ne 200) { throw "Login failed ($($r.StatusCode))" } } Check-Item "Python UTF-8 encoding" { $env:PYTHONIOENCODING = "utf-8" $out = & python -c "print('OK')" 2>&1 if ("$out" -notmatch "OK") { throw "Encoding error: $out" } } Check-Item "Nginx config" { nginx -t 2>&1 } Write-Host "" Write-Host "Results: Pass $pass / Fail $fail" if ($fail -eq 0) { Write-OK "All checks passed - GUARDiA ITSM installed successfully" } else { Write-Fail "Some checks failed - see log: $LogFile" } Stop-Transcript | Out-Null exit 0 } # ============================================================== # Admin check # ============================================================== $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ) if (-not $isAdmin) { Write-Fail "Run as Administrator (right-click -> Run as administrator)" } # ============================================================== # [1/10] Chocolatey # ============================================================== Write-Host "" Write-Host "[1/10] Chocolatey package manager..." if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) $mp = [System.Environment]::GetEnvironmentVariable("Path", "Machine") $up = [System.Environment]::GetEnvironmentVariable("Path", "User") $env:Path = $mp + ";" + $up } Write-OK "Chocolatey ready" # ============================================================== # [2/10] Required packages: Python, Java, PostgreSQL, Redis, Nginx, NSSM # ============================================================== Write-Host "" Write-Host "[2/10] Installing required packages..." $packages = @( "python --version=3.11.9", "openjdk --version=17.0.11", "postgresql", "redis-64", "nginx-winssl", "nssm", "git" ) foreach ($pkg in $packages) { $name = $pkg.Split(" ")[0] Write-Host " Installing: $name" choco install $pkg -y --no-progress 2>&1 | Out-Null } $mp2 = [System.Environment]::GetEnvironmentVariable("Path", "Machine") $up2 = [System.Environment]::GetEnvironmentVariable("Path", "User") $env:Path = $mp2 + ";" + $up2 # JAVA_HOME (PowerShell 5.1 compatible - no ?. operator) $javaHome = "" $adoptiumItems = Get-ChildItem "C:\Program Files\Eclipse Adoptium" -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 if ($adoptiumItems) { $javaHome = $adoptiumItems.FullName } if (-not $javaHome) { $javaItems = Get-ChildItem "C:\Program Files\Java" -Filter "jdk*17*" -ErrorAction SilentlyContinue | Select-Object -First 1 if ($javaItems) { $javaHome = $javaItems.FullName } } if (-not $javaHome) { $javaCmd = Get-Command java -ErrorAction SilentlyContinue if ($javaCmd) { $javaHome = Split-Path (Split-Path $javaCmd.Source) } } if ($javaHome) { [System.Environment]::SetEnvironmentVariable("JAVA_HOME", $javaHome, "Machine") $env:JAVA_HOME = $javaHome Write-OK "OpenJDK 17 ready (JAVA_HOME=$javaHome)" } else { Write-Warn "JAVA_HOME not found - set manually" } Write-OK "Packages installed" # ============================================================== # [3/10] Tomcat 9 # ============================================================== Write-Host "" Write-Host "[3/10] Installing Tomcat 9..." $TomcatVer = if ($env:TOMCAT_VER) { $env:TOMCAT_VER } else { "9.0.98" } $TomcatHome = "C:\app\tomcat" $TomcatMirror = if ($env:TOMCAT_MIRROR) { $env:TOMCAT_MIRROR } else { "https://archive.apache.org/dist/tomcat/tomcat-9/v$TomcatVer/bin" } if (-not (Test-Path "$TomcatHome\bin\startup.bat")) { $tarName = "apache-tomcat-$TomcatVer.zip" $tarUrl = "$TomcatMirror/$tarName" Write-Host " Downloading: $tarUrl" try { Invoke-WebRequest $tarUrl -OutFile "$env:TEMP\$tarName" -TimeoutSec 120 -UseBasicParsing Expand-Archive "$env:TEMP\$tarName" -DestinationPath "C:\app" -Force $extractedDir = "C:\app\apache-tomcat-$TomcatVer" if (Test-Path $extractedDir) { if (Test-Path $TomcatHome) { Remove-Item $TomcatHome -Recurse -Force } Rename-Item $extractedDir $TomcatHome } } catch { Write-Warn "Tomcat download failed: $_ - set TOMCAT_MIRROR to internal mirror" } } else { Write-Info "Tomcat already installed: $TomcatHome" } # Add opsagent to Tomcat Manager $TomcatUsersXml = "$TomcatHome\conf\tomcat-users.xml" if (Test-Path $TomcatUsersXml) { $content = Get-Content $TomcatUsersXml -Raw if ($content -notmatch "opsagent") { $addEntry = ' ' + "`r`n" + ' ' + "`r`n" + ' ' + "`r`n" + '' $newContent = $content -replace '', $addEntry Set-Content $TomcatUsersXml -Value $newContent -Encoding UTF8 } } # Register Tomcat as Windows service via NSSM $tcSvc = "tomcat9" try { nssm stop $tcSvc 2>$null } catch {} try { nssm remove $tcSvc confirm 2>$null } catch {} $tcExe = "$TomcatHome\bin\tomcat9.exe" if (Test-Path $tcExe) { nssm install $tcSvc $tcExe } else { nssm install $tcSvc "cmd.exe" $batPath = "$TomcatHome\bin\startup.bat" nssm set $tcSvc AppParameters ("/c `"$batPath`"") } nssm set $tcSvc AppDirectory $TomcatHome nssm set $tcSvc AppEnvironmentExtra ("JAVA_HOME=" + $env:JAVA_HOME + " CATALINA_HOME=" + $TomcatHome) nssm set $tcSvc Start SERVICE_AUTO_START New-Item -ItemType Directory -Force "C:\guardia\logs" | Out-Null nssm set $tcSvc AppStdout "C:\guardia\logs\tomcat9.log" nssm start $tcSvc Write-OK "Tomcat 9 service registered (port 8080)" # ============================================================== # [4/10] Python virtual environment # ============================================================== Write-Host "" Write-Host "[4/10] Python virtual environment..." $venvPath = "C:\guardia\venv" if (-not (Test-Path $venvPath)) { python -m venv $venvPath } $pip = "$venvPath\Scripts\pip.exe" & $pip install --upgrade pip -q & $pip install -r "$GuardiaRoot\itsm\requirements.txt" -q Write-OK "Python packages installed" # ============================================================== # [5/10] PostgreSQL # ============================================================== Write-Host "" Write-Host "[5/10] PostgreSQL setup..." $pgBin = "" $pgVersionDirs = Get-ChildItem "C:\Program Files\PostgreSQL" -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 if ($pgVersionDirs) { $pgBin = $pgVersionDirs.FullName + "\bin" } if (-not $pgBin -or -not (Test-Path $pgBin)) { $pgBin = "C:\Program Files\PostgreSQL\16\bin" } $env:PGPASSWORD = "postgres" $psql = "$pgBin\psql.exe" # Write SQL to temp file to avoid quoting issues $tmpSql = "$env:TEMP\guardia_pg_setup.sql" $q = [char]39 # single quote $sqlLines = "CREATE USER guardia WITH PASSWORD " + $q + "guardia_secure_pw" + $q + ";" + "`n" $sqlLines += "CREATE DATABASE guardia OWNER guardia;" + "`n" Set-Content $tmpSql -Value $sqlLines -Encoding ASCII try { & $psql -U postgres -f $tmpSql 2>$null } catch {} Remove-Item $tmpSql -ErrorAction SilentlyContinue Write-OK "PostgreSQL setup complete" # ============================================================== # [6/10] Redis # ============================================================== Write-Host "" Write-Host "[6/10] Redis service..." $redisCmd = Get-Command redis-server -ErrorAction SilentlyContinue if ($redisCmd) { $redisExe = $redisCmd.Source try { nssm stop redis-server 2>$null } catch {} try { nssm remove redis-server confirm 2>$null } catch {} nssm install redis-server $redisExe nssm start redis-server } Write-OK "Redis ready" # ============================================================== # [7/10] Environment file (.env) # ============================================================== Write-Host "" Write-Host "[7/10] Creating .env file..." $envFile = "$GuardiaRoot\itsm\.env" if (-not (Test-Path $envFile)) { $envLines = @( "DATABASE_URL=postgresql+asyncpg://guardia:guardia_secure_pw@localhost:5432/guardia", "SECRET_KEY=change_this_secret_key_in_production_min_32chars", "ALGORITHM=HS256", "ACCESS_TOKEN_EXPIRE_MINUTES=480", "REDIS_URL=redis://localhost:6379/0", "OLLAMA_BASE_URL=http://localhost:11434", "GUARDIA_LLM_MODEL=llama3.1:8b", "MESSENGER_BASE_URL=http://localhost:8002", "MESSENGER_OPS_ROOM=ops", "CATALINA_HOME=C:\app\tomcat" ) Set-Content $envFile -Value ($envLines -join "`r`n") -Encoding UTF8 Write-Warn ".env created - change SECRET_KEY before production use: $envFile" } # ============================================================== # [8/10] DB initialization (auto schema repair) # ============================================================== Write-Host "" Write-Host "[8/10] DB initialization..." $portProc = Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if ($portProc) { Write-Warn "Port 8001 in use (PID $($portProc.OwningProcess)) - stopping..." Stop-Process -Id $portProc.OwningProcess -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 } Push-Location "$GuardiaRoot\itsm" $env:PYTHONIOENCODING = "utf-8" $env:PYTHONUNBUFFERED = "1" $dbResult = & "$venvPath\Scripts\python.exe" tools\db_init.py --force 2>&1 $dbResult | ForEach-Object { Write-Host " $_" } if ($LASTEXITCODE -ne 0) { Write-Fail "DB init failed - check log: $LogFile" } Pop-Location Write-OK "DB initialized" # ============================================================== # [9/10] GUARDiA ITSM Windows service (NSSM) # ============================================================== Write-Host "" Write-Host "[9/10] GUARDiA ITSM service..." $uvicorn = "$venvPath\Scripts\uvicorn.exe" $svcName = "guardia-itsm" try { nssm stop $svcName 2>$null; Start-Sleep -Seconds 2 } catch {} try { nssm remove $svcName confirm 2>$null } catch {} nssm install $svcName $uvicorn nssm set $svcName AppParameters "main:app --host 0.0.0.0 --port 8001 --workers 4" nssm set $svcName AppDirectory "$GuardiaRoot\itsm" $extraEnv = "PATH=" + $venvPath + "\Scripts;" + $env:PATH + " PYTHONIOENCODING=utf-8 PYTHONUNBUFFERED=1" nssm set $svcName AppEnvironmentExtra $extraEnv nssm set $svcName Start SERVICE_AUTO_START nssm set $svcName AppStdout "C:\guardia\logs\guardia-itsm.log" nssm set $svcName AppStderr "C:\guardia\logs\guardia-itsm-err.log" nssm start $svcName Write-OK "GUARDiA ITSM service registered" # Nginx reverse proxy $nginxBase = "C:\tools\nginx-winssl" if (-not (Test-Path $nginxBase)) { $nginxCmd = Get-Command nginx -ErrorAction SilentlyContinue | Select-Object -First 1 if ($nginxCmd) { $nginxBase = Split-Path $nginxCmd.Source } } if ($nginxBase) { $nginxConf = "$nginxBase\conf\conf.d\guardia.conf" New-Item -ItemType Directory -Force (Split-Path $nginxConf) | Out-Null $ngxLines = @( "server {", " listen 80;", " server_name _;", " client_max_body_size 100M;", " location / {", " proxy_pass http://127.0.0.1:8001;", " proxy_http_version 1.1;", ' proxy_set_header Upgrade $http_upgrade;', ' proxy_set_header Connection "upgrade";', ' proxy_set_header Host $host;', ' proxy_set_header X-Real-IP $remote_addr;', " proxy_read_timeout 300s;", " }", "}" ) Set-Content $nginxConf -Value ($ngxLines -join "`r`n") -Encoding UTF8 Write-OK "Nginx configured" } # ============================================================== # [10/10] Ollama + Firewall # ============================================================== Write-Host "" Write-Host "[10/10] Ollama + Firewall..." $OllamaInstall = if ($env:OLLAMA_INSTALL) { $env:OLLAMA_INSTALL } else { "online" } $OllamaModels = if ($env:OLLAMA_MODELS) { $env:OLLAMA_MODELS } else { "llama3.1:8b" } if ($OllamaInstall -eq "skip") { Write-Warn "Ollama skipped (OLLAMA_INSTALL=skip)" } elseif ($OllamaInstall -eq "offline") { $binPath = if ($env:OLLAMA_BIN_PATH) { $env:OLLAMA_BIN_PATH } else { "$env:TEMP\OllamaSetup.exe" } if (Test-Path $binPath) { & $binPath /S 2>$null Write-OK "Ollama offline install complete" } else { Write-Warn "Offline: set OLLAMA_BIN_PATH to installer file" } } else { try { choco install ollama -y --no-progress 2>&1 | Out-Null $mp3 = [System.Environment]::GetEnvironmentVariable("Path", "Machine") $up3 = [System.Environment]::GetEnvironmentVariable("Path", "User") $env:Path = $mp3 + ";" + $up3 Start-Sleep -Seconds 3 if (Get-Command ollama -ErrorAction SilentlyContinue) { foreach ($model in $OllamaModels.Split(" ")) { Write-Host " Pulling model: $model" ollama pull $model 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" } } } Write-OK "Ollama ready (http://localhost:11434)" } catch { Write-Warn "Ollama install failed: $_ - manual: https://ollama.com/download" } } # Firewall rules try { New-NetFirewallRule -DisplayName "GUARDiA HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow -ErrorAction SilentlyContinue | Out-Null New-NetFirewallRule -DisplayName "GUARDiA HTTPS" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow -ErrorAction SilentlyContinue | Out-Null New-NetFirewallRule -DisplayName "Block Ollama" -Direction Inbound -Protocol TCP -LocalPort 11434 -Action Block -ErrorAction SilentlyContinue | Out-Null New-NetFirewallRule -DisplayName "Block Tomcat" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Block -ErrorAction SilentlyContinue | Out-Null Write-OK "Firewall rules applied" } catch { Write-Warn "Firewall setup failed: $_" } # ============================================================== # Summary # ============================================================== Write-Host "" Write-Host "==================================================" Write-OK "GUARDiA ITSM Setup Complete - Windows Server [10/10]" Write-Host "" $serverIp = "" $ipAddrs = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.InterfaceAlias -notlike "*Loopback*" -and $_.PrefixOrigin -ne "WellKnown" } | Select-Object -First 1 if ($ipAddrs) { $serverIp = $ipAddrs.IPAddress } Write-Info "GUARDiA URL: http://$serverIp" Write-Info "Tomcat URL: http://${serverIp}:8080 (internal only)" Write-Info "Ollama URL: http://localhost:11434 (internal only)" Write-Info "Log: $LogFile" Write-Info "Verify: .\setup_windows.ps1 -Test" Write-Host "" Write-Warn "Security actions required:" Write-Warn " 1. Change SECRET_KEY in $envFile" Write-Warn " 2. Change PostgreSQL postgres password" Write-Warn " 3. Change Tomcat opsagent password" Write-Host "==================================================" Stop-Transcript | Out-Null