- 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
392 lines
15 KiB
Markdown
392 lines
15 KiB
Markdown
# 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에 남을 수 있음 — 캡처 파일 즉시 삭제
|