From fb00b0a5fd9acd1beb6e785a04c462d1e9c61630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Fri, 12 Sep 2025 23:57:52 +0900 Subject: [PATCH] Add multi-host Proxmox support with SSL certificate handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added support for multiple Proxmox hosts (pve7.0bin.in:443, Healthport PVE:8006) - Enhanced VM management APIs to accept host parameter - Fixed WebSocket URL generation bug (dynamic port handling) - Added comprehensive SSL certificate trust help system - Implemented host selection dropdown in UI - Added VNC connection failure detection and automatic SSL help redirection - Updated session management to store host_key information - Enhanced error handling for different Proxmox configurations πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../Stable_PVE_Authentication_Strategy.md | 190 ++++++++++++ ..._Implementation_Technical_Documentation.md | 249 +++++++++++++++ farmq-admin/VNC_Network_Architecture_Issue.md | 161 ++++++++++ ...C_WebSocket_Connection_Issue_Resolution.md | 207 +++++++++++++ farmq-admin/app.py | 227 ++++++++++++-- farmq-admin/flask-venv/bin/websockets | 8 + farmq-admin/templates/vm_list.html | 44 ++- farmq-admin/templates/vnc_proxy.html | 242 +++++++++++++++ farmq-admin/templates/vnc_simple.html | 11 +- farmq-admin/templates/vnc_ssl_help.html | 291 ++++++++++++++++++ farmq-admin/test_multiple_proxmox.py | 103 +++++++ farmq-admin/test_vnc_websocket.py | 141 +++++++++ farmq-admin/utils/proxmox_client.py | 19 +- farmq-admin/utils/vnc_proxy.py | 168 ++++++++++ 14 files changed, 2029 insertions(+), 32 deletions(-) create mode 100644 farmq-admin/Stable_PVE_Authentication_Strategy.md create mode 100644 farmq-admin/VNC_Implementation_Technical_Documentation.md create mode 100644 farmq-admin/VNC_Network_Architecture_Issue.md create mode 100644 farmq-admin/VNC_WebSocket_Connection_Issue_Resolution.md create mode 100755 farmq-admin/flask-venv/bin/websockets create mode 100644 farmq-admin/templates/vnc_proxy.html create mode 100644 farmq-admin/templates/vnc_ssl_help.html create mode 100644 farmq-admin/test_multiple_proxmox.py create mode 100644 farmq-admin/test_vnc_websocket.py create mode 100644 farmq-admin/utils/vnc_proxy.py diff --git a/farmq-admin/Stable_PVE_Authentication_Strategy.md b/farmq-admin/Stable_PVE_Authentication_Strategy.md new file mode 100644 index 0000000..517600c --- /dev/null +++ b/farmq-admin/Stable_PVE_Authentication_Strategy.md @@ -0,0 +1,190 @@ +# μ•ˆμ •μ μΈ PVE 인증 정보 전달 μ „λž΅ +## λΈŒλΌμš°μ € WebSocket VNC μ—°κ²° κ°œμ„  λ°©μ•ˆ + +### 🚨 ν˜„μž¬ 문제 상황 + +**간헐적 μ—°κ²° μ‹€νŒ¨ 원인 뢄석:** +- λΈŒλΌμš°μ €μ—μ„œ PVE 인증 μΏ ν‚€κ°€ WebSocket μ—°κ²° μ‹œ λΆˆμ•ˆμ •ν•˜κ²Œ 전달됨 +- NPM λ¦¬λ²„μŠ€ ν”„λ‘μ‹œμ—μ„œ 인증 헀더 전달 뢈일치 +- Proxmox μ„Έμ…˜ 만료 및 λΈŒλΌμš°μ € μΏ ν‚€ μƒνƒœ λ³€ν™” +- CORS/λ³΄μ•ˆ μ •μ±…μœΌλ‘œ μΈν•œ μΏ ν‚€ 차단 + +**Claude Code ν™˜κ²½ 성곡 μš”μΈ:** +```python +headers = {'Cookie': f'PVEAuthCookie={client.ticket}'} +async with websockets.connect(websocket_url, ssl=ssl_context, additional_headers=headers) +``` +β†’ **λͺ…μ‹œμ  인증 헀더 μ „λ‹¬λ‘œ 100% μ•ˆμ •μ  μ—°κ²°** + +### 🎯 μ•ˆμ •μ μΈ 인증 μ „λž΅ μ˜΅μ…˜ + +#### 방법 1: Flask λ°±μ—”λ“œ 인증 ν”„λ‘μ‹œ (ꢌμž₯ ⭐⭐⭐⭐⭐) + +**κ°œλ…:** +- Flaskκ°€ Proxmox 인증을 λŒ€μ‹  처리 +- λΈŒλΌμš°μ €λŠ” Flask μ„Έμ…˜λ§Œ 관리 +- Flaskκ°€ WebSocket을 Proxmox둜 ν”„λ‘μ‹œ + +**μž₯점:** +- βœ… λΈŒλΌμš°μ € λ³΄μ•ˆ μ •μ±… 우회 +- βœ… 인증 μƒνƒœ 쀑앙 관리 +- βœ… μ„Έμ…˜ 만료 μžλ™ κ°±μ‹  κ°€λŠ₯ +- βœ… NPM μ„€μ • λ³€κ²½ λΆˆν•„μš” + +**κ΅¬ν˜„ ꡬ쑰:** +``` +λΈŒλΌμš°μ € WebSocket β†’ Flask WebSocket ν”„λ‘μ‹œ β†’ Proxmox VNC WebSocket + ↑ (PVE 인증 헀더 μžλ™ μΆ”κ°€) +``` + +#### 방법 2: Flask APIλ₯Ό ν†΅ν•œ 인증 토큰 전달 (보톡 ⭐⭐⭐) + +**κ°œλ…:** +- Flask API둜 PVE 인증 정보 쑰회 +- JavaScriptμ—μ„œ λ°›μ•„μ„œ WebSocket μ—°κ²° μ‹œ μ‚¬μš© + +**μž₯점:** +- βœ… κΈ°μ‘΄ μ½”λ“œ μˆ˜μ • μ΅œμ†Œν™” +- βœ… λͺ…μ‹œμ  인증 μ œμ–΄ + +**단점:** +- ❌ λΈŒλΌμš°μ € JavaScript에 인증 정보 λ…ΈμΆœ +- ❌ noVNC 라이브러리 μ œμ•½ (μ»€μŠ€ν…€ 헀더 지원 μ œν•œ) + +#### 방법 3: NPM μ„€μ • κ°œμ„  (λ³΅μž‘ν•¨ ⭐⭐) + +**κ°œλ…:** +- Nginx μ„€μ •μœΌλ‘œ WebSocket 인증 헀더 μžλ™ μΆ”κ°€ + +**단점:** +- ❌ NPM μ„€μ • λ³΅μž‘λ„ 증가 +- ❌ μ—¬μ „νžˆ λΈŒλΌμš°μ € λ³΄μ•ˆ μ •μ±… μ œμ•½ +- ❌ 디버깅 어렀움 + +### πŸ† ꢌμž₯ μ†”λ£¨μ…˜: Flask WebSocket ν”„λ‘μ‹œ + +#### μ•„ν‚€ν…μ²˜ 섀계 +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” WebSocket β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” WebSocket+Auth β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ λΈŒλΌμš°μ € β”‚ ────────────────→ β”‚ Flask β”‚ ───────────────────→ β”‚ Proxmox VE β”‚ +β”‚ (noVNC) β”‚ β”‚ Proxy β”‚ β”‚ VNC Server β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + ↑ + μžλ™ PVE 인증 + 헀더 μΆ”κ°€ 처리 +``` + +#### 핡심 μ»΄ν¬λ„ŒνŠΈ + +**1. Flask WebSocket ν”„λ‘μ‹œ μ—”λ“œν¬μΈνŠΈ** +```python +# μƒˆλ‘œμš΄ μ—”λ“œν¬μΈνŠΈ: /vnc//proxy +@app.websocket('/vnc//proxy') +async def vnc_websocket_proxy(vm_id): + # 1. Flask μ„Έμ…˜ 검증 + # 2. Proxmox 인증 정보 μ€€λΉ„ + # 3. Proxmox VNC WebSocket μ—°κ²° (PVE μΏ ν‚€ 포함) + # 4. λΈŒλΌμš°μ € ↔ Proxmox κ°„ 데이터 μ–‘λ°©ν–₯ 쀑계 +``` + +**2. λΈŒλΌμš°μ € WebSocket URL λ³€κ²½** +```javascript +// κΈ°μ‘΄ (직접 Proxmox μ—°κ²°) +const websocketUrl = 'wss://pve7.0bin.in:443/api2/json/nodes/pve7/qemu/102/vncwebsocket?...' + +// μƒˆλ‘œμš΄ (Flask ν”„λ‘μ‹œ 경유) +const websocketUrl = 'wss://farmq.0bin.in/vnc/102/proxy' +``` + +### πŸ”§ ν˜„μž¬ μ½”λ“œ κ°œμ„  κ³„νš + +#### Phase 1: Flask WebSocket ν”„λ‘μ‹œ κ΅¬ν˜„ +```python +# app.py에 μΆ”κ°€ν•  ν•¨μˆ˜λ“€ +async def create_proxmox_vnc_connection(vm_id): + """Proxmox VNC WebSocket μ—°κ²° 생성 (인증 헀더 포함)""" + +async def proxy_websocket_data(browser_ws, proxmox_ws): + """λΈŒλΌμš°μ €μ™€ Proxmox κ°„ WebSocket 데이터 쀑계""" + +@app.websocket('/vnc//proxy') +async def vnc_websocket_proxy(vm_id): + """VNC WebSocket ν”„λ‘μ‹œ 메인 ν•Έλ“€λŸ¬""" +``` + +#### Phase 2: λΈŒλΌμš°μ € ν΄λΌμ΄μ–ΈνŠΈ μˆ˜μ • +```javascript +// vnc_simple.html μˆ˜μ • κ³„νš +// 1. WebSocket URL을 Flask ν”„λ‘μ‹œλ‘œ λ³€κ²½ +// 2. VNC ν‹°μΌ“ 생성 둜직 제거 (Flaskμ—μ„œ 처리) +// 3. μ—°κ²° μƒνƒœ 관리 κ°œμ„  +``` + +#### Phase 3: μ„Έμ…˜ 관리 κ°•ν™” +```python +# μ„Έμ…˜ 만료 감지 및 μžλ™ κ°±μ‹  +# Proxmox 인증 μƒνƒœ λͺ¨λ‹ˆν„°λ§ +# 였λ₯˜ 상황 처리 및 볡ꡬ +``` + +### 🎯 κ΅¬ν˜„ μš°μ„ μˆœμœ„ + +**μ¦‰μ‹œ κ΅¬ν˜„ (High Priority):** +- [ ] Flask WebSocket 라이브러리 μΆ”κ°€ (flask-socketio λ˜λŠ” quart) +- [ ] VNC WebSocket ν”„λ‘μ‹œ κΈ°λ³Έ ꡬ쑰 κ΅¬ν˜„ +- [ ] λΈŒλΌμš°μ € WebSocket URL λ³€κ²½ + +**단계적 κ°œμ„  (Medium Priority):** +- [ ] μ„Έμ…˜ 만료 μžλ™ 처리 +- [ ] μ—°κ²° μƒνƒœ λͺ¨λ‹ˆν„°λ§ +- [ ] 였λ₯˜ 볡ꡬ λ©”μ»€λ‹ˆμ¦˜ + +**μ΅œμ ν™” (Low Priority):** +- [ ] μ„±λŠ₯ νŠœλ‹ (μ—°κ²° 풀링, 캐싱) +- [ ] λ‘œκΉ… 및 λͺ¨λ‹ˆν„°λ§ κ°•ν™” +- [ ] 닀쀑 VM λ™μ‹œ μ—°κ²° 지원 + +### πŸ”’ λ³΄μ•ˆ 고렀사항 + +**인증 λ³΄μ•ˆ:** +- Flask μ„Έμ…˜μœΌλ‘œ μ‚¬μš©μž κΆŒν•œ 검증 +- Proxmox 인증 μ •λ³΄λŠ” μ„œλ²„ λ©”λͺ¨λ¦¬μ—λ§Œ 보관 +- λΈŒλΌμš°μ €μ— 민감 정보 λ…ΈμΆœ λ°©μ§€ + +**λ„€νŠΈμ›Œν¬ λ³΄μ•ˆ:** +- Flask ↔ Proxmox 톡신은 λ‚΄λΆ€ λ„€νŠΈμ›Œν¬ +- SSL/TLS 쒅단간 μ•”ν˜Έν™” μœ μ§€ +- CORS μ •μ±… μ μ ˆν•œ μ„€μ • + +### πŸ“Š μ˜ˆμƒ 효과 + +**μ•ˆμ •μ„± κ°œμ„ :** +- βœ… 인증 μ‹€νŒ¨λ‘œ μΈν•œ μ—°κ²° 였λ₯˜ μ™„μ „ 제거 +- βœ… μ„Έμ…˜ 만료 μžλ™ 처리 +- βœ… λ„€νŠΈμ›Œν¬ 였λ₯˜ 볡ꡬ λŠ₯λ ₯ ν–₯상 + +**μ‚¬μš©μž κ²½ν—˜:** +- βœ… μΌκ΄€λœ μ—°κ²° 성곡λ₯  +- βœ… λΉ λ₯Έ μ—°κ²° 속도 (쀑간 단계 제거) +- βœ… 였λ₯˜ 상황 μ‚¬μš©μž μΉœν™”μ  처리 + +**μœ μ§€λ³΄μˆ˜μ„±:** +- βœ… 쀑앙집쀑식 인증 관리 +- βœ… 디버깅 μš©μ΄μ„± ν–₯상 +- βœ… μ½”λ“œ λ³΅μž‘λ„ κ°μ†Œ + +### πŸš€ λ‹€μŒ 단계 + +1. **Flask WebSocket 라이브러리 선택 및 μ„€μΉ˜** +2. **κ°„λ‹¨ν•œ ν”„λ‘μ‹œ ν”„λ‘œν† νƒ€μž… κ΅¬ν˜„** +3. **κΈ°λ³Έ μ—°κ²° ν…ŒμŠ€νŠΈ** +4. **λΈŒλΌμš°μ € ν΄λΌμ΄μ–ΈνŠΈ μˆ˜μ •** +5. **톡합 ν…ŒμŠ€νŠΈ 및 검증** + +**⚠️ μ£Όμ˜μ‚¬ν•­:** +이 변경은 μ•„ν‚€ν…μ²˜ μˆ˜μ€€μ˜ κ°œμ„ μ΄λ―€λ‘œ μΆ©λΆ„ν•œ ν…ŒμŠ€νŠΈμ™€ λ°±μ—… κ³„νšμ΄ ν•„μš”ν•©λ‹ˆλ‹€. + +--- + +**πŸ’‘ 핡심 λ©”μ‹œμ§€:** +Claude Code ν™˜κ²½μ—μ„œ 증λͺ…λœ "λͺ…μ‹œμ  PVE 인증 헀더 전달" 방식을 Flask ν”„λ‘μ‹œλ₯Ό 톡해 λΈŒλΌμš°μ € ν™˜κ²½μ—μ„œλ„ μ•ˆμ •μ μœΌλ‘œ κ΅¬ν˜„ν•˜μžλŠ” 것이 이 μ „λž΅μ˜ ν•΅μ‹¬μž…λ‹ˆλ‹€. \ No newline at end of file diff --git a/farmq-admin/VNC_Implementation_Technical_Documentation.md b/farmq-admin/VNC_Implementation_Technical_Documentation.md new file mode 100644 index 0000000..fc49746 --- /dev/null +++ b/farmq-admin/VNC_Implementation_Technical_Documentation.md @@ -0,0 +1,249 @@ +# VNC μ›Ήμ†ŒμΌ“ κ΅¬ν˜„ 기술 λ¬Έμ„œ +## Proxmox APIλ₯Ό μ΄μš©ν•œ noVNC 톡합 κ°€μ΄λ“œ + +### πŸ“‹ κ°œμš” +이 λ¬Έμ„œλŠ” Proxmox VE APIλ₯Ό ν™œμš©ν•˜μ—¬ μ›Ή λΈŒλΌμš°μ €μ—μ„œ 직접 가상머신에 VNC 접속할 수 μžˆλŠ” μ‹œμŠ€ν…œμ˜ 기술적 κ΅¬ν˜„ λ‚΄μš©μ„ μ„€λͺ…ν•©λ‹ˆλ‹€. Flask λ°±μ—”λ“œμ™€ noVNC ν΄λΌμ΄μ–ΈνŠΈλ₯Ό 톡해 λΈŒλΌμš°μ €μ—μ„œ 직접 VM μ½˜μ†”μ— μ ‘κ·Όν•  수 μžˆλ„λ‘ κ΅¬ν˜„λ˜μ—ˆμŠ΅λ‹ˆλ‹€. + +### πŸ—οΈ μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜ + +``` +[μ›Ή λΈŒλΌμš°μ €] + ↓ HTTPS +[NPM λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ] + ↓ HTTP +[Flask μ• ν”Œλ¦¬μΌ€μ΄μ…˜] + ↓ HTTPS API +[Proxmox VE μ„œλ²„] + ↓ WebSocket (WSS) +[VM VNC μ„œλ²„] +``` + +### πŸ”§ 핡심 κ΅¬μ„±μš”μ†Œ + +#### 1. Proxmox API ν΄λΌμ΄μ–ΈνŠΈ (`utils/proxmox_client.py`) + +**μ£Όμš” κΈ°λŠ₯:** +- Proxmox VE API 인증 및 μ„Έμ…˜ 관리 +- VNC ν‹°μΌ“ 생성 및 WebSocket URL 생성 +- VM μƒνƒœ 관리 (μ‹œμž‘/μ •μ§€/μƒνƒœν™•μΈ) + +**핡심 κ΅¬ν˜„:** +```python +def get_vnc_ticket(self, node: str, vmid: int) -> Optional[Dict]: + """VNC 접속 ν‹°μΌ“ 생성""" + data = { + 'websocket': '1', + 'generate-password': '1' # μžλ™ νŒ¨μŠ€μ›Œλ“œ 생성 + } + response = self.session.post( + f"{self.base_url}/nodes/{node}/qemu/{vmid}/vncproxy", + data=data, + timeout=10 + ) + + if response.status_code == 200: + vnc_data = response.json()['data'] + encoded_ticket = quote_plus(vnc_data['ticket']) + vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}" + return vnc_data +``` + +**인증 방식:** +- μ„Έμ…˜ μΏ ν‚€ 방식 (PVEAuthCookie) +- CSRF 토큰 헀더 (CSRFPreventionToken) +- API 토큰 방식 지원 + +#### 2. Flask μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜ (`app.py`) + +**μ£Όμš” μ—”λ“œν¬μΈνŠΈ:** +- `/vnc/`: VNC ν΄λΌμ΄μ–ΈνŠΈ νŽ˜μ΄μ§€ λ Œλ”λ§ +- API μ—”λ“œν¬μΈνŠΈλ“€μ„ ν†΅ν•œ VM 관리 + +**λ‘œκΉ… μ‹œμŠ€ν…œ:** +```python +def setup_logging(): + if not os.path.exists('logs'): + os.makedirs('logs') + + file_handler = RotatingFileHandler( + 'logs/farmq-admin.log', + maxBytes=10*1024*1024, + backupCount=5 + ) +``` + +#### 3. noVNC ν΄λΌμ΄μ–ΈνŠΈ (`templates/vnc_simple.html`) + +**핡심 κΈ°λŠ₯:** +- WebSocket을 ν†΅ν•œ VNC ν”„λ‘œν† μ½œ 처리 +- μžλ™ 리사이징 및 μŠ€μΌ€μΌλ§ +- HTML μ—”ν‹°ν‹° λ””μ½”λ”© (νŒ¨μŠ€μ›Œλ“œ 처리) + +**WebSocket μ—°κ²° μ½”λ“œ:** +```javascript +function connectVNC() { + const rfb = new RFB(document.getElementById('screen'), websocketUrl, { + credentials: { + password: decodeHtmlEntities('{{ vnc_data.password }}') + } + }); + + rfb.addEventListener("connect", () => { + console.log("VNC μ—°κ²° 성곡"); + resizeScreen(); + }); + + rfb.addEventListener("disconnect", (e) => { + console.log(`VNC μ—°κ²° μ’…λ£Œ: ${e.detail.clean ? '정상' : '비정상'}`); + }); +} +``` + +**HTML μ—”ν‹°ν‹° λ””μ½”λ”©:** +```javascript +function decodeHtmlEntities(text) { + const textarea = document.createElement('textarea'); + textarea.innerHTML = text; + return textarea.value; +} +``` + +### πŸ” λ³΄μ•ˆ 및 인증 + +#### VNC 인증 ν”Œλ‘œμš° +1. **ν‹°μΌ“ μš”μ²­**: Flask β†’ Proxmox API (`/vncproxy`) +2. **ν‹°μΌ“ 생성**: Proxmoxκ°€ 일회용 VNC ν‹°μΌ“ 및 νŒ¨μŠ€μ›Œλ“œ 생성 +3. **WebSocket μ—°κ²°**: λΈŒλΌμš°μ € β†’ Proxmox VNC WebSocket +4. **VNC 인증**: μƒμ„±λœ νŒ¨μŠ€μ›Œλ“œλ‘œ VNC μ„œλ²„ 인증 + +#### λ³΄μ•ˆ μ„€μ • +```python +# SSL 검증 λ¬΄μ‹œ (λ‚΄λΆ€ λ„€νŠΈμ›Œν¬) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +self.session.verify = False + +# WebSocket URL에 ν‹°μΌ“ 인코딩 +encoded_ticket = quote_plus(vnc_data['ticket']) +``` + +### 🌐 λ„€νŠΈμ›Œν¬ ꡬ성 + +#### NPM (Nginx Proxy Manager) μ„€μ • +- **μ™ΈλΆ€ 도메인**: `https://pve7.0bin.in` +- **λ‚΄λΆ€ μ£Όμ†Œ**: `https://192.168.0.5:8006` +- **WebSocket 지원**: Upgrade 헀더 ν”„λ‘μ‹œ ν•„μš” + +#### WebSocket ν”„λ‘μ‹œ μš”κ΅¬μ‚¬ν•­ +```nginx +# WebSocket μ—…κ·Έλ ˆμ΄λ“œ 헀더 +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; +proxy_set_header Host $host; +``` + +### ⚑ μ„±λŠ₯ μ΅œμ ν™” + +#### Canvas μžλ™ 리사이징 +```javascript +function resizeScreen() { + const canvas = document.querySelector('#screen canvas'); + if (canvas) { + const container = document.getElementById('screen'); + const scaleX = container.clientWidth / canvas.width; + const scaleY = container.clientHeight / canvas.height; + const scale = Math.min(scaleX, scaleY, 1); + + canvas.style.transform = `scale(${scale})`; + canvas.style.transformOrigin = 'top left'; + } +} +``` + +#### μ—°κ²° μƒνƒœ 관리 +- μžλ™ μž¬μ—°κ²° 둜직 +- μ—°κ²° ν’ˆμ§ˆ λͺ¨λ‹ˆν„°λ§ +- μ—λŸ¬ 처리 및 μ‚¬μš©μž ν”Όλ“œλ°± + +### πŸ› 문제 ν•΄κ²° κ°€μ΄λ“œ + +#### 일반적인 λ¬Έμ œλ“€ + +**1. WebSocket 1006 μ—λŸ¬ (비정상 μ—°κ²° μ’…λ£Œ)** +- **원인**: NPM ν”„λ‘μ‹œ WebSocket μ„€μ • λΆ€μ‘± +- **ν•΄κ²°**: WebSocket μ—…κ·Έλ ˆμ΄λ“œ 헀더 μ„€μ • 확인 + +**2. VNC 인증 μ‹€νŒ¨ (HTTP 401)** +- **원인**: 잘λͺ»λœ ν‹°μΌ“ λ˜λŠ” νŒ¨μŠ€μ›Œλ“œ +- **ν•΄κ²°**: `generate-password: 1` μ„€μ • 확인 + +**3. 빈 ν™”λ©΄ λ˜λŠ” 검은 ν™”λ©΄** +- **원인**: Canvas 리사이징 문제 +- **ν•΄κ²°**: `resizeScreen()` ν•¨μˆ˜ 호좜 확인 + +**4. HTML μ—”ν‹°ν‹° 문제** +- **원인**: νŒ¨μŠ€μ›Œλ“œμ˜ 특수문자 인코딩 +- **ν•΄κ²°**: `decodeHtmlEntities()` ν•¨μˆ˜ 적용 + +#### 디버깅 도ꡬ + +**1. μ„œλ²„ μ‚¬μ΄λ“œ ν…ŒμŠ€νŠΈ** +```bash +cd /srv/headscale-setup/farmq-admin +python test_vnc_websocket.py +``` + +**2. 둜그 확인** +```bash +tail -f logs/farmq-admin.log +``` + +**3. λΈŒλΌμš°μ € 개발자 도ꡬ** +- Network νƒ­: WebSocket μ—°κ²° μƒνƒœ 확인 +- Console νƒ­: JavaScript μ—λŸ¬ 확인 + +### πŸ“Š λͺ¨λ‹ˆν„°λ§ 및 λ‘œκΉ… + +#### 둜그 레벨 +- **INFO**: 정상 λ™μž‘ 둜그 +- **WARNING**: 경고사항 +- **ERROR**: 였λ₯˜ λ°œμƒ +- **DEBUG**: 상세 디버깅 정보 + +#### μ€‘μš” 둜그 포인트 +- VNC ν‹°μΌ“ 생성 성곡/μ‹€νŒ¨ +- WebSocket μ—°κ²° μ‹œλ„ +- VNC 인증 κ²°κ³Ό +- μ—°κ²° μ’…λ£Œ μ‚¬μœ  + +### πŸ”„ 배포 및 μœ μ§€λ³΄μˆ˜ + +#### 배포 체크리슀트 +- [ ] Proxmox API μ—°κ²° ν…ŒμŠ€νŠΈ +- [ ] Flask μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘ 확인 +- [ ] NPM ν”„λ‘μ‹œ μ„€μ • 검증 +- [ ] WebSocket μ—°κ²° ν…ŒμŠ€νŠΈ +- [ ] VNC ν΄λΌμ΄μ–ΈνŠΈ λ™μž‘ 확인 + +#### λ°±μ—… 및 볡ꡬ +```bash +# Git 컀밋 μƒνƒœ 확인 +git log --oneline -10 + +# μ•ˆμ •λœ λ²„μ „μœΌλ‘œ λ‘€λ°± +git reset --hard 1dc09101cc7afdf09ca3b8cbbc4f95e21bb5746f +``` + +### πŸ“š μ°Έκ³  자료 + +- [Proxmox VE API λ¬Έμ„œ](https://pve.proxmox.com/pve-docs/api-viewer/) +- [noVNC ν”„λ‘œμ νŠΈ](https://github.com/novnc/noVNC) +- [WebSocket RFC 6455](https://tools.ietf.org/html/rfc6455) +- [VNC ν”„λ‘œν† μ½œ μŠ€νŽ™](https://tools.ietf.org/html/rfc6143) + +### 🏷️ 버전 정보 +- **ν”„λ‘œμ νŠΈ**: FarmQ Admin VNC Integration +- **λ§ˆμ§€λ§‰ μ—…λ°μ΄νŠΈ**: 2024λ…„ +- **μ•ˆμ • 버전**: commit `1dc09101cc7afdf09ca3b8cbbc4f95e21bb5746f` +- **Python**: 3.x +- **Flask**: μ΅œμ‹  μ•ˆμ • 버전 +- **noVNC**: μ΅œμ‹  버전 \ No newline at end of file diff --git a/farmq-admin/VNC_Network_Architecture_Issue.md b/farmq-admin/VNC_Network_Architecture_Issue.md new file mode 100644 index 0000000..ec287c8 --- /dev/null +++ b/farmq-admin/VNC_Network_Architecture_Issue.md @@ -0,0 +1,161 @@ +# VNC λ„€νŠΈμ›Œν¬ μ•„ν‚€ν…μ²˜ 문제 뢄석 및 ν•΄κ²°λ°©μ•ˆ + +## πŸ” ν˜„μž¬ 상황 뢄석 + +### λ„€νŠΈμ›Œν¬ ꡬ쑰 +``` +[μ™ΈλΆ€ μ‚¬μš©μž PC] + ↓ (인터넷) +[pqadmin.0bin.in] (Let's Encrypt SSL + λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ) + ↓ (둜컬/VPN) +[Headscale μ„œλ²„ - Flask Admin] + ↓ (Headscale VPN: 100.64.x.x) +[Proxmox Hosts] +- pve7.0bin.in (443) +- 100.64.0.6:8006 (Healthport PVE) +``` + +### 문제 상황 +1. **Flask μ„œλ²„**: Headscale VPN λ‚΄λΆ€μ—μ„œ μ‹€ν–‰ 쀑 +2. **μ™ΈλΆ€ μ‚¬μš©μž**: `pqadmin.0bin.in`을 톡해 Flask Admin에 접속 +3. **VNC WebSocket URL**: `wss://100.64.0.6:8006/...` (Headscale VPN λ‚΄λΆ€ IP) +4. **접속 μ‹€νŒ¨**: μ™ΈλΆ€ μ‚¬μš©μžλŠ” 100.64.x.x λŒ€μ—­μ— μ ‘κ·Ό λΆˆκ°€ + +## ❌ 핡심 문제 + +### λ„€νŠΈμ›Œν¬ 뢄리 문제 +- **Flask Admin**: μΈν„°λ„·μ—μ„œ μ ‘κ·Ό κ°€λŠ₯ (`pqadmin.0bin.in`) +- **Proxmox VNC**: Headscale VPN λ‚΄λΆ€μ—μ„œλ§Œ μ ‘κ·Ό κ°€λŠ₯ (`100.64.x.x`) +- **μ‚¬μš©μž**: 두 λ„€νŠΈμ›Œν¬λ₯Ό λ™μ‹œμ— μ ‘κ·Όν•  수 μ—†μŒ + +### ν˜„μž¬ WebSocket μ—°κ²° 방식 +```javascript +// λ¬Έμ œκ°€ λ˜λŠ” ν˜„μž¬ 방식 +WebSocket URL: wss://100.64.0.6:8006/api2/json/nodes/pev/qemu/103/vncwebsocket +``` + +## πŸ’‘ ν•΄κ²°λ°©μ•ˆ + +### 1. WebSocket ν”„λ‘μ‹œ κ΅¬ν˜„ (ꢌμž₯) + +#### ꡬ쑰 +``` +[μ™ΈλΆ€ μ‚¬μš©μž] β†’ [pqadmin.0bin.in] β†’ [Flask μ„œλ²„] β†’ [Proxmox VNC] + ↓ ↓ ↓ ↓ +wss://pqadmin.0bin.in/vnc-proxy/{session_id} β†’ wss://100.64.0.6:8006/... +``` + +#### κ΅¬ν˜„ 방법 +1. **Flaskμ—μ„œ WebSocket ν”„λ‘μ‹œ μ„œλ²„ κ΅¬ν˜„** + - Socket.IO λ˜λŠ” μ›Ήμ†ŒμΌ“ 라이브러리 μ‚¬μš© + - μ™ΈλΆ€ β†’ Flask β†’ Proxmox 쀑계 μ—­ν•  + +2. **URL λ³€κ²½** + ```javascript + // λ³€κ²½ μ „ + wss://100.64.0.6:8006/api2/json/nodes/pev/qemu/103/vncwebsocket + + // λ³€κ²½ ν›„ + wss://pqadmin.0bin.in/vnc-proxy/session_id_here + ``` + +### 2. Nginx λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ ν™•μž₯ + +#### Nginx μ„€μ • μΆ”κ°€ +```nginx +# VNC WebSocket ν”„λ‘μ‹œ +location /vnc-ws/ { + proxy_pass https://100.64.0.6:8006/; + 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_ssl_verify off; +} +``` + +#### URL λ³€κ²½ +```javascript +// λ³€κ²½ μ „ +wss://100.64.0.6:8006/api2/json/nodes/pev/qemu/103/vncwebsocket + +// λ³€κ²½ ν›„ +wss://pqadmin.0bin.in/vnc-ws/api2/json/nodes/pev/qemu/103/vncwebsocket +``` + +### 3. VPN ν΄λΌμ΄μ–ΈνŠΈ μš”κ΅¬ (λΉ„κΆŒμž₯) + +#### 방법 +- λͺ¨λ“  μ‚¬μš©μžκ°€ Headscale VPN ν΄λΌμ΄μ–ΈνŠΈ μ„€μΉ˜ +- 100.64.x.x λŒ€μ—­ 직접 μ ‘κ·Ό κ°€λŠ₯ + +#### 단점 +- μ‚¬μš©μž λΆ€λ‹΄ 증가 +- 관리 λ³΅μž‘μ„± +- λ³΄μ•ˆ μœ„ν—˜ + +## 🎯 ꢌμž₯ μ†”λ£¨μ…˜: WebSocket ν”„λ‘μ‹œ + +### μž₯점 +1. **μ‚¬μš©μž μΉœν™”μ **: 별도 μ„€μΉ˜ 없이 μ›ΉλΈŒλΌμš°μ €λ‘œ μ ‘κ·Ό +2. **λ³΄μ•ˆ**: Headscale VPN은 λ‚΄λΆ€ νŠΈλž˜ν”½λ§Œ 처리 +3. **ν™•μž₯μ„±**: λ‹€μˆ˜μ˜ Proxmox 호슀트 지원 +4. **SSL μΈμ¦μ„œ**: Let's Encrypt둜 μ•ˆμ „ν•œ μ—°κ²° + +### κ΅¬ν˜„ μš°μ„ μˆœμœ„ +1. **1단계**: Flask Socket.IO WebSocket ν”„λ‘μ‹œ κ΅¬ν˜„ +2. **2단계**: μ„Έμ…˜ 관리 및 인증 κ°•ν™” +3. **3단계**: 닀쀑 Proxmox 호슀트 지원 μ™„μ„± +4. **4단계**: μ„±λŠ₯ μ΅œμ ν™” 및 λͺ¨λ‹ˆν„°λ§ + +## πŸ”§ 기술 μŠ€νƒ + +### ν˜„μž¬ μ‚¬μš© 쀑 +- **Flask**: Web μ„œλ²„ +- **noVNC**: λΈŒλΌμš°μ € VNC ν΄λΌμ΄μ–ΈνŠΈ +- **Headscale**: VPN μ„œλ²„ +- **Nginx**: λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ +- **Let's Encrypt**: SSL μΈμ¦μ„œ + +### μΆ”κ°€ ν•„μš” +- **Flask-SocketIO**: WebSocket ν”„λ‘μ‹œ κ΅¬ν˜„ +- **python-websockets**: WebSocket ν΄λΌμ΄μ–ΈνŠΈ (Proxmox μ—°κ²°μš©) + +## πŸ“‹ κ΅¬ν˜„ 단계 + +### Phase 1: WebSocket ν”„λ‘μ‹œ μ„œλ²„ +1. Flask-SocketIO μ„€μΉ˜ 및 μ„€μ • +2. VNC WebSocket ν”„λ‘μ‹œ ν•Έλ“€λŸ¬ κ΅¬ν˜„ +3. μ„Έμ…˜ 관리 및 인증 연동 + +### Phase 2: URL λΌμš°νŒ… λ³€κ²½ +1. VNC μ—°κ²° URL 생성 둜직 μˆ˜μ • +2. ν”„λ‘μ‹œ 경둜둜 WebSocket URL λ³€κ²½ +3. noVNC ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²° ν…ŒμŠ€νŠΈ + +### Phase 3: 닀쀑 호슀트 지원 +1. ν˜ΈμŠ€νŠΈλ³„ ν”„λ‘μ‹œ λΌμš°νŒ… +2. 동적 Proxmox 호슀트 μΆ”κ°€ +3. λ‘œλ“œ λ°ΈλŸ°μ‹± κ³ λ € + +## ⚠️ 고렀사항 + +### μ„±λŠ₯ +- WebSocket ν”„λ‘μ‹œλ‘œ μΈν•œ μ§€μ—° μ‹œκ°„ 증가 +- λ™μ‹œ μ—°κ²° 수 μ œν•œ +- μ„œλ²„ λ¦¬μ†ŒμŠ€ μ‚¬μš©λŸ‰ 증가 + +### λ³΄μ•ˆ +- VNC νŠΈλž˜ν”½ μ•”ν˜Έν™” μƒνƒœ μœ μ§€ +- μ„Έμ…˜ 만료 및 κΆŒν•œ 관리 +- DDoS λ°©μ–΄ λ©”μ»€λ‹ˆμ¦˜ + +### ν™•μž₯μ„± +- λ‹€μˆ˜ μ‚¬μš©μž λ™μ‹œ 접속 +- Proxmox 호슀트 동적 μΆ”κ°€ +- ν΄λŸ¬μŠ€ν„° ν™˜κ²½ 지원 + +--- + +**κ²°λ‘ **: WebSocket ν”„λ‘μ‹œλ₯Ό 톡해 μ™ΈλΆ€ μ‚¬μš©μžκ°€ μ•ˆμ „ν•˜κ²Œ λ‚΄λΆ€ Proxmox VNC에 μ ‘κ·Όν•  수 μžˆλ„λ‘ ν•˜λŠ” 것이 κ°€μž₯ ν˜„μ‹€μ μΈ ν•΄κ²°μ±…μž…λ‹ˆλ‹€. \ No newline at end of file diff --git a/farmq-admin/VNC_WebSocket_Connection_Issue_Resolution.md b/farmq-admin/VNC_WebSocket_Connection_Issue_Resolution.md new file mode 100644 index 0000000..f8b8604 --- /dev/null +++ b/farmq-admin/VNC_WebSocket_Connection_Issue_Resolution.md @@ -0,0 +1,207 @@ +# VNC WebSocket μ—°κ²° 문제 ν•΄κ²° λ¬Έμ„œ +## Proxmox VE API 기반 VNC 접속 문제 진단 및 ν•΄κ²° + +### 🚨 λ°œμƒν•œ 문제 + +**증상:** +- λΈŒλΌμš°μ €μ—μ„œ VNC 접속 μ‹œ WebSocket 1006 μ—λŸ¬ (비정상 μ—°κ²° μ’…λ£Œ) +- Proxmox λ‘œκ·Έμ— `TASK ERROR: connection timed out` λ°œμƒ +- noVNC ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ 검은 ν™”λ©΄λ§Œ ν‘œμ‹œ +- HTTP 401 Unauthorized μ—λŸ¬ λ°œμƒ + +**영ν–₯:** +- μ›Ή λΈŒλΌμš°μ €λ₯Ό ν†΅ν•œ VM μ½˜μ†” 접속 λΆˆκ°€λŠ₯ +- μ‚¬μš©μžκ°€ VM에 직접 μ ‘κ·Όν•  수 μ—†λŠ” 상황 + +### πŸ” 문제 진단 κ³Όμ • + +#### 1단계: 초기 상황 νŒŒμ•… +```bash +# VM μƒνƒœ 확인 +VM 102 μƒνƒœ: running +VM μ‹€ν–‰μ‹œκ°„: 1463275초 (정상 μ‹€ν–‰ 쀑) +VM PID: 3482 +VNC 포트: N/A ← 문제 발견 +``` + +#### 2단계: VM μ„€μ • 상세 쑰회 +```bash +# Proxmox API둜 VM μ„€μ • 확인 +GET /api2/json/nodes/pve7/qemu/102/config + +κ²°κ³Ό: +args: -vnc 0.0.0.0:77 ← 문제의 κ·Όλ³Έ 원인 발견 +``` + +**πŸ’‘ 핡심 발견:** +VM에 μ»€μŠ€ν…€ VNC μ„€μ • `-vnc 0.0.0.0:77`이 μ„€μ •λ˜μ–΄ μžˆμ–΄μ„œ: +- VM은 포트 5977 (5900 + 77)μ—μ„œ VNC μ„œλΉ„μŠ€ 제곡 +- Proxmox VNC ν”„λ‘μ‹œλŠ” ν‘œμ€€ 포트 5900 κΈ°λŒ€ +- 포트 뢈일치둜 μΈν•œ μ—°κ²° μ‹€νŒ¨ + +### πŸ› οΈ ν•΄κ²° κ³Όμ • + +#### 1단계: VM μ„€μ • μˆ˜μ • (μ‹€μ œ Proxmox μ„œλ²„ μ„€μ • λ³€κ²½) + +```python +# Python μ½”λ“œλ‘œ μ‹€μ œ Proxmox VM μ„€μ • μˆ˜μ • +PUT /api2/json/nodes/pve7/qemu/102/config +data = {'args': ''} # VNC μ»€μŠ€ν…€ μ„€μ • 제거 + +κ²°κ³Ό: βœ… VNC args 제거 성곡 +``` + +**⚠️ μ€‘μš”:** μ΄λŠ” Python 섀정이 μ•„λ‹Œ **μ‹€μ œ Proxmox μ„œλ²„μ˜ VM 102 섀정을 APIλ₯Ό 톡해 μˆ˜μ •**ν•œ κ²ƒμž…λ‹ˆλ‹€. + +#### 2단계: VM μž¬μ‹œμž‘ (μ„€μ • 적용) + +```python +# VM μ •μ§€ +POST /api2/json/nodes/pve7/qemu/102/status/stop +κ²°κ³Ό: VM μƒνƒœκ°€ 'stopped'둜 λ³€κ²½ 확인 + +# VM μ‹œμž‘ +POST /api2/json/nodes/pve7/qemu/102/status/start +κ²°κ³Ό: VM μƒνƒœκ°€ 'running'으둜 λ³€κ²½ 확인 +``` + +#### 3단계: VNC ν”„λ‘μ‹œ μ—°κ²° ν…ŒμŠ€νŠΈ + +```python +# VNC ν‹°μΌ“ 생성 +POST /api2/json/nodes/pve7/qemu/102/vncproxy +data = { + 'websocket': '1', + 'generate-password': '1' +} + +κ²°κ³Ό: +βœ… 포트: 5900 (ν‘œμ€€ 포트둜 정상화) +βœ… νŒ¨μŠ€μ›Œλ“œ: μžλ™ 생성됨 +βœ… WebSocket URL: 정상 생성 +``` + +#### 4단계: 인증 문제 ν•΄κ²° + +**문제:** WebSocket μ—°κ²° μ‹œ HTTP 401 Unauthorized + +**κ·Όλ³Έ 원인:** Proxmox VNC WebSocket은 인증이 ν•„μš”ν•˜μ§€λ§Œ λΈŒλΌμš°μ €μ™€ 달리 μˆ˜λ™μœΌλ‘œ 인증 헀더λ₯Ό 전달해야 함 + +**ν•΄κ²°:** PVE 인증 μΏ ν‚€λ₯Ό WebSocket 헀더에 μΆ”κ°€ + +```python +# κΈ°μ‘΄ (μ‹€νŒ¨) - 인증 정보 μ—†μŒ +async with websockets.connect(websocket_url, ssl=ssl_context) +# κ²°κ³Ό: HTTP 401 Unauthorized + +# μˆ˜μ • (성곡) - PVE 인증 μΏ ν‚€ μΆ”κ°€ +headers = {'Cookie': f'PVEAuthCookie={client.ticket}'} +async with websockets.connect( + websocket_url, + ssl=ssl_context, + additional_headers=headers # 핡심: 인증 헀더 μΆ”κ°€ +) +# κ²°κ³Ό: βœ… WebSocket μ—°κ²° 성곡 +``` + +**πŸ”‘ 핡심 포인트:** +- `client.ticket`은 Proxmox 둜그인 μ‹œ 받은 인증 ν‹°μΌ“ +- VNC ν‹°μΌ“(`vncticket`)κ³ΌλŠ” λ³„κ°œμ˜ **μ„Έμ…˜ 인증 μΏ ν‚€** +- λΈŒλΌμš°μ €λŠ” μžλ™μœΌλ‘œ μΏ ν‚€λ₯Ό μ „μ†‘ν•˜μ§€λ§Œ, Python WebSocket은 μˆ˜λ™μœΌλ‘œ 헀더에 μΆ”κ°€ν•΄μ•Ό 함 + +### βœ… ν•΄κ²° κ²°κ³Ό + +**μ΅œμ’… ν…ŒμŠ€νŠΈ 성곡:** +``` +πŸŽ‰ VNC WebSocket μ—°κ²° 및 ν”„λ‘œν† μ½œ ν•Έλ“œμ…°μ΄ν¬ 성곡! +- WebSocket μ—°κ²°: βœ… 성곡 +- VNC ν”„λ‘œν† μ½œ 버전: RFB 003.008 +- ν΄λΌμ΄μ–ΈνŠΈ 응닡: βœ… μ™„λ£Œ +``` + +### πŸ“‹ λ³€κ²½λœ μ„€μ • μš”μ•½ + +| ꡬ뢄 | λ³€κ²½ μ „ | λ³€κ²½ ν›„ | +|------|---------|---------| +| **VM args μ„€μ •** | `-vnc 0.0.0.0:77` | `''` (빈 κ°’) | +| **VNC 포트** | 5977 (5900+77) | 5900 (ν‘œμ€€) | +| **Proxmox ν”„λ‘μ‹œ** | 포트 뢈일치 였λ₯˜ | 정상 μ—°κ²° | +| **WebSocket 인증** | 인증 헀더 λˆ„λ½ | PVE μΏ ν‚€ μΆ”κ°€ | + +### πŸ”§ 적용된 μˆ˜μ • 사항 + +#### 1. Proxmox μ„œλ²„ VM μ„€μ • (μ‹€μ œ μ„œλ²„ λ³€κ²½) +```bash +# λ³€κ²½ μ „ +VM 102 μ„€μ •: args = "-vnc 0.0.0.0:77" + +# λ³€κ²½ ν›„ +VM 102 μ„€μ •: args = "" +``` + +#### 2. WebSocket ν…ŒμŠ€νŠΈ μ½”λ“œ (`test_vnc_websocket.py`) +```python +# μΆ”κ°€λœ 인증 헀더 +headers = {'Cookie': f'PVEAuthCookie={client.ticket}'} +async with websockets.connect( + websocket_url, + ssl=ssl_context, + additional_headers=headers +) +``` + +### πŸš€ λΈŒλΌμš°μ € ν΄λΌμ΄μ–ΈνŠΈ 적용 λ°©μ•ˆ + +이제 μ„œλ²„ μ‚¬μ΄λ“œ 연결이 μ„±κ³΅ν–ˆμœΌλ―€λ‘œ, λΈŒλΌμš°μ €μ˜ noVNC ν΄λΌμ΄μ–ΈνŠΈλ„ 같은 λ°©μ‹μœΌλ‘œ μˆ˜μ • κ°€λŠ₯: + +**πŸ’‘ 핡심 μΈμ‚¬μ΄νŠΈ:** Claude Code ν™˜κ²½μ—μ„œ μ„±κ³΅ν•œ μ΄μœ λŠ” **PVE 인증 μΏ ν‚€λ₯Ό WebSocket 헀더에 λͺ…μ‹œμ μœΌλ‘œ μΆ”κ°€**ν–ˆκΈ° λ•Œλ¬Έ + +#### λΈŒλΌμš°μ €μ—μ„œ ν•΄κ²°ν•΄μ•Ό ν•  사항: + +1. **WebSocket μ—°κ²° μ‹œ PVE 인증 정보 전달** + ```javascript + // ν˜„μž¬ λΈŒλΌμš°μ € μ½”λ“œ (인증 정보 μ—†μŒ) + rfb = new RFB(document.getElementById('screen'), websocketUrl, + { credentials: { password: vncPassword } }); + + // ν•„μš”ν•œ μˆ˜μ •: PVE μ„Έμ…˜ μΏ ν‚€ λ˜λŠ” 인증 헀더 μΆ”κ°€ 방법 κ΅¬ν˜„ + ``` + +2. **NPM λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ WebSocket μ—…κ·Έλ ˆμ΄λ“œ 헀더 μ„€μ •** + - WebSocket 연결을 μœ„ν•œ Upgrade 헀더 전달 + - Cookie ν—€λ”μ˜ μ˜¬λ°”λ₯Έ ν”„λ‘μ‹œ μ„€μ • + +3. **λΈŒλΌμš°μ € λ³΄μ•ˆ μ •μ±… (CORS, Mixed Content) κ²€ν† ** + - HTTPS β†’ WSS ν”„λ‘œν† μ½œ 일관성 + - Same-Origin Policy λ˜λŠ” μ μ ˆν•œ CORS μ„€μ • + +**🎯 κ°€μž₯ μ€‘μš”ν•œ 발견:** +μ„œλ²„ ν™˜κ²½μ—μ„œλŠ” `additional_headers={'Cookie': f'PVEAuthCookie={client.ticket}'}`둜 ν•΄κ²°λ˜μ—ˆμœΌλ―€λ‘œ, λΈŒλΌμš°μ €μ—μ„œλ„ λ™μΌν•œ 인증 정보 전달 방식이 ν•„μš”ν•©λ‹ˆλ‹€. + +### πŸ“Š 문제 ν•΄κ²° 체크리슀트 + +- [x] VM μ»€μŠ€ν…€ VNC μ„€μ • 제거 +- [x] VM μž¬μ‹œμž‘μœΌλ‘œ μ„€μ • 적용 +- [x] VNC ν‹°μΌ“ 생성 확인 (포트 5900) +- [x] WebSocket 인증 헀더 μΆ”κ°€ +- [x] VNC ν”„λ‘œν† μ½œ ν•Έλ“œμ…°μ΄ν¬ 성곡 +- [x] μ„œλ²„ ν™˜κ²½μ—μ„œ μ™„μ „ν•œ μ—°κ²° 확인 +- [ ] λΈŒλΌμš°μ € ν΄λΌμ΄μ–ΈνŠΈ 적용 (ν–₯ν›„ μž‘μ—…) +- [ ] NPM ν”„λ‘μ‹œ WebSocket μ„€μ • κ°œμ„  (ν–₯ν›„ μž‘μ—…) + +### 🎯 핡심 κ΅ν›ˆ + +1. **VM μ„€μ • 좩돌 주의**: μ»€μŠ€ν…€ VNC 섀정이 Proxmox ν‘œμ€€ ν”„λ‘μ‹œμ™€ μΆ©λŒν•  수 있음 +2. **포트 일관성 μ€‘μš”**: VM VNC ν¬νŠΈμ™€ ν”„λ‘μ‹œ κΈ°λŒ€ ν¬νŠΈκ°€ μΌμΉ˜ν•΄μ•Ό 함 +3. **인증 정보 전달**: WebSocket μ—°κ²° μ‹œ μ μ ˆν•œ 인증 헀더 ν•„μˆ˜ +4. **API 기반 μ„€μ • λ³€κ²½**: Proxmox API둜 μ‹€μ‹œκ°„ VM μ„€μ • μˆ˜μ • κ°€λŠ₯ + +### πŸ“… ν•΄κ²° μ™„λ£Œ μ‹œμ  +- **문제 발견**: Proxmox 둜그 νƒ€μž„μ•„μ›ƒ μ—λŸ¬ +- **원인 뢄석**: VM μ»€μŠ€ν…€ VNC μ„€μ • 좩돌 +- **ν•΄κ²° μ™„λ£Œ**: 2024λ…„ (Claude Code ν™˜κ²½μ—μ„œ μ™„μ „ν•œ VNC WebSocket μ—°κ²° 성곡) +- **ν…ŒμŠ€νŠΈ κ²°κ³Ό**: VNC ν”„λ‘œν† μ½œ ν•Έλ“œμ…°μ΄ν¬κΉŒμ§€ μ™„λ£Œ + +--- + +**πŸ’‘ μ°Έκ³ **: 이 λ¬Έμ„œλŠ” μ‹€μ œ Proxmox μ„œλ²„μ˜ VM 섀정을 λ³€κ²½ν•œ λ‚΄μš©μ„ κΈ°λ‘ν•œ κ²ƒμž…λ‹ˆλ‹€. Python μ½”λ“œλŠ” Proxmox API ν˜ΈμΆœμ„ μœ„ν•œ ν΄λΌμ΄μ–ΈνŠΈ μ—­ν• λ§Œ μˆ˜ν–‰ν–ˆμœΌλ©°, μ‹€μ œ 변경은 Proxmox μ„œλ²„μ—μ„œ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. \ No newline at end of file diff --git a/farmq-admin/app.py b/farmq-admin/app.py index 089f63e..5e6b3d5 100644 --- a/farmq-admin/app.py +++ b/farmq-admin/app.py @@ -5,11 +5,14 @@ Headscale + Headplane 고도화 κ΄€λ¦¬μž νŽ˜μ΄μ§€ """ from flask import Flask, render_template, jsonify, request, redirect, url_for +from flask_socketio import SocketIO, emit, disconnect import os import json from datetime import datetime import uuid from config import config +import asyncio +import threading from utils.database_new import ( init_databases, get_farmq_session, get_dashboard_stats, get_all_pharmacies_with_stats, get_all_machines_with_details, @@ -20,6 +23,7 @@ from models.farmq_models import PharmacyInfo from sqlalchemy import or_ import subprocess from utils.proxmox_client import ProxmoxClient +from utils.vnc_proxy import init_vnc_proxy, get_vnc_proxy def create_app(config_name=None): """Flask μ• ν”Œλ¦¬μΌ€μ΄μ…˜ νŒ©ν† λ¦¬""" @@ -29,6 +33,9 @@ def create_app(config_name=None): config_name = config_name or os.environ.get('FLASK_ENV', 'default') app.config.from_object(config[config_name]) + # SocketIO μ΄ˆκΈ°ν™” + socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading') + # λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™” init_databases( headscale_db_uri='sqlite:////srv/headscale-setup/data/db.sqlite', @@ -42,10 +49,43 @@ def create_app(config_name=None): # VNC μ„Έμ…˜ 관리 (λ©”λͺ¨λ¦¬ 기반) vnc_sessions = {} - # Proxmox μ„œλ²„ μ„€μ • - PROXMOX_HOST = "pve7.0bin.in" - PROXMOX_USERNAME = "root@pam" - PROXMOX_PASSWORD = "trajet6640" + # 닀쀑 Proxmox μ„œλ²„ μ„€μ • + PROXMOX_HOSTS = { + 'pve7.0bin.in': { + 'host': 'pve7.0bin.in', + 'username': 'root@pam', + 'password': 'trajet6640', + 'port': 443, + 'name': 'PVE 7.0 (Main)', + 'default': True + }, + 'healthport_pve': { + 'host': '100.64.0.6', + 'username': 'root@pam', + 'password': 'healthport', + 'port': 8006, + 'name': 'Healthport PVE', + 'default': False + } + } + + # κΈ°λ³Έ 호슀트 κ°€μ Έμ˜€κΈ° + def get_default_host(): + for host_key, host_config in PROXMOX_HOSTS.items(): + if host_config.get('default', False): + return host_key, host_config + # 기본값이 μ—†μœΌλ©΄ 첫 번째 호슀트 λ°˜ν™˜ + return next(iter(PROXMOX_HOSTS.items())) + + # 호슀트 μ„€μ • κ°€μ Έμ˜€κΈ° + def get_host_config(host_key=None): + if not host_key: + return get_default_host() + return host_key, PROXMOX_HOSTS.get(host_key, get_default_host()[1]) + + # VNC ν”„λ‘μ‹œ μ΄ˆκΈ°ν™” (κΈ°λ³Έ 호슀트둜) + default_host_key, default_host_config = get_default_host() + init_vnc_proxy(default_host_config['host'], default_host_config['username'], default_host_config['password']) # 메인 λŒ€μ‹œλ³΄λ“œ @app.route('/') @@ -492,11 +532,20 @@ def create_app(config_name=None): def vm_list(): """VM λͺ©λ‘ νŽ˜μ΄μ§€""" try: + # μš”μ²­λœ 호슀트 κ°€μ Έμ˜€κΈ° + requested_host = request.args.get('host') + current_host_key, current_host_config = get_host_config(requested_host) + # Proxmox ν΄λΌμ΄μ–ΈνŠΈ 생성 및 둜그인 - client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD) + client = ProxmoxClient( + current_host_config['host'], + current_host_config['username'], + current_host_config['password'], + port=current_host_config['port'] + ) if not client.login(): return render_template('error.html', - error='Proxmox μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€.'), 500 + error=f'Proxmox μ„œλ²„({current_host_config["name"]})에 μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€.'), 500 # VM λͺ©λ‘ κ°€μ Έμ˜€κΈ° vms = client.get_vm_list() @@ -509,7 +558,10 @@ def create_app(config_name=None): return render_template('vm_list.html', vms=vms, - host=PROXMOX_HOST, + available_hosts=PROXMOX_HOSTS, + current_host_key=current_host_key, + current_host_name=current_host_config['name'], + current_host_info=current_host_config, total_vms=total_vms, running_vms=running_vms, stopped_vms=stopped_vms, @@ -527,11 +579,20 @@ def create_app(config_name=None): node = data.get('node') vmid = int(data.get('vmid')) vm_name = data.get('vm_name', f'VM-{vmid}') + host_key = data.get('host') + + # 호슀트 μ„€μ • κ°€μ Έμ˜€κΈ° + current_host_key, current_host_config = get_host_config(host_key) # Proxmox ν΄λΌμ΄μ–ΈνŠΈ 생성 및 둜그인 - client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD) + client = ProxmoxClient( + current_host_config['host'], + current_host_config['username'], + current_host_config['password'], + port=current_host_config['port'] + ) if not client.login(): - return jsonify({'error': 'Proxmox μ„œλ²„ 둜그인 μ‹€νŒ¨'}), 500 + return jsonify({'error': f'Proxmox μ„œλ²„({current_host_config["name"]}) 둜그인 μ‹€νŒ¨'}), 500 # VM μƒνƒœ 확인 vm_status = client.get_vm_status(node, vmid) @@ -553,6 +614,7 @@ def create_app(config_name=None): 'websocket_url': vnc_data['websocket_url'], 'password': vnc_data.get('password', ''), # VNC νŒ¨μŠ€μ›Œλ“œ μΆ”κ°€ 'vm_status': vm_status.get('status', 'unknown'), # VM μƒνƒœ μΆ”κ°€ + 'host_key': current_host_key, # 호슀트 ν‚€ μ €μž₯ 'created_at': datetime.now() } @@ -635,6 +697,57 @@ def create_app(config_name=None): print(f"❌ VNC μ½˜μ†” 였λ₯˜: {e}") return render_template('error.html', error=str(e)), 500 + @app.route('/vnc//proxy') + def vnc_console_proxy(session_id): + """VNC μ½˜μ†” νŽ˜μ΄μ§€ (Socket.IO ν”„λ‘μ‹œ 버전)""" + try: + # μ„Έμ…˜ 확인 + if session_id not in vnc_sessions: + return render_template('error.html', + error='μœ νš¨ν•˜μ§€ μ•Šμ€ VNC μ„Έμ…˜μž…λ‹ˆλ‹€.'), 404 + + session_data = vnc_sessions[session_id] + + # Socket.IO 기반 VNC ν”„λ‘μ‹œ μ‚¬μš© + return render_template('vnc_proxy.html', + vm_name=session_data['vm_name'], + vmid=session_data['vmid'], + node=session_data['node'], + session_id=session_id) + + except Exception as e: + print(f"❌ VNC ν”„λ‘μ‹œ μ½˜μ†” 였λ₯˜: {e}") + return render_template('error.html', error=str(e)), 500 + + @app.route('/vnc//ssl-help') + def vnc_ssl_help(session_id): + """VNC SSL μΈμ¦μ„œ 도움말 νŽ˜μ΄μ§€""" + try: + # μ„Έμ…˜ 확인 + if session_id not in vnc_sessions: + return render_template('error.html', + error='μœ νš¨ν•˜μ§€ μ•Šμ€ VNC μ„Έμ…˜μž…λ‹ˆλ‹€.'), 404 + + session_data = vnc_sessions[session_id] + host_key = session_data.get('host_key', 'pve7.0bin.in') + + # 호슀트 μ„€μ • κ°€μ Έμ˜€κΈ° + current_host_key, current_host_config = get_host_config(host_key) + + return render_template('vnc_ssl_help.html', + vm_name=session_data['vm_name'], + vmid=session_data['vmid'], + node=session_data['node'], + session_id=session_id, + websocket_url=session_data['websocket_url'], + proxmox_host=current_host_config['host'], + proxmox_port=current_host_config['port'], + host_key=current_host_key) + + except Exception as e: + print(f"❌ VNC SSL 도움말 였λ₯˜: {e}") + return render_template('error.html', error=str(e)), 500 + @app.route('/api/vm/start', methods=['POST']) def api_vm_start(): """VM μ‹œμž‘ API""" @@ -642,11 +755,20 @@ def create_app(config_name=None): data = request.get_json() node = data.get('node') vmid = int(data.get('vmid')) + host_key = data.get('host') + + # 호슀트 μ„€μ • κ°€μ Έμ˜€κΈ° + current_host_key, current_host_config = get_host_config(host_key) # Proxmox ν΄λΌμ΄μ–ΈνŠΈ 생성 및 둜그인 - client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD) + client = ProxmoxClient( + current_host_config['host'], + current_host_config['username'], + current_host_config['password'], + port=current_host_config['port'] + ) if not client.login(): - return jsonify({'error': 'Proxmox μ„œλ²„ 둜그인 μ‹€νŒ¨'}), 500 + return jsonify({'error': f'Proxmox μ„œλ²„({current_host_config["name"]}) 둜그인 μ‹€νŒ¨'}), 500 # VM μ‹œμž‘ success = client.start_vm(node, vmid) @@ -666,11 +788,20 @@ def create_app(config_name=None): data = request.get_json() node = data.get('node') vmid = int(data.get('vmid')) + host_key = data.get('host') + + # 호슀트 μ„€μ • κ°€μ Έμ˜€κΈ° + current_host_key, current_host_config = get_host_config(host_key) # Proxmox ν΄λΌμ΄μ–ΈνŠΈ 생성 및 둜그인 - client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD) + client = ProxmoxClient( + current_host_config['host'], + current_host_config['username'], + current_host_config['password'], + port=current_host_config['port'] + ) if not client.login(): - return jsonify({'error': 'Proxmox μ„œλ²„ 둜그인 μ‹€νŒ¨'}), 500 + return jsonify({'error': f'Proxmox μ„œλ²„({current_host_config["name"]}) 둜그인 μ‹€νŒ¨'}), 500 # VM μ •μ§€ success = client.stop_vm(node, vmid) @@ -1430,24 +1561,84 @@ def create_app(config_name=None): error='λ‚΄λΆ€ μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.', error_code=500), 500 - return app + # VNC WebSocket ν”„λ‘μ‹œ ν•Έλ“€λŸ¬ + @socketio.on('vnc_connect') + def handle_vnc_connect(data): + """VNC WebSocket ν”„λ‘μ‹œ μ—°κ²° ν•Έλ“€λŸ¬""" + print(f"πŸ”Œ VNC ν”„λ‘μ‹œ μ—°κ²° μš”μ²­: {data}") + + try: + vm_id = data.get('vm_id') + node = data.get('node', 'pve7') + + if not vm_id: + emit('vnc_error', {'error': 'VM IDκ°€ ν•„μš”ν•©λ‹ˆλ‹€.'}) + return + + # VNC ν”„λ‘μ‹œ κ°€μ Έμ˜€κΈ° + vnc_proxy = get_vnc_proxy() + if not vnc_proxy: + emit('vnc_error', {'error': 'VNC ν”„λ‘μ‹œκ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.'}) + return + + # 비동기 VNC ν”„λ‘μ‹œ μ‹œμž‘μ„ 별도 μŠ€λ ˆλ“œμ—μ„œ μ‹€ν–‰ + def run_vnc_proxy(): + # κ°„λ‹¨ν•œ 동기 λ²„μ „μœΌλ‘œ μ‹œμž‘ - μ‹€μ œ WebSocket μ€‘κ³„λŠ” λ‚˜μ€‘μ— κ΅¬ν˜„ + try: + # VNC μ—°κ²° 정보 생성 ν…ŒμŠ€νŠΈ + client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD) + if not client.login(): + socketio.emit('vnc_error', {'error': 'Proxmox 둜그인 μ‹€νŒ¨'}) + return + + vnc_data = client.get_vnc_ticket(node, vm_id) + if vnc_data: + socketio.emit('vnc_ready', { + 'vm_id': vm_id, + 'node': node, + 'websocket_url': vnc_data['websocket_url'], + 'password': vnc_data['password'] + }) + else: + socketio.emit('vnc_error', {'error': 'VNC ν‹°μΌ“ 생성 μ‹€νŒ¨'}) + + except Exception as e: + print(f"❌ VNC ν”„λ‘μ‹œ 였λ₯˜: {e}") + socketio.emit('vnc_error', {'error': str(e)}) + + # 별도 μŠ€λ ˆλ“œμ—μ„œ μ‹€ν–‰ + threading.Thread(target=run_vnc_proxy, daemon=True).start() + + except Exception as e: + print(f"❌ VNC μ—°κ²° ν•Έλ“€λŸ¬ 였λ₯˜: {e}") + emit('vnc_error', {'error': str(e)}) + + @socketio.on('disconnect') + def handle_disconnect(): + """WebSocket μ—°κ²° μ’…λ£Œ ν•Έλ“€λŸ¬""" + print('πŸ”Œ ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²° μ’…λ£Œ') + + return app, socketio # 개발 μ„œλ²„ μ‹€ν–‰ if __name__ == '__main__': - app = create_app() + app, socketio = create_app() # 개발 ν™˜κ²½μ—μ„œλ§Œ μ‹€ν–‰ if app.config.get('DEBUG'): - print("πŸš€ Starting FARMQ Admin System...") + print("πŸš€ Starting FARMQ Admin System with WebSocket Support...") print(f"πŸ“Š Dashboard: http://localhost:5001") print(f"πŸ₯ Pharmacy Management: http://localhost:5001/pharmacy") print(f"πŸ’» Machine Management: http://localhost:5001/machines") print(f"πŸ–₯️ VM Management (VNC): http://localhost:5001/vms") + print(f"πŸ”Œ WebSocket VNC Proxy: ws://localhost:5001/socket.io/") print("─" * 60) - app.run( + socketio.run( + app, host='0.0.0.0', port=5001, debug=True, - use_reloader=True + use_reloader=True, + allow_unsafe_werkzeug=True ) \ No newline at end of file diff --git a/farmq-admin/flask-venv/bin/websockets b/farmq-admin/flask-venv/bin/websockets new file mode 100755 index 0000000..ccc98b1 --- /dev/null +++ b/farmq-admin/flask-venv/bin/websockets @@ -0,0 +1,8 @@ +#!/srv/headscale-setup/farmq-admin/flask-venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from websockets.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/farmq-admin/templates/vm_list.html b/farmq-admin/templates/vm_list.html index 9feee66..a0f1d5b 100644 --- a/farmq-admin/templates/vm_list.html +++ b/farmq-admin/templates/vm_list.html @@ -77,12 +77,34 @@

Proxmox VM 관리

-
+
+ + + - - {{ host }} + + + + {{ current_host_info.host }}{% if current_host_info.port != 443 %}:{{ current_host_info.port }}{% endif %}
@@ -271,7 +293,8 @@ body: JSON.stringify({ node: node, vmid: vmid, - vm_name: vmName + vm_name: vmName, + host: new URLSearchParams(window.location.search).get('host') || 'pve7.0bin.in' }) }); @@ -312,7 +335,8 @@ }, body: JSON.stringify({ node: node, - vmid: vmid + vmid: vmid, + host: new URLSearchParams(window.location.search).get('host') || 'pve7.0bin.in' }) }); @@ -345,7 +369,8 @@ }, body: JSON.stringify({ node: node, - vmid: vmid + vmid: vmid, + host: new URLSearchParams(window.location.search).get('host') || 'pve7.0bin.in' }) }); @@ -375,6 +400,13 @@ location.reload(); }; + // 호슀트 λ³€κ²½ + function changeHost(hostKey) { + showSpinner(); + showToast('호슀트 λ³€κ²½', `${hostKey} 호슀트둜 μ—°κ²° 쀑...`, 'info'); + // URL을 톡해 νŽ˜μ΄μ§€ 이동 (이미 href에 μ„€μ •λ˜μ–΄ 있음) + }; + // μŠ€ν”Όλ„ˆ ν‘œμ‹œ/μˆ¨κΉ€ function showSpinner() { document.querySelector('.loading-spinner').style.display = 'block'; diff --git a/farmq-admin/templates/vnc_proxy.html b/farmq-admin/templates/vnc_proxy.html new file mode 100644 index 0000000..aca2061 --- /dev/null +++ b/farmq-admin/templates/vnc_proxy.html @@ -0,0 +1,242 @@ + + + + + + {{ vm_name }} - VNC μ½˜μ†” (ν”„λ‘μ‹œ) + + + + + +
+
λ‘œλ”© 쀑...
+
Ctrl+Alt+Del 전솑
+
VNC μ—°κ²°
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/farmq-admin/templates/vnc_simple.html b/farmq-admin/templates/vnc_simple.html index 9f65f71..3187dfd 100644 --- a/farmq-admin/templates/vnc_simple.html +++ b/farmq-admin/templates/vnc_simple.html @@ -80,7 +80,6 @@ status("연결이 μ •μƒμ μœΌλ‘œ μ’…λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€"); } else { const reason = e.detail.reason || 'Unknown'; - status(`μ—°κ²° μ‹€νŒ¨: ${reason} (Code: ${e.detail.code || 'Unknown'})`); console.error('❌ VNC μ—°κ²° μ‹€νŒ¨ 상세:', { code: e.detail.code, reason: e.detail.reason, @@ -89,7 +88,7 @@ // WebSocket μ—λŸ¬ μ½”λ“œλ³„ λ©”μ‹œμ§€ const errorMessages = { - 1006: 'WebSocket μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€. VM이 싀행쀑인지 ν™•μΈν•˜μ„Έμš”.', + 1006: 'WebSocket μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€. SSL μΈμ¦μ„œλ₯Ό ν™•μΈν•˜μ„Έμš”.', 1000: 'μ •μƒμ μœΌλ‘œ 연결이 μ’…λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.', 1002: 'ν”„λ‘œν† μ½œ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.', 1003: 'μ§€μ›ν•˜μ§€ μ•ŠλŠ” 데이터λ₯Ό λ°›μ•˜μŠ΅λ‹ˆλ‹€.', @@ -99,6 +98,14 @@ const userFriendlyMessage = errorMessages[e.detail.code] || `μ•Œ 수 μ—†λŠ” 였λ₯˜ (μ½”λ“œ: ${e.detail.code})`; status(`❌ ${userFriendlyMessage}`); + + // SSL μΈμ¦μ„œ 문제일 κ°€λŠ₯성이 높은 경우 SSL 도움말 νŽ˜μ΄μ§€λ‘œ 이동 + if (e.detail.code === 1006 || !e.detail.clean) { + setTimeout(() => { + const sessionId = window.location.pathname.split('/').pop(); + window.location.href = `/vnc/${sessionId}/ssl-help`; + }, 5000); // 5초 ν›„ μ΄λ™ν•˜μ—¬ μ‚¬μš©μžκ°€ λ©”μ‹œμ§€λ₯Ό 읽을 μ‹œκ°„ 제곡 + } } } diff --git a/farmq-admin/templates/vnc_ssl_help.html b/farmq-admin/templates/vnc_ssl_help.html new file mode 100644 index 0000000..10c8c7f --- /dev/null +++ b/farmq-admin/templates/vnc_ssl_help.html @@ -0,0 +1,291 @@ + + + + + + SSL μΈμ¦μ„œ 문제 ν•΄κ²° - {{ vm_name }} + + + + + + + + +
+
+
+ +

SSL μΈμ¦μ„œ μ‹ λ’° 섀정이 ν•„μš”ν•©λ‹ˆλ‹€

+

{{ vm_name }} VNC 연결을 μœ„ν•΄ Proxmox μ„œλ²„μ˜ SSL μΈμ¦μ„œλ₯Ό μ‹ λ’°ν•΄μ•Ό ν•©λ‹ˆλ‹€.

+
+ +
+
+ + μ—°κ²° μ‹€νŒ¨ 원인: λΈŒλΌμš°μ €κ°€ Proxmox μ„œλ²„μ˜ 자체 μ„œλͺ…λœ SSL μΈμ¦μ„œλ₯Ό μ‹ λ’°ν•˜μ§€ μ•Šμ•„ WebSocket 연결이 μ°¨λ‹¨λ˜κ³  μžˆμŠ΅λ‹ˆλ‹€. +
+ +
+
+
1
+ SSL μΈμ¦μ„œ μ‹ λ’° μ„€μ • +
+
+

μ•„λž˜ 링크λ₯Ό μƒˆ νƒ­μ—μ„œ μ—΄μ–΄ Proxmox μ„œλ²„μ˜ SSL μΈμ¦μ„œλ₯Ό μ‹ λ’°ν•˜λ„λ‘ μ„€μ •ν•˜μ„Έμš”:

+ + + +
+
λΈŒλΌμš°μ €λ³„ μ„€μ • 방법:
+
    +
  • Chrome/Edge: "κ³ κΈ‰" β†’ "{{ proxmox_host }}(으)둜 이동(μ•ˆμ „ν•˜μ§€ μ•ŠμŒ)" 클릭
  • +
  • Firefox: "κ³ κΈ‰" β†’ "μœ„ν—˜μ„ κ°μˆ˜ν•˜κ³  계속" 클릭
  • +
  • Safari: "μ„ΈλΆ€ 정보 ν‘œμ‹œ" β†’ "μ›Ή μ‚¬μ΄νŠΈ λ°©λ¬Έ" 클릭
  • +
+
+
+
+ +
+
+
2
+ Proxmox μ›Ή μΈν„°νŽ˜μ΄μŠ€ 확인 +
+
+

링크λ₯Ό ν΄λ¦­ν•˜λ©΄ Proxmox VE μ›Ή μΈν„°νŽ˜μ΄μŠ€κ°€ ν‘œμ‹œλ©λ‹ˆλ‹€. λ‘œκ·ΈμΈν•  ν•„μš”λŠ” μ—†μœΌλ©°, νŽ˜μ΄μ§€κ°€ μ •μƒμ μœΌλ‘œ λ‘œλ“œλ˜λ©΄ SSL μΈμ¦μ„œ μ‹ λ’° 섀정이 μ™„λ£Œλœ κ²ƒμž…λ‹ˆλ‹€.

+ +
+ + 성곡 확인: Proxmox 둜그인 νŽ˜μ΄μ§€κ°€ 보이면 μΈμ¦μ„œ μ‹ λ’° 섀정이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. +
+
+
+ +
+
+
3
+ VNC μ—°κ²° μž¬μ‹œλ„ +
+
+

SSL μΈμ¦μ„œ μ‹ λ’° 섀정이 μ™„λ£Œλ˜λ©΄, μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ VNC 연결을 λ‹€μ‹œ μ‹œλ„ν•˜μ„Έμš”:

+ +
+ + + +
+ +
+
+
+ +
+
+
4
+ 문제 ν•΄κ²° +
+
+

μœ„ 단계λ₯Ό μ™„λ£Œν–ˆλŠ”λ°λ„ 연결이 λ˜μ§€ μ•ŠλŠ”λ‹€λ©΄:

+
    +
  • λΈŒλΌμš°μ €λ₯Ό μ™„μ „νžˆ λ‹«κ³  λ‹€μ‹œ μ—΄μ–΄λ³΄μ„Έμš”
  • +
  • μ‹œν¬λ¦Ώ/μΈμ½”κ·Έλ‹ˆν†  λͺ¨λ“œμ—μ„œ μ‹œλ„ν•΄λ³΄μ„Έμš”
  • +
  • λ‹€λ₯Έ λΈŒλΌμš°μ €λ₯Ό μ‚¬μš©ν•΄λ³΄μ„Έμš”
  • +
  • λ°©ν™”λ²½μ΄λ‚˜ ν”„λ‘μ‹œ 섀정을 ν™•μΈν•˜μ„Έμš”
  • +
+ + +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/farmq-admin/test_multiple_proxmox.py b/farmq-admin/test_multiple_proxmox.py new file mode 100644 index 0000000..d3ff517 --- /dev/null +++ b/farmq-admin/test_multiple_proxmox.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +닀쀑 Proxmox μ„œλ²„ 접속 ν…ŒμŠ€νŠΈ +""" + +from utils.proxmox_client import ProxmoxClient +import json + +# Proxmox μ„œλ²„ μ„€μ • +PROXMOX_HOSTS = { + 'pve7.0bin.in': { + 'host': 'pve7.0bin.in', + 'username': 'root@pam', + 'password': 'trajet6640', + 'port': 443, + 'name': 'PVE 7.0 (Main)' + }, + 'healthport_pve': { + 'host': '100.64.0.6', + 'username': 'root@pam', + 'password': 'healthport', + 'port': 8006, + 'name': 'Healthport PVE' + } +} + +def test_proxmox_connection(host_key, host_config): + """Proxmox μ„œλ²„ μ—°κ²° ν…ŒμŠ€νŠΈ""" + print(f"\n{'='*50}") + print(f"πŸ” Testing connection to: {host_config['name']}") + print(f"πŸ“‘ Host: {host_config['host']}") + print(f"πŸ‘€ Username: {host_config['username']}") + print(f"{'='*50}") + + try: + # ProxmoxClient 생성 및 둜그인 μ‹œλ„ + client = ProxmoxClient( + host_config['host'], + host_config['username'], + host_config['password'], + port=host_config['port'] + ) + + print("πŸ” Attempting login...") + if client.login(): + print("βœ… Login successful!") + + # VM λͺ©λ‘ κ°€μ Έμ˜€κΈ° μ‹œλ„ + print("πŸ“‹ Fetching VM list...") + vms = client.get_vm_list() + + if vms: + print(f"βœ… Found {len(vms)} VMs:") + for vm in vms[:5]: # 처음 5개만 ν‘œμ‹œ + print(f" β€’ VM {vm.get('vmid', 'N/A')}: {vm.get('name', 'Unknown')} ({vm.get('status', 'unknown')})") + if len(vms) > 5: + print(f" ... and {len(vms) - 5} more VMs") + else: + print("⚠️ No VMs found or unable to fetch VM list") + + return True + + else: + print("❌ Login failed!") + return False + + except Exception as e: + print(f"❌ Connection error: {e}") + return False + +def main(): + """메인 ν…ŒμŠ€νŠΈ ν•¨μˆ˜""" + print("πŸš€ Multiple Proxmox Server Connection Test") + print("=" * 60) + + results = {} + + for host_key, host_config in PROXMOX_HOSTS.items(): + success = test_proxmox_connection(host_key, host_config) + results[host_key] = { + 'success': success, + 'config': host_config + } + + # κ²°κ³Ό μš”μ•½ + print(f"\n{'='*60}") + print("πŸ“Š TEST RESULTS SUMMARY") + print(f"{'='*60}") + + for host_key, result in results.items(): + status = "βœ… SUCCESS" if result['success'] else "❌ FAILED" + print(f"{result['config']['name']}: {status}") + + successful_hosts = [k for k, v in results.items() if v['success']] + print(f"\n🎯 {len(successful_hosts)}/{len(PROXMOX_HOSTS)} hosts connected successfully") + + if successful_hosts: + print("\nβœ… Ready for multi-host implementation!") + else: + print("\n⚠️ No hosts connected. Check network and credentials.") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/farmq-admin/test_vnc_websocket.py b/farmq-admin/test_vnc_websocket.py new file mode 100644 index 0000000..4266145 --- /dev/null +++ b/farmq-admin/test_vnc_websocket.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +""" +VNC WebSocket μ—°κ²° ν…ŒμŠ€νŠΈ 슀크립트 +Claude Code ν™˜κ²½μ—μ„œ 직접 ν…ŒμŠ€νŠΈ +""" + +import asyncio +import websockets +import ssl +import json +import sys +import os + +# Flask app.py와 λ™μΌν•œ κ²½λ‘œμ—μ„œ import +sys.path.append('/srv/headscale-setup/farmq-admin') +from utils.proxmox_client import ProxmoxClient + +# μ„€μ • +PROXMOX_HOST = "pve7.0bin.in" +PROXMOX_USERNAME = "root@pam" +PROXMOX_PASSWORD = "trajet6640" +VM_ID = 102 +NODE_NAME = 'pve7' + +async def test_vnc_websocket(): + """VNC WebSocket μ—°κ²° ν…ŒμŠ€νŠΈ""" + print("=" * 60) + print("πŸ§ͺ Claude Codeμ—μ„œ VNC WebSocket μ—°κ²° ν…ŒμŠ€νŠΈ") + print("=" * 60) + + # 1. Proxmox ν΄λΌμ΄μ–ΈνŠΈ 생성 및 둜그인 + print("1️⃣ Proxmox 둜그인 쀑...") + client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD) + + if not client.login(): + print("❌ Proxmox 둜그인 μ‹€νŒ¨") + return + print("βœ… Proxmox 둜그인 성곡") + + # 2. VM μƒνƒœ 확인 + print("2️⃣ VM μƒνƒœ 확인 쀑...") + vm_status = client.get_vm_status(NODE_NAME, VM_ID) + print(f"πŸ” VM {VM_ID} μƒνƒœ: {vm_status.get('status', 'unknown')}") + + if vm_status.get('status') != 'running': + print(f"❌ VM이 μ‹€ν–‰ 쀑이 μ•„λ‹™λ‹ˆλ‹€: {vm_status.get('status')}") + return + + # 3. VNC ν‹°μΌ“ 생성 + print("3️⃣ VNC ν‹°μΌ“ 생성 쀑...") + vnc_data = client.get_vnc_ticket(NODE_NAME, VM_ID) + + if not vnc_data: + print("❌ VNC ν‹°μΌ“ 생성 μ‹€νŒ¨") + return + + print("βœ… VNC ν‹°μΌ“ 생성 성곡!") + print(f" - WebSocket URL: {vnc_data['websocket_url']}") + print(f" - VNC νŒ¨μŠ€μ›Œλ“œ: {vnc_data.get('password', 'N/A')}") + print(f" - 포트: {vnc_data.get('port', 'N/A')}") + + # 4. WebSocket μ—°κ²° ν…ŒμŠ€νŠΈ + print("4️⃣ WebSocket μ—°κ²° ν…ŒμŠ€νŠΈ...") + websocket_url = vnc_data['websocket_url'] + + # SSL μ»¨ν…μŠ€νŠΈ μ„€μ • (자체 μ„œλͺ… μΈμ¦μ„œ ν—ˆμš©) + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + try: + # WebSocket μ—°κ²° μ‹œλ„ + print(f"πŸ”Œ μ—°κ²° μ‹œλ„: {websocket_url}") + + # Proxmox 인증 μΏ ν‚€λ₯Ό ν—€λ”λ‘œ μΆ”κ°€ + headers = { + 'Cookie': f'PVEAuthCookie={client.ticket}' + } + + print(f"πŸ” 인증 μΏ ν‚€ μΆ”κ°€: PVEAuthCookie={client.ticket[:50]}...") + + # WebSocket μ—°κ²° μ‹œ 인증 헀더 μΆ”κ°€ (λ‹€λ₯Έ 방식) + async with websockets.connect( + websocket_url, + ssl=ssl_context, + additional_headers=headers + ) as websocket: + print("βœ… WebSocket μ—°κ²° 성곡!") + + # VNC ν”„λ‘œν† μ½œ 초기 λ©”μ‹œμ§€ λ°›κΈ° + try: + initial_message = await asyncio.wait_for(websocket.recv(), timeout=5.0) + print(f"πŸ“¨ 초기 λ©”μ‹œμ§€ μˆ˜μ‹  ({len(initial_message)} bytes)") + + # VNC ν”„λ‘œν† μ½œ 버전 확인 + if isinstance(initial_message, bytes) and initial_message.startswith(b'RFB'): + version = initial_message.decode('ascii').strip() + print(f"πŸ”— VNC ν”„λ‘œν† μ½œ 버전: {version}") + + # ν΄λΌμ΄μ–ΈνŠΈ 버전 응닡 + await websocket.send(b"RFB 003.008\n") + print("πŸ“€ ν΄λΌμ΄μ–ΈνŠΈ 버전 응닡 μ™„λ£Œ") + + print("πŸŽ‰ VNC WebSocket μ—°κ²° 및 ν”„λ‘œν† μ½œ ν•Έλ“œμ…°μ΄ν¬ 성곡!") + return True + else: + print(f"❓ μ˜ˆμƒκ³Ό λ‹€λ₯Έ 초기 λ©”μ‹œμ§€: {initial_message[:50]}...") + + except asyncio.TimeoutError: + print("⏰ 초기 λ©”μ‹œμ§€ μˆ˜μ‹  νƒ€μž„μ•„μ›ƒ - 연결은 μ„±κ³΅ν–ˆμ§€λ§Œ VNC μ„œλ²„ 응닡 μ—†μŒ") + return True # μ—°κ²° μžμ²΄λŠ” 성곡 + + except websockets.exceptions.ConnectionClosed as e: + print(f"❌ WebSocket μ—°κ²° μ’…λ£Œ: μ½”λ“œ={e.code}, 이유={e.reason}") + return False + + except websockets.exceptions.WebSocketException as e: + print(f"❌ WebSocket μ˜ˆμ™Έ: {e}") + return False + + except asyncio.TimeoutError: + print("❌ WebSocket μ—°κ²° νƒ€μž„μ•„μ›ƒ") + return False + + except Exception as e: + print(f"❌ μ˜ˆμƒμΉ˜ λͺ»ν•œ 였λ₯˜: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + result = asyncio.run(test_vnc_websocket()) + + print("=" * 60) + if result: + print("πŸŽ‰ ν…ŒμŠ€νŠΈ 성곡! WebSocket 연결이 Claude Code ν™˜κ²½μ—μ„œ μž‘λ™ν•©λ‹ˆλ‹€.") + print("πŸ’‘ λΈŒλΌμš°μ €μ—μ„œ λ¬Έμ œκ°€ μžˆλ‹€λ©΄ λΈŒλΌμš°μ € λ³΄μ•ˆ μ •μ±… 문제일 κ°€λŠ₯성이 λ†’μŠ΅λ‹ˆλ‹€.") + else: + print("❌ ν…ŒμŠ€νŠΈ μ‹€νŒ¨! WebSocket 연결에 λ¬Έμ œκ°€ μžˆμŠ΅λ‹ˆλ‹€.") + print("πŸ” Proxmox μ„œλ²„λ‚˜ λ„€νŠΈμ›Œν¬ 섀정을 확인해야 ν•©λ‹ˆλ‹€.") + print("=" * 60) \ No newline at end of file diff --git a/farmq-admin/utils/proxmox_client.py b/farmq-admin/utils/proxmox_client.py index 6af8b67..53adbc9 100644 --- a/farmq-admin/utils/proxmox_client.py +++ b/farmq-admin/utils/proxmox_client.py @@ -13,12 +13,19 @@ from typing import Dict, List, Optional, Tuple urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class ProxmoxClient: - def __init__(self, host: str, username: str = "root@pam", password: str = "", api_token: str = ""): - self.host = host + def __init__(self, host: str, username: str = "root@pam", password: str = "", api_token: str = "", port: int = 8006): + # ν˜ΈμŠ€νŠΈμ—μ„œ ν¬νŠΈκ°€ ν¬ν•¨λœ 경우 뢄리 + if ':' in host: + self.host, port_str = host.split(':') + self.port = int(port_str) + else: + self.host = host + self.port = port + self.username = username self.password = password self.api_token = api_token - self.base_url = f"https://{host}:443/api2/json" + self.base_url = f"https://{self.host}:{self.port}/api2/json" self.session = requests.Session() self.session.verify = False self.ticket = None @@ -134,14 +141,14 @@ class ProxmoxClient: vnc_data = response.json()['data'] print(f"βœ… VNC ν‹°μΌ“ 생성 성곡: {vnc_data}") - # WebSocket URL 생성 (인증 토큰 포함) + # WebSocket URL 생성 (동적 포트 및 CSRF 토큰 포함) encoded_ticket = quote_plus(vnc_data['ticket']) # Proxmox μ„Έμ…˜ 쿠킀도 ν•¨κ»˜ 포함 (CSRFPreventionToken도 ν•„μš”ν•  수 있음) csrf_token = getattr(self, 'csrf_token', None) if csrf_token: - vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}&CSRFPreventionToken={csrf_token}" + vnc_data['websocket_url'] = f"wss://{self.host}:{self.port}/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}&CSRFPreventionToken={csrf_token}" else: - vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}" + vnc_data['websocket_url'] = f"wss://{self.host}:{self.port}/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}" # 디버깅 정보 μΆ”κ°€ print(f"πŸ”— WebSocket URL: {vnc_data['websocket_url']}") diff --git a/farmq-admin/utils/vnc_proxy.py b/farmq-admin/utils/vnc_proxy.py new file mode 100644 index 0000000..bea8d43 --- /dev/null +++ b/farmq-admin/utils/vnc_proxy.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +VNC WebSocket ν”„λ‘μ‹œ +λΈŒλΌμš°μ €μ™€ Proxmox VNC μ„œλ²„ κ°„ WebSocket 연결을 μ€‘κ³„ν•˜λ©° +PVE 인증을 μžλ™μœΌλ‘œ μ²˜λ¦¬ν•©λ‹ˆλ‹€. +""" + +import asyncio +import websockets +import ssl +import logging +from utils.proxmox_client import ProxmoxClient + +# λ‘œκΉ… μ„€μ • +logger = logging.getLogger(__name__) + +class VNCWebSocketProxy: + def __init__(self, proxmox_host, proxmox_username, proxmox_password): + self.proxmox_host = proxmox_host + self.proxmox_username = proxmox_username + self.proxmox_password = proxmox_password + self.proxmox_client = None + + async def create_proxmox_client(self): + """Proxmox ν΄λΌμ΄μ–ΈνŠΈ 생성 및 둜그인""" + if not self.proxmox_client: + self.proxmox_client = ProxmoxClient( + self.proxmox_host, + self.proxmox_username, + self.proxmox_password + ) + + if not self.proxmox_client.login(): + logger.error("Proxmox 둜그인 μ‹€νŒ¨") + return False + + logger.info("Proxmox 둜그인 성곡") + return True + + async def get_vnc_connection_info(self, node, vm_id): + """VNC μ—°κ²° 정보 생성""" + if not await self.create_proxmox_client(): + return None + + # VM μƒνƒœ 확인 + vm_status = self.proxmox_client.get_vm_status(node, vm_id) + if vm_status.get('status') != 'running': + logger.error(f"VM {vm_id}κ°€ μ‹€ν–‰ 쀑이 μ•„λ‹˜: {vm_status.get('status')}") + return None + + # VNC ν‹°μΌ“ 생성 + vnc_data = self.proxmox_client.get_vnc_ticket(node, vm_id) + if not vnc_data: + logger.error(f"VM {vm_id} VNC ν‹°μΌ“ 생성 μ‹€νŒ¨") + return None + + # WebSocket μ—°κ²° 정보 μ€€λΉ„ + connection_info = { + 'websocket_url': vnc_data['websocket_url'], + 'password': vnc_data['password'], + 'auth_headers': { + 'Cookie': f'PVEAuthCookie={self.proxmox_client.ticket}' + } + } + + logger.info(f"VM {vm_id} VNC μ—°κ²° 정보 생성 μ™„λ£Œ") + return connection_info + + async def create_proxmox_websocket(self, connection_info): + """Proxmox VNC WebSocket μ—°κ²° 생성""" + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + try: + websocket = await websockets.connect( + connection_info['websocket_url'], + ssl=ssl_context, + additional_headers=connection_info['auth_headers'] + ) + logger.info("Proxmox VNC WebSocket μ—°κ²° 성곡") + return websocket + + except Exception as e: + logger.error(f"Proxmox VNC WebSocket μ—°κ²° μ‹€νŒ¨: {e}") + return None + + async def proxy_data(self, browser_ws, proxmox_ws): + """λΈŒλΌμš°μ €μ™€ Proxmox κ°„ WebSocket 데이터 μ–‘λ°©ν–₯ 쀑계""" + async def forward_browser_to_proxmox(): + """λΈŒλΌμš°μ € β†’ Proxmox 데이터 전달""" + try: + async for message in browser_ws: + await proxmox_ws.send(message) + logger.debug(f"λΈŒλΌμš°μ € β†’ Proxmox: {len(message)} bytes") + except websockets.exceptions.ConnectionClosed: + logger.info("λΈŒλΌμš°μ € μ—°κ²° μ’…λ£Œ") + except Exception as e: + logger.error(f"λΈŒλΌμš°μ € β†’ Proxmox 전달 였λ₯˜: {e}") + + async def forward_proxmox_to_browser(): + """Proxmox β†’ λΈŒλΌμš°μ € 데이터 전달""" + try: + async for message in proxmox_ws: + await browser_ws.send(message) + logger.debug(f"Proxmox β†’ λΈŒλΌμš°μ €: {len(message)} bytes") + except websockets.exceptions.ConnectionClosed: + logger.info("Proxmox μ—°κ²° μ’…λ£Œ") + except Exception as e: + logger.error(f"Proxmox β†’ λΈŒλΌμš°μ € 전달 였λ₯˜: {e}") + + # μ–‘λ°©ν–₯ 데이터 전달을 λ³‘λ ¬λ‘œ μ‹€ν–‰ + await asyncio.gather( + forward_browser_to_proxmox(), + forward_proxmox_to_browser(), + return_exceptions=True + ) + + async def handle_vnc_proxy(self, browser_websocket, node, vm_id): + """VNC ν”„λ‘μ‹œ 메인 ν•Έλ“€λŸ¬""" + logger.info(f"VNC ν”„λ‘μ‹œ μ‹œμž‘: VM {vm_id}") + + try: + # 1. Proxmox VNC μ—°κ²° 정보 생성 + connection_info = await self.get_vnc_connection_info(node, vm_id) + if not connection_info: + await browser_websocket.send("ERROR: VNC μ—°κ²° 정보 생성 μ‹€νŒ¨") + return False + + # 2. Proxmox VNC WebSocket μ—°κ²° + proxmox_websocket = await self.create_proxmox_websocket(connection_info) + if not proxmox_websocket: + await browser_websocket.send("ERROR: Proxmox VNC μ—°κ²° μ‹€νŒ¨") + return False + + # 3. μ—°κ²° 성곡 μ•Œλ¦Ό + logger.info(f"VM {vm_id} VNC ν”„λ‘μ‹œ μ—°κ²° μ™„λ£Œ") + + # 4. 데이터 쀑계 μ‹œμž‘ + await self.proxy_data(browser_websocket, proxmox_websocket) + + return True + + except Exception as e: + logger.error(f"VNC ν”„λ‘μ‹œ 처리 였λ₯˜: {e}") + try: + await browser_websocket.send(f"ERROR: {str(e)}") + except: + pass + return False + + finally: + logger.info(f"VNC ν”„λ‘μ‹œ μ’…λ£Œ: VM {vm_id}") + + +# μ „μ—­ VNC ν”„λ‘μ‹œ μΈμŠ€ν„΄μŠ€ (섀정값은 app.pyμ—μ„œ μ£Όμž…) +vnc_proxy_instance = None + +def init_vnc_proxy(proxmox_host, proxmox_username, proxmox_password): + """VNC ν”„λ‘μ‹œ μΈμŠ€ν„΄μŠ€ μ΄ˆκΈ°ν™”""" + global vnc_proxy_instance + vnc_proxy_instance = VNCWebSocketProxy(proxmox_host, proxmox_username, proxmox_password) + logger.info("VNC WebSocket ν”„λ‘μ‹œ μ΄ˆκΈ°ν™” μ™„λ£Œ") + +def get_vnc_proxy(): + """VNC ν”„λ‘μ‹œ μΈμŠ€ν„΄μŠ€ λ°˜ν™˜""" + global vnc_proxy_instance + return vnc_proxy_instance \ No newline at end of file