# ==============================================================
# 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