Initial: PVE VM 원격 제어 가이드 + VirtIO 마이그레이션 함정 분석

- GUIDE.ko.md: VM 원격 제어 (화면 캡처, QGA, qm sendkey, vhd 임포트, DR 이전, 트러블슈팅)
- JOURNEY.md: SATA→VirtIO-SCSI 마이그레이션 실패 연대기 15단계
- 핵심 발견: vioscsi START_TYPE=BOOT_START(0) 승격 필수 (pnputil만으론 부족)
- screenshots: 부팅 검증 화면

검증 환경: PVE 8.1, Win10 Pro 19045, virtio-win 0.1.285
This commit is contained in:
2026-05-19 18:11:26 +09:00
commit bf11a3aeb4
6 changed files with 866 additions and 0 deletions

391
GUIDE.ko.md Normal file
View File

@@ -0,0 +1,391 @@
# VM 원격 제어 가이드 (PVE + QEMU)
> Claude Code 환경에서 Proxmox 위의 Windows/Linux VM을 화면 캡처 + 명령 실행 + GUI 조작까지 원격으로 다루는 방법.
>
> 작성일: 2026-05-18 / 검증 환경: Windows 10 Pro VM on Proxmox VE 8.1, virtio-scsi-single + iothread + discard
## 표기 규약
- `<PVE_HOST>` `<PVE_HOST_2>`: Proxmox 노드 IP
- `<PVE_PASSWORD>` `<PVE_PASSWORD_2>`: 각 노드 root 비밀번호 (또는 SSH key 사용 권장)
- `<SMB_HOST>` `<SMB_USER>` `<SMB_PASSWORD>`: SMB/CIFS 파일 서버 접속 정보
- `<windows_user>`: Windows 게스트 로그인 사용자
- VMID 예시는 `101`, zvol 예시는 `vm-101-disk-N` — 본인 환경 값으로 치환
---
## 1. 인프라 개요
### Proxmox 호스트
| 호스트 | IP | SSH | 비고 |
|---|---|---|---|
| pve5 | `<PVE_HOST>` | `root` / `<PVE_PASSWORD>` | 메인 VM 호스트 (운영) |
| pve7 | `<PVE_HOST_2>` | `root` / `<PVE_PASSWORD_2>` | 보조 / 백업 호스트 |
### SMB 파일 서버
- 주소: `<SMB_HOST>`
- 인증: `<SMB_USER>` / `<SMB_PASSWORD>`
- 주요 공유: `vhd` (VHD 디스크 이미지 보관)
### 접속 패턴
```bash
# pve5 SSH (모든 VM 작업은 여기서)
sshpass -p "$PVE_PASSWORD" ssh -o StrictHostKeyChecking=no root@<PVE_HOST> '<명령>'
# SMB 마운트 (pve5 또는 작업 VM에서)
mkdir -p /mnt/vhd-src
mount -t cifs //<SMB_HOST>/vhd /mnt/vhd-src \
-o username=<SMB_USER>,password=$SMB_PASSWORD,ro,vers=3.0,iocharset=utf8,file_mode=0444,dir_mode=0555
```
---
## 2. VM 화면 캡처 (보기)
PVE 8.x에는 `qm screendump` 명령이 **없음**. QEMU monitor에 직접 명령 보내야 함.
### 한 줄 캡처 (pve5에서)
```bash
echo "screendump /tmp/vm101.ppm" | qm monitor 101
```
결과는 PPM(Netpbm raw bitmap) 형식. 일반 뷰어가 못 읽으므로 PNG로 변환 필요.
### Claude 환경에서 보기까지 풀 흐름
```bash
# 1) pve5에서 screendump
sshpass -p "$PVE_PASSWORD" ssh root@<PVE_HOST> \
'echo "screendump /tmp/vm101.ppm" | qm monitor 101'
# 2) PPM 가져오기
sshpass -p "$PVE_PASSWORD" scp root@<PVE_HOST>:/tmp/vm101.ppm /tmp/vm101.ppm
# 3) PNG 변환 (ffmpeg는 작업 VM에 설치돼있음, pve5에는 없음)
ffmpeg -y -i /tmp/vm101.ppm /tmp/vm101.png
# 4) Read 툴로 열기 → 멀티모달 모델이 이미지 인식
```
### PVE API 방식 (대안)
```bash
pvesh get /nodes/pve5/qemu/101/screenshot > /tmp/vm101.png
```
*이 엔드포인트는 PNG 직접 반환. 단 PVE 권한 토큰 설정에 따라 동작 여부 다름.*
---
## 3. QGA (QEMU Guest Agent)로 명령 실행
### 전제 조건
- VM config에 `agent: 1`
- 게스트 OS에 qemu-ga 서비스 설치/실행 중
- 확인: `qm agent <vmid> ping` (응답 없으면 = OK)
### 핵심 명령
```bash
# 상태 확인
qm agent 101 ping
qm agent 101 get-osinfo
qm agent 101 get-host-name
qm agent 101 get-users
# 명령 실행 (동기 / 결과 회수)
qm guest exec 101 --timeout 30 -- cmd.exe /c whoami
qm guest exec 101 --timeout 30 -- powershell.exe -NoProfile -Command "<script>"
qm guest exec 101 --timeout 30 -- C:\Path\To\program.exe arg1 arg2
# 비동기 (PID만 받고 나중에 결과)
PID=$(qm guest exec 101 --no-wait -- powershell.exe -Command "Start-Sleep 60" | jq -r .pid)
qm guest exec-status 101 $PID # 진행 상태/exit code/stdout 회수
```
### 파일 송수신
```bash
# 게스트로 파일 쓰기 (base64 또는 raw content)
qm guest file-write 101 'C:\temp\script.ps1' --content "$(cat local.ps1)"
# 게스트에서 파일 읽기
qm guest file-read 101 'C:\temp\output.txt'
```
### ⚠️ 권한과 세션
- QGA로 실행된 프로세스는 **NT AUTHORITY\SYSTEM (Session 0, 비대화형 서비스 세션)** 으로 동작
- GUI를 띄워도 **사용자 데스크톱에 안 보임** (Windows 보안상 분리)
- 시스템 권한이라 거의 모든 작업 가능 (레지스트리, 서비스, 파일)
- 단 사용자 프로필(`%USERPROFILE%`)이 SYSTEM 계정 기준임에 주의
---
## 4. qm sendkey로 GUI 조작
키보드 이벤트를 QEMU에 직접 주입. **현재 포커스 있는 곳(=로그인 사용자 데스크톱)** 에 입력됨.
### 기본 사용
```bash
qm sendkey 101 <key>
```
### 주요 키 이름
| 키 | 이름 |
|---|---|
| Enter | `ret` 또는 `kp_enter` |
| Esc | `esc` |
| 윈도우 키 | `meta_l` / `meta_r` |
| Ctrl+Alt+Del | `ctrl-alt-delete` |
| 대문자 A | `shift-a` |
| 화살표 | `up`, `down`, `left`, `right` |
| 특수기호 | `exclam`(!), `at`(@), `minus`(-), `slash`(/), `dot`(.) 등 |
### 패턴: Win+R → 프로그램 실행
```bash
sshpass -p "$PVE_PASSWORD" ssh root@<PVE_HOST> '
qm sendkey 101 esc # 떠있는 다이얼로그 닫기
sleep 0.3
qm sendkey 101 meta_l-r # 실행창 열기
sleep 0.5
for c in n o t e p a d; do qm sendkey 101 $c; sleep 0.05; done
qm sendkey 101 ret
'
```
### 제약
- 키보드 입력만 가능 (마우스 ❌)
- 한글/IME 입력 매우 어려움 — 영문 명령어/경로 위주로 설계
- 화면 상태 모르고 보내면 의도와 다른 곳에 들어감 → screendump으로 확인 권장
- 결과 회수 = screendump 또는 QGA로 프로세스 확인
---
## 5. ★ 최강 조합: QGA + schtasks로 사용자 세션에서 실행
QGA의 결과 회수성 + 사용자 세션에서의 GUI 가시성을 모두 얻는 방법.
```bash
qm guest exec 101 --timeout 30 -- powershell.exe -NoProfile -Command "
schtasks /create /tn RunInUser /tr 'C:\path\program.exe arg' /sc once /st 23:59 /it /ru '<windows_user>' /f
schtasks /run /tn RunInUser
Start-Sleep 1
schtasks /delete /tn RunInUser /f
"
```
- `/it` = interactive (사용자 데스크톱에 GUI 보임)
- `/ru '<windows_user>'` = 지정 사용자의 세션 컨텍스트
- `schtasks /run`은 일정 시각과 무관하게 즉시 실행
- 결과 파일을 게스트에 쓰게 한 다음 `qm guest file-read`로 회수 → 비동기 + 가시성 + 회수성
---
## 6. VM 라이프사이클 명령 (자주 쓰는 것만)
```bash
qm status 101 # 상태 (running/stopped/...)
qm start 101 # 시작
qm stop 101 # 강제종료 (전원 끊기)
qm shutdown 101 # ACPI shutdown (OS에 종료 신호)
qm reboot 101 # ACPI 재시작
qm reset 101 # 하드 리셋
qm config 101 # config 보기
qm list # 전체 VM 목록
qm set 101 --boot order=sata1;sata0;ide0 # 부팅 순서
qm set 101 --sata1 pve-zfs:vm-101-disk-1 # 디스크 attach
qm set 101 --delete unused0 # 디스크 detach
```
---
## 7. VHD/디스크 임포트 패턴 (실전: SMB → ZFS raw zvol)
```bash
# 1) SMB 마운트 (pve5에서)
mkdir -p /mnt/vhd-src
mount -t cifs //<SMB_HOST>/vhd /mnt/vhd-src \
-o username=<SMB_USER>,password=$SMB_PASSWORD,ro,vers=3.0,iocharset=utf8
# 2) vhd 메타데이터 확인
qemu-img info /mnt/vhd-src/vm100.vhd
# 3) qm importdisk 한방 (vhd → raw zvol on pve-zfs)
# nohup으로 백그라운드 권장 (106GB 약 30-40분 소요)
nohup qm importdisk 101 /mnt/vhd-src/vm100.vhd pve-zfs --format raw \
> /tmp/import.log 2>&1 &
# 4) 완료 후 unused0 → sata1 (또는 원하는 슬롯)
qm set 101 --sata1 pve-zfs:vm-101-disk-1
# 5) 부팅 순서 변경
qm set 101 --boot 'order=sata1;sata0;ide0'
# 6) SMB 정리
umount /mnt/vhd-src
```
### 디스크 검증 (부팅 전)
```bash
fdisk -l /dev/zvol/pve-zfs/vm-101-disk-1
# Windows UEFI 정상 = part1(Recovery) + part2(EFI System) + part3(MSR) + part4(NTFS)
mount -o ro /dev/zvol/pve-zfs/vm-101-disk-1-part2 /mnt/efi
ls /mnt/efi/EFI/Microsoft/Boot/bootmgfw.efi # 있으면 부팅 가능
umount /mnt/efi
```
---
## 8. 흔한 트러블슈팅
### `WARN: no efidisk configured! Using temporary efivars disk.`
OVMF 부팅 VM에 NVRAM 저장 디스크가 없음. 매 부팅 시 EFI 부트 엔트리가 초기화됨. 영구 NVRAM 만들려면 VM 종료 후:
```bash
qm set 101 --efidisk0 pve-zfs:1,efitype=4m,pre-enrolled-keys=0
```
fallback boot path(`\EFI\Microsoft\Boot\bootmgfw.efi`, `\EFI\Boot\bootx64.efi`)가 있으면 efidisk 없어도 부팅은 됨.
### `GPT PMBR size mismatch` 경고
zvol 크기가 vhd 가상 크기보다 약간 큰 경우. Windows 부팅에는 영향 없음. 수정하려면 게스트에서 `diskpart`로 GPT 백업 위치 재기록.
### qm sendkey 입력이 안 먹힘
- 화면 잠겨있거나 다른 모달 다이얼로그 떠있는지 screendump으로 확인
- 키 사이 간격 너무 짧으면 일부 키 손실 → `sleep 0.05` 삽입
- IME가 한글모드라면 영문으로 전환 필요 (`shift` + 한/영 키는 표준 매핑 없음, OS GUI에서 미리 영문으로)
### qm guest exec가 "guest agent not running"
- VM config에 `agent: 1` 있는지 확인 (`qm config 101 | grep agent`)
- 게스트 안에서 서비스 확인: `Get-Service QEMU-GA` (Windows) / `systemctl status qemu-guest-agent` (Linux)
- VM이 막 부팅됐다면 agent 서비스 시작까지 30~60초 대기
### qemu-ga가 SYSTEM이라 사용자 환경변수가 다름
`%USERPROFILE%`, 매핑 드라이브, 사용자 설치 프로그램 등에 접근 못 함. 사용자 세션 컨텍스트 필요하면 § 5 schtasks 패턴 사용.
### Windows SATA → VirtIO-SCSI 디스크 컨트롤러 전환 (마스터화)
가장 흔한 함정: `pnputil /add-driver vioscsi.inf /install`로 등록만 하면 BSOD `0xc0000017` / `0xc0000225` 발생. 이유는 vioscsi 서비스가 **DEMAND_START(=3)** 으로 등록되어 boot critical이 아니기 때문. winload가 vioscsi.sys를 부팅 시점에 로드하지 않아 OS 디스크 접근 실패.
**해결책**: 게스트 안에서 vioscsi를 BOOT_START(=0)으로 명시 승격.
```cmd
sc config vioscsi start= boot
reg query HKLM\SYSTEM\CurrentControlSet\Services\vioscsi /v Start
# 결과가 0x0 이어야 함
```
**검증된 마스터화 절차** (현재 SATA 부팅 → VirtIO-SCSI 단독 마스터):
1. virtio-win.iso 마운트 + `pnputil /add-driver` 로 vioscsi/viostor/netkvm INF 등록
2. **`sc config vioscsi start= boot`** ← 핵심
3. Windows ACPI shutdown (NTFS dirty + hive transaction log 정리)
4. ZFS 클론 (같은 풀 내):
```bash
zfs snapshot pve-zfs/vm-<vmid>-disk-0@master
zfs send pve-zfs/vm-<vmid>-disk-0@master | zfs recv pve-zfs/vm-<vmid>-disk-NEW
zfs destroy pve-zfs/vm-<vmid>-disk-0@master
zfs destroy pve-zfs/vm-<vmid>-disk-NEW@master
```
5. config 교체:
```bash
qm set <vmid> --delete sata0
qm set <vmid> --scsi0 pve-zfs:vm-<vmid>-disk-NEW,iothread=1,discard=on,ssd=1
qm set <vmid> --scsihw virtio-scsi-single
qm set <vmid> --boot 'order=scsi0'
```
6. 부팅 검증
**주의 — 동일 GPT signature 디스크 두 개 동시 attach 시 함정**: SATA 디스크와 그 raw 변환본/클론을 동시에 attach하면 NT namespace 매핑 이상으로 BSOD 발생. 한쪽 detach 후 단독으로 검증.
### vhd → raw 변환본이 부팅 안 될 때 (sata 원본은 부팅 OK)
원인 가능성: `qm importdisk`로 vhd → raw 변환 시 disk size에 1MB 패딩 추가됨 (`vmcfg sata1: ...size=122881M` 같이 +1 표기). 이 padding이 GPT backup header 위치를 미세하게 흔들 수 있음. 또는 우리 작업 경험상 SYSTEM hive 손상 가능성도 있음. 해결책:
- **부팅 가능한 sata 원본을 ZFS 클론**해서 그걸 마스터로 사용 (vhd 변환본 폐기)
- 또는 sata 원본 자체를 in-place로 virtio-scsi 인터페이스 변경 (drive number는 NT 동적이라 OK)
---
## 9. 참고 명령 치트시트
```bash
# 화면 한 번 보기 (pve5에서 + Claude 환경으로 회수)
sshpass -p "$PVE_PASSWORD" ssh root@<PVE_HOST> 'echo "screendump /tmp/s.ppm" | qm monitor 101'
sshpass -p "$PVE_PASSWORD" scp root@<PVE_HOST>:/tmp/s.ppm /tmp/s.ppm && ffmpeg -y -i /tmp/s.ppm /tmp/s.png
# 게스트 정보 한 번에
sshpass -p "$PVE_PASSWORD" ssh root@<PVE_HOST> '
qm agent 101 get-osinfo
qm agent 101 get-host-name
qm agent 101 get-users
'
# 게스트에서 PowerShell 한 줄
sshpass -p "$PVE_PASSWORD" ssh root@<PVE_HOST> \
'qm guest exec 101 --timeout 30 -- powershell.exe -NoProfile -Command "Get-Process | Select Name,Id"'
# 게스트에 Python 스크립트 던지고 실행
LOCAL_PY=/tmp/probe.py
cat > $LOCAL_PY <<'EOF'
import os, json
print(json.dumps({"cwd": os.getcwd(), "env_user": os.environ.get("USERNAME")}))
EOF
sshpass -p "$PVE_PASSWORD" ssh root@<PVE_HOST> "
qm guest file-write 101 'C:\\temp\\probe.py' --content \"\$(cat)\" <<< \"\$(cat $LOCAL_PY)\"
qm guest exec 101 --timeout 30 -- python.exe 'C:\\temp\\probe.py'
"
```
---
## 10. DR 이전 (다른 PVE 노드로 옮기기)
원본 정지 후 다른 노드에서 켜는 시나리오. pve5 ↔ pve7 모두 같은 PBS(`<PBS_STORAGE_A>` / `<PBS_STORAGE_B>`)에 연결돼있어 PBS 경로가 가장 깔끔.
### 사전 점검 (이전하기 좋은 상태 만들기)
- [ ] `efidisk0` 설정됨 (없으면 매 부팅 NVRAM 초기화 — `qm set <vmid> --efidisk0 <stor>:1,efitype=4m,pre-enrolled-keys=0`)
- [ ] VirtIO 드라이버 설치 상태 확인 (`Get-PnpDevice | Where Manufacturer -like "*Red Hat*"`)
- [ ] 현재 인터페이스(sata/e1000 등)를 대상 PVE도 지원하는지 — 동일 인터페이스 유지가 가장 안전
- [ ] `cpu: host`면 대상 노드 CPU 세대가 같은지 확인 (다르면 `cpu: x86-64-v2-AES` 등 호환 모델로 사전 변경)
- [ ] 불필요한 디스크 detach (사용 안 하는 zvol/qcow2 정리)
### 절차 (PBS 백업 → 다른 PVE에서 restore)
```bash
# 1) 원본 PVE에서 백업 (실행 중에도 가능, snapshot 모드)
ssh root@pve5 'vzdump 101 --storage <PBS_STORAGE_A> --mode snapshot --compress zstd'
# 2) 백업 ID 확인
ssh root@pve5 'pvesm list <PBS_STORAGE_A> --vmid 101 | tail -5'
# 3) 대상 PVE에서 restore (새 VMID 또는 동일 VMID)
ssh root@pve7 '
pvesm list <PBS_STORAGE_B> --vmid 101 | tail -5 # 같은 PBS면 보임
qmrestore <BACKUP_VOLID> <NEW_VMID> --storage local-lvm
# 예: qmrestore <PBS_STORAGE_B>:backup/vm/101/2026-05-19T... 201 --storage local-lvm
'
# 4) 원본 정지 + 대상 시작
ssh root@pve5 'qm shutdown 101'
ssh root@pve7 'qm start <NEW_VMID>'
```
### 절차 (vzdump → scp → qmrestore, PBS 없을 때)
```bash
ssh root@pve5 'vzdump 101 --dumpdir /var/lib/vz/dump --mode snapshot --compress zstd'
scp root@pve5:/var/lib/vz/dump/vzdump-qemu-101-*.vma.zst root@pve7:/var/lib/vz/dump/
ssh root@pve7 'qmrestore /var/lib/vz/dump/vzdump-qemu-101-*.vma.zst 201 --storage local-lvm'
```
### 스토리지 차이 주의
- pve5 = `pve-zfs` (ZFS zvol, raw)
- pve7 = `local-lvm` (LVM-thin, raw)
- 둘 다 raw 기반이라 restore 자동 변환됨. qcow2 dir로 가면 qcow2로 변환.
### MAC/UUID 충돌 (동시 운영 시에만)
원본을 *정지*하고 옮기면 충돌 없음. 동시 운영이면:
```bash
qm set <NEW_VMID> --net0 e1000=AUTO,bridge=vmbr0 # MAC 자동 재생성
qm set <NEW_VMID> --vmgenid 1 # vmgenid 새로 (Windows AD/DC 인식 변경)
qm set <NEW_VMID> --smbios1 uuid=auto # SMBIOS uuid 새로
```
---
## 11. 보안 메모
- QGA는 **SYSTEM 권한**이라 게스트 OS 안에서 사실상 할 수 있는 게 무제한. 신뢰되는 게스트에만 사용.
- pve5 SSH 키 또는 적어도 ssh-agent 사용을 권장 (현재는 sshpass + 평문 패스워드)
- SMB 마운트는 `ro` 기본, 쓰기 작업 필요할 때만 `rw`
- qm sendkey로 비밀번호 같은 민감 정보 입력 시 screendump에 남을 수 있음 — 캡처 파일 즉시 삭제

381
JOURNEY.md Normal file
View File

@@ -0,0 +1,381 @@
# 실패의 연대기 (Failure Journal)
> Windows VM의 디스크 컨트롤러를 **SATA → VirtIO-SCSI**로 마이그레이션하는 과정에서 만난 모든 함정의 기록.
> 진짜 원인 — `vioscsi START_TYPE=0 (BOOT_START)` 누락 — 을 찾기까지 수 시간을 소비했다.
> 같은 길을 가는 사람이 이 글을 읽고 30분에 끝낼 수 있길.
작성일: 2026-05-19
작업 환경: Proxmox VE 8.1.3 / Windows 10 Pro 19045 / virtio-win-0.1.285
---
## 출발점
목표:
- SMB에 보관된 `vm100.vhd` (120GiB virt, 106GiB sparse) Windows 디스크 이미지를 Proxmox VM에 활용
- 가능하면 SATA 에뮬레이션 대신 **VirtIO-SCSI 컨트롤러**로 운영해서 성능/효율 확보 (멀티큐, iothread, discard/TRIM)
도구:
- `qm importdisk` (vhd → raw zvol 변환)
- ZFS pool `pve-zfs` (vol storage)
- `pnputil` (Windows 드라이버 등록)
- `qm sendkey`, QEMU monitor screendump (원격 시각 확인)
---
## Step 1 — vhd → raw zvol 변환
```bash
qm importdisk <VMID> /mnt/smb/vm100.vhd pve-zfs --format raw
```
결과: `pve-zfs:vm-<VMID>-disk-1` 생성 (REFER 73.4GB sparse). config에 `unused0:`로 자동 등록.
**관찰**: 변환 후 zvol 크기가 120GiB + ~1MB 패딩. fdisk가 `GPT PMBR size mismatch` 경고. 이때는 대수롭지 않게 넘어감.
---
## Step 2 — sata1로 attach + 부팅 시도
```bash
qm set <VMID> --sata1 pve-zfs:vm-<VMID>-disk-1
qm set <VMID> --boot 'order=sata1;sata0;ide0'
qm start <VMID>
```
**결과**: Windows 10 데스크톱 정상 진입! 사용자도 다 보임. 데이터 모두 살아있음.
**우리가 잘못 알았던 것**: "vhd 변환이 잘 됐구나" — 사실은 **sata0 (기존에 갖고 있던 vm-101-disk-0 zvol)에서 부팅된 것**이었음. `Get-Disk`로 확인 안 했으니 모름.
---
## Step 3 — 안정화: efidisk 추가, VirtIO 드라이버 등록
부팅이 잘 되니 마스터화 준비. 두 가지 작업 진행.
### 3-1. efidisk0 추가 (OVMF NVRAM 영구화)
```bash
qm set <VMID> --efidisk0 pve-zfs:1,efitype=4m,pre-enrolled-keys=0
```
이전엔 `WARN: no efidisk configured` 떠서 매 부팅마다 OVMF가 NVRAM 새로 만들고 fallback path로 부팅했음. 영구화하면 부팅 엔트리 저장됨.
### 3-2. VirtIO 드라이버 사전 설치
virtio-win.iso를 attach하고 임시 SCSI 디스크도 같이 띄워 게스트가 새 PCI 디바이스 인식하게 만든 다음:
```cmd
pnputil /add-driver F:\vioscsi\w10\amd64\vioscsi.inf /install
pnputil /add-driver F:\viostor\w10\amd64\viostor.inf /install
pnputil /add-driver F:\NetKVM\w10\amd64\netkvm.inf /install
```
`pnputil`이 INF 파일을 driver store에 등록 + `/install` 옵션이 매칭되는 PnP 디바이스에 즉시 설치. `Get-PnpDevice`에서 "Red Hat VirtIO SCSI pass-through controller" Status=OK 보임. `Get-Service vioscsi` → RUNNING.
**우리가 잘못 알았던 것**: "드라이버 등록 완료, 이제 부팅 디스크 인터페이스 바꿔도 BSOD 안 나겠지" — **틀렸다**.
---
## Step 4 — 인터페이스 교체: sata1 → scsi0
깨끗한 마스터를 위해 sata1 detach, scsi0로 같은 zvol attach:
```bash
qm set <VMID> --scsi0 pve-zfs:vm-<VMID>-disk-1,iothread=1,discard=on,ssd=1
qm set <VMID> --delete sata1
qm set <VMID> --boot 'order=scsi0'
qm start <VMID>
```
**결과**: **BSOD 0xc0000225** (Recovery / "필요한 장치가 연결되어 있지 않거나 장치에 액세스할 수 없습니다") — `\Windows\system32\config\system` 위치 못 찾음.
이때 처음으로 뭔가 잘못됐다고 느낌. 그러나 원인을 GPT signature 충돌로 오해.
---
## Step 5 — 오해의 시작: "GPT signature 충돌이 원인이다"
`Get-Disk`로 보니 두 디스크 GUID 완전 동일:
```
Disk 0: SATA, GUID {4bca652d-2f35-4bd7-8175-8951593eb8c1}, 120GB
Disk 1: SAS, GUID {4bca652d-2f35-4bd7-8175-8951593eb8c1}, 120GB (1MB padding 더 큼)
```
호스트에서 `fdisk -l /dev/zvol/...`로 검증 — 두 zvol의 Disk identifier 동일, 모든 partition UUID 동일.
**가설 (틀린 것)**: Windows가 동일 GPT signature 두 디스크 보면 한쪽을 자동 Offline 처리 → sata0 detach 후 scsi0 단독으로는 BCD device path 매칭 실패.
이 가설로 BCD 수정 시도들이 시작됨.
---
## Step 6 — BCD 수정 시도 1: device/osdevice → "boot" 키워드
```cmd
bcdedit /export C:\bcd-backup-20260519.bcd
bcdedit /set {bootmgr} device boot # 거부됨 (정상)
bcdedit /set {default} device boot # 성공
bcdedit /set {default} osdevice boot # 성공
```
`boot` 키워드 의미: "이 BCD가 로드된 partition을 동적으로 추적해서 사용". 매혹적이지만 **잘못된 적용**.
**검증**: shutdown → sata0 detach → 부팅 → **여전히 BSOD 0xc0000225**.
**왜 안 먹혔나**: `osdevice = boot`는 사실 잘못된 의미. winload는 "boot device = system partition (EFI 100MB FAT32)"으로 해석해서 거기서 `\Windows\system32\winload.efi` 찾으려고 함. EFI partition에 \Windows 없으니 fail.
---
## Step 7 — BCD 수정 시도 2: bcdboot으로 새 BCD 생성
scsi0의 EFI partition을 마운트해서 BCD 재구성:
```cmd
diskpart 스크립트로 Disk 1 Partition 2에 S: 할당
bcdboot C:\Windows /s S: /f UEFI /v
```
bcdboot이 정상 동작:
```
BFSVC: Creating Recovery directory.
BFSVC: Creating OsLoader object.
BFSVC: Setting {default} to {bfc003a5-...}
부팅 파일을 만들었습니다.
```
새 BCD enum:
```
{default}.device = partition=\Device\Harddisk0\Partition4
{default}.osdevice = partition=\Device\Harddisk0\Partition4
```
**검증**: shutdown → sata0 detach → 부팅 → **다른 BSOD 0xc0000017** (system registry file missing).
이제 한 단계 더 진행됨: OVMF→boot manager→winload까지 OK, OS partition 도달, 거기서 SYSTEM hive 못 읽음.
---
## Step 8 — NTFS 무결성 검사: ntfsfix
호스트에서 NTFS 마운트:
```bash
ntfs-3g /dev/zvol/pve-zfs/vm-<VMID>-disk-1-part4 /mnt/winc -o ro
ls /mnt/winc/Windows/System32/config/SYSTEM # 24.5MB 존재
hexdump -C -n 16 /mnt/winc/Windows/System32/config/SYSTEM
# 00000000 72 65 67 66 ... ← "regf" 시그니처 OK
umount /mnt/winc
ntfsfix /dev/zvol/pve-zfs/vm-<VMID>-disk-1-part4
# Processing of $MFT and $MFTMirr completed successfully.
# Checking the alternate boot sector... OK
# NTFS partition was processed successfully.
```
**검증**: 부팅 시도 → **BSOD 0xc0000017 그대로**.
SYSTEM hive 파일은 존재 + 헤더 정상 + NTFS metadata clean. 그러나 부팅 실패.
---
## Step 9 — NVRAM 초기화 시도
가설: OVMF NVRAM에 옛 부트 엔트리(detach된 디스크 가리키는 것)가 캐시되어 fallback 실패.
```bash
qm set <VMID> --delete efidisk0
qm set <VMID> --efidisk0 pve-zfs:1,efitype=4m,pre-enrolled-keys=0
qm start <VMID>
```
**결과**: 새 NVRAM 만들고 부팅 → **BSOD 0xc0000017 그대로**.
---
## Step 10 — WinRE Startup Repair 시도
BSOD 화면에 "F1 to enter Recovery Environment" 안내가 있음. `qm sendkey <VMID> f1`로 시도.
**결과**: WinRE 진입 안 됨. vhd 백업본에 WinRE WIM이 비활성/누락 상태. 막다른 길.
---
## Step 11 — SSD 마이그레이션식 클론 시도
> 사용자 제안: "보통은 디스크 두 개 만들고 1:1 복사하는 도구로 옮기잖아, SSD 마이그레이션처럼"
ZFS send/recv로 vhd 변환본을 새 zvol에 클론:
```bash
zfs snapshot pve-zfs/vm-<VMID>-disk-1@clone-master
time (zfs send pve-zfs/vm-<VMID>-disk-1@clone-master | zfs recv pve-zfs/vm-<VMID>-disk-4)
# real 10m50.003s
```
config 교체: 기존 vhd 변환본 detach + 클론을 scsi0로 attach → boot order=scsi0 단독.
**결과**: **BSOD 0xc0000017 — 클론도 동일 증상**.
이로써 가설이 확정적으로 깨짐: GPT 충돌이 단독 원인이라면 단일 디스크 상태에서 해결됐어야 함. 그런데 안 됨.
---
## Step 12 — 결정적 진단: sata0 단독 부팅 검증
```bash
qm set <VMID> --delete scsi0
qm set <VMID> --sata0 pve-zfs:vm-<VMID>-disk-0,size=120G
qm set <VMID> --boot 'order=sata0'
qm start <VMID>
```
**결과**: ✅ **정상 부팅** (Windows 10 잠금화면, 0bin 사용자).
이게 결정적. **sata0 단독은 부팅 가능 + vhd 변환본 단독은 부팅 불가**. 두 zvol 데이터가 GPT signature는 같지만 어딘가 다르다 — 또는 컨트롤러 차이 자체가 원인.
게스트 상태 확인:
```
Get-Disk → Disk 0 SATA Online
Get-Partition → DiskNumber=0 IsBoot=True
Get-Service vioscsi → RUNNING
sc qc vioscsi → START_TYPE: 3 DEMAND_START ← 🔥 발견!
```
**진짜 원인 발견**: vioscsi 서비스가 등록은 되어 있지만 **boot critical(=0)이 아니라 수동 시작(=3)**. winload가 부팅 시점에 vioscsi.sys를 로드하지 않음 → VirtIO-SCSI 디스크에서 부팅 시 디스크 접근 못 함 → BSOD.
`pnputil /add-driver vioscsi.inf /install`은 드라이버 파일과 PnP 매칭만 등록. **서비스의 Start 값을 자동으로 boot critical로 승격하지 않는다**.
---
## Step 13 — 진짜 해결: vioscsi boot critical 승격
```cmd
sc config vioscsi start= boot
```
검증:
```
sc qc vioscsi → START_TYPE: 0 BOOT_START
reg query HKLM\SYSTEM\CurrentControlSet\Services\vioscsi /v Start
→ Start REG_DWORD 0x0
```
---
## Step 14 — Windows 정상 종료 + ZFS 클론 + 마스터화
```bash
# Windows ACPI shutdown
qm shutdown <VMID> --timeout 180
# 검증된 sata0 디스크를 새 zvol로 클론
zfs snapshot pve-zfs/vm-<VMID>-disk-0@master-final
time (zfs send pve-zfs/vm-<VMID>-disk-0@master-final | zfs recv pve-zfs/vm-<VMID>-disk-5)
# real 28m43.568s
# config 교체
qm set <VMID> --delete sata0
qm set <VMID> --scsi0 pve-zfs:vm-<VMID>-disk-5,iothread=1,discard=on,ssd=1
qm set <VMID> --scsihw virtio-scsi-single
qm set <VMID> --boot 'order=scsi0'
qm start <VMID>
```
---
## Step 15 — 검증: 진짜 마스터 완성
```
Get-Disk → Disk 0 QEMU QEMU HARDDISK BusType=SAS 120GB Online
Get-Partition → DiskNumber=0 IsBoot=True
sc query vioscsi → RUNNING
sc qc vioscsi → START_TYPE: 0 BOOT_START
```
**단일 디스크 + virtio-scsi-single + iothread + discard + boot critical** = 마스터 완성. Windows 10 잠금화면 정상 진입.
---
## 교훈 (lessons learned)
### 진짜 원인은 vioscsi의 START_TYPE
다른 모든 가설(GPT signature 충돌, BCD 손상, NVRAM 캐시, vhd 변환 미세 손상)은 부분적으로 그럴듯했지만 **단독 원인이 아니었다**. 진짜 원인은:
```
vioscsi Service Start=3 (DEMAND_START)
winload가 부팅 시점에 vioscsi.sys 안 로드
virtio-scsi 디스크에서 부팅 시 디스크 IO 핸들러 부재
INACCESSIBLE_BOOT_DEVICE → BSOD 0xc0000017 또는 0xc0000225
```
### pnputil의 한계
- `pnputil /add-driver vioscsi.inf /install`
- 드라이버 파일과 PnP 디바이스 매칭은 잘 한다
- 그러나 **서비스의 boot priority는 건드리지 않는다**
- 그래서 매뉴얼하게 `sc config vioscsi start= boot` 또는 레지스트리 직접 수정 필요
### "vhd → raw 변환본"은 부팅 보증 안 된다
- `qm importdisk`로 vhd → raw 변환 시 사이즈에 패딩 추가, GPT backup 위치 미세 차이
- 검증된 sata 원본을 ZFS 클론하는 게 더 안전
- vhd는 "백업본"이지 "라이브 부팅 가능 디스크"가 아닐 수 있음
### 동일 GPT signature 디스크 두 개를 동시 attach하지 말 것
- Windows가 한쪽을 자동 Offline 처리하지만 NT namespace 매핑/BCD path 종속성으로 detach 시 BSOD
- 마이그레이션 작업은 단계별로 단독 attach 검증 권장
### bcdedit `osdevice = boot`은 함정
- "boot manager가 로드된 partition을 동적 추적"이라는 매혹적인 표현
- 그러나 winload는 그걸 "system partition (EFI)"으로 해석 → \Windows 못 찾음 → BSOD
- 잘못된 BCD 수정 패턴. **사용하지 말 것**.
### bcdboot은 깔끔하지만 만능 아님
- BCD store 새로 생성 + 자기 자신 가리키도록 설정
- 그러나 partition UUID는 정확히 같은 zvol이라야 매칭됨
- 동일 GPT signature 디스크가 같이 있을 때는 효과 제한적
---
## 부록: 알아두면 좋은 진단 명령
```cmd
:: 서비스 시작 유형
sc qc vioscsi | findstr START_TYPE
reg query HKLM\SYSTEM\CurrentControlSet\Services\vioscsi /v Start
:: VirtIO 디바이스 인식
powershell "Get-PnpDevice | Where Manufacturer -like '*Red Hat*' | Format-Table -AutoSize"
:: 부팅 디스크
powershell "Get-Disk | Format-Table -AutoSize Number,FriendlyName,BusType,Size"
powershell "Get-Partition | Where IsBoot -eq \$true | Format-List"
:: BCD 진단
bcdedit /enum {bootmgr}
bcdedit /enum {default}
:: driverstore의 OEM INF 등록 상태
pnputil /enum-drivers | findstr /i /c:vioscsi /c:viostor /c:netkvm
```
```bash
# 호스트 (PVE)에서 게스트 디스크 NTFS 검증
mount -t ntfs-3g -o ro /dev/zvol/<pool>/vm-<VMID>-disk-N-part4 /mnt/winc
ls /mnt/winc/Windows/System32/config/SYSTEM
hexdump -C -n 16 /mnt/winc/Windows/System32/config/SYSTEM # "regf" 시그니처
umount /mnt/winc
ntfsfix /dev/zvol/<pool>/vm-<VMID>-disk-N-part4
# 두 zvol 비교
fdisk -l /dev/zvol/<pool>/vm-<VMID>-disk-N
```

12
LICENSE Normal file
View File

@@ -0,0 +1,12 @@
Attribution 4.0 International (CC BY 4.0)
You are free to:
Share — copy and redistribute the material in any medium or format
Adapt — remix, transform, and build upon the material for any purpose, even commercially.
Under the following terms:
Attribution — You must give appropriate credit, provide a link to the license,
and indicate if changes were made. You may do so in any reasonable manner,
but not in any way that suggests the licensor endorses you or your use.
Full license text: https://creativecommons.org/licenses/by/4.0/legalcode

82
README.md Normal file
View File

@@ -0,0 +1,82 @@
# pve-vm-ops-guide
Proxmox VE에서 Windows/Linux VM을 **원격으로 다루는 실전 가이드** + **SATA → VirtIO-SCSI 디스크 컨트롤러 마이그레이션 함정** 분석.
## ⭐ 이 리포의 핵심 발견
Windows VM의 디스크 컨트롤러를 SATA에서 VirtIO-SCSI로 옮길 때 흔히 부딪치는 BSOD (`0xc0000017` / `0xc0000225`)의 진짜 원인:
```cmd
sc qc vioscsi
> START_TYPE: 3 DEMAND_START ← 이게 문제. boot critical 아님.
```
해결:
```cmd
sc config vioscsi start= boot
```
`pnputil /add-driver`로 드라이버 등록만으로는 **부족**. 서비스를 BOOT_START(0)로 명시 승격해야 winload가 부팅 시점에 vioscsi.sys를 로드한다.
자세한 진단 과정과 다른 잘못된 해결책들의 기록은 [JOURNEY.md](JOURNEY.md) 참조.
---
## 📁 구성
| 파일 | 내용 |
|---|---|
| [GUIDE.ko.md](GUIDE.ko.md) | **본 가이드** — VM 화면 캡처, QGA, qm sendkey, vhd 임포트, virtio-scsi 마이그레이션, DR 이전 |
| [JOURNEY.md](JOURNEY.md) | **실패의 연대기** — 진짜 원인 찾기까지 거친 15단계 |
| [screenshots/](screenshots/) | 부팅 검증 화면 캡처 |
---
## 🎯 가이드가 다루는 것
1. **VM 원격 제어** (Proxmox 호스트 SSH + QEMU monitor + QGA)
- 화면 캡처 (PPM → PNG)
- 게스트 안에서 명령 실행 (`qm guest exec`)
- GUI 키 입력 (`qm sendkey`)
- 사용자 세션 컨텍스트 실행 (QGA + schtasks)
2. **VHD → ZFS raw zvol 변환 + attach**
3. **VirtIO 드라이버 사전 설치 + SATA→SCSI 인터페이스 전환**
4. **VM DR (Disaster Recovery) 이전** — PBS 백업 + 다른 노드 restore
5. **트러블슈팅**
- `WARN: no efidisk configured`
- `GPT PMBR size mismatch`
- BSOD `0xc0000017` / `0xc0000225` (vioscsi boot critical 미설정)
---
## 🛠 검증 환경
- Proxmox VE 8.1.3
- ZFS pool 기반 storage (raw zvol)
- Windows 10 Pro (build 19045)
- virtio-win-0.1.285
- OVMF (UEFI) + machine: pc-i440fx-7.1
---
## 📋 표기 규약
가이드 안 placeholder:
- `<PVE_HOST>` `<PVE_PASSWORD>`: Proxmox 노드 root 접속
- `<SMB_HOST>` `<SMB_USER>` `<SMB_PASSWORD>`: SMB/CIFS 파일 서버
- `<windows_user>`: Windows 게스트 사용자
- VMID 예시는 `101`, zvol 예시는 `vm-101-disk-N` — 본인 환경 값으로 치환
---
## 🤝 기여
같은 함정에 빠진 적 있거나 다른 환경(machine: q35, viostor 기반 부팅, sysprep 이미지 등)에서의 케이스를 추가하고 싶다면 PR/issue 환영.
---
## 📄 라이선스
CC BY 4.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB