main.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. from fastapi import FastAPI, UploadFile, File
  2. from fastapi.staticfiles import StaticFiles
  3. from fastapi.responses import FileResponse
  4. import os
  5. import libvirt
  6. import subprocess
  7. import random
  8. import xml.etree.ElementTree as ET
  9. import socket
  10. import time
  11. from pydantic import BaseModel
  12. app = FastAPI(title="VirtManager API", description="API for managing virtual machines")
  13. app.mount("/static", StaticFiles(directory="static"), name="static")
  14. ws_processes = {}
  15. stats_cache = {} # Cache for CPU stats to calculate deltas
  16. def get_free_port():
  17. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  18. s.bind(('', 0))
  19. return s.getsockname()[1]
  20. def get_state_string(state_code):
  21. states = {
  22. 0: "no state",
  23. 1: "running",
  24. 2: "blocked",
  25. 3: "paused",
  26. 4: "shutdown",
  27. 5: "shut off",
  28. 6: "crashed",
  29. 7: "pm suspended"
  30. }
  31. return states.get(state_code, "unknown")
  32. class BootOrder(BaseModel):
  33. order: list[str]
  34. # Connect to libvirt
  35. conn = libvirt.open('qemu:///system')
  36. @app.get("/")
  37. async def root():
  38. return FileResponse("static/index.html")
  39. @app.get("/vms")
  40. async def list_vms():
  41. domains = conn.listAllDomains()
  42. vms = []
  43. for domain in domains:
  44. xml_desc = domain.XMLDesc()
  45. root = ET.fromstring(xml_desc)
  46. graphics = root.find(".//graphics[@type='vnc']")
  47. vnc_port = None
  48. if graphics is not None:
  49. port_str = graphics.get('port')
  50. if port_str and port_str != '-1':
  51. vnc_port = int(port_str)
  52. vms.append({
  53. "id": domain.ID(),
  54. "name": domain.name(),
  55. "state": get_state_string(domain.state()[0]),
  56. "vnc_port": vnc_port
  57. })
  58. return {"vms": vms}
  59. @app.get("/vms/{vm_name}/info")
  60. async def get_vm_info(vm_name: str):
  61. try:
  62. domain = conn.lookupByName(vm_name)
  63. xml_desc = domain.XMLDesc()
  64. root = ET.fromstring(xml_desc)
  65. # Get basic info
  66. memory = root.find("memory").text if root.find("memory") is not None else "Unknown"
  67. cpus = root.find("vcpu").text if root.find("vcpu") is not None else "Unknown"
  68. # Get VNC port
  69. graphics = root.find(".//graphics[@type='vnc']")
  70. vnc_port = None
  71. if graphics is not None:
  72. port_str = graphics.get('port')
  73. if port_str and port_str != '-1':
  74. vnc_port = int(port_str)
  75. # Get disks
  76. disks = []
  77. for disk in root.findall(".//disk"):
  78. device = disk.get('device')
  79. driver = disk.find("driver")
  80. source = disk.find("source")
  81. target = disk.find("target")
  82. disks.append({
  83. "type": device,
  84. "driver": driver.get('type') if driver is not None else "unknown",
  85. "source": source.get('file') if source is not None else "unknown",
  86. "target": target.get('dev') if target is not None else "unknown"
  87. })
  88. # Get network interfaces
  89. interfaces = []
  90. for iface in root.findall(".//interface"):
  91. mac = iface.find("mac")
  92. source = iface.find("source")
  93. model = iface.find("model")
  94. interfaces.append({
  95. "mac": mac.get('address') if mac is not None else "unknown",
  96. "network": source.get('network') if source is not None else "unknown",
  97. "model": model.get('type') if model is not None else "unknown"
  98. })
  99. return {
  100. "id": domain.ID(),
  101. "name": domain.name(),
  102. "state": get_state_string(domain.state()[0]),
  103. "memory": int(memory),
  104. "cpus": int(cpus),
  105. "vnc_port": vnc_port,
  106. "disks": disks,
  107. "interfaces": interfaces
  108. }
  109. except Exception as e:
  110. return {"error": str(e)}, 500
  111. @app.get("/vms/{vm_name}/stats")
  112. async def get_vm_stats(vm_name: str):
  113. """Get CPU and memory usage statistics for a VM"""
  114. try:
  115. domain = conn.lookupByName(vm_name)
  116. # Check if VM is running
  117. if not domain.isActive():
  118. return {
  119. "name": vm_name,
  120. "state": "shut off",
  121. "cpu_percent": 0,
  122. "memory_used": 0,
  123. "memory_total": 0,
  124. "memory_percent": 0
  125. }
  126. # Get domain info (memory is in MiB)
  127. info = domain.info()
  128. memory_total = info[1] # Maximum memory in MiB
  129. cpu_count = info[3] # Number of virtual CPUs
  130. # Get CPU stats - calculate real CPU percentage
  131. cpu_percent = 0
  132. try:
  133. cpu_stats = domain.getCPUStats(True)
  134. if cpu_stats and len(cpu_stats) > 0:
  135. current_cpu_time_ns = cpu_stats[0].get('cpu_time', 0)
  136. current_time = time.time()
  137. # Initialize or update cache
  138. if vm_name not in stats_cache:
  139. stats_cache[vm_name] = {
  140. 'cpu_time_ns': current_cpu_time_ns,
  141. 'time': current_time,
  142. 'memory': 0
  143. }
  144. cpu_percent = 0
  145. else:
  146. prev_data = stats_cache[vm_name]
  147. prev_cpu_time_ns = prev_data['cpu_time_ns']
  148. prev_time = prev_data['time']
  149. # Calculate deltas
  150. cpu_time_delta_ns = current_cpu_time_ns - prev_cpu_time_ns
  151. time_delta_seconds = current_time - prev_time
  152. if time_delta_seconds > 0:
  153. # Convert CPU time delta to percentage
  154. # cpu_count vCPUs * 100% each = total possible CPU seconds in time_delta
  155. max_cpu_ns = (time_delta_seconds * cpu_count) * 1e9
  156. if max_cpu_ns > 0:
  157. cpu_percent = min(100, (cpu_time_delta_ns / max_cpu_ns) * 100)
  158. # Update cache
  159. stats_cache[vm_name] = {
  160. 'cpu_time_ns': current_cpu_time_ns,
  161. 'time': current_time,
  162. 'memory': 0
  163. }
  164. except Exception as e:
  165. cpu_percent = 0
  166. # Get memory stats
  167. memory_used = 0
  168. memory_percent = 0
  169. try:
  170. memory_stats = domain.memoryStats()
  171. if memory_stats:
  172. # Try 'actual' first (actual memory used in KB)
  173. if 'actual' in memory_stats:
  174. memory_used_kb = memory_stats.get('actual', 0)
  175. memory_used = memory_used_kb // 1024 # Convert to MiB
  176. # Fallback to 'unused' to calculate used memory
  177. elif 'unused' in memory_stats:
  178. unused_kb = memory_stats.get('unused', 0)
  179. total_kb = memory_total * 1024
  180. memory_used_kb = total_kb - unused_kb
  181. memory_used = memory_used_kb // 1024
  182. else:
  183. memory_used = 0
  184. # Calculate percentage
  185. if memory_total > 0:
  186. memory_percent = (memory_used / memory_total) * 100
  187. except Exception as e:
  188. memory_used = 0
  189. memory_percent = 0
  190. return {
  191. "name": vm_name,
  192. "state": "running",
  193. "cpu_percent": round(cpu_percent, 2),
  194. "memory_used": memory_used,
  195. "memory_total": memory_total,
  196. "memory_percent": round(memory_percent, 2)
  197. }
  198. except Exception as e:
  199. return {"error": str(e), "state": "error"}
  200. @app.get("/vms/{vm_name}/start")
  201. async def start_vm(vm_name: str):
  202. try:
  203. domain = conn.lookupByName(vm_name)
  204. domain.create()
  205. return {"message": f"VM {vm_name} started"}
  206. except Exception as e:
  207. return {"error": str(e)}
  208. @app.get("/vms/{vm_name}/stop")
  209. async def stop_vm(vm_name: str):
  210. try:
  211. domain = conn.lookupByName(vm_name)
  212. domain.destroy()
  213. return {"message": f"VM {vm_name} stopped"}
  214. except Exception as e:
  215. return {"error": str(e)}
  216. @app.post("/vms")
  217. async def create_vm(name: str, memory: int, disk_size: int, cpus: int = 1, iso: str = ""):
  218. if not iso:
  219. return {"error": "ISO path is required"}
  220. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  221. disk_path = f"{images_path}/{name}.qcow2"
  222. iso_path = iso
  223. print(f"[CREATE_VM] Starting VM creation: name={name}, memory={memory}, disk_size={disk_size}, cpus={cpus}")
  224. print(f"[CREATE_VM] Using ISO path: {iso_path}")
  225. # Create disk using sudo script for proper permissions
  226. try:
  227. # Get absolute path to the script (up one level from app directory)
  228. app_dir = os.path.dirname(os.path.abspath(__file__))
  229. project_root = os.path.dirname(app_dir)
  230. script_path = os.path.join(project_root, 'create_disk.sh')
  231. print(f"[CREATE_VM] Script path: {script_path}")
  232. result = subprocess.run(['sudo', script_path, disk_path, f'{disk_size}G'], capture_output=True, text=True, timeout=60)
  233. print(f"[CREATE_VM] Disk creation return code: {result.returncode}")
  234. if result.returncode != 0:
  235. error_msg = result.stderr if result.stderr else result.stdout
  236. print(f"[CREATE_VM] ERROR creating disk: {error_msg}")
  237. return {"error": f"Failed to create disk: {error_msg}"}
  238. print(f"[CREATE_VM] Disk created successfully: {disk_path}")
  239. except subprocess.TimeoutExpired:
  240. return {"error": "Disk creation timed out"}
  241. except Exception as e:
  242. print(f"[CREATE_VM] Exception creating disk: {str(e)}")
  243. return {"error": f"Error creating disk: {str(e)}"}
  244. # Verify disk exists
  245. if not os.path.exists(disk_path):
  246. print(f"[CREATE_VM] ERROR: Disk file not found after creation: {disk_path}")
  247. return {"error": f"Disk file not created at {disk_path}"}
  248. # Generate unique MAC
  249. mac = f"52:54:00:{random.randint(0, 255):02x}:{random.randint(0, 255):02x}:{random.randint(0, 255):02x}"
  250. print(f"[CREATE_VM] Generated MAC: {mac}")
  251. # Basic XML template
  252. xml_template = f"""<domain type='kvm'>
  253. <name>{name}</name>
  254. <memory unit='MiB'>{memory}</memory>
  255. <currentMemory unit='MiB'>{memory}</currentMemory>
  256. <vcpu placement='static'>{cpus}</vcpu>
  257. <os>
  258. <type arch='x86_64' machine='pc-i440fx-6.2'>hvm</type>
  259. <boot dev='cdrom'/>
  260. </os>
  261. <features>
  262. <acpi/>
  263. <apic/>
  264. </features>
  265. <cpu mode='host-model' check='partial'/>
  266. <clock offset='utc'>
  267. <timer name='rtc' tickpolicy='catchup'/>
  268. <timer name='pit' tickpolicy='delay'/>
  269. <timer name='hpet' present='no'/>
  270. </clock>
  271. <on_poweroff>destroy</on_poweroff>
  272. <on_reboot>restart</on_reboot>
  273. <on_crash>destroy</on_crash>
  274. <pm>
  275. <suspend-to-mem enabled='no'/>
  276. <suspend-to-disk enabled='no'/>
  277. </pm>
  278. <devices>
  279. <emulator>/usr/bin/kvm</emulator>
  280. <disk type='file' device='disk'>
  281. <driver name='qemu' type='qcow2'/>
  282. <source file='{disk_path}'/>
  283. <target dev='vda' bus='virtio'/>
  284. </disk>
  285. <disk type='file' device='cdrom'>
  286. <driver name='qemu' type='raw'/>
  287. <source file='{iso_path}'/>
  288. <target dev='hda' bus='ide'/>
  289. <readonly/>
  290. </disk>
  291. <interface type='network'>
  292. <mac address='{mac}'/>
  293. <source network='default'/>
  294. <model type='virtio'/>
  295. </interface>
  296. <console type='pty'>
  297. <target type='serial' port='0'/>
  298. </console>
  299. <input type='tablet' bus='usb'>
  300. <address type='usb' bus='0' port='1'/>
  301. </input>
  302. <input type='mouse' bus='ps2'/>
  303. <input type='keyboard' bus='ps2'/>
  304. <graphics type='vnc' port='-1' autoport='yes'/>
  305. <video>
  306. <model type='cirrus' vram='16384' heads='1' primary='yes'/>
  307. </video>
  308. <memballoon model='virtio'>
  309. <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
  310. </memballoon>
  311. </devices>
  312. </domain>"""
  313. print(f"[CREATE_VM] XML template prepared for VM definition")
  314. try:
  315. print(f"[CREATE_VM] Defining VM with libvirt...")
  316. domain = conn.defineXML(xml_template)
  317. print(f"[CREATE_VM] VM defined successfully. Starting VM...")
  318. domain.create()
  319. print(f"[CREATE_VM] VM started successfully!")
  320. return {"message": f"VM {name} created with {cpus} CPUs, {memory} MiB memory, {disk_size}G disk, booting from {iso}"}
  321. except libvirt.libvirtError as e:
  322. print(f"[CREATE_VM] Libvirt error: {str(e)}")
  323. # Try to clean up the disk if VM definition failed
  324. try:
  325. os.remove(disk_path)
  326. print(f"[CREATE_VM] Cleaned up disk file after error")
  327. except:
  328. pass
  329. return {"error": f"Libvirt error: {str(e)}"}
  330. except Exception as e:
  331. print(f"[CREATE_VM] Unexpected error: {str(e)}")
  332. # Try to clean up the disk if VM definition failed
  333. try:
  334. os.remove(disk_path)
  335. print(f"[CREATE_VM] Cleaned up disk file after error")
  336. except:
  337. pass
  338. return {"error": f"Error: {str(e)}"}
  339. @app.get("/console/{vm_name}")
  340. async def get_console(vm_name: str):
  341. try:
  342. console_conn = libvirt.open('qemu:///system')
  343. domain = console_conn.lookupByName(vm_name)
  344. xml_desc = domain.XMLDesc()
  345. root = ET.fromstring(xml_desc)
  346. graphics = root.find(".//graphics[@type='vnc']")
  347. console_conn.close()
  348. if graphics is None:
  349. return {"error": "No VNC graphics found"}
  350. vnc_port = graphics.get('port')
  351. if not vnc_port or vnc_port == '-1':
  352. return {"error": "VNC port not assigned"}
  353. vnc_port = int(vnc_port)
  354. if vm_name not in ws_processes:
  355. ws_port = get_free_port()
  356. try:
  357. proc = subprocess.Popen(['websockify', str(ws_port), f'localhost:{vnc_port}'],
  358. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  359. ws_processes[vm_name] = (proc, ws_port)
  360. except FileNotFoundError:
  361. return {"error": "websockify not found. Please install it: apt-get install websockify"}
  362. _, ws_port = ws_processes[vm_name]
  363. return {"url": f"/static/novnc/vnc.html?host={socket.gethostname()}&port={ws_port}"}
  364. except Exception as e:
  365. import traceback
  366. error_msg = f"Error: {str(e)}\n{traceback.format_exc()}"
  367. print(error_msg)
  368. return {"error": str(e)}
  369. @app.delete("/vms/{vm_name}")
  370. async def delete_vm(vm_name: str):
  371. try:
  372. domain = conn.lookupByName(vm_name)
  373. if domain.isActive():
  374. domain.destroy()
  375. domain.undefine()
  376. # Kill websockify process if exists
  377. if vm_name in ws_processes:
  378. proc, _ = ws_processes[vm_name]
  379. proc.terminate()
  380. proc.wait()
  381. del ws_processes[vm_name]
  382. return {"message": f"VM {vm_name} deleted"}
  383. except Exception as e:
  384. return {"error": str(e)}
  385. @app.put("/vms/{vm_name}/boot-order")
  386. async def set_boot_order(vm_name: str, boot_order: BootOrder):
  387. try:
  388. domain = conn.lookupByName(vm_name)
  389. if domain.isActive():
  390. return {"error": "VM must be shut off to change boot order"}
  391. xml_desc = domain.XMLDesc()
  392. root = ET.fromstring(xml_desc)
  393. os_section = root.find("os")
  394. # Remove existing boot elements
  395. for boot in os_section.findall("boot"):
  396. os_section.remove(boot)
  397. # Add new boot elements
  398. for dev in boot_order.order:
  399. ET.SubElement(os_section, "boot", {"dev": dev})
  400. new_xml = ET.tostring(root, encoding='unicode')
  401. # Undefine and redefine
  402. domain.undefine()
  403. new_domain = conn.defineXML(new_xml)
  404. return {"message": f"Boot order for {vm_name} updated to {boot_order.order}"}
  405. except Exception as e:
  406. return {"error": str(e)}
  407. @app.get("/isos")
  408. async def list_isos():
  409. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  410. iso_dir = f"{images_path}/isos"
  411. if os.path.exists(iso_dir):
  412. isos = [{"name": f, "path": os.path.join(iso_dir, f)} for f in os.listdir(iso_dir) if f.endswith('.iso')]
  413. return {"isos": isos}
  414. return {"isos": []}
  415. @app.post("/upload-iso")
  416. async def upload_iso(file: UploadFile = File(...)):
  417. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  418. iso_dir = f"{images_path}/isos"
  419. os.makedirs(iso_dir, exist_ok=True)
  420. file_path = os.path.join(iso_dir, file.filename)
  421. with open(file_path, "wb") as f:
  422. while chunk := await file.read(1024 * 1024): # Read in 1MB chunks
  423. f.write(chunk)
  424. return {"message": f"ISO {file.filename} uploaded successfully", "path": file_path}
  425. @app.delete("/isos/{iso_name}")
  426. async def delete_iso(iso_name: str):
  427. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  428. iso_dir = f"{images_path}/isos"
  429. file_path = os.path.join(iso_dir, iso_name)
  430. if os.path.exists(file_path) and iso_name.endswith('.iso'):
  431. os.remove(file_path)
  432. return {"message": f"ISO {iso_name} deleted successfully"}
  433. else:
  434. return {"error": "ISO not found or invalid"}
  435. @app.post("/vms/{vm_name}/add-disk")
  436. async def add_disk_to_vm(vm_name: str, disk_name: str, disk_size: int):
  437. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  438. disk_path = f"{images_path}/{disk_name}.qcow2"
  439. print(f"[ADD_DISK] Starting disk addition: vm={vm_name}, disk_name={disk_name}, disk_size={disk_size}")
  440. try:
  441. # Get the VM first to check its state
  442. print(f"[ADD_DISK] Looking up VM: {vm_name}")
  443. domain = conn.lookupByName(vm_name)
  444. # Check if VM is running BEFORE creating disk
  445. print(f"[ADD_DISK] Checking VM state...")
  446. if domain.isActive():
  447. print(f"[ADD_DISK] ERROR: VM is running, must be shut off to add disk")
  448. return {"error": "Error: La máquina virtual debe estar PARADA (shut off) para agregar un disco"}
  449. print(f"[ADD_DISK] VM state OK (shut off)")
  450. # Check if disk already exists
  451. print(f"[ADD_DISK] Checking if disk exists: {disk_path}")
  452. if os.path.exists(disk_path):
  453. print(f"[ADD_DISK] ERROR: Disk already exists")
  454. return {"error": f"Error: El disco {disk_name}.qcow2 ya existe. Usa un nombre de disco diferente"}
  455. print(f"[ADD_DISK] Disk does not exist (OK)")
  456. # Create the disk using sudo script for proper permissions
  457. print(f"[ADD_DISK] Creating disk...")
  458. app_dir = os.path.dirname(os.path.abspath(__file__))
  459. project_root = os.path.dirname(app_dir)
  460. script_path = os.path.join(project_root, 'create_disk.sh')
  461. result = subprocess.run(['sudo', script_path, disk_path, f'{disk_size}G'],
  462. capture_output=True, text=True, timeout=60)
  463. if result.returncode != 0:
  464. error_msg = result.stderr if result.stderr else result.stdout
  465. print(f"[ADD_DISK] ERROR creating disk: {error_msg}")
  466. return {"error": f"Error al crear disco: {error_msg}"}
  467. print(f"[ADD_DISK] Disk created successfully")
  468. # Get the XML and add the disk
  469. print(f"[ADD_DISK] Getting VM XML...")
  470. xml_desc = domain.XMLDesc()
  471. root = ET.fromstring(xml_desc)
  472. # Find the devices section
  473. devices = root.find("devices")
  474. if devices is None:
  475. print(f"[ADD_DISK] ERROR: No devices section in XML")
  476. return {"error": "Error: No se encontró sección devices en la configuración de la VM"}
  477. # Find the next available disk target (vda, vdb, vdc, etc.)
  478. print(f"[ADD_DISK] Finding next available disk target...")
  479. existing_targets = set()
  480. for disk in root.findall(".//disk/target"):
  481. existing_targets.add(disk.get('dev'))
  482. print(f"[ADD_DISK] Existing targets: {existing_targets}")
  483. # Generate next available target
  484. target_letters = "abcdefghijklmnopqrstuvwxyz"
  485. next_target = None
  486. for i, letter in enumerate(target_letters):
  487. target = f"vd{letter}"
  488. if target not in existing_targets:
  489. next_target = target
  490. break
  491. if next_target is None:
  492. print(f"[ADD_DISK] ERROR: No available disk targets")
  493. return {"error": "Error: No hay dispositivos de disco disponibles"}
  494. print(f"[ADD_DISK] Next target: {next_target}")
  495. # Create the new disk element
  496. print(f"[ADD_DISK] Creating disk XML element...")
  497. new_disk = ET.Element("disk", {"type": "file", "device": "disk"})
  498. ET.SubElement(new_disk, "driver", {"name": "qemu", "type": "qcow2"})
  499. ET.SubElement(new_disk, "source", {"file": disk_path})
  500. ET.SubElement(new_disk, "target", {"dev": next_target, "bus": "virtio"})
  501. # Add the disk to devices
  502. devices.append(new_disk)
  503. # Convert back to string and redefine the VM
  504. print(f"[ADD_DISK] Redefining VM with new disk...")
  505. new_xml = ET.tostring(root, encoding='unicode')
  506. domain.undefine()
  507. conn.defineXML(new_xml)
  508. print(f"[ADD_DISK] VM redefined successfully")
  509. print(f"[ADD_DISK] SUCCESS: Disk added")
  510. return {"message": f"Disco {disk_name}.qcow2 ({disk_size}G) agregado a {vm_name} como {next_target}"}
  511. except libvirt.libvirtError as e:
  512. print(f"[ADD_DISK] Libvirt error: {str(e)}")
  513. return {"error": f"Error de libvirt: {str(e)}"}
  514. except Exception as e:
  515. print(f"[ADD_DISK] Unexpected error: {str(e)}")
  516. return {"error": f"Error: {str(e)}"}
  517. @app.post("/disks")
  518. async def create_disk(name: str, size: int):
  519. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  520. disk_path = f"{images_path}/{name}.qcow2"
  521. try:
  522. result = subprocess.run(['qemu-img', 'create', '-f', 'qcow2', disk_path, f'{size}G'], capture_output=True, text=True)
  523. if result.returncode == 0:
  524. return {"message": f"Disk {name}.qcow2 created with size {size}G"}
  525. else:
  526. return {"error": result.stderr}
  527. except Exception as e:
  528. return {"error": str(e)}
  529. @app.get("/vms/{vm_name}/config")
  530. async def get_vm_config(vm_name: str):
  531. """Get VM configuration in XML format"""
  532. try:
  533. conn = libvirt.open(None)
  534. domain = conn.lookupByName(vm_name)
  535. xml_desc = domain.XMLDesc()
  536. conn.close()
  537. return {
  538. "name": vm_name,
  539. "xml": xml_desc
  540. }
  541. except libvirt.libvirtError as e:
  542. return {"error": f"Error de libvirt: {str(e)}"}
  543. @app.post("/vms/{vm_name}/snapshot")
  544. async def create_snapshot(vm_name: str, snapshot_name: str, description: str = ""):
  545. """Create a snapshot of a VM"""
  546. try:
  547. conn = libvirt.open(None)
  548. domain = conn.lookupByName(vm_name)
  549. # Create snapshot XML
  550. snapshot_xml = f"""<domainsnapshot>
  551. <name>{snapshot_name}</name>
  552. <description>{description}</description>
  553. </domainsnapshot>"""
  554. print(f"[SNAPSHOT] Creating snapshot '{snapshot_name}' for VM '{vm_name}'")
  555. domain.snapshotCreateXML(snapshot_xml, 0)
  556. conn.close()
  557. print(f"[SNAPSHOT] Snapshot '{snapshot_name}' created successfully")
  558. return {"message": f"Snapshot '{snapshot_name}' creado exitosamente"}
  559. except libvirt.libvirtError as e:
  560. error_msg = str(e)
  561. print(f"[SNAPSHOT] Error creating snapshot: {error_msg}")
  562. return {"error": f"Error al crear snapshot: {error_msg}"}
  563. except Exception as e:
  564. error_msg = str(e)
  565. print(f"[SNAPSHOT] Unexpected error: {error_msg}")
  566. return {"error": f"Error: {error_msg}"}
  567. @app.get("/vms/{vm_name}/snapshots")
  568. async def list_snapshots(vm_name: str):
  569. """List all snapshots of a VM"""
  570. try:
  571. conn = libvirt.open(None)
  572. domain = conn.lookupByName(vm_name)
  573. snapshots = []
  574. try:
  575. # Get all snapshots
  576. snapshot_list = domain.listAllSnapshots()
  577. for snapshot in snapshot_list:
  578. snap_name = snapshot.getName()
  579. # Get snapshot info using available methods
  580. try:
  581. # Try to get current status
  582. is_current = snapshot.isCurrent()
  583. state = "current" if is_current else "available"
  584. except:
  585. state = "available"
  586. # Try to get creation time from snapshot info
  587. created_time = 0
  588. try:
  589. # Get snapshot XML description using getXMLDesc()
  590. snap_xml = snapshot.getXMLDesc()
  591. # Parse XML to get creationTime
  592. root = ET.fromstring(snap_xml)
  593. creation_elem = root.find('.//creationTime')
  594. if creation_elem is not None and creation_elem.text:
  595. created_time = int(creation_elem.text)
  596. except Exception as e:
  597. print(f"[SNAPSHOT] Error obteniendo creationTime: {str(e)}")
  598. created_time = 0
  599. snapshots.append({
  600. "name": snap_name,
  601. "created": created_time,
  602. "description": "",
  603. "state": state
  604. })
  605. except libvirt.libvirtError as e:
  606. # No snapshots available
  607. print(f"[SNAPSHOT] No snapshots found: {str(e)}")
  608. pass
  609. conn.close()
  610. return {
  611. "vm_name": vm_name,
  612. "snapshots": snapshots
  613. }
  614. except libvirt.libvirtError as e:
  615. error_msg = str(e)
  616. print(f"[SNAPSHOT] Error listing snapshots: {error_msg}")
  617. return {"error": f"Error al listar snapshots: {error_msg}"}
  618. @app.delete("/vms/{vm_name}/snapshots/{snapshot_name}")
  619. async def delete_snapshot(vm_name: str, snapshot_name: str):
  620. """Delete a snapshot of a VM"""
  621. try:
  622. conn = libvirt.open(None)
  623. domain = conn.lookupByName(vm_name)
  624. print(f"[SNAPSHOT] Deleting snapshot '{snapshot_name}' from VM '{vm_name}'")
  625. snapshot = domain.snapshotLookupByName(snapshot_name)
  626. snapshot.delete(0)
  627. conn.close()
  628. print(f"[SNAPSHOT] Snapshot '{snapshot_name}' deleted successfully")
  629. return {"message": f"Snapshot '{snapshot_name}' eliminado exitosamente"}
  630. except libvirt.libvirtError as e:
  631. error_msg = str(e)
  632. print(f"[SNAPSHOT] Error deleting snapshot: {error_msg}")
  633. return {"error": f"Error al eliminar snapshot: {error_msg}"}
  634. except Exception as e:
  635. error_msg = str(e)
  636. print(f"[SNAPSHOT] Unexpected error: {error_msg}")