AutoPercenty3/test/Vm_Manager/vm_manager.py

841 lines
39 KiB
Python

# vm_manager.py
import os
import tempfile
import subprocess
import requests
import zipfile
import psutil
import logging
import json
from pathlib import Path
import shutil
import glob
from PySide6.QtWidgets import QApplication
logger = logging.getLogger("HyperVDeployer.VMManager")
class VMManager:
DEFAULT_SETTINGS = {
"base_name": "PartTimer",
"memory_gb": 8,
"disk_gb": 32,
"virtual_switch": "Default Switch",
"processor_count": 2,
"dynamic_memory": True,
"memory_minimum_gb": 2,
"memory_maximum_gb": 16,
"memory_startup_gb": 4,
"checkpoint_type": "Standard", # Standard, Production, ProductionOnly, Disabled
"integration_services": {
"Guest Service Interface": True,
"Heartbeat": True,
"Key-Value Pair Exchange": True,
"Shutdown": True,
"Time Synchronization": True,
"VSS": True
},
"automatic_stop_action": "Save", # Save, ShutDown, TurnOff
"automatic_start_action": "Nothing", # Nothing, StartIfRunning, Start
"smart_paging_file_path": None, # None = VHD 경로와 동일
"snapshot_file_path": None, # None = VHD 경로와 동일
}
def __init__(self, workdir=None, settings=None, parent_widget=None):
"""
workdir: 작업용 임시 폴더 (None이면 시스템 tmp)
settings: VM 설정 딕셔너리 (None이면 기본값 사용)
parent_widget: 프로그레스 다이얼로그의 부모 위젯
"""
self.workdir = workdir
self.settings = {**self.DEFAULT_SETTINGS, **(settings or {})}
self.parent_widget = parent_widget
self.progress_callback = None
def get_cpu_info(self):
"""
WMI로 Win32_Processor 정보(JSON) 읽기
"""
cmd = (
"Get-WmiObject Win32_Processor | "
"Select Name,VirtualizationFirmwareEnabled | "
"ConvertTo-Json"
)
out = self._run_powershell(cmd)
try:
info = json.loads(out)
except json.JSONDecodeError as e:
logger.error("CPU 정보 JSON 파싱 실패: %s", e)
raise RuntimeError("CPU 정보 파싱 실패")
if isinstance(info, list):
info = info[0]
logger.info("CPU 정보: %s", info)
return info
def list_vms(self):
"""
현재 호스트에 등록된 VM 이름 목록을 반환(set)
"""
out = self._run_powershell(
'Get-VM | Select -ExpandProperty Name | ConvertTo-Json'
)
try:
names = json.loads(out)
if isinstance(names, str):
names = [names]
except json.JSONDecodeError:
names = []
return set(names)
def check_resources(self, base_path=None):
"""
호스트 메모리/디스크(총/여유) 확인 및 VM 최대 생성 대수 계산
return: (total_mem_gb, free_mem_gb, total_disk_gb, free_disk_gb, max_vms)
"""
base = base_path or os.getcwd()
mem = psutil.virtual_memory()
total_mem = mem.total / (1024**3)
free_mem = mem.available / (1024**3)
disk = psutil.disk_usage(base)
total_disk = disk.total / (1024**3)
free_disk = disk.free / (1024**3)
max_by_mem = int(free_mem // self.settings["memory_gb"])
max_by_disk = int(free_disk // self.settings["disk_gb"])
max_vms = min(max_by_mem, max_by_disk)
logger.info(
"자원: 전체메모리=%.2fGB, 여유메모리=%.2fGB, 전체디스크=%.2fGB, 여유디스크=%.2fGB, 추천VM=%d",
total_mem, free_mem, total_disk, free_disk, max_vms
)
return total_mem, free_mem, total_disk, free_disk, max_vms
def download_zip(self, url):
"""
URL에서 ZIP 파일 다운로드
return: (zip_path, tmp_root)
"""
tmp_root = self.workdir or tempfile.mkdtemp(prefix="hv_import_")
local_zip = os.path.join(tmp_root, "vm_export.zip")
logger.info("ZIP 다운로드 시작: %s", url)
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(local_zip, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
logger.info("ZIP 다운로드 완료: %s", local_zip)
return local_zip, tmp_root
def extract_zip(self, zip_path, extract_to):
"""
ZIP 압축 해제
"""
logger.info("ZIP 압축 해제: %s%s", zip_path, extract_to)
with zipfile.ZipFile(zip_path, "r") as z:
z.extractall(extract_to)
logger.info("압축 해제 완료")
def _get_default_vhd_path(self):
"""
Hyper-V의 기본 VHD 경로를 가져옴
"""
cmd = (
"Get-VMHost | "
"Select-Object -ExpandProperty VirtualHardDiskPath"
)
try:
vhd_path = self._run_powershell(cmd)
logger.info("기본 VHD 경로: %s", vhd_path)
return vhd_path
except RuntimeError as e:
logger.warning("기본 VHD 경로를 가져오는데 실패했습니다: %s", e)
return "C:\\ProgramData\\Microsoft\\Windows\\Virtual Hard Disks"
def _generate_unique_name(self, base_name="PartTimer"):
"""
사용 가능한 고유한 VM 이름 생성
base_name에 1부터 시작하는 숫자를 붙여가며 사용 가능한 이름 찾기
"""
existing_vms = self.list_vms()
counter = 1
vhd_base_path = self._get_default_vhd_path()
while True:
new_name = f"{base_name}{counter}"
vhdx_path = os.path.join(vhd_base_path, f"{new_name}.vhdx")
if new_name not in existing_vms and not os.path.exists(vhdx_path):
return new_name
counter += 1
def _check_powershell_version(self):
"""
PowerShell 버전과 Hyper-V 모듈 버전 확인
"""
ps_version_cmd = "$PSVersionTable.PSVersion.Major"
hyperv_version_cmd = (
"Get-Module -Name Hyper-V -ListAvailable | "
"Select-Object -ExpandProperty Version | "
"Select-Object -First 1"
)
try:
ps_version = int(self._run_powershell(ps_version_cmd))
hyperv_version = self._run_powershell(hyperv_version_cmd)
logger.info("PowerShell 버전: %d, Hyper-V 모듈 버전: %s",
ps_version, hyperv_version)
return ps_version, hyperv_version
except (ValueError, RuntimeError) as e:
logger.warning("버전 확인 실패: %s", e)
return None, None
def _set_network_adapter(self, vm_name):
"""
VM 네트워크 어댑터 설정 (강화된 버전)
"""
logger.info("VM 네트워크 어댑터 설정 시작: %s", vm_name)
try:
# 먼저 현재 네트워크 어댑터 상태 확인
check_cmd = f'Get-VMNetworkAdapter -VMName "{vm_name}" | Select-Object Name, MacAddress, DynamicMacAddressEnabled | ConvertTo-Json'
current_state = self._run_powershell(check_cmd)
logger.info("현재 네트워크 어댑터 상태: %s", current_state)
# 이미 동적 MAC 주소가 활성화되어 있는지 확인
try:
adapter_info = json.loads(current_state)
if adapter_info.get("DynamicMacAddressEnabled") == True:
logger.info("동적 MAC 주소가 이미 활성화되어 있음")
return
except (json.JSONDecodeError, KeyError):
logger.warning("네트워크 어댑터 상태 파싱 실패, 설정 계속 진행")
# 동적 MAC 주소 설정 시도
success = False
commands = [
# 올바른 매개변수 사용
f'Set-VMNetworkAdapter -VMName "{vm_name}" -DynamicMacAddressEnabled $true',
# 대체 방법 1 - VM 객체를 통한 설정
f'Get-VM -Name "{vm_name}" | Get-VMNetworkAdapter | Set-VMNetworkAdapter -DynamicMacAddressEnabled $true',
# 대체 방법 2 - 어댑터별 설정
f'Get-VMNetworkAdapter -VMName "{vm_name}" | ForEach-Object {{ Set-VMNetworkAdapter -VMNetworkAdapter $_ -DynamicMacAddressEnabled $true }}'
]
for i, cmd in enumerate(commands, 1):
try:
logger.info("네트워크 어댑터 설정 시도 %d/%d", i, len(commands))
self._run_powershell(cmd)
success = True
logger.info("네트워크 어댑터 설정 성공 (방법 %d)", i)
break
except RuntimeError as e:
logger.warning("네트워크 어댑터 설정 실패 (방법 %d): %s", i, e)
continue
if success:
# 설정 확인
try:
final_state = self._run_powershell(check_cmd)
logger.info("변경 후 네트워크 어댑터 상태: %s", final_state)
except Exception as e:
logger.warning("네트워크 어댑터 상태 확인 실패: %s", e)
else:
logger.error("모든 네트워크 어댑터 설정 시도 실패")
# 네트워크 설정 실패해도 VM 생성은 계속 진행
logger.info("네트워크 설정 실패했지만 VM 생성 계속 진행")
except Exception as e:
logger.error("네트워크 어댑터 설정 중 예외 발생: %s", e)
# 네트워크 설정 실패해도 VM 생성은 계속 진행
logger.info("네트워크 설정 예외 발생했지만 VM 생성 계속 진행")
def _generate_vm_id(self):
"""새로운 VM ID(GUID) 생성"""
cmd = "(New-Guid).Guid"
return self._run_powershell(cmd)
def _modify_vm_config(self, src_config, dst_config, target_name):
"""
VM 구성 파일(XML)의 설정 수정
"""
import xml.etree.ElementTree as ET
tree = ET.parse(src_config)
root = tree.getroot()
# 메모리 설정
memory = root.find(".//memory")
if memory is not None:
if self.settings["dynamic_memory"]:
memory.set("dynamic", "true")
memory.set("minimum", str(self.settings["memory_minimum_gb"] * 1024 * 1024 * 1024))
memory.set("maximum", str(self.settings["memory_maximum_gb"] * 1024 * 1024 * 1024))
memory.set("startup", str(self.settings["memory_startup_gb"] * 1024 * 1024 * 1024))
else:
memory.set("dynamic", "false")
memory.set("startup", str(self.settings["memory_gb"] * 1024 * 1024 * 1024))
# 프로세서 설정
processor = root.find(".//processor")
if processor is not None:
processor.set("count", str(self.settings["processor_count"]))
# 체크포인트 타입
checkpoint = root.find(".//checkpoint")
if checkpoint is not None:
checkpoint.set("type", self.settings["checkpoint_type"])
# 자동 시작/중지 동작
auto_start = root.find(".//auto_start")
if auto_start is not None:
auto_start.set("action", self.settings["automatic_start_action"])
auto_stop = root.find(".//auto_stop")
if auto_stop is not None:
auto_stop.set("action", self.settings["automatic_stop_action"])
# 통합 서비스 설정
integration_services = root.find(".//integration_services")
if integration_services is not None:
for service in integration_services.findall("service"):
name = service.get("name")
if name in self.settings["integration_services"]:
service.set("enabled",
"true" if self.settings["integration_services"][name] else "false")
# 네트워크 어댑터 설정
for adapter in root.findall(".//network_adapter"):
switch_name = adapter.find("switch_name")
if switch_name is not None:
switch_name.text = self.settings["virtual_switch"]
# 수정된 설정 저장
tree.write(dst_config)
logger.info("VM 구성 파일 수정 완료: %s", dst_config)
def _prepare_vm_files(self, vm_export_path, target_name):
"""
VM 파일들을 임시 폴더에 복사하고 설정을 수정
"""
# 경로 정규화
vm_export_path = os.path.normpath(vm_export_path)
logger.info("VM 내보내기 경로: %s", vm_export_path)
# 임시 폴더 생성
temp_export = os.path.join(self.workdir or tempfile.mkdtemp(), "temp_export")
os.makedirs(temp_export, exist_ok=True)
try:
# 새로운 VM ID 생성 (로그용)
new_vm_id = self._generate_vm_id()
logger.info("새 VM ID 생성: %s", new_vm_id)
# Hyper-V 표준 폴더 구조 확인
vm_folders = {
"vm": os.path.join(vm_export_path, "Virtual Machines"),
"vhd": os.path.join(vm_export_path, "Virtual Hard Disks"),
"snapshot": os.path.join(vm_export_path, "Snapshots")
}
# 폴더 존재 여부 확인
found_folders = 0
for folder_type, folder_path in vm_folders.items():
if os.path.exists(folder_path):
logger.info("VM %s 폴더 발견: %s", folder_type, folder_path)
found_folders += 1
else:
logger.warning("VM %s 폴더 없음: %s", folder_type, folder_path)
# 표준 폴더가 하나도 없으면 상위 폴더 확인
if found_folders == 0:
parent_path = os.path.dirname(vm_export_path)
if os.path.exists(os.path.join(parent_path, "Virtual Machines")):
logger.info("상위 폴더에서 VM 폴더 구조 발견")
vm_export_path = parent_path
vm_folders = {
"vm": os.path.join(vm_export_path, "Virtual Machines"),
"vhd": os.path.join(vm_export_path, "Virtual Hard Disks"),
"snapshot": os.path.join(vm_export_path, "Snapshots")
}
# 모든 VM 관련 파일 복사
vm_files = []
# Virtual Machines 폴더에서 구성 파일 검색
for ext in ['*.vmcx', '*.xml']:
pattern = os.path.join(vm_folders["vm"], "**", ext)
vm_files.extend(glob.glob(pattern, recursive=True))
# Virtual Hard Disks 폴더에서 디스크 파일 검색
for ext in ['*.vhd*']:
pattern = os.path.join(vm_folders["vhd"], "**", ext)
vm_files.extend(glob.glob(pattern, recursive=True))
# 상태 파일들은 존재하는 경우에만 포함 (.vmrs, .vmgs)
state_files = []
for ext in ['*.vmrs', '*.vmgs', '*.vsv']:
pattern = os.path.join(vm_folders["vm"], "**", ext)
found_files = glob.glob(pattern, recursive=True)
for file in found_files:
if os.path.exists(file) and os.path.getsize(file) > 0:
state_files.append(file)
logger.info("상태 파일 발견: %s", file)
else:
logger.warning("상태 파일이 비어있거나 손상됨: %s", file)
vm_files.extend(state_files)
# 루트 폴더에서도 검색 (예외적인 경우)
for ext in ['*.vmcx', '*.vhd*', '*.xml']:
pattern = os.path.join(vm_export_path, ext)
vm_files.extend(glob.glob(pattern))
if not vm_files:
raise RuntimeError(
f"VM 파일을 찾을 수 없습니다. 다음 경로를 확인해주세요:\n"
f"- {vm_folders['vm']}\n"
f"- {vm_folders['vhd']}\n"
f"- {vm_folders['snapshot']}"
)
logger.info("발견된 VM 파일들:")
for file in vm_files:
logger.info("- %s", file)
# 필수 파일 확인 (.vmcx와 .vhdx)
has_vmcx = any(f.lower().endswith('.vmcx') for f in vm_files)
has_vhdx = any(f.lower().endswith('.vhdx') for f in vm_files)
if not has_vmcx:
raise RuntimeError("VM 구성 파일(.vmcx)을 찾을 수 없습니다.")
if not has_vhdx:
raise RuntimeError("VM 디스크 파일(.vhdx)을 찾을 수 없습니다.")
# 파일 복사 및 이름 변경
copied_files = []
for file in vm_files:
try:
# 파일 존재 및 크기 확인
if not os.path.exists(file):
logger.warning("파일이 존재하지 않음: %s", file)
continue
file_size = os.path.getsize(file)
if file_size == 0:
logger.warning("빈 파일 건너뜀: %s", file)
continue
base_name = os.path.basename(file)
ext = os.path.splitext(base_name)[1].lower()
# 파일 종류에 따라 새 이름 결정 (모든 파일에 target_name 적용)
if ext == '.vhdx':
new_name = os.path.join(temp_export, f"{target_name}.vhdx")
elif ext == '.vmcx':
new_name = os.path.join(temp_export, f"{target_name}.vmcx")
elif ext == '.vmrs':
new_name = os.path.join(temp_export, f"{target_name}.vmrs")
elif ext == '.vmgs':
new_name = os.path.join(temp_export, f"{target_name}.vmgs")
else:
# 기타 파일들은 원본 이름에 target_name 접두사 추가
name_part = os.path.splitext(base_name)[0]
new_name = os.path.join(temp_export, f"{target_name}_{base_name}")
# 파일이 이미 존재하면 덮어쓰기
if os.path.exists(new_name):
os.remove(new_name)
shutil.copy2(file, new_name)
copied_files.append(new_name)
logger.info("파일 복사: %s%s (크기: %d bytes)", file, new_name, file_size)
except PermissionError:
logger.warning("권한 오류로 파일 복사 실패: %s", file)
# 관리자 권한으로 복사 시도
try:
cmd = f'Copy-Item -Path "{file}" -Destination "{new_name}" -Force'
self._run_powershell(cmd)
copied_files.append(new_name)
logger.info("관리자 권한으로 파일 복사 성공: %s", file)
except Exception as e:
logger.error("관리자 권한으로도 파일 복사 실패: %s - %s", file, e)
# 필수 파일이 아니면 계속 진행
if not (file.lower().endswith('.vmcx') or file.lower().endswith('.vhdx')):
continue
raise
except Exception as e:
logger.error("파일 복사 중 오류: %s - %s", file, e)
# 필수 파일이 아니면 계속 진행
if not (file.lower().endswith('.vmcx') or file.lower().endswith('.vhdx')):
continue
raise
logger.info("복사된 파일 목록:")
for file in copied_files:
logger.info("- %s", file)
# 구성 파일 찾기
config = self._find_config(temp_export)
# XML 파일인 경우 설정 수정
if config.lower().endswith('.xml'):
self._modify_vm_config(config, config, target_name)
return temp_export, config
except Exception as e:
logger.error("VM 파일 준비 실패: %s", e)
try:
shutil.rmtree(temp_export)
except Exception as cleanup_error:
logger.warning("임시 폴더 삭제 실패: %s", cleanup_error)
raise
def import_vm(self, vm_export_path, target_name=None):
"""
VM 가져오기 및 설정
"""
try:
# PowerShell/Hyper-V 버전 체크
ps_version, hyperv_version = self._check_powershell_version()
logger.info("시스템 버전 - PowerShell: %s, Hyper-V: %s",
ps_version, hyperv_version)
self._update_vm_progress("시스템 버전 확인 완료", 10)
if target_name is None:
target_name = self._generate_unique_name(self.settings["base_name"])
else:
# target_name이 이미 사용 중이면 새로운 이름 생성
vhd_base_path = self._get_default_vhd_path()
vhdx_path = os.path.join(vhd_base_path, f"{target_name}.vhdx")
if target_name in self.list_vms() or os.path.exists(vhdx_path):
logger.info("지정된 이름 %s이(가) 이미 사용 중입니다. 새 이름을 생성합니다.", target_name)
target_name = self._generate_unique_name(target_name)
self._update_vm_progress("VM 이름 생성 완료", 20)
# VM 파일 준비
temp_export, new_config = self._prepare_vm_files(vm_export_path, target_name)
self._update_vm_progress("VM 파일 준비 완료", 30)
try:
# VHDX 파일 경로 설정
vhd_base_path = self._get_default_vhd_path()
smart_paging_path = self.settings["smart_paging_file_path"] or vhd_base_path
snapshot_path = self.settings["snapshot_file_path"] or vhd_base_path
# 대상 경로에 쓰기 권한 확인 및 폴더 생성
for path in [vhd_base_path, smart_paging_path, snapshot_path]:
os.makedirs(path, exist_ok=True)
# Import 전 VM 목록 저장
before_vms = self.list_vms()
logger.info("Import 전 VM 목록: %s", before_vms)
# Compare-VM으로 구성 검사 및 수정
logger.info("VM 구성 검사 시작")
# 먼저 단순한 Import-VM 시도 (상태 파일 무시)
try:
logger.info("단순 Import-VM 시도 (상태 파일 무시)")
simple_import_cmd = (
f'Import-VM -Path "{new_config}" '
f'-Copy -GenerateNewId '
f'-VhdDestinationPath "{vhd_base_path}" '
f'-SnapshotFilePath "{snapshot_path}" '
f'-SmartPagingFilePath "{smart_paging_path}"'
)
logger.info("Import-VM 명령어: %s", simple_import_cmd)
self._run_powershell(simple_import_cmd)
logger.info("단순 Import-VM 성공")
except Exception as simple_error:
logger.warning("단순 Import-VM 실패: %s", simple_error)
# Compare-VM으로 문제 분석 및 해결 시도
try:
logger.info("Compare-VM으로 문제 분석 시작")
compare_cmd = f'Compare-VM -Path "{new_config}" | ConvertTo-Json -Depth 5'
compare_result = self._run_powershell(compare_cmd)
logger.info("Compare-VM 결과: %s", compare_result)
# Compare-VM 결과를 이용한 복구 시도
repair_cmd = (
f'$compareResult = Compare-VM -Path "{new_config}"; '
f'if ($compareResult.Incompatibilities) {{ '
f' Write-Host "호환성 문제 발견, 복구 시도"; '
f' $compareResult | Import-VM -Copy -GenerateNewId '
f' -VhdDestinationPath "{vhd_base_path}" '
f' -SnapshotFilePath "{snapshot_path}" '
f' -SmartPagingFilePath "{smart_paging_path}"; '
f'}} else {{ '
f' Write-Host "호환성 문제 없음, 직접 Import"; '
f' Import-VM -Path "{new_config}" '
f' -Copy -GenerateNewId '
f' -VhdDestinationPath "{vhd_base_path}" '
f' -SnapshotFilePath "{snapshot_path}" '
f' -SmartPagingFilePath "{smart_paging_path}"; '
f'}}'
)
logger.info("복구 Import-VM 실행: %s", repair_cmd)
self._run_powershell(repair_cmd)
logger.info("복구 Import-VM 성공")
except Exception as repair_error:
logger.error("복구 Import-VM도 실패: %s", repair_error)
# 마지막 시도: Register 모드 (파일 이동 없이)
try:
logger.info("Register 모드로 최종 시도")
register_cmd = f'Import-VM -Path "{new_config}" -Register -GenerateNewId'
logger.info("Register Import-VM 명령어: %s", register_cmd)
self._run_powershell(register_cmd)
logger.info("Register Import-VM 성공")
except Exception as register_error:
logger.error("Register Import-VM도 실패: %s", register_error)
raise RuntimeError(
f"모든 Import-VM 시도 실패:\n"
f"1. 단순 Import: {simple_error}\n"
f"2. 복구 Import: {repair_error}\n"
f"3. Register Import: {register_error}"
)
self._update_vm_progress("VM 가져오기 완료", 50)
finally:
# 임시 폴더 정리
try:
shutil.rmtree(temp_export)
except Exception as e:
logger.warning("임시 폴더 삭제 실패: %s", e)
after = self.list_vms()
logger.info("Import 후 VM 목록: %s", after)
# 먼저 목표 이름으로 VM이 생성되었는지 확인
if target_name in after:
new_name = target_name
logger.info("목표 이름으로 VM 생성 확인: %s", new_name)
else:
# VM 목록 차이로 새로 추가된 VM 찾기
added = after - before_vms
if not added:
# VM 목록에서 직접 찾기 시도
logger.warning("VM 목록 차이로 찾지 못함, 직접 검색 시도")
potential_vms = [vm for vm in after if target_name in vm or vm.startswith(self.settings["base_name"])]
if potential_vms:
new_name = potential_vms[0]
logger.info("직접 검색으로 VM 발견: %s", new_name)
else:
raise RuntimeError(f"Import된 VM을 찾을 수 없습니다. 현재 VM 목록: {after}")
else:
new_name = added.pop()
logger.info("VM 목록 차이로 새 VM 발견: %s", new_name)
# VM 이름 변경 (강화된 로직)
logger.info("현재 VM 이름: '%s', 목표 이름: '%s'", new_name, target_name)
# 이미 목표 이름이면 이름 변경 불필요
if new_name == target_name:
logger.info("VM이 이미 목표 이름으로 생성됨: %s", target_name)
else:
# 이름이 다르거나 Import된 VM의 기본 패턴인 경우 강제 변경
should_rename = (new_name != target_name) or any(pattern in new_name.lower() for pattern in ['copy', 'clone', 'import'])
if should_rename:
try:
logger.info("VM 이름 변경 시도: %s%s", new_name, target_name)
# 기존 이름과 충돌 확인
existing_vms = self.list_vms()
if target_name in existing_vms and target_name != new_name:
logger.warning("목표 이름 %s이(가) 이미 존재함, 고유 이름 생성", target_name)
target_name = self._generate_unique_name(target_name)
logger.info("새로운 목표 이름: %s", target_name)
# VM 이름 변경 실행
rename_cmd = f'Rename-VM -VMName "{new_name}" -NewName "{target_name}"'
self._run_powershell(rename_cmd)
logger.info("VM 이름 변경 성공: %s%s", new_name, target_name)
# 변경 확인
updated_vms = self.list_vms()
if target_name in updated_vms:
logger.info("VM 이름 변경 확인됨: %s", target_name)
else:
logger.warning("VM 이름 변경 확인 실패, 현재 VM 목록: %s", updated_vms)
except Exception as e:
logger.error("VM 이름 변경 실패: %s", e)
# 이름 변경에 실패해도 계속 진행
logger.info("이름 변경 실패했지만 계속 진행: %s", new_name)
target_name = new_name
else:
logger.info("VM 이름 변경 불필요: %s", new_name)
target_name = new_name
self._update_vm_progress("VM 이름 변경 완료", 60)
# 맥주소 동적 재할당 (버전별 처리)
logger.info("맥주소 동적 할당: %s", target_name)
self._set_network_adapter(target_name)
self._update_vm_progress("네트워크 설정 완료", 70)
# VM 시작
try:
# VM 시작 전 상태 확인
vm_state_cmd = f'(Get-VM -Name "{target_name}").State'
vm_state = self._run_powershell(vm_state_cmd)
logger.info("VM 시작 전 상태: %s", vm_state)
if vm_state.lower() == "running":
logger.info("VM이 이미 실행 중입니다: %s", target_name)
else:
# VM 파일 경로 확인 및 수정
logger.info("VM 하드 디스크 경로 확인 및 수정")
check_vhd_cmd = f'''
$vm = Get-VM -Name "{target_name}"
$vhds = $vm | Get-VMHardDiskDrive
foreach ($vhd in $vhds) {{
$currentPath = $vhd.Path
$fileName = [System.IO.Path]::GetFileName($currentPath)
$directory = [System.IO.Path]::GetDirectoryName($currentPath)
$newPath = Join-Path $directory "{target_name}.vhdx"
Write-Host "현재 VHD 경로: $currentPath"
Write-Host "새 VHD 경로: $newPath"
# 새 경로의 파일이 존재하는지 확인
if (Test-Path $newPath) {{
Write-Host "새 VHD 파일이 존재함, 경로 업데이트 진행"
# VM의 하드 디스크 경로를 새 파일로 변경
Set-VMHardDiskDrive -VMName "{target_name}" -ControllerType $vhd.ControllerType -ControllerNumber $vhd.ControllerNumber -ControllerLocation $vhd.ControllerLocation -Path $newPath
Write-Host "VHD 경로 업데이트 완료: $currentPath -> $newPath"
}} else {{
Write-Host "새 VHD 파일이 존재하지 않음: $newPath"
# 원본 파일을 새 이름으로 복사
if (Test-Path $currentPath) {{
Write-Host "원본 VHD 파일 복사 시작: $currentPath -> $newPath"
Copy-Item -Path $currentPath -Destination $newPath -Force
Write-Host "VHD 파일 복사 완료"
# VM의 하드 디스크 경로를 새 파일로 변경
Set-VMHardDiskDrive -VMName "{target_name}" -ControllerType $vhd.ControllerType -ControllerNumber $vhd.ControllerNumber -ControllerLocation $vhd.ControllerLocation -Path $newPath
Write-Host "VHD 경로 업데이트 완료: $currentPath -> $newPath"
}} else {{
Write-Host "원본 VHD 파일이 존재하지 않음: $currentPath"
}}
}}
}}
# 업데이트 후 확인
Write-Host "=== VHD 경로 업데이트 후 확인 ==="
$updatedVhds = Get-VM -Name "{target_name}" | Get-VMHardDiskDrive
foreach ($vhd in $updatedVhds) {{
Write-Host "최종 VHD 경로: $($vhd.Path)"
}}
'''
try:
vhd_result = self._run_powershell(check_vhd_cmd)
logger.info("VHD 경로 확인 및 업데이트 결과: %s", vhd_result)
except Exception as e:
logger.warning("VHD 경로 확인 실패 (계속 진행): %s", e)
# VM 시작 (사용자 요청에 따라 주석 처리)
# logger.info("VM 시작 시도: %s", target_name)
# self._run_powershell(f'Start-VM -Name "{target_name}"')
# logger.info("VM 시작 성공: %s", target_name)
except Exception as e:
logger.error("VM 설정 실패: %s", e)
# VM 설정 실패해도 VM은 생성되었으므로 계속 진행
logger.info("VM 설정 실패했지만 VM 생성은 완료됨. 수동으로 시작할 수 있습니다.")
# VM 생성 완료 - 사용자가 수동으로 시작하도록 함
logger.info("VM 생성 완료: %s", target_name)
logger.info("VM은 중지 상태로 생성되었습니다. Hyper-V 관리자에서 설정을 확인한 후 시작해주세요.")
self._update_vm_progress("VM 생성 완료 (중지 상태)", 100)
logger.info("VM 생성 및 설정 완료: %s", target_name)
return target_name
except Exception as e:
logger.error("VM 가져오기 실패: %s", e)
raise
def configure_network_and_name(self, vm_name):
"""
1) MAC 동적 할당 보장
2) VM 시작
3) PowerShell Direct로 내부 접속 → DHCP 활성화, 컴퓨터 이름 유지
"""
logger.info("VM 설정 시작: %s", vm_name)
self._run_powershell(
f'Set-VMNetworkAdapter -VMName "{vm_name}" -DynamicMacAddress $true'
)
self._run_powershell(f'Start-VM -Name "{vm_name}"')
guest_script = f"""
$if = Get-NetAdapter | Where-Object {{$_.Status -eq "Up"}} | Select -First 1
Get-NetIPAddress -InterfaceIndex $if.IfIndex -AddressFamily IPv4 |
Remove-NetIPAddress -Confirm:$false
Set-NetIPInterface -InterfaceIndex $if.IfIndex -Dhcp Enabled
"""
self._run_powershell(
f'Invoke-Command -VMName "{vm_name}" -ScriptBlock {{ {guest_script} }}'
)
logger.info("네트워크 설정 완료: %s", vm_name)
def _find_config(self, export_path):
"""
export_path가 폴더면 .vmcx/.xml 파일을 찾아 반환,
파일이면 그대로 반환
"""
p = Path(export_path)
if p.is_file() and p.suffix.lower() in (".vmcx", ".xml"):
return str(p)
if p.is_dir():
for ext in ("*.vmcx", "*.xml"):
files = list(p.rglob(ext))
if files:
return str(files[0])
raise RuntimeError("VM 구성 파일(.vmcx 또는 .xml)을 찾을 수 없습니다.")
def _run_powershell(self, command):
"""
PowerShell 명령 실행 헬퍼
"""
completed = subprocess.run(
["powershell", "-NoProfile", "-Command", command],
capture_output=True, text=True
)
if completed.returncode != 0:
logger.error("PowerShell 오류: %s", completed.stderr.strip())
raise RuntimeError(completed.stderr.strip())
return completed.stdout.strip()
def import_multiple_vms(self, vm_export_path, count, base_name="PartTimer"):
"""여러 개의 VM을 순차적으로 가져오기"""
imported_vms = []
for i in range(count):
target_name = f"{base_name}-{i+1}"
logger.info("VM %d/%d 가져오기 시작: %s", i+1, count, target_name)
self._update_total_progress(i, count)
try:
vm_name = self.import_vm(vm_export_path, target_name)
imported_vms.append(vm_name)
except Exception as e:
logger.error("VM %s 가져오기 실패: %s", target_name, e)
continue
self._update_total_progress(count, count)
if not imported_vms:
raise RuntimeError("VM을 가져오지 못했습니다.")
return imported_vms
def _update_vm_progress(self, status_text, progress_percent):
"""현재 VM의 진행 상황 업데이트"""
if self.progress_callback:
self.progress_callback("vm", status_text, progress_percent)
def _update_total_progress(self, current, total):
"""전체 진행 상황 업데이트"""
if self.progress_callback:
self.progress_callback("total", current, total)