Add multi-host Proxmox support with SSL certificate handling
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
ac620a0e15
commit
fb00b0a5fd
190
farmq-admin/Stable_PVE_Authentication_Strategy.md
Normal file
190
farmq-admin/Stable_PVE_Authentication_Strategy.md
Normal file
@ -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/<vm_id>/proxy
|
||||||
|
@app.websocket('/vnc/<int:vm_id>/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/<int:vm_id>/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 프록시를 통해 브라우저 환경에서도 안정적으로 구현하자는 것이 이 전략의 핵심입니다.
|
||||||
249
farmq-admin/VNC_Implementation_Technical_Documentation.md
Normal file
249
farmq-admin/VNC_Implementation_Technical_Documentation.md
Normal file
@ -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/<int:vm_id>`: 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**: 최신 버전
|
||||||
161
farmq-admin/VNC_Network_Architecture_Issue.md
Normal file
161
farmq-admin/VNC_Network_Architecture_Issue.md
Normal file
@ -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에 접근할 수 있도록 하는 것이 가장 현실적인 해결책입니다.
|
||||||
207
farmq-admin/VNC_WebSocket_Connection_Issue_Resolution.md
Normal file
207
farmq-admin/VNC_WebSocket_Connection_Issue_Resolution.md
Normal file
@ -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 서버에서 발생했습니다.
|
||||||
@ -5,11 +5,14 @@ Headscale + Headplane 고도화 관리자 페이지
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Flask, render_template, jsonify, request, redirect, url_for
|
from flask import Flask, render_template, jsonify, request, redirect, url_for
|
||||||
|
from flask_socketio import SocketIO, emit, disconnect
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
from config import config
|
from config import config
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
from utils.database_new import (
|
from utils.database_new import (
|
||||||
init_databases, get_farmq_session,
|
init_databases, get_farmq_session,
|
||||||
get_dashboard_stats, get_all_pharmacies_with_stats, get_all_machines_with_details,
|
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_
|
from sqlalchemy import or_
|
||||||
import subprocess
|
import subprocess
|
||||||
from utils.proxmox_client import ProxmoxClient
|
from utils.proxmox_client import ProxmoxClient
|
||||||
|
from utils.vnc_proxy import init_vnc_proxy, get_vnc_proxy
|
||||||
|
|
||||||
def create_app(config_name=None):
|
def create_app(config_name=None):
|
||||||
"""Flask 애플리케이션 팩토리"""
|
"""Flask 애플리케이션 팩토리"""
|
||||||
@ -29,6 +33,9 @@ def create_app(config_name=None):
|
|||||||
config_name = config_name or os.environ.get('FLASK_ENV', 'default')
|
config_name = config_name or os.environ.get('FLASK_ENV', 'default')
|
||||||
app.config.from_object(config[config_name])
|
app.config.from_object(config[config_name])
|
||||||
|
|
||||||
|
# SocketIO 초기화
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
||||||
|
|
||||||
# 데이터베이스 초기화
|
# 데이터베이스 초기화
|
||||||
init_databases(
|
init_databases(
|
||||||
headscale_db_uri='sqlite:////srv/headscale-setup/data/db.sqlite',
|
headscale_db_uri='sqlite:////srv/headscale-setup/data/db.sqlite',
|
||||||
@ -42,10 +49,43 @@ def create_app(config_name=None):
|
|||||||
# VNC 세션 관리 (메모리 기반)
|
# VNC 세션 관리 (메모리 기반)
|
||||||
vnc_sessions = {}
|
vnc_sessions = {}
|
||||||
|
|
||||||
# Proxmox 서버 설정
|
# 다중 Proxmox 서버 설정
|
||||||
PROXMOX_HOST = "pve7.0bin.in"
|
PROXMOX_HOSTS = {
|
||||||
PROXMOX_USERNAME = "root@pam"
|
'pve7.0bin.in': {
|
||||||
PROXMOX_PASSWORD = "trajet6640"
|
'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('/')
|
@app.route('/')
|
||||||
@ -492,11 +532,20 @@ def create_app(config_name=None):
|
|||||||
def vm_list():
|
def vm_list():
|
||||||
"""VM 목록 페이지"""
|
"""VM 목록 페이지"""
|
||||||
try:
|
try:
|
||||||
|
# 요청된 호스트 가져오기
|
||||||
|
requested_host = request.args.get('host')
|
||||||
|
current_host_key, current_host_config = get_host_config(requested_host)
|
||||||
|
|
||||||
# Proxmox 클라이언트 생성 및 로그인
|
# 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():
|
if not client.login():
|
||||||
return render_template('error.html',
|
return render_template('error.html',
|
||||||
error='Proxmox 서버에 연결할 수 없습니다.'), 500
|
error=f'Proxmox 서버({current_host_config["name"]})에 연결할 수 없습니다.'), 500
|
||||||
|
|
||||||
# VM 목록 가져오기
|
# VM 목록 가져오기
|
||||||
vms = client.get_vm_list()
|
vms = client.get_vm_list()
|
||||||
@ -509,7 +558,10 @@ def create_app(config_name=None):
|
|||||||
|
|
||||||
return render_template('vm_list.html',
|
return render_template('vm_list.html',
|
||||||
vms=vms,
|
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,
|
total_vms=total_vms,
|
||||||
running_vms=running_vms,
|
running_vms=running_vms,
|
||||||
stopped_vms=stopped_vms,
|
stopped_vms=stopped_vms,
|
||||||
@ -527,11 +579,20 @@ def create_app(config_name=None):
|
|||||||
node = data.get('node')
|
node = data.get('node')
|
||||||
vmid = int(data.get('vmid'))
|
vmid = int(data.get('vmid'))
|
||||||
vm_name = data.get('vm_name', f'VM-{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 클라이언트 생성 및 로그인
|
# 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():
|
if not client.login():
|
||||||
return jsonify({'error': 'Proxmox 서버 로그인 실패'}), 500
|
return jsonify({'error': f'Proxmox 서버({current_host_config["name"]}) 로그인 실패'}), 500
|
||||||
|
|
||||||
# VM 상태 확인
|
# VM 상태 확인
|
||||||
vm_status = client.get_vm_status(node, vmid)
|
vm_status = client.get_vm_status(node, vmid)
|
||||||
@ -553,6 +614,7 @@ def create_app(config_name=None):
|
|||||||
'websocket_url': vnc_data['websocket_url'],
|
'websocket_url': vnc_data['websocket_url'],
|
||||||
'password': vnc_data.get('password', ''), # VNC 패스워드 추가
|
'password': vnc_data.get('password', ''), # VNC 패스워드 추가
|
||||||
'vm_status': vm_status.get('status', 'unknown'), # VM 상태 추가
|
'vm_status': vm_status.get('status', 'unknown'), # VM 상태 추가
|
||||||
|
'host_key': current_host_key, # 호스트 키 저장
|
||||||
'created_at': datetime.now()
|
'created_at': datetime.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -635,6 +697,57 @@ def create_app(config_name=None):
|
|||||||
print(f"❌ VNC 콘솔 오류: {e}")
|
print(f"❌ VNC 콘솔 오류: {e}")
|
||||||
return render_template('error.html', error=str(e)), 500
|
return render_template('error.html', error=str(e)), 500
|
||||||
|
|
||||||
|
@app.route('/vnc/<session_id>/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/<session_id>/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'])
|
@app.route('/api/vm/start', methods=['POST'])
|
||||||
def api_vm_start():
|
def api_vm_start():
|
||||||
"""VM 시작 API"""
|
"""VM 시작 API"""
|
||||||
@ -642,11 +755,20 @@ def create_app(config_name=None):
|
|||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
node = data.get('node')
|
node = data.get('node')
|
||||||
vmid = int(data.get('vmid'))
|
vmid = int(data.get('vmid'))
|
||||||
|
host_key = data.get('host')
|
||||||
|
|
||||||
|
# 호스트 설정 가져오기
|
||||||
|
current_host_key, current_host_config = get_host_config(host_key)
|
||||||
|
|
||||||
# Proxmox 클라이언트 생성 및 로그인
|
# 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():
|
if not client.login():
|
||||||
return jsonify({'error': 'Proxmox 서버 로그인 실패'}), 500
|
return jsonify({'error': f'Proxmox 서버({current_host_config["name"]}) 로그인 실패'}), 500
|
||||||
|
|
||||||
# VM 시작
|
# VM 시작
|
||||||
success = client.start_vm(node, vmid)
|
success = client.start_vm(node, vmid)
|
||||||
@ -666,11 +788,20 @@ def create_app(config_name=None):
|
|||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
node = data.get('node')
|
node = data.get('node')
|
||||||
vmid = int(data.get('vmid'))
|
vmid = int(data.get('vmid'))
|
||||||
|
host_key = data.get('host')
|
||||||
|
|
||||||
|
# 호스트 설정 가져오기
|
||||||
|
current_host_key, current_host_config = get_host_config(host_key)
|
||||||
|
|
||||||
# Proxmox 클라이언트 생성 및 로그인
|
# 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():
|
if not client.login():
|
||||||
return jsonify({'error': 'Proxmox 서버 로그인 실패'}), 500
|
return jsonify({'error': f'Proxmox 서버({current_host_config["name"]}) 로그인 실패'}), 500
|
||||||
|
|
||||||
# VM 정지
|
# VM 정지
|
||||||
success = client.stop_vm(node, vmid)
|
success = client.stop_vm(node, vmid)
|
||||||
@ -1430,24 +1561,84 @@ def create_app(config_name=None):
|
|||||||
error='내부 서버 오류가 발생했습니다.',
|
error='내부 서버 오류가 발생했습니다.',
|
||||||
error_code=500), 500
|
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__':
|
if __name__ == '__main__':
|
||||||
app = create_app()
|
app, socketio = create_app()
|
||||||
|
|
||||||
# 개발 환경에서만 실행
|
# 개발 환경에서만 실행
|
||||||
if app.config.get('DEBUG'):
|
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"📊 Dashboard: http://localhost:5001")
|
||||||
print(f"🏥 Pharmacy Management: http://localhost:5001/pharmacy")
|
print(f"🏥 Pharmacy Management: http://localhost:5001/pharmacy")
|
||||||
print(f"💻 Machine Management: http://localhost:5001/machines")
|
print(f"💻 Machine Management: http://localhost:5001/machines")
|
||||||
print(f"🖥️ VM Management (VNC): http://localhost:5001/vms")
|
print(f"🖥️ VM Management (VNC): http://localhost:5001/vms")
|
||||||
|
print(f"🔌 WebSocket VNC Proxy: ws://localhost:5001/socket.io/")
|
||||||
print("─" * 60)
|
print("─" * 60)
|
||||||
|
|
||||||
app.run(
|
socketio.run(
|
||||||
|
app,
|
||||||
host='0.0.0.0',
|
host='0.0.0.0',
|
||||||
port=5001,
|
port=5001,
|
||||||
debug=True,
|
debug=True,
|
||||||
use_reloader=True
|
use_reloader=True,
|
||||||
|
allow_unsafe_werkzeug=True
|
||||||
)
|
)
|
||||||
8
farmq-admin/flask-venv/bin/websockets
Executable file
8
farmq-admin/flask-venv/bin/websockets
Executable file
@ -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())
|
||||||
@ -77,12 +77,34 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2><i class="fas fa-desktop"></i> Proxmox VM 관리</h2>
|
<h2><i class="fas fa-desktop"></i> Proxmox VM 관리</h2>
|
||||||
<div>
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<!-- 호스트 선택 드롭다운 -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="hostSelector" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fas fa-server"></i> {{ current_host_name or '호스트 선택' }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="hostSelector">
|
||||||
|
{% for host_key, host_info in available_hosts.items() %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if host_key == current_host_key %}active{% endif %}"
|
||||||
|
href="/vms?host={{ host_key }}"
|
||||||
|
onclick="changeHost('{{ host_key }}')">
|
||||||
|
<i class="fas fa-server me-2"></i>
|
||||||
|
<strong>{{ host_info.name }}</strong><br>
|
||||||
|
<small class="text-muted">{{ host_info.host }}{% if host_info.port != 443 %}:{{ host_info.port }}{% endif %}</small>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button id="refresh-btn" class="btn btn-outline-primary">
|
<button id="refresh-btn" class="btn btn-outline-primary">
|
||||||
<i class="fas fa-sync-alt"></i> 새로고침
|
<i class="fas fa-sync-alt"></i> 새로고침
|
||||||
</button>
|
</button>
|
||||||
<span class="text-muted ms-3">
|
|
||||||
<i class="fas fa-server"></i> {{ host }}
|
<span class="text-muted">
|
||||||
|
<i class="fas fa-network-wired"></i>
|
||||||
|
<small>{{ current_host_info.host }}{% if current_host_info.port != 443 %}:{{ current_host_info.port }}{% endif %}</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -271,7 +293,8 @@
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
node: node,
|
node: node,
|
||||||
vmid: vmid,
|
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({
|
body: JSON.stringify({
|
||||||
node: node,
|
node: node,
|
||||||
vmid: vmid
|
vmid: vmid,
|
||||||
|
host: new URLSearchParams(window.location.search).get('host') || 'pve7.0bin.in'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -345,7 +369,8 @@
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
node: node,
|
node: node,
|
||||||
vmid: vmid
|
vmid: vmid,
|
||||||
|
host: new URLSearchParams(window.location.search).get('host') || 'pve7.0bin.in'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -375,6 +400,13 @@
|
|||||||
location.reload();
|
location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 호스트 변경
|
||||||
|
function changeHost(hostKey) {
|
||||||
|
showSpinner();
|
||||||
|
showToast('호스트 변경', `${hostKey} 호스트로 연결 중...`, 'info');
|
||||||
|
// URL을 통해 페이지 이동 (이미 href에 설정되어 있음)
|
||||||
|
};
|
||||||
|
|
||||||
// 스피너 표시/숨김
|
// 스피너 표시/숨김
|
||||||
function showSpinner() {
|
function showSpinner() {
|
||||||
document.querySelector('.loading-spinner').style.display = 'block';
|
document.querySelector('.loading-spinner').style.display = 'block';
|
||||||
|
|||||||
242
farmq-admin/templates/vnc_proxy.html
Normal file
242
farmq-admin/templates/vnc_proxy.html
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ vm_name }} - VNC 콘솔 (프록시)</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: dimgrey;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top_bar {
|
||||||
|
background-color: #6e84a3;
|
||||||
|
color: white;
|
||||||
|
font: bold 12px Helvetica;
|
||||||
|
padding: 6px 5px 4px 5px;
|
||||||
|
border-bottom: 1px outset;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#sendCtrlAltDelButton {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
right: 120px;
|
||||||
|
border: 1px outset;
|
||||||
|
padding: 5px 5px 4px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connectButton {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
border: 1px outset;
|
||||||
|
padding: 5px 5px 4px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#screen {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
background-color: #ffe6e6;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #28a745;
|
||||||
|
background-color: #e6ffe6;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #28a745;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="top_bar">
|
||||||
|
<div id="status">로딩 중...</div>
|
||||||
|
<div id="sendCtrlAltDelButton">Ctrl+Alt+Del 전송</div>
|
||||||
|
<div id="connectButton">VNC 연결</div>
|
||||||
|
</div>
|
||||||
|
<div id="screen">
|
||||||
|
<div id="connectionMessages"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Socket.IO 클라이언트 -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
|
||||||
|
|
||||||
|
<!-- noVNC 라이브러리 -->
|
||||||
|
<script type="module" crossorigin="anonymous">
|
||||||
|
import RFB from '/static/novnc/core/rfb.js';
|
||||||
|
|
||||||
|
let rfb;
|
||||||
|
let desktopName;
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
// 상태 표시 함수
|
||||||
|
function status(text) {
|
||||||
|
document.getElementById('status').textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(message, type = 'info') {
|
||||||
|
const messagesDiv = document.getElementById('connectionMessages');
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = type;
|
||||||
|
messageDiv.textContent = message;
|
||||||
|
messagesDiv.appendChild(messageDiv);
|
||||||
|
|
||||||
|
// 5초 후 메시지 제거
|
||||||
|
setTimeout(() => {
|
||||||
|
if (messageDiv.parentNode) {
|
||||||
|
messageDiv.parentNode.removeChild(messageDiv);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// noVNC 이벤트 핸들러
|
||||||
|
function connectedToServer(e) {
|
||||||
|
status("연결됨");
|
||||||
|
showMessage("VNC 서버에 성공적으로 연결되었습니다.", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectedFromServer(e) {
|
||||||
|
if (e.detail.clean) {
|
||||||
|
status("연결 종료");
|
||||||
|
showMessage("VNC 연결이 정상적으로 종료되었습니다.", 'info');
|
||||||
|
} else {
|
||||||
|
status("연결 실패");
|
||||||
|
showMessage("VNC 연결이 예기치 않게 종료되었습니다.", 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function credentialsAreRequired(e) {
|
||||||
|
status("인증 필요");
|
||||||
|
showMessage("VNC 서버에서 추가 인증을 요구합니다.", 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDesktopName(e) {
|
||||||
|
desktopName = e.detail.name;
|
||||||
|
status("연결됨: " + desktopName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSecurityFailure(e) {
|
||||||
|
status("보안 인증 실패");
|
||||||
|
showMessage("VNC 보안 인증에 실패했습니다: " + e.detail.reason, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket.IO 연결 및 이벤트 핸들러
|
||||||
|
function initSocketIO() {
|
||||||
|
socket = io();
|
||||||
|
|
||||||
|
socket.on('connect', function() {
|
||||||
|
console.log('Socket.IO 연결됨');
|
||||||
|
status("서버 연결됨 - VNC 연결 대기 중");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', function() {
|
||||||
|
console.log('Socket.IO 연결 종료');
|
||||||
|
status("서버 연결 종료");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('vnc_ready', function(data) {
|
||||||
|
console.log('VNC 연결 준비 완료:', data);
|
||||||
|
status("VNC 연결 중...");
|
||||||
|
|
||||||
|
// noVNC로 직접 연결 (테스트용)
|
||||||
|
try {
|
||||||
|
const websocketUrl = data.websocket_url;
|
||||||
|
const password = data.password;
|
||||||
|
|
||||||
|
console.log('WebSocket URL:', websocketUrl);
|
||||||
|
console.log('VNC Password:', password);
|
||||||
|
|
||||||
|
rfb = new RFB(document.getElementById('screen'), websocketUrl,
|
||||||
|
{ credentials: { password: password } });
|
||||||
|
|
||||||
|
// 이벤트 리스너 등록
|
||||||
|
rfb.addEventListener("connect", connectedToServer);
|
||||||
|
rfb.addEventListener("disconnect", disconnectedFromServer);
|
||||||
|
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
|
||||||
|
rfb.addEventListener("desktopname", updateDesktopName);
|
||||||
|
rfb.addEventListener("securityfailure", onSecurityFailure);
|
||||||
|
|
||||||
|
showMessage("VNC 연결을 시도 중입니다...", 'info');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('VNC 연결 오류:', error);
|
||||||
|
showMessage("VNC 연결 중 오류가 발생했습니다: " + error.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('vnc_error', function(data) {
|
||||||
|
console.error('VNC 프록시 오류:', data);
|
||||||
|
status("VNC 연결 실패");
|
||||||
|
showMessage("VNC 연결 실패: " + data.error, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// VNC 연결 시작
|
||||||
|
function connectVNC() {
|
||||||
|
if (socket && socket.connected) {
|
||||||
|
showMessage("VNC 연결을 요청 중입니다...", 'info');
|
||||||
|
status("VNC 연결 요청 중...");
|
||||||
|
|
||||||
|
socket.emit('vnc_connect', {
|
||||||
|
vm_id: {{ vmid }},
|
||||||
|
node: '{{ node }}',
|
||||||
|
vm_name: '{{ vm_name }}'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showMessage("서버 연결이 필요합니다. 잠시 후 다시 시도해주세요.", 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+Alt+Del 전송
|
||||||
|
function sendCtrlAltDel() {
|
||||||
|
if (rfb) {
|
||||||
|
rfb.sendCtrlAltDel();
|
||||||
|
showMessage("Ctrl+Alt+Del을 전송했습니다.", 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이벤트 리스너 설정
|
||||||
|
document.getElementById('connectButton').onclick = connectVNC;
|
||||||
|
document.getElementById('sendCtrlAltDelButton').onclick = sendCtrlAltDel;
|
||||||
|
|
||||||
|
// 페이지 로드 시 Socket.IO 초기화
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
initSocketIO();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 페이지 언로드 시 연결 정리
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
if (rfb) {
|
||||||
|
rfb.disconnect();
|
||||||
|
}
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -80,7 +80,6 @@
|
|||||||
status("연결이 정상적으로 종료되었습니다");
|
status("연결이 정상적으로 종료되었습니다");
|
||||||
} else {
|
} else {
|
||||||
const reason = e.detail.reason || 'Unknown';
|
const reason = e.detail.reason || 'Unknown';
|
||||||
status(`연결 실패: ${reason} (Code: ${e.detail.code || 'Unknown'})`);
|
|
||||||
console.error('❌ VNC 연결 실패 상세:', {
|
console.error('❌ VNC 연결 실패 상세:', {
|
||||||
code: e.detail.code,
|
code: e.detail.code,
|
||||||
reason: e.detail.reason,
|
reason: e.detail.reason,
|
||||||
@ -89,7 +88,7 @@
|
|||||||
|
|
||||||
// WebSocket 에러 코드별 메시지
|
// WebSocket 에러 코드별 메시지
|
||||||
const errorMessages = {
|
const errorMessages = {
|
||||||
1006: 'WebSocket 서버에 연결할 수 없습니다. VM이 실행중인지 확인하세요.',
|
1006: 'WebSocket 서버에 연결할 수 없습니다. SSL 인증서를 확인하세요.',
|
||||||
1000: '정상적으로 연결이 종료되었습니다.',
|
1000: '정상적으로 연결이 종료되었습니다.',
|
||||||
1002: '프로토콜 오류가 발생했습니다.',
|
1002: '프로토콜 오류가 발생했습니다.',
|
||||||
1003: '지원하지 않는 데이터를 받았습니다.',
|
1003: '지원하지 않는 데이터를 받았습니다.',
|
||||||
@ -99,6 +98,14 @@
|
|||||||
|
|
||||||
const userFriendlyMessage = errorMessages[e.detail.code] || `알 수 없는 오류 (코드: ${e.detail.code})`;
|
const userFriendlyMessage = errorMessages[e.detail.code] || `알 수 없는 오류 (코드: ${e.detail.code})`;
|
||||||
status(`❌ ${userFriendlyMessage}`);
|
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초 후 이동하여 사용자가 메시지를 읽을 시간 제공
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
291
farmq-admin/templates/vnc_ssl_help.html
Normal file
291
farmq-admin/templates/vnc_ssl_help.html
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SSL 인증서 문제 해결 - {{ vm_name }}</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssl-help-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
margin: 50px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssl-help-header {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b, #ffd93d);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssl-help-body {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-card {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-box {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-box {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="ssl-help-container">
|
||||||
|
<div class="ssl-help-header">
|
||||||
|
<i class="fas fa-shield-alt fa-3x mb-3"></i>
|
||||||
|
<h2>SSL 인증서 신뢰 설정이 필요합니다</h2>
|
||||||
|
<p class="mb-0">{{ vm_name }} VNC 연결을 위해 Proxmox 서버의 SSL 인증서를 신뢰해야 합니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ssl-help-body">
|
||||||
|
<div class="warning-box">
|
||||||
|
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
|
||||||
|
<strong>연결 실패 원인:</strong> 브라우저가 Proxmox 서버의 자체 서명된 SSL 인증서를 신뢰하지 않아 WebSocket 연결이 차단되고 있습니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="step-header">
|
||||||
|
<div class="step-number">1</div>
|
||||||
|
SSL 인증서 신뢰 설정
|
||||||
|
</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<p>아래 링크를 <strong>새 탭</strong>에서 열어 Proxmox 서버의 SSL 인증서를 신뢰하도록 설정하세요:</p>
|
||||||
|
|
||||||
|
<div class="url-box">
|
||||||
|
<a href="https://{{ proxmox_host }}:{{ proxmox_port }}" target="_blank" id="proxmoxUrl">
|
||||||
|
https://{{ proxmox_host }}:{{ proxmox_port }}
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary btn-copy" onclick="copyUrl()">
|
||||||
|
<i class="fas fa-copy"></i> 복사
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6><i class="fas fa-info-circle text-info me-2"></i>브라우저별 설정 방법:</h6>
|
||||||
|
<ul class="list-unstyled ms-3">
|
||||||
|
<li><strong>Chrome/Edge:</strong> "고급" → "{{ proxmox_host }}(으)로 이동(안전하지 않음)" 클릭</li>
|
||||||
|
<li><strong>Firefox:</strong> "고급" → "위험을 감수하고 계속" 클릭</li>
|
||||||
|
<li><strong>Safari:</strong> "세부 정보 표시" → "웹 사이트 방문" 클릭</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="step-header">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
Proxmox 웹 인터페이스 확인
|
||||||
|
</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<p>링크를 클릭하면 Proxmox VE 웹 인터페이스가 표시됩니다. 로그인할 필요는 없으며, 페이지가 정상적으로 로드되면 SSL 인증서 신뢰 설정이 완료된 것입니다.</p>
|
||||||
|
|
||||||
|
<div class="success-box">
|
||||||
|
<i class="fas fa-check-circle text-success me-2"></i>
|
||||||
|
<strong>성공 확인:</strong> Proxmox 로그인 페이지가 보이면 인증서 신뢰 설정이 완료되었습니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="step-header">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
VNC 연결 재시도
|
||||||
|
</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<p>SSL 인증서 신뢰 설정이 완료되면, 아래 버튼을 클릭하여 VNC 연결을 다시 시도하세요:</p>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<button class="btn btn-primary btn-lg" onclick="retryVncConnection()">
|
||||||
|
<i class="fas fa-desktop me-2"></i>VNC 연결 재시도
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-outline-secondary btn-lg ms-3" onclick="testWebSocket()">
|
||||||
|
<i class="fas fa-wifi me-2"></i>연결 테스트
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="connectionStatus" class="mt-3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step-card">
|
||||||
|
<div class="step-header">
|
||||||
|
<div class="step-number">4</div>
|
||||||
|
문제 해결
|
||||||
|
</div>
|
||||||
|
<div class="step-content">
|
||||||
|
<p>위 단계를 완료했는데도 연결이 되지 않는다면:</p>
|
||||||
|
<ul>
|
||||||
|
<li>브라우저를 완전히 닫고 다시 열어보세요</li>
|
||||||
|
<li>시크릿/인코그니토 모드에서 시도해보세요</li>
|
||||||
|
<li>다른 브라우저를 사용해보세요</li>
|
||||||
|
<li>방화벽이나 프록시 설정을 확인하세요</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/vms?host={{ host_key }}" class="btn btn-outline-dark">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>VM 목록으로 돌아가기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// URL 복사 함수
|
||||||
|
function copyUrl() {
|
||||||
|
const url = document.getElementById('proxmoxUrl').href;
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
alert('URL이 클립보드에 복사되었습니다.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// VNC 연결 재시도
|
||||||
|
function retryVncConnection() {
|
||||||
|
const sessionId = '{{ session_id }}';
|
||||||
|
window.location.href = `/vnc/${sessionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket 연결 테스트
|
||||||
|
function testWebSocket() {
|
||||||
|
const statusDiv = document.getElementById('connectionStatus');
|
||||||
|
statusDiv.innerHTML = '<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 연결 테스트 중...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const websocketUrl = '{{ websocket_url }}';
|
||||||
|
const ws = new WebSocket(websocketUrl);
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ws.close();
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
<strong>연결 시간 초과</strong><br>
|
||||||
|
SSL 인증서 신뢰 설정을 먼저 완료해주세요.
|
||||||
|
</div>`;
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
ws.onopen = function() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.close();
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>
|
||||||
|
<strong>연결 성공!</strong><br>
|
||||||
|
VNC 연결 재시도 버튼을 클릭하세요.
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fas fa-times-circle me-2"></i>
|
||||||
|
<strong>연결 실패</strong><br>
|
||||||
|
SSL 인증서 신뢰 설정이 필요합니다.
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function(event) {
|
||||||
|
if (event.code !== 1000) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
<strong>연결 종료</strong><br>
|
||||||
|
SSL 인증서를 신뢰한 후 다시 시도해주세요.
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fas fa-times-circle me-2"></i>
|
||||||
|
<strong>테스트 오류</strong><br>
|
||||||
|
${error.message}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
103
farmq-admin/test_multiple_proxmox.py
Normal file
103
farmq-admin/test_multiple_proxmox.py
Normal file
@ -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()
|
||||||
141
farmq-admin/test_vnc_websocket.py
Normal file
141
farmq-admin/test_vnc_websocket.py
Normal file
@ -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)
|
||||||
@ -13,12 +13,19 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
class ProxmoxClient:
|
class ProxmoxClient:
|
||||||
def __init__(self, host: str, username: str = "root@pam", password: str = "", api_token: str = ""):
|
def __init__(self, host: str, username: str = "root@pam", password: str = "", api_token: str = "", port: int = 8006):
|
||||||
self.host = host
|
# 호스트에서 포트가 포함된 경우 분리
|
||||||
|
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.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.api_token = api_token
|
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 = requests.Session()
|
||||||
self.session.verify = False
|
self.session.verify = False
|
||||||
self.ticket = None
|
self.ticket = None
|
||||||
@ -134,14 +141,14 @@ class ProxmoxClient:
|
|||||||
vnc_data = response.json()['data']
|
vnc_data = response.json()['data']
|
||||||
print(f"✅ VNC 티켓 생성 성공: {vnc_data}")
|
print(f"✅ VNC 티켓 생성 성공: {vnc_data}")
|
||||||
|
|
||||||
# WebSocket URL 생성 (인증 토큰 포함)
|
# WebSocket URL 생성 (동적 포트 및 CSRF 토큰 포함)
|
||||||
encoded_ticket = quote_plus(vnc_data['ticket'])
|
encoded_ticket = quote_plus(vnc_data['ticket'])
|
||||||
# Proxmox 세션 쿠키도 함께 포함 (CSRFPreventionToken도 필요할 수 있음)
|
# Proxmox 세션 쿠키도 함께 포함 (CSRFPreventionToken도 필요할 수 있음)
|
||||||
csrf_token = getattr(self, 'csrf_token', None)
|
csrf_token = getattr(self, 'csrf_token', None)
|
||||||
if csrf_token:
|
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:
|
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']}")
|
print(f"🔗 WebSocket URL: {vnc_data['websocket_url']}")
|
||||||
|
|||||||
168
farmq-admin/utils/vnc_proxy.py
Normal file
168
farmq-admin/utils/vnc_proxy.py
Normal file
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user