66from app .utils .vm import wait_for_state
77from typing import Optional
88from app .core .constants import VMS_DIR , IMAGES_DIR , VM_STATE_NAMES
9- from app .models .vm import VmCreate , VmRead , VmCredentialCreateRequest , RecoverableVm , RecoverableVmCreate
9+ from app .models .vm import VmCreate , VmRead , VmCredentialCreateRequest , RecoverableVm , RecoverableVmCreate , VmCreateXML
1010from app .models .image import ImageRead
1111from app .core .xml_builder import build_xml
1212from app .core .config import QEMUConfig , engine
2424from app .services .image_service import ImageService
2525import yaml
2626from pathlib import Path
27+ from sqlalchemy .orm import make_transient
2728
2829logger = logging .getLogger (__name__ )
2930
@@ -101,7 +102,14 @@ def __init__(self, vm_create: VmCreate):
101102 ["qemu-img" , "resize" , vm_path , f"+{ self .disk_size } G" ],
102103 check = True ,
103104 )
104- vm_xml = build_xml (self )
105+ vm_xml = build_xml (VmCreateXML (
106+ id = self .id ,
107+ os = self .os ,
108+ name = self .name ,
109+ mem = self .mem ,
110+ vcpus = self .vcpus ,
111+ disk_size = self .disk_size
112+ ))
105113 conn = QEMUConfig .get_connection ()
106114 conn .defineXML (vm_xml )
107115 with Session (engine ) as session :
@@ -292,6 +300,7 @@ def _get_vm_or_404(session: Session, vm_id: str) -> VmORM:
292300 )
293301 return vm_record
294302
303+
295304 @staticmethod
296305 def _get_slave_for_vm (vm_id : str ) -> Optional [SlaveORM ]:
297306 """Check if a VM is hosted on a slave node."""
@@ -301,6 +310,21 @@ def _get_slave_for_vm(vm_id: str) -> Optional[SlaveORM]:
301310 return session .get (SlaveORM , vm_record .slave_id )
302311 return None
303312
313+
314+ @staticmethod
315+ def _get_duplicate_name (session : Session , vm_name : str ) -> str :
316+ base_name = f"Duplicate of { vm_name } "
317+ search_pattern = f"{ base_name } %"
318+
319+ statement = select (
320+ func .count (
321+ VmORM .id )).where (
322+ VmORM .name .like (search_pattern ))
323+ count = session .exec (statement ).one ()
324+ if count == 0 :
325+ return base_name
326+ return f"{ base_name } ({ count } )"
327+
304328 def get_vm_list ():
305329 with Session (engine ) as session :
306330 vm_records = session .scalars (select (VmORM )).all ()
@@ -678,3 +702,35 @@ def remove_all_recoverable_vms():
678702 rmtree (VMS_DIR / v .name )
679703 break
680704 return
705+
706+ @staticmethod
707+ def duplicate_vm (vm_id : str ):
708+ with Session (engine ) as session :
709+ vm_to_duplicate = VmService ._get_vm_or_404 (session , vm_id )
710+ duplicate_vm = VmORM (** vm_to_duplicate .model_dump ())
711+ duplicate_vm .id = uuid .uuid4 ()
712+ duplicate_vm .name = VmService ._get_duplicate_name (
713+ session , duplicate_vm .name )
714+
715+ src_path = VMS_DIR / vm_id / duplicate_vm .os
716+ dest_path = VMS_DIR / str (duplicate_vm .id )
717+
718+ dest_path .mkdir (parents = True , exist_ok = True )
719+ copy (src_path , dest_path / duplicate_vm .os )
720+
721+ vm_xml = build_xml (VmCreateXML (** duplicate_vm .model_dump ()))
722+
723+ try :
724+ conn = QEMUConfig .get_connection ()
725+ conn .defineXML (vm_xml )
726+ session .add (duplicate_vm )
727+ session .commit ()
728+ except Exception as e :
729+ if dest_path .exists ():
730+ rmtree (dest_path )
731+ raise HTTPException (
732+ status_code = 500 ,
733+ detail = f"Failed to duplicate VM: { str (e )} "
734+ )
735+
736+ return Vm .get (str (duplicate_vm .id ))
0 commit comments