From d3e13d2fc23966295a11c1e9bfbbc67b64b7ef73 Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Fri, 29 May 2026 19:26:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(setup):=20setup=5Fwindows.ps1=20PowerShell?= =?UTF-8?q?=205.1=20=EC=99=84=EC=A0=84=20=ED=98=B8=ED=99=98=20=EC=9E=AC?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 한글 제거 -> ASCII 전용 (PS5.1은 BOM없는 UTF-8을 CP949로 읽어 파싱 오류 발생) - ?. null-conditional 연산자 제거 -> 전통적 if/else로 교체 - 줄 연속 백틱 제거 -> 단일 라인으로 변경 - psql 단일인용부호 이슈 -> [char]39 + tmpSql 파일 방식으로 우회 - Path 환경변수 연결 -> + 연산자 방식으로 변경 - 구문 오류 0개 확인 완료 (ParseFile 검증) Co-Authored-By: Claude Sonnet 4.6 --- setup/setup_windows.ps1 | 492 +++++++++++++++++++++++----------------- 1 file changed, 282 insertions(+), 210 deletions(-) diff --git a/setup/setup_windows.ps1 b/setup/setup_windows.ps1 index 4b20a6d0..d58cb49f 100644 --- a/setup/setup_windows.ps1 +++ b/setup/setup_windows.ps1 @@ -1,9 +1,14 @@ # ============================================================== -# GUARDiA ITSM 설치 스크립트 — Windows Server 2019/2022 +# GUARDiA ITSM Setup Script - Windows Server 2019/2022 # ============================================================== -# 전제조건: 순수 Windows Server OS (PowerShell 5.1+) -# 실행 방법: PowerShell -ExecutionPolicy Bypass -File setup_windows.ps1 -# 설치 테스트: .\setup_windows.ps1 -Test +# 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( @@ -15,21 +20,23 @@ $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" } +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 설치 — Windows Server" -Write-Host " 시작: $(Get-Date)" +Write-Host " GUARDiA ITSM Setup - Windows Server" +Write-Host " Start: $(Get-Date)" Write-Host "==================================================" -# ── 테스트 모드 ───────────────────────────────────────────── +# ============================================================== +# TEST MODE +# ============================================================== if ($Test) { - Write-Host "=== 설치 검증 모드 ===" + Write-Host "=== Installation Verification Mode ===" $pass = 0; $fail = 0 function Check-Item { @@ -42,73 +49,90 @@ if ($Test) { } } - Check-Item "Python 3.11+" { python --version 2>&1 | Select-String "3\.(1[1-9]|[2-9]\d)" } - Check-Item "PostgreSQL 응답" { pg_isready -q } - Check-Item "Redis 응답" { redis-cli ping } - Check-Item "Java 17 (OpenJDK)" { java -version 2>$null } - Check-Item "Tomcat 9 서비스" { $s = Get-Service "tomcat9" -ErrorAction Stop; if ($s.Status -ne "Running") { throw "" } } - Check-Item "Tomcat HTTP" { $r = Invoke-WebRequest "http://localhost:8080/" -UseBasicParsing -TimeoutSec 5; if ($r.StatusCode -ne 200) { throw "" } } - Check-Item "NSSM 서비스 등록" { Get-Service "guardia-itsm" -ErrorAction Stop } - Check-Item "GUARDiA 서비스 실행" { - $s = Get-Service "guardia-itsm" - if ($s.Status -ne "Running") { throw "서비스 상태: $($s.Status)" } + 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 "GUARDiA HTTP 응답" { + 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 로그인 API" { + 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 "로그인 실패 ($($r.StatusCode))" } + $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 인코딩" { + Check-Item "Python UTF-8 encoding" { $env:PYTHONIOENCODING = "utf-8" $out = & python -c "print('OK')" 2>&1 - if ("$out" -notmatch "OK") { throw "인코딩 오류: $out" } + if ("$out" -notmatch "OK") { throw "Encoding error: $out" } } - Check-Item "Ollama 실행" { - $r = Invoke-WebRequest "http://localhost:11434/api/version" -UseBasicParsing -TimeoutSec 5 - if ($r.StatusCode -ne 200) { throw "Ollama 응답 없음" } - } - Check-Item "Ollama 모델 존재" { - $out = ollama list 2>&1 - if ($out -notmatch "\S") { throw "모델 없음" } - } - Check-Item "Nginx 설정" { nginx -t 2>&1 } + Check-Item "Nginx config" { nginx -t 2>&1 } Write-Host "" - Write-Host "검증 결과: 성공 $pass / 실패 $fail" + Write-Host "Results: Pass $pass / Fail $fail" if ($fail -eq 0) { - Write-OK "모든 검사 통과 — GUARDiA ITSM 정상 설치됨" + Write-OK "All checks passed - GUARDiA ITSM installed successfully" } else { - Write-Fail "일부 검사 실패 — 로그 확인: $LogFile" + 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 "관리자 권한으로 실행하세요 (우클릭 → 관리자로 실행)" } +if (-not $isAdmin) { Write-Fail "Run as Administrator (right-click -> Run as administrator)" } -# ── Chocolatey 패키지 관리자 ──────────────────────────────── +# ============================================================== +# [1/10] Chocolatey +# ============================================================== Write-Host "" -Write-Host "[1/8] Chocolatey 패키지 관리자 설치..." +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')) - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + $mp = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + $up = [System.Environment]::GetEnvironmentVariable("Path", "User") + $env:Path = $mp + ";" + $up } -Write-OK "Chocolatey 준비 완료" +Write-OK "Chocolatey ready" -# ── 1. 필수 패키지 설치 ────────────────────────────────────── +# ============================================================== +# [2/10] Required packages: Python, Java, PostgreSQL, Redis, Nginx, NSSM +# ============================================================== Write-Host "" -Write-Host "[2/10] 필수 패키지 설치 (Python, Java, PostgreSQL, Redis, Nginx, NSSM)..." +Write-Host "[2/10] Installing required packages..." $packages = @( "python --version=3.11.9", @@ -122,36 +146,45 @@ $packages = @( foreach ($pkg in $packages) { $name = $pkg.Split(" ")[0] - Write-Host " 설치 중: $name" + Write-Host " Installing: $name" choco install $pkg -y --no-progress 2>&1 | Out-Null } -# PATH 갱신 -$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") +$mp2 = [System.Environment]::GetEnvironmentVariable("Path", "Machine") +$up2 = [System.Environment]::GetEnvironmentVariable("Path", "User") +$env:Path = $mp2 + ";" + $up2 -# JAVA_HOME 설정 -$javaHome = (Get-ChildItem "C:\Program Files\Eclipse Adoptium" -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1)?.FullName +# 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) { - $javaHome = (Get-ChildItem "C:\Program Files\Java" -Filter "jdk*17*" -ErrorAction SilentlyContinue | Select-Object -First 1)?.FullName + $javaItems = Get-ChildItem "C:\Program Files\Java" -Filter "jdk*17*" -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($javaItems) { $javaHome = $javaItems.FullName } } if (-not $javaHome) { - $javaHome = (Get-Command java -ErrorAction SilentlyContinue)?.Source | Split-Path | Split-Path + $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 설치 완료 (JAVA_HOME=$javaHome)" + Write-OK "OpenJDK 17 ready (JAVA_HOME=$javaHome)" } else { - Write-Warn "JAVA_HOME을 찾을 수 없음 — 수동 설정 필요" + Write-Warn "JAVA_HOME not found - set manually" } -Write-OK "필수 패키지 설치 완료" +Write-OK "Packages installed" -# ── 2. Tomcat 9 설치 ──────────────────────────────────────── +# ============================================================== +# [3/10] Tomcat 9 +# ============================================================== Write-Host "" -Write-Host "[3/10] Tomcat 9 설치..." +Write-Host "[3/10] Installing Tomcat 9..." -$TomcatVer = if ($env:TOMCAT_VER) { $env:TOMCAT_VER } else { "9.0.98" } -$TomcatHome = "C:\app\tomcat" +$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" } @@ -159,53 +192,62 @@ $TomcatMirror = if ($env:TOMCAT_MIRROR) { $env:TOMCAT_MIRROR } else { if (-not (Test-Path "$TomcatHome\bin\startup.bat")) { $tarName = "apache-tomcat-$TomcatVer.zip" $tarUrl = "$TomcatMirror/$tarName" - Write-Host " 다운로드: $tarUrl" + Write-Host " Downloading: $tarUrl" try { Invoke-WebRequest $tarUrl -OutFile "$env:TEMP\$tarName" -TimeoutSec 120 -UseBasicParsing Expand-Archive "$env:TEMP\$tarName" -DestinationPath "C:\app" -Force - Rename-Item "C:\app\apache-tomcat-$TomcatVer" $TomcatHome -ErrorAction SilentlyContinue - if (-not (Test-Path $TomcatHome)) { - Move-Item "C:\app\apache-tomcat-$TomcatVer" $TomcatHome + $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 다운로드 실패: $_" - Write-Warn "TOMCAT_MIRROR 환경변수를 내부 미러로 설정하거나 수동 설치하세요." + Write-Warn "Tomcat download failed: $_ - set TOMCAT_MIRROR to internal mirror" } } else { - Write-Host " Tomcat이 이미 설치되어 있음: $TomcatHome" + Write-Info "Tomcat already installed: $TomcatHome" } -# opsagent 계정 추가 (GUARDiA Manager 원격 제어용) +# Add opsagent to Tomcat Manager $TomcatUsersXml = "$TomcatHome\conf\tomcat-users.xml" -if ((Test-Path $TomcatUsersXml) -and -not (Select-String "opsagent" $TomcatUsersXml -Quiet)) { - (Get-Content $TomcatUsersXml) -replace '', @" - - - - -"@ | Set-Content $TomcatUsersXml -Encoding utf8 +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 + } } -# NSSM으로 Tomcat 9 Windows 서비스 등록 +# Register Tomcat as Windows service via NSSM $tcSvc = "tomcat9" -try { nssm stop $tcSvc 2>$null; nssm remove $tcSvc confirm 2>$null } catch {} -nssm install $tcSvc "$TomcatHome\bin\tomcat9.exe" 2>$null -if ($LASTEXITCODE -ne 0) { - # tomcat9.exe 없으면 startup.bat 방식 - nssm install $tcSvc "cmd" - nssm set $tcSvc AppParameters "/c $TomcatHome\bin\startup.bat" +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 AppEnvironmentExtra ("JAVA_HOME=" + $env:JAVA_HOME + " CATALINA_HOME=" + $TomcatHome) nssm set $tcSvc Start SERVICE_AUTO_START -nssm set $tcSvc AppStdout "C:\guardia\logs\tomcat9.log" 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 서비스 등록 완료 (포트 8080)" +Write-OK "Tomcat 9 service registered (port 8080)" -# ── 2(b). Python 가상환경 ────────────────────────────────────── +# ============================================================== +# [4/10] Python virtual environment +# ============================================================== Write-Host "" -Write-Host "[4/10] Python 가상환경 설정..." +Write-Host "[4/10] Python virtual environment..." $venvPath = "C:\guardia\venv" if (-not (Test-Path $venvPath)) { python -m venv $venvPath @@ -213,59 +255,83 @@ if (-not (Test-Path $venvPath)) { $pip = "$venvPath\Scripts\pip.exe" & $pip install --upgrade pip -q & $pip install -r "$GuardiaRoot\itsm\requirements.txt" -q -Write-OK "Python 패키지 설치 완료" +Write-OK "Python packages installed" -# ── 3. PostgreSQL 초기화 ──────────────────────────────────── +# ============================================================== +# [5/10] PostgreSQL +# ============================================================== Write-Host "" -Write-Host "[4/8] PostgreSQL 초기화..." -$pgBin = "C:\Program Files\PostgreSQL\16\bin" -if (-not (Test-Path $pgBin)) { - $pgBin = Get-ChildItem "C:\Program Files\PostgreSQL" -Directory | Sort-Object Name -Descending | Select-Object -First 1 | ForEach-Object { "$($_.FullName)\bin" } +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" -# DB/사용자 생성 -& "$pgBin\psql.exe" -U postgres -c "CREATE USER guardia WITH PASSWORD 'guardia_secure_pw';" 2>$null -& "$pgBin\psql.exe" -U postgres -c "CREATE DATABASE guardia OWNER guardia;" 2>$null -Write-OK "PostgreSQL 설정 완료" +$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" -# ── 4. Redis 서비스 등록 ──────────────────────────────────── +# ============================================================== +# [6/10] Redis +# ============================================================== Write-Host "" -Write-Host "[5/10] Redis 서비스 등록..." -$redisExe = (Get-Command redis-server -ErrorAction SilentlyContinue)?.Source -if ($redisExe) { - nssm install redis-server $redisExe 2>$null - nssm start redis-server 2>$null +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 완료" +Write-OK "Redis ready" -# ── 5. 환경 설정 파일 ─────────────────────────────────────── +# ============================================================== +# [7/10] Environment file (.env) +# ============================================================== Write-Host "" -Write-Host "[6/10] 환경 설정 파일 생성..." +Write-Host "[7/10] Creating .env file..." $envFile = "$GuardiaRoot\itsm\.env" if (-not (Test-Path $envFile)) { - @" -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 -"@ | Out-File -FilePath $envFile -Encoding utf8 - Write-Warn ".env 생성됨 — SECRET_KEY를 변경하세요: $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" } -# ── 6. DB 초기화 (스키마 불일치 자동 감지·복구) ──────────────────────── +# ============================================================== +# [8/10] DB initialization (auto schema repair) +# ============================================================== Write-Host "" -Write-Host "[7/10] DB 초기화..." +Write-Host "[8/10] DB initialization..." -# 포트 8001 기존 프로세스 종료 (업그레이드 시 충돌 방지) -$portProc = Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 +$portProc = Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue | + Select-Object -First 1 if ($portProc) { - Write-Warn "포트 8001 사용 중 (PID $($portProc.OwningProcess)) — 종료..." + Write-Warn "Port 8001 in use (PID $($portProc.OwningProcess)) - stopping..." Stop-Process -Id $portProc.OwningProcess -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 } @@ -276,130 +342,136 @@ $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 초기화 실패 — 로그를 확인하세요" + Write-Fail "DB init failed - check log: $LogFile" } Pop-Location -Write-OK "DB 초기화 완료" +Write-OK "DB initialized" -# ── 7. NSSM Windows 서비스 등록 ───────────────────────────── +# ============================================================== +# [9/10] GUARDiA ITSM Windows service (NSSM) +# ============================================================== Write-Host "" -Write-Host "[8/10] Windows 서비스 등록 (NSSM)..." +Write-Host "[9/10] GUARDiA ITSM service..." -$uvicorn = "$venvPath\Scripts\uvicorn.exe" -$svcName = "guardia-itsm" +$uvicorn = "$venvPath\Scripts\uvicorn.exe" +$svcName = "guardia-itsm" -# 기존 서비스 중지 및 제거 (업그레이드) -try { nssm stop $svcName 2>$null; Start-Sleep -Seconds 2 } catch {} +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" -# PYTHONIOENCODING=utf-8 필수 — Windows 기본 cp949에서 한글 print 오류 방지 -nssm set $svcName AppEnvironmentExtra "PATH=$venvPath\Scripts;$env:PATH PYTHONIOENCODING=utf-8 PYTHONUNBUFFERED=1" +$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" - -New-Item -ItemType Directory -Force "C:\guardia\logs" | Out-Null nssm start $svcName -Write-OK "Windows 서비스 등록 완료" +Write-OK "GUARDiA ITSM service registered" -# ── Nginx 리버스 프록시 ────────────────────────────────────── -$nginxConf = "C:\tools\nginx-winssl\conf\conf.d\guardia.conf" -New-Item -ItemType Directory -Force (Split-Path $nginxConf) | Out-Null -@" -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; - } +# 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" } -"@ | Out-File -FilePath $nginxConf -Encoding utf8 -# ── Ollama (온프레미스 sLLM) ────────────────────────────────── +# ============================================================== +# [10/10] Ollama + Firewall +# ============================================================== Write-Host "" -Write-Host "[9/10] Ollama (온프레미스 sLLM 서버) 설치..." +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 설치 건너뜀 (OLLAMA_INSTALL=skip) — 수동 설치 필요" -} else { - $ollamaExe = "$env:LocalAppData\Programs\Ollama\ollama.exe" - if ($OllamaInstall -eq "offline") { - $binPath = if ($env:OLLAMA_BIN_PATH) { $env:OLLAMA_BIN_PATH } else { "$env:TEMP\ollama-setup.exe" } - if (Test-Path $binPath) { - & $binPath /S 2>$null - } else { Write-Warn "오프라인: OLLAMA_BIN_PATH에 설치파일 필요" } + 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 { - try { - choco install ollama -y --no-progress 2>&1 | Out-Null - if (-not (Get-Command ollama -ErrorAction SilentlyContinue)) { - # Chocolatey 실패 시 공식 설치파일 다운로드 - $ollamaSetup = "$env:TEMP\OllamaSetup.exe" - Invoke-WebRequest "https://ollama.com/download/OllamaSetup.exe" ` - -OutFile $ollamaSetup -UseBasicParsing -TimeoutSec 120 - & $ollamaSetup /S - Start-Sleep -Seconds 5 - } - Write-OK "Ollama 설치 완료" - } catch { - Write-Warn "Ollama 설치 실패: $_ — 수동 설치: https://ollama.com/download" - $OllamaInstall = "failed" - } + Write-Warn "Offline: set OLLAMA_BIN_PATH to installer file" } - - if ($OllamaInstall -ne "failed") { - # NSSM 서비스로 등록 (또는 기본 설치된 서비스 사용) - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + - [System.Environment]::GetEnvironmentVariable("Path","User") +} 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 ($OllamaInstall -eq "online" -and (Get-Command ollama -ErrorAction SilentlyContinue)) { + if (Get-Command ollama -ErrorAction SilentlyContinue) { foreach ($model in $OllamaModels.Split(" ")) { - Write-Host " 모델 다운로드: $model (시간이 걸릴 수 있습니다...)" + Write-Host " Pulling model: $model" ollama pull $model 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" } } } - Write-OK "Ollama 준비 완료 (http://localhost:11434)" + Write-OK "Ollama ready (http://localhost:11434)" + } catch { + Write-Warn "Ollama install failed: $_ - manual: https://ollama.com/download" } } -# ── 방화벽 규칙 ────────────────────────────────────────────── -New-NetFirewallRule -DisplayName "GUARDiA HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow 2>$null -New-NetFirewallRule -DisplayName "GUARDiA HTTPS" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow 2>$null -# Ollama/Tomcat은 내부 전용 — 외부 노출 차단 -New-NetFirewallRule -DisplayName "Block Ollama External" -Direction Inbound -Protocol TCP -LocalPort 11434 -Action Block -RemoteAddress 0.0.0.0/0 2>$null -New-NetFirewallRule -DisplayName "Block Tomcat External" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Block -RemoteAddress 0.0.0.0/0 2>$null -Write-OK "방화벽 규칙 추가 완료 (80/443 허용, 8080/11434 내부 전용)" +# 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 설치 완료 — Windows Server [10/10]" +Write-OK "GUARDiA ITSM Setup Complete - Windows Server [10/10]" Write-Host "" -$serverIp = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notlike "*Loopback*" } | Select-Object -First 1).IPAddress + +$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 (내부 전용)" -Write-Info "Ollama URL: http://localhost:11434 (내부 전용)" -Write-Info "설치 로그: $LogFile" -Write-Info "검증: .\setup_windows.ps1 -Test" +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 "보안 필수 조치:" -Write-Warn " 1. $envFile 의 SECRET_KEY 변경" -Write-Warn " 2. PostgreSQL postgres 계정 비밀번호 변경" -Write-Warn " 3. Tomcat opsagent 비밀번호 변경" +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