603 lines
24 KiB
Python
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}")
|