- 노드에 SSD를 추가해 Proxmox에 별도 LVM-Thin 스토리지로 등록해도, 백엔드가 그 스토리지를 쓰게 할 방법이 없다
- 백엔드는 템플릿을 클론할 때 스토리지를 지정하지 않는다 (
backend/services/vm_service.pycreate_vm()의clone.post(newid, name, full=1, target)). 콘솔로 VM을 만들어 확인해 보면 디스크가 템플릿과 같은 기존 스토리지(local-lvm)에 생기고, 새 스토리지는 쓰이지 않는다 - 반면 VM 생성 전 SSD 80% 검사(
get_server_resource_usage)와 대시보드 표시(vmcontrol.py)는 노드의 모든lvmthin스토리지를 합산한다. 그래서 새 스토리지를 더하면 합산 사용률이 낮아져 검사는 통과하지만, VM은 여유가 없는 기존 스토리지에 계속 쌓일 수 있다
- 설정한 노드에서는 후보 스토리지 중 남은 용량(GB)이 가장 큰 곳에 VM을 만든다
- 80% 검사를 클론이 실제로 들어갈 스토리지 기준으로 한다
- 설정하지 않은 노드(gsmgpu1·2)는 지금 동작을 하나도 바꾸지 않는다
- Proxmox VE 「Copies and Clones」(
chapter-qm.html#qm_copy_and_clone): full clone은 "It is possible to select a Target Storage, so one can use this to migrate a VM to a totally different storage." 따라서 템플릿이local-lvm에 있어도 full clone으로 다른 스토리지에 만들 수 있다 - 같은 절: "It is not possible to change the Target storage for linked clones." 백엔드는 이미
full=1이라 해당하지 않는다 - 같은 절: "A full clone needs to read and copy all VM image data." 디스크를 건너는 복사라 시간이 걸릴 수 있다 (아래 배포 전 확인 1번)
qm clone --storage: "Target storage for full clone."- lvmthin(7): thin pool이 가득 차면 쓰기를 기본 60초 대기시킨 뒤 에러를 돌려주고 파일 시스템이 손상될 수 있다. "Thin snapshots also keep thin pool chunks from being freed." 그래서 남은 용량 검사와 함께 80% 여유 기준을 유지한다
.env에 노드별 후보 스토리지 목록을 둔다. 예:NODE_3_CLONE_STORAGES=local-lvm,extra-ssd- 목록이 있는 노드에서 VM을 만들 때, 각 후보를 아래 조건으로 거른다
- Proxmox가 돌려준 노드 스토리지 목록에 그 이름이 있다
active == 1이고,enabled값이 있으면1이다content에images가 들어 있다total > 0이고 사용률(used / total × 100)이 80% 미만이다- 남은 용량(
avail)이 이 VM의 디스크 크기 이상이다
- 남은 조건을 통과한 후보 중
avail이 가장 큰 것을 고른다. 같으면 목록에서 앞에 적힌 것을 고른다 - 고른 스토리지를
clone.post(..., storage=선택)으로 넘긴다 - 목록이 있는 노드는 기존의 합산 SSD 80% 검사를 하지 않는다 (2번의 스토리지별 검사가 대신한다). RAM·CPU 80% 검사와 가용 RAM 검사는 그대로 한다
- 목록이 없는 노드는 지금과 똑같다: 합산 SSD 80% 검사,
storage인자 없이 클론 - 대시보드(
vmcontrol.py)와 Discord 일일 리포트(main.py)는 지금처럼 합산해서 보여 준다 - 실패하면 실패 그대로 알린다. 다른 스토리지로 몰래 다시 시도하지 않는다
- 클론을 시작한 뒤 실패하면, 예외 종류(
HTTPException포함)와 관계없이 Proxmox에 만들어진 VM을 정리한다 (아래 「실패 정리」)
| 항목 | 결정 | 이유 |
|---|---|---|
| 설정 위치 | .env 노드별 변수 |
노드 접속 정보와 같은 방식. DB 스키마를 바꾸지 않는다 (운영 DB는 create_all이 기존 표를 바꾸지 않아 직접 ALTER해야 한다) |
| 후보 범위 | .env에 적은 노드·스토리지만 |
모든 lvmthin을 자동 후보로 삼으면 lvmthin이 여러 개인 노드(대시보드 코드 주석: "data, vm 등 여러 파티션")의 동작까지 바뀐다 |
| 비교 기준 | 남은 용량(바이트) | 두 스토리지 크기가 달라서 사용률(%)로는 실제로 들어갈 자리를 비교할 수 없다 |
| 필요 용량 | 이 VM의 디스크 크기 전체 | thin이라 처음에는 조금만 차지하지만, 나중에 디스크 크기까지 늘어날 수 있다 |
| 80% 검사 | 후보 조건에 유지 | 남은 용량만 보면 pool이 끝까지 찬다. pool이 차면 그 위 VM 전체가 쓰기에 실패한다 (lvmthin(7)) |
| 동시 생성 | 기존 VM 생성 큐를 따른다 | 아래 「동시성」 참고 |
- DB 스키마 변경 없음.
Server모델,init_servers.py그대로 backend/core/config.pyNODE_1_CLONE_STORAGES: str = "",NODE_2_CLONE_STORAGES: str = "",NODE_3_CLONE_STORAGES: str = ""추가. 빈 문자열이면 기존 동작Settings.get_clone_storages(node_name: str) -> list[str]추가NODE_{i}_NAME == node_name인 노드의 값을 쉼표로 나누고, 앞뒤 공백을 지우고, 빈 항목을 버리고, 순서를 지키며 중복을 없앤다- 맞는 노드가 없거나 값이 비어 있으면
[]
.env.example에 노드마다NODE_{i}_CLONE_STORAGES=줄과 설명 주석을 추가한다 (값은 비워 둔다)
- 엔드포인트·요청·응답 형식 변경 없음
- 실패 시 VM 생성 작업(
VmCreationJob)의error_message에 아래 「오류」의 메시지가 들어간다 (기존 큐 동작:HTTPException.detail을 그대로 기록) - 응답과 작업 결과에 스토리지 이름을 넣지 않는다
backend/services/mon_service.py에 추가한다 (스토리지 조회 코드가 이미 여기 있다).
def select_clone_storage(server: Server, candidates: list[str], required_disk_gb: int) -> str:
...proxmox.nodes(server.name).storage.get()을 한 번 호출한다. 예외가 나거나 응답이 리스트가 아니면 로그를 남기고 503을 낸다. 리스트 안의 딕셔너리가 아닌 항목은 무시한다required_bytes = required_disk_gb * 1024 ** 3avail이 없으면total - used로 계산한다- 후보를 목록 순서대로 보고, 조건에 맞지 않는 후보는 이유와 함께 로그를 남긴다. 설정에 있는데 노드에 없는 이름은 설정 오류일 수 있으므로
logger.error로 남긴다 - 고른 스토리지와 남은 용량(GB)을
logger.info로 남긴다 - 80%는
create_vm의 기존THRESHOLD = 80.0과 같은 값이어야 한다. 두 곳이 같은 상수를 보도록core/constants.py에 상수로 둔다
specs["disk"]가 확정된 뒤(custom_disk반영 후)의 값을 필요 용량으로 쓴다. 현재 BASIC·STANDARD 20 GB, PROJECT_CUSTOM 20~40 GB- 서버를 정한 뒤
clone_storages = settings.get_clone_storages(server.name) - 기존 「1-1. 자원 조회」 블록에서
clone_storages가 있으면 합산disk_pct검사만 건너뛴다. 이 블록의except Exception: ... 체크 건너뜀은 RAM·CPU 검사에 대해서는 지금 동작을 유지한다 - 그 블록 바깥에서
clone_storages가 있으면select_clone_storage(server, clone_storages, specs["disk"])를 호출한다- 블록 안에 두면 조회 실패가
except Exception에 먹혀서 검사 없이 진행되므로 반드시 바깥에 둔다 - VMID 선택·IP 할당·DB 커밋(3·4단계) 전에 호출한다. 실패해도 DB 기록·VMID·Proxmox에 아무것도 남지 않게 하기 위해서다
- 블록 안에 두면 조회 실패가
- 5단계 클론에서
clone_storage가 있을 때만storage=clone_storage를 넣는다. 없으면 지금과 같은 인자로 호출한다 - 클론 대상 스토리지를 VMID와 함께 로그로 남긴다
지금은 except HTTPException 분기가 DB 레코드만 지운다. 클론 작업이 실패하거나 120초를 넘기면 _wait_for_clone이 HTTPException을 던지는데, 이때 Proxmox VM이 남는다. Proxmox VM 삭제, iptables 정리, 스니펫 삭제는 except Exception 분기에만 있다.
except Exception분기의 정리 코드(Proxmox VM 삭제, iptables 삭제, 스니펫 삭제)를 함수 하나로 뺀다. 예:_cleanup_failed_vm_resources(server, proxmox, vmid, internal_ip, *, clone_started, iptables_added)- 두 분기 모두 이 함수를 부른 뒤
_cleanup_vm_db_record를 부른다except HTTPException: 원래 예외를 그대로 다시 던진다 (메시지 유지)except Exception: 지금처럼 500 "VM 생성에 실패했습니다. 잠시 후 다시 시도해주세요."
- Proxmox VM 삭제는
clone_started가 참일 때만 한다.clone.post가 성공해 돌아온 뒤에만 참이 되므로, 남의 VM을 지울 일이 없다 - 지우기 전에 그 VM이 Proxmox에 아직 있는지
qemu(vmid).status.current.get()으로 확인한다. Proxmox API 오류(ResourceException)의 내용에does not exist가 있을 때만 없다고 보고(클론 실패 때 Proxmox가 지운 경우), lock 대기와 삭제를 건너뛰고 info 로그만 남긴다. 그 밖의ResourceException(401/403·5xx 같은 권한·일시 오류)과 연결 오류 같은 다른 예외는 확인할 수 없다고 보고 경고 로그를 남긴 뒤 지금처럼 삭제를 시도한다- 이유: proxmoxer는 HTTP 400 이상이면 모두
ResourceException을 던진다. 전부 "없음"으로 보면 일시 오류 때 삭제를 건너뛰어 VM이 남는다 (코드 리뷰 반영) _is_vmid_free_in_proxmox(클러스터nextidAPI)는 쓰지 않는다. 기존 테스트 목업은nextid가 항상 "비어 있음"을 돌려주게 되어 있어서, 쓰면 기존 정리 테스트가 깨진다- 이유: 없는 VM이면 지금 코드는
config.get실패로 3초씩 10번(30초) 기다린 뒤 삭제에도 실패해 "수동 확인 필요" 경고를 남긴다
- 이유: proxmoxer는 HTTP 400 이상이면 모두
- lock 대기(3초 × 10번)와
delete(purge=1), 실패 시 "수동 확인 필요" 경고는 지금 동작 그대로다. 클론이 아직 진행 중이라 30초 안에 lock이 풀리지 않으면 삭제에 실패하고 경고가 남는다
- VM 생성 요청은 큐에 들어가고, 워커 스레드 하나가 하나씩 차례로
create_vm()을 실행한다 (backend/services/vm_creation_queue.py).create_vm()은 클론이 끝날 때까지 기다린다 - 따라서 스토리지 선택을
create_vm()안에 두면 생성 작업끼리 겹치지 않는다. 큐에 넣는 시점의 검사(validate_vm_creation_request)에 두지 않는다. 그러면 대기 중인 작업들이 모두 같은 예전 값을 보게 된다 - 전제: 백엔드가 프로세스 하나로 돈다. 큐는 프로세스 메모리 안에 있어서 프로세스가 여럿이면 워커도 여럿이 된다.
backend/Dockerfile은--workers없이 uvicorn을 실행하고WEB_CONCURRENCY기본값은 1이다. 운영 값은 배포 전에 확인한다
| 상황 | 상태 코드 | 사용자 메시지 | 로그 |
|---|---|---|---|
| 노드 스토리지 목록 조회 실패 | 503 | VM 저장 공간 상태를 확인할 수 없습니다. 잠시 후 다시 시도해주세요. | 노드 이름, 예외 |
| 조건에 맞는 후보 없음 (설정 이름 오타·비활성·80% 이상·남은 용량 부족 포함) | 507 | 선택한 사양의 VM을 만들 SSD 공간이 부족합니다. 관리자에게 문의하세요. | 후보별 탈락 이유, 필요 용량 |
| 클론 작업 실패 | 500 | VM {vmid} 클론에 실패했습니다. 잠시 후 다시 시도해주세요. | Proxmox 작업 종료 상태(exitstatus) 원문 + 대상 스토리지 |
| 클론 타임아웃 | 500 | 기존 메시지 (VM {vmid} 클론 타임아웃) | 대상 스토리지 |
- 메시지에는 스토리지 이름과 용량 숫자를 넣지 않는다
_wait_for_clone은 지금까지 Proxmoxexitstatus원문을 사용자 메시지에 담았다. 원문에는 스토리지·볼륨 이름이 들어갈 수 있어서 로그에만 남기고 사용자에게는 일반 문구를 보낸다 (코드 리뷰 반영)
- 스토리지 이름은
.env에서만 온다. 요청 값으로 받지 않으므로 사용자가 Proxmox API 인자를 바꿀 수 없다 - 새 엔드포인트가 없고 권한 검사는 그대로다 (gsmgpu3은 큐 등록과
create_vm()양쪽에서PROJECT_OWNER·ADMIN만 허용) - 사용자 메시지에 인프라 이름을 넣지 않는다
backend/tests/에 추가한다. Proxmox 호출은 mock한다. conftest.py가 get_server_resource_usage를 이미 mock하고 있으므로 스토리지 선택은 storage.get() 응답을 mock해서 시험한다.
get_clone_storages: 공백·빈 항목·중복 처리, 순서 유지, 값이 없는 노드와 없는 노드 이름은[]
- 두 후보 모두 조건을 만족하면
avail이 큰 쪽을 고른다 avail이 같으면 목록 앞쪽을 고른다- 사용률 80% 이상이면
avail이 더 커도 제외한다 avail이 필요 용량보다 작으면 제외한다 (20 GB, 40 GB 각각)active == 0,enabled == 0,content에images없음, 노드에 없는 이름은 제외한다- 모두 제외되면 507
storage.get()예외면 503, 응답이 리스트가 아니면(None, 딕셔너리, 문자열) 503avail이 없으면total - used로 계산해 비교한다
- 목록이 있는 노드:
clone.post에storage가 들어간다. 합산disk_pct가 80% 이상이어도 후보가 조건을 만족하면 통과한다 - 목록이 없는 노드:
clone.post에storage가 없다. 합산disk_pct80% 이상이면 지금처럼 507 - 선택이 실패하면
Vm레코드가 남지 않고 VMID 선택·클론을 호출하지 않는다 - PROJECT_CUSTOM
custom_disk=40이면 40 GB로 검사한다
- 클론 뒤
_wait_for_clone이HTTPException(클론 실패·타임아웃)을 던지면: Proxmox VMdelete(purge=1)를 호출하고, DB 레코드를 지우고, 원래HTTPException이 그대로 올라간다 - 같은 상황에서 VMID가 Proxmox에 이미 없으면:
delete를 호출하지 않고 lock 대기도 하지 않는다 - 존재 확인이
does not exist가 아닌ResourceException(503, 401)이나 연결 오류면: 삭제를 시도한다 clone.post자체가 실패하면(clone_started가 거짓): Proxmox VMdelete를 호출하지 않는다- 클론 뒤 일반 예외(예: iptables 등록 실패)면: 지금처럼 VM·iptables·스니펫을 정리하고 500을 낸다
_wait_for_clone이 실패한 작업의exitstatus(스토리지·볼륨 이름 포함)를 사용자 메시지에 넣지 않는다
- 선택 실패(507) 작업은
FAILED가 되고error_message에 메시지가 그대로 들어간다. 기존test_vm_lifecycle.py의test_failed_job_keeps_http_exception_detail이 이미 검증한다
- 클론 시간 측정 (완료). Proxmox 웹 UI에서 템플릿을 Full Clone, Target Storage를 새 스토리지로 해서 임시 VMID에 클론해 보니 대기 시간(120초)보다 훨씬 짧게 끝났다. 임시 VM은 지웠다.
_wait_for_clone의 대기 시간은 바꾸지 않는다 - 운영 백엔드가 프로세스 하나인지 확인한다 (
WEB_CONCURRENCY가 없거나 1,--workers없음) - 운영
.env에 해당 노드의NODE_{N}_CLONE_STORAGES(기존 스토리지와 새 스토리지)를 넣고 백엔드를 재시작한다 - 콘솔에서 gsmgpu3에 VM을 하나 만들고, Proxmox Hardware에서 Hard Disk가 새 스토리지 이름으로 시작하는지(
{스토리지}:vm-…) 본다. 확인 뒤 콘솔에서 지운다 - 이전 테스트로 만든 VM은 콘솔에서 지운다
- 클론 실패·타임아웃 때 Proxmox에 VM이 남던 기존 문제는 이번 범위에서 고친다 (「실패 정리」). 다만 클론이 진행 중인 채로 타임아웃이 나서 30초 안에 lock이 풀리지 않으면 여전히 남을 수 있다. 이때는 "수동 확인 필요" 경고 로그가 남는다. 클론 작업 중지나 관리자 알림으로 보완하는 일은 별도 이슈로 다룬다 (코드 리뷰 반영)
- 대시보드는 합산이라 여유가 있어 보여도, 스토리지마다 따로 판단하므로 생성이 거절될 수 있다
.env의NODE_{N}_CLONE_STORAGES를 지우거나 비우고 재시작하면 예전 동작으로 돌아간다- 이미 새 스토리지에 만든 VM은 그대로 동작한다. 시작·정지·삭제·스냅샷은 VM 설정에 적힌 스토리지를 Proxmox가 따라가므로 백엔드 변경이 필요 없다
- 대시보드에 스토리지별 표시
- 기존 VM 디스크를 새 스토리지로 옮기기
- gsmgpu1·2 설정
- 생성 후 디스크 크기 변경 기능 (지금은 CPU·RAM만 바꿀 수 있다)