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:
2025-09-12 23:57:52 +09:00
parent ac620a0e15
commit fb00b0a5fd
14 changed files with 2029 additions and 32 deletions

View File

@@ -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/<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'])
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
)