AutoPercenty3/test/Vm_Manager/gui.py

603 lines
24 KiB
Python

import os
import platform
import logging
import subprocess
import psutil
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton,
QMessageBox, QFileDialog, QSpinBox,
QDialog, QProgressBar, QComboBox,
QCheckBox, QGroupBox, QFormLayout,
QDialogButtonBox
)
from PySide6.QtCore import Qt, Signal, QThread
from vm_manager import VMManager
import win32gui
import win32con
import win32process
from PySide6.QtWidgets import QApplication
import time
logger = logging.getLogger("HyperVDeployer.GUI")
class ImportProgressDialog(QDialog):
"""VM 가져오기 진행 상황을 표시하는 다이얼로그"""
# 진행 상황 업데이트 시그널
update_total_progress = Signal(int, int) # current, total
update_vm_progress = Signal(str, int) # status_text, progress_percent
def __init__(self, total_vms, parent=None):
super().__init__(parent)
self.setWindowTitle("VM 가져오기")
self.setWindowFlags(Qt.Dialog | Qt.WindowStaysOnTopHint)
self.setModal(True)
self.setMinimumWidth(400)
# 레이아웃 설정
layout = QVBoxLayout(self)
# 전체 진행 상황
self.total_label = QLabel(f"전체 진행 상황: 0/{total_vms}")
layout.addWidget(self.total_label)
self.total_progress = QProgressBar()
self.total_progress.setRange(0, total_vms)
self.total_progress.setValue(0)
self.total_progress.setFormat("%v/%m (%p%)")
layout.addWidget(self.total_progress)
# 현재 VM 진행 상황
self.current_label = QLabel("현재 작업: 준비 중...")
layout.addWidget(self.current_label)
self.vm_progress = QProgressBar()
self.vm_progress.setRange(0, 100)
self.vm_progress.setValue(0)
self.vm_progress.setFormat("%p%")
layout.addWidget(self.vm_progress)
# 시그널 연결
self.update_total_progress.connect(self._update_total)
self.update_vm_progress.connect(self._update_vm)
# 다이얼로그를 부모 위젯의 중앙에 위치시킴
if parent:
self.move(
parent.x() + parent.width()//2 - self.width()//2,
parent.y() + parent.height()//2 - self.height()//2
)
def _update_total(self, current, total):
"""전체 진행 상황 업데이트"""
self.total_label.setText(f"전체 진행 상황: {current}/{total}")
self.total_progress.setValue(current)
def _update_vm(self, status_text, progress_percent):
"""현재 VM 진행 상황 업데이트"""
self.current_label.setText(f"현재 작업: {status_text}")
self.vm_progress.setValue(progress_percent)
class VMSettingsDialog(QDialog):
"""VM 설정 다이얼로그"""
def __init__(self, settings, parent=None):
super().__init__(parent)
self.setWindowTitle("VM 설정")
self.setModal(True)
self.settings = settings.copy()
layout = QVBoxLayout(self)
# 메모리 설정
memory_group = QGroupBox("메모리 설정")
memory_layout = QFormLayout()
self.dynamic_memory = QCheckBox("동적 메모리 사용")
self.dynamic_memory.setChecked(settings["dynamic_memory"])
self.dynamic_memory.stateChanged.connect(self._on_dynamic_memory_changed)
memory_layout.addRow(self.dynamic_memory)
self.memory_gb = QSpinBox()
self.memory_gb.setRange(1, 64)
self.memory_gb.setValue(settings["memory_gb"])
memory_layout.addRow("고정 메모리 (GB):", self.memory_gb)
self.memory_min = QSpinBox()
self.memory_min.setRange(1, 64)
self.memory_min.setValue(settings["memory_minimum_gb"])
memory_layout.addRow("최소 메모리 (GB):", self.memory_min)
self.memory_max = QSpinBox()
self.memory_max.setRange(1, 64)
self.memory_max.setValue(settings["memory_maximum_gb"])
memory_layout.addRow("최대 메모리 (GB):", self.memory_max)
memory_group.setLayout(memory_layout)
layout.addWidget(memory_group)
# 프로세서 설정
processor_group = QGroupBox("프로세서 설정")
processor_layout = QFormLayout()
self.processor_count = QSpinBox()
self.processor_count.setRange(1, 16)
self.processor_count.setValue(settings["processor_count"])
processor_layout.addRow("프로세서 수:", self.processor_count)
processor_group.setLayout(processor_layout)
layout.addWidget(processor_group)
# 네트워크 설정
network_group = QGroupBox("네트워크 설정")
network_layout = QFormLayout()
self.virtual_switch = QComboBox()
# 가상 스위치 목록 가져오기
switches = self._get_virtual_switches()
self.virtual_switch.addItems(switches)
current_switch = settings["virtual_switch"]
if current_switch in switches:
self.virtual_switch.setCurrentText(current_switch)
network_layout.addRow("가상 스위치:", self.virtual_switch)
network_group.setLayout(network_layout)
layout.addWidget(network_group)
# 체크포인트 설정
checkpoint_group = QGroupBox("체크포인트 설정")
checkpoint_layout = QFormLayout()
self.checkpoint_type = QComboBox()
self.checkpoint_type.addItems(["Standard", "Production", "ProductionOnly", "Disabled"])
self.checkpoint_type.setCurrentText(settings["checkpoint_type"])
checkpoint_layout.addRow("체크포인트 타입:", self.checkpoint_type)
checkpoint_group.setLayout(checkpoint_layout)
layout.addWidget(checkpoint_group)
# 버튼
buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
Qt.Horizontal
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self._on_dynamic_memory_changed()
def _on_dynamic_memory_changed(self):
"""동적 메모리 체크박스 상태에 따라 UI 업데이트"""
is_dynamic = self.dynamic_memory.isChecked()
self.memory_gb.setEnabled(not is_dynamic)
self.memory_min.setEnabled(is_dynamic)
self.memory_max.setEnabled(is_dynamic)
def _get_virtual_switches(self):
"""Hyper-V 가상 스위치 목록 가져오기"""
try:
cmd = (
"Get-VMSwitch | "
"Select-Object -ExpandProperty Name | "
"ConvertTo-Json"
)
result = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True, text=True
)
if result.returncode == 0:
import json
switches = json.loads(result.stdout)
if isinstance(switches, str):
switches = [switches]
return switches
except Exception as e:
logger.error("가상 스위치 목록 가져오기 실패: %s", e)
return ["Default Switch"]
def get_settings(self):
"""설정 값 반환"""
self.settings.update({
"dynamic_memory": self.dynamic_memory.isChecked(),
"memory_gb": self.memory_gb.value(),
"memory_minimum_gb": self.memory_min.value(),
"memory_maximum_gb": self.memory_max.value(),
"processor_count": self.processor_count.value(),
"virtual_switch": self.virtual_switch.currentText(),
"checkpoint_type": self.checkpoint_type.currentText()
})
return self.settings
class VMDeployWorker(QThread):
"""VM 배포 작업을 처리하는 워커 스레드"""
finished = Signal(bool, str) # success, error_message
vm_progress = Signal(str, int) # status_text, progress_percent
total_progress = Signal(int, int) # current, total
def __init__(self, vm_manager, source_path, count, base_name):
super().__init__()
self.vm_manager = vm_manager
self.source_path = source_path
self.count = count
self.base_name = base_name
# 프로그레스 시그널 연결
self.vm_manager.progress_callback = self._progress_callback
def _progress_callback(self, progress_type, *args):
"""VMManager로부터의 진행상황 콜백"""
if progress_type == "vm":
self.vm_progress.emit(*args)
elif progress_type == "total":
self.total_progress.emit(*args)
def run(self):
try:
if os.path.isdir(self.source_path):
extract_dir = self.source_path
logger.info("로컬 폴더 사용: %s", self.source_path)
else:
# ZIP 다운로드 및 압축 해제
self.vm_progress.emit("ZIP 파일 다운로드 중...", 0)
zip_path, tmp = self.vm_manager.download_zip(self.source_path)
self.vm_progress.emit("ZIP 파일 압축 해제 중...", 10)
extract_dir = os.path.join(tmp, "vm_folder")
os.makedirs(extract_dir, exist_ok=True)
self.vm_manager.extract_zip(zip_path, extract_dir)
# VM 생성
self.vm_progress.emit("VM 가져오기 시작...", 20)
self.vm_manager.import_multiple_vms(extract_dir, self.count, self.base_name)
self.finished.emit(True, "")
except Exception as e:
logger.exception("VM 배포 실패")
self.finished.emit(False, str(e))
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Hyper-V VM 자동 배포 도구")
self.resize(700, 450)
self.vm_manager = VMManager() # parent_widget은 나중에 설정
self.deploy_worker = None
self.progress_dialog = None
layout = QVBoxLayout(self)
# ── 환경 점검 ──
self.lbl_win = QLabel("Windows 버전: 점검 중...")
self.lbl_cpu = QLabel("CPU 모델: 점검 중...")
self.lbl_vtx = QLabel("가상화(FW) 활성화: 점검 중...")
self.lbl_hv = QLabel("Hyper-V 설치 여부: 점검 중...")
for lbl in (self.lbl_win, self.lbl_cpu, self.lbl_vtx, self.lbl_hv):
lbl.setAlignment(Qt.AlignLeft)
layout.addWidget(lbl)
# ── 자원 체크 ──
self.lbl_res = QLabel("자원 체크: 실행 후 확인")
self.lbl_res.setAlignment(Qt.AlignLeft)
layout.addWidget(self.lbl_res)
row_count = QHBoxLayout()
row_count.addWidget(QLabel("생성할 VM 개수:"))
self.spin_count = QSpinBox()
self.spin_count.setMinimum(1)
self.spin_count.setMaximum(1)
row_count.addWidget(self.spin_count)
layout.addLayout(row_count)
# ── URL 입력 ──
row_url = QHBoxLayout()
row_url.addWidget(QLabel("VM ZIP URL:"))
self.le_url = QLineEdit()
row_url.addWidget(self.le_url)
layout.addLayout(row_url)
# ── 로컬 폴더 ──
row_folder = QHBoxLayout()
row_folder.addWidget(QLabel("로컬 VM 폴더:"))
self.le_folder = QLineEdit()
row_folder.addWidget(self.le_folder)
btn_browse = QPushButton("폴더 선택")
btn_browse.clicked.connect(self.browse_folder)
row_folder.addWidget(btn_browse)
layout.addLayout(row_folder)
# ── VM 이름 ──
row_name = QHBoxLayout()
row_name.addWidget(QLabel("베이스 VM 이름:"))
self.le_vmname = QLineEdit("PartTimerVM")
row_name.addWidget(self.le_vmname)
layout.addLayout(row_name)
# ── 실행 버튼 ──
self.btn_run = QPushButton("가져오기 및 설정 실행")
self.btn_run.clicked.connect(self.on_run)
layout.addWidget(self.btn_run, alignment=Qt.AlignCenter)
# 최초 점검
self.run_env_checks()
self.run_res_checks()
def browse_folder(self):
fld = QFileDialog.getExistingDirectory(self, "VM 폴더 선택")
if fld:
self.le_folder.setText(fld)
def run_env_checks(self):
errors = []
warnings = []
# Windows 버전 체크
if platform.system() != "Windows":
errors.append({
"title": "운영체제 오류",
"message": "이 프로그램은 Windows에서만 실행할 수 있습니다.",
"solution": "Windows 10 이상의 운영체제에서 실행해주세요."
})
elif int(platform.version().split(".")[0]) < 10:
errors.append({
"title": "Windows 버전 오류",
"message": f"현재 Windows 버전({platform.release()})이 지원되지 않습니다.",
"solution": "Windows 10 이상으로 업그레이드하거나, 최신 Windows 업데이트를 설치해주세요."
})
else:
self.lbl_win.setText(f"Windows {platform.release()} — OK")
self.lbl_win.setStyleSheet("color: green;")
# CPU 정보 & 가상화(FW) 활성화
try:
info = self.vm_manager.get_cpu_info()
name = info.get("Name", "").strip()
virt = info.get("VirtualizationFirmwareEnabled", False)
self.lbl_cpu.setText(f"CPU 모델: {name}")
self.lbl_cpu.setStyleSheet("color: black;")
if not virt:
errors.append({
"title": "가상화 비활성화",
"message": "BIOS/UEFI에서 가상화가 비활성화되어 있습니다.",
"solution": (
"다음 단계를 따라 가상화를 활성화해주세요:\n"
"1. PC를 재시작하고 BIOS/UEFI 설정으로 진입\n"
"2. 'Virtualization Technology', 'Intel VT-x/AMD-V' 또는 'SVM Mode' 찾기\n"
"3. 해당 옵션을 활성화\n"
"4. 설정 저장 후 재시작"
)
})
else:
self.lbl_vtx.setText("가상화(FW) 활성화됨 — OK")
self.lbl_vtx.setStyleSheet("color: green;")
except Exception as e:
logger.exception("CPU 점검 오류")
errors.append({
"title": "CPU 정보 오류",
"message": "CPU 정보를 확인할 수 없습니다.",
"solution": "관리자 권한으로 프로그램을 실행해주세요."
})
# Hyper-V 설치 여부
try:
state = subprocess.run(
["powershell","-NoProfile","-Command",
"(Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All).State"],
capture_output=True, text=True
).stdout.strip().lower()
if state != "enabled":
errors.append({
"title": "Hyper-V 미설치",
"message": "Hyper-V가 설치되어 있지 않습니다.",
"solution": (
"다음 방법 중 하나로 Hyper-V를 설치해주세요:\n\n"
"방법 1) Windows 기능 켜기/끄기 사용:\n"
"1. 제어판 → 프로그램 → Windows 기능 켜기/끄기\n"
"2. 'Hyper-V' 체크박스 선택\n"
"3. 확인 후 재시작\n\n"
"방법 2) PowerShell 사용 (관리자 권한):\n"
"1. PowerShell을 관리자 권한으로 실행\n"
"2. 다음 명령어 실행:\n"
" Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All\n"
"3. 완료 후 재시작"
)
})
else:
self.lbl_hv.setText("Hyper-V 설치됨 — OK")
self.lbl_hv.setStyleSheet("color: green;")
except Exception:
errors.append({
"title": "Hyper-V 확인 오류",
"message": "Hyper-V 설치 상태를 확인할 수 없습니다.",
"solution": "관리자 권한으로 프로그램을 실행해주세요."
})
# 메모리 체크
try:
mem = psutil.virtual_memory()
if mem.available < 4 * 1024 * 1024 * 1024: # 4GB
warnings.append({
"title": "메모리 부족",
"message": f"사용 가능한 메모리가 부족합니다. (현재 여유: {mem.available/(1024**3):.1f}GB)",
"solution": (
"원활한 VM 실행을 위해:\n"
"1. 불필요한 프로그램을 종료하거나\n"
"2. VM 설정에서 메모리 크기를 줄이세요."
)
})
except Exception:
warnings.append({
"title": "메모리 확인 오류",
"message": "메모리 상태를 확인할 수 없습니다.",
"solution": "관리자 권한으로 프로그램을 실행해주세요."
})
# 오류/경고 메시지 표시
if errors:
error_text = "다음 문제들을 해결해야 VM을 생성할 수 있습니다:\n\n"
for error in errors:
error_text += f"[{error['title']}]\n"
error_text += f"문제: {error['message']}\n"
error_text += f"해결방법:\n{error['solution']}\n\n"
QMessageBox.critical(self, "환경 설정 오류", error_text)
self.btn_run.setEnabled(False)
elif warnings:
warning_text = "다음 문제들이 발견되었습니다:\n\n"
for warning in warnings:
warning_text += f"[{warning['title']}]\n"
warning_text += f"문제: {warning['message']}\n"
warning_text += f"권장사항:\n{warning['solution']}\n\n"
warning_text += "\nVM 생성은 가능하지만, 성능에 영향을 줄 수 있습니다."
QMessageBox.warning(self, "환경 설정 경고", warning_text)
self.btn_run.setEnabled(True)
else:
self.btn_run.setEnabled(True)
def run_res_checks(self):
tot_m, free_m, tot_d, free_d, max_v = self.vm_manager.check_resources()
self.lbl_res.setText(
f"메모리: {free_m:.1f}/{tot_m:.1f} GB, "
f"디스크: {free_d:.1f}/{tot_d:.1f} GB → "
f"추천 생성 가능 VM: {max_v}"
)
self.spin_count.setMaximum(max_v if max_v>0 else 1)
# self.spin_count.setValue(max_v if max_v>0 else 1)
self.spin_count.setValue(1)
def on_run(self):
url = self.le_url.text().strip()
folder = self.le_folder.text().strip()
base = self.le_vmname.text().strip()
count = self.spin_count.value()
if not base or (not url and not folder):
QMessageBox.warning(self, "입력 오류", "이름과 URL/폴더를 모두 지정하세요.")
return
# VM 설정 다이얼로그 표시
settings_dialog = VMSettingsDialog(self.vm_manager.DEFAULT_SETTINGS, self)
if settings_dialog.exec() != QDialog.Accepted:
return
# VM 매니저에 설정 적용
self.vm_manager.settings.update(settings_dialog.get_settings())
# 최신 자원 정보 갱신
self.run_res_checks()
# 프로그레스 다이얼로그 초기화 및 표시
if self.progress_dialog:
self.progress_dialog.close()
self.progress_dialog = ImportProgressDialog(count, self)
self.progress_dialog.setWindowModality(Qt.ApplicationModal)
self.progress_dialog.show()
QApplication.processEvents() # UI 업데이트 강제
# VM 매니저에 프로그레스 다이얼로그 연결
self.vm_manager.parent_widget = self
# 배포 작업 시작
source = folder if folder else url
self.deploy_worker = VMDeployWorker(self.vm_manager, source, count, base)
# 시그널 연결
self.deploy_worker.finished.connect(self._on_deploy_finished)
self.deploy_worker.vm_progress.connect(self.progress_dialog.update_vm_progress.emit)
self.deploy_worker.total_progress.connect(self.progress_dialog.update_total_progress.emit)
# 작업 시작
self.deploy_worker.start()
# 버튼 비활성화
self.btn_run.setEnabled(False)
def _on_deploy_finished(self, success, error_message):
"""배포 완료 처리"""
self.progress_dialog.accept()
if success:
message = ("VM 배포 완료!\n\n"
"VM이 중지 상태로 생성되었습니다.\n"
"Hyper-V 관리자에서 다음 설정을 확인한 후 VM을 시작해주세요:\n\n"
"• 메모리 설정 (동적 메모리 포함)\n"
"• 프로세서 설정\n"
"• 네트워크 어댑터 설정\n"
"• 가상 하드 디스크 경로\n\n"
"Hyper-V 관리자를 열까요?")
reply = QMessageBox.question(
self, "배포 완료", message,
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
try:
self.open_hyperv_manager()
except Exception as e:
logger.warning("Hyper-V 관리자 실행 중 오류 발생: %s", e)
QMessageBox.information(
self, "알림",
"Hyper-V 관리자를 자동으로 열 수 없습니다.\n"
"수동으로 'virtmgmt.msc'를 실행하거나\n"
"시작 메뉴에서 'Hyper-V 관리자'를 검색하여 실행해주세요."
)
else:
QMessageBox.critical(
self, "배포 실패",
f"VM 배포 중 오류가 발생했습니다:\n\n{error_message}"
)
# UI 상태 복원
self.setEnabled(True)
self.btn_run.setText("배포 시작")
self.btn_run.setEnabled(True)
def open_hyperv_manager(self):
"""Hyper-V 관리자 실행"""
try:
# virtmgmt.msc 실행
proc = subprocess.Popen(["mmc", "virtmgmt.msc"])
# 잠시 대기 후 창을 앞으로 가져오기 시도
QApplication.processEvents()
time.sleep(2)
try:
import win32gui
import win32con
def enum_win(hwnd, pid):
if win32gui.GetWindowThreadProcessId(hwnd)[1] == pid:
try:
# 창이 보이는 경우에만 활성화 시도
if win32gui.IsWindowVisible(hwnd):
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
win32gui.SetForegroundWindow(hwnd)
except Exception as e:
# SetForegroundWindow 실패는 무시하고 계속 진행
logger.warning("창 활성화 실패 (무시됨): %s", e)
return False # 첫 번째 창만 처리
return True
# 프로세스 정보가 있는 경우에만 창 활성화 시도
if hasattr(proc, 'pid') and proc.pid:
win32gui.EnumWindows(enum_win, proc.pid)
except ImportError:
logger.info("win32gui 모듈이 없어 창 활성화를 건너뜁니다.")
except Exception as e:
logger.warning("창 활성화 중 오류 발생 (무시됨): %s", e)
except Exception as e:
logger.error("Hyper-V 관리자 실행 실패: %s", e)
QMessageBox.warning(self, "오류", f"Hyper-V 관리자를 실행할 수 없습니다:\n{e}")