main.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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. from pydantic import BaseModel
  11. app = FastAPI(title="VirtManager API", description="API for managing virtual machines")
  12. app.mount("/static", StaticFiles(directory="static"), name="static")
  13. ws_processes = {}
  14. def get_free_port():
  15. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  16. s.bind(('', 0))
  17. return s.getsockname()[1]
  18. def get_state_string(state_code):
  19. states = {
  20. 0: "no state",
  21. 1: "running",
  22. 2: "blocked",
  23. 3: "paused",
  24. 4: "shutdown",
  25. 5: "shut off",
  26. 6: "crashed",
  27. 7: "pm suspended"
  28. }
  29. return states.get(state_code, "unknown")
  30. class BootOrder(BaseModel):
  31. order: list[str]
  32. # Connect to libvirt
  33. conn = libvirt.open('qemu:///system')
  34. @app.get("/")
  35. async def root():
  36. return FileResponse("static/index.html")
  37. @app.get("/vms")
  38. async def list_vms():
  39. domains = conn.listAllDomains()
  40. vms = []
  41. for domain in domains:
  42. xml_desc = domain.XMLDesc()
  43. root = ET.fromstring(xml_desc)
  44. graphics = root.find(".//graphics[@type='vnc']")
  45. vnc_port = None
  46. if graphics is not None:
  47. port_str = graphics.get('port')
  48. if port_str and port_str != '-1':
  49. vnc_port = int(port_str)
  50. vms.append({
  51. "id": domain.ID(),
  52. "name": domain.name(),
  53. "state": get_state_string(domain.state()[0]),
  54. "vnc_port": vnc_port
  55. })
  56. return {"vms": vms}
  57. @app.get("/vms/{vm_name}/info")
  58. async def get_vm_info(vm_name: str):
  59. try:
  60. domain = conn.lookupByName(vm_name)
  61. xml_desc = domain.XMLDesc()
  62. root = ET.fromstring(xml_desc)
  63. # Get basic info
  64. memory = root.find("memory").text if root.find("memory") is not None else "Unknown"
  65. cpus = root.find("vcpu").text if root.find("vcpu") is not None else "Unknown"
  66. # Get VNC port
  67. graphics = root.find(".//graphics[@type='vnc']")
  68. vnc_port = None
  69. if graphics is not None:
  70. port_str = graphics.get('port')
  71. if port_str and port_str != '-1':
  72. vnc_port = int(port_str)
  73. # Get disks
  74. disks = []
  75. for disk in root.findall(".//disk"):
  76. device = disk.get('device')
  77. driver = disk.find("driver")
  78. source = disk.find("source")
  79. target = disk.find("target")
  80. disks.append({
  81. "type": device,
  82. "driver": driver.get('type') if driver is not None else "unknown",
  83. "source": source.get('file') if source is not None else "unknown",
  84. "target": target.get('dev') if target is not None else "unknown"
  85. })
  86. # Get network interfaces
  87. interfaces = []
  88. for iface in root.findall(".//interface"):
  89. mac = iface.find("mac")
  90. source = iface.find("source")
  91. model = iface.find("model")
  92. interfaces.append({
  93. "mac": mac.get('address') if mac is not None else "unknown",
  94. "network": source.get('network') if source is not None else "unknown",
  95. "model": model.get('type') if model is not None else "unknown"
  96. })
  97. return {
  98. "id": domain.ID(),
  99. "name": domain.name(),
  100. "state": get_state_string(domain.state()[0]),
  101. "memory": int(memory),
  102. "cpus": int(cpus),
  103. "vnc_port": vnc_port,
  104. "disks": disks,
  105. "interfaces": interfaces
  106. }
  107. except Exception as e:
  108. return {"error": str(e)}, 500
  109. @app.get("/vms/{vm_name}/start")
  110. async def start_vm(vm_name: str):
  111. try:
  112. domain = conn.lookupByName(vm_name)
  113. domain.create()
  114. return {"message": f"VM {vm_name} started"}
  115. except Exception as e:
  116. return {"error": str(e)}
  117. @app.get("/vms/{vm_name}/stop")
  118. async def stop_vm(vm_name: str):
  119. try:
  120. domain = conn.lookupByName(vm_name)
  121. domain.destroy()
  122. return {"message": f"VM {vm_name} stopped"}
  123. except Exception as e:
  124. return {"error": str(e)}
  125. @app.post("/vms")
  126. async def create_vm(name: str, memory: int, disk_size: int, cpus: int = 1, iso: str = ""):
  127. if not iso:
  128. return {"error": "ISO path is required"}
  129. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  130. disk_path = f"{images_path}/{name}.qcow2"
  131. iso_path = iso # iso ya es la ruta completa desde el select
  132. print(f"Using ISO path: {iso_path}")
  133. # Create disk
  134. try:
  135. result = subprocess.run(['qemu-img', 'create', '-f', 'qcow2', disk_path, f'{disk_size}G'], capture_output=True, text=True)
  136. if result.returncode != 0:
  137. return {"error": f"Failed to create disk: {result.stderr}"}
  138. except Exception as e:
  139. return {"error": f"Error creating disk: {str(e)}"}
  140. # Generate unique MAC
  141. mac = f"52:54:00:{random.randint(0, 255):02x}:{random.randint(0, 255):02x}:{random.randint(0, 255):02x}"
  142. # Basic XML template
  143. xml_template = f"""<domain type='kvm'>
  144. <name>{name}</name>
  145. <memory unit='MiB'>{memory}</memory>
  146. <currentMemory unit='MiB'>{memory}</currentMemory>
  147. <vcpu placement='static'>{cpus}</vcpu>
  148. <os>
  149. <type arch='x86_64' machine='pc-i440fx-6.2'>hvm</type>
  150. <boot dev='cdrom'/>
  151. </os>
  152. <features>
  153. <acpi/>
  154. <apic/>
  155. </features>
  156. <cpu mode='host-model' check='partial'/>
  157. <clock offset='utc'>
  158. <timer name='rtc' tickpolicy='catchup'/>
  159. <timer name='pit' tickpolicy='delay'/>
  160. <timer name='hpet' present='no'/>
  161. </clock>
  162. <on_poweroff>destroy</on_poweroff>
  163. <on_reboot>restart</on_reboot>
  164. <on_crash>destroy</on_crash>
  165. <pm>
  166. <suspend-to-mem enabled='no'/>
  167. <suspend-to-disk enabled='no'/>
  168. </pm>
  169. <devices>
  170. <emulator>/usr/bin/kvm</emulator>
  171. <disk type='file' device='disk'>
  172. <driver name='qemu' type='qcow2'/>
  173. <source file='{disk_path}'/>
  174. <target dev='vda' bus='virtio'/>
  175. </disk>
  176. <disk type='file' device='cdrom'>
  177. <driver name='qemu' type='raw'/>
  178. <source file='{iso_path}'/>
  179. <target dev='hda' bus='ide'/>
  180. <readonly/>
  181. </disk>
  182. <interface type='network'>
  183. <mac address='{mac}'/>
  184. <source network='default'/>
  185. <model type='virtio'/>
  186. </interface>
  187. <console type='pty'>
  188. <target type='serial' port='0'/>
  189. </console>
  190. <input type='tablet' bus='usb'>
  191. <address type='usb' bus='0' port='1'/>
  192. </input>
  193. <input type='mouse' bus='ps2'/>
  194. <input type='keyboard' bus='ps2'/>
  195. <graphics type='vnc' port='-1' autoport='yes'/>
  196. <video>
  197. <model type='cirrus' vram='16384' heads='1' primary='yes'/>
  198. </video>
  199. <memballoon model='virtio'>
  200. <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
  201. </memballoon>
  202. </devices>
  203. </domain>"""
  204. try:
  205. domain = conn.defineXML(xml_template)
  206. domain.create()
  207. return {"message": f"VM {name} created with {cpus} CPUs, {memory} MiB memory, {disk_size}G disk, booting from {iso}"}
  208. except Exception as e:
  209. return {"error": str(e)}
  210. @app.get("/console/{vm_name}")
  211. async def get_console(vm_name: str):
  212. try:
  213. domain = conn.lookupByName(vm_name)
  214. xml_desc = domain.XMLDesc()
  215. root = ET.fromstring(xml_desc)
  216. graphics = root.find(".//graphics[@type='vnc']")
  217. if graphics is None:
  218. return {"error": "No VNC graphics found"}
  219. vnc_port = graphics.get('port')
  220. if not vnc_port or vnc_port == '-1':
  221. return {"error": "VNC port not assigned"}
  222. vnc_port = int(vnc_port)
  223. if vm_name not in ws_processes:
  224. ws_port = get_free_port()
  225. proc = subprocess.Popen(['websockify', str(ws_port), f'localhost:{vnc_port}'])
  226. ws_processes[vm_name] = (proc, ws_port)
  227. _, ws_port = ws_processes[vm_name]
  228. return {"url": f"/static/novnc/vnc.html?host={socket.gethostname()}&port={ws_port}"}
  229. except Exception as e:
  230. return {"error": str(e)}
  231. @app.delete("/vms/{vm_name}")
  232. async def delete_vm(vm_name: str):
  233. try:
  234. domain = conn.lookupByName(vm_name)
  235. if domain.isActive():
  236. domain.destroy()
  237. domain.undefine()
  238. # Kill websockify process if exists
  239. if vm_name in ws_processes:
  240. proc, _ = ws_processes[vm_name]
  241. proc.terminate()
  242. proc.wait()
  243. del ws_processes[vm_name]
  244. return {"message": f"VM {vm_name} deleted"}
  245. except Exception as e:
  246. return {"error": str(e)}
  247. @app.put("/vms/{vm_name}/boot-order")
  248. async def set_boot_order(vm_name: str, boot_order: BootOrder):
  249. try:
  250. domain = conn.lookupByName(vm_name)
  251. if domain.isActive():
  252. return {"error": "VM must be shut off to change boot order"}
  253. xml_desc = domain.XMLDesc()
  254. root = ET.fromstring(xml_desc)
  255. os_section = root.find("os")
  256. # Remove existing boot elements
  257. for boot in os_section.findall("boot"):
  258. os_section.remove(boot)
  259. # Add new boot elements
  260. for dev in boot_order.order:
  261. ET.SubElement(os_section, "boot", {"dev": dev})
  262. new_xml = ET.tostring(root, encoding='unicode')
  263. # Undefine and redefine
  264. domain.undefine()
  265. new_domain = conn.defineXML(new_xml)
  266. return {"message": f"Boot order for {vm_name} updated to {boot_order.order}"}
  267. except Exception as e:
  268. return {"error": str(e)}
  269. @app.get("/isos")
  270. async def list_isos():
  271. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  272. iso_dir = f"{images_path}/isos"
  273. if os.path.exists(iso_dir):
  274. isos = [{"name": f, "path": os.path.join(iso_dir, f)} for f in os.listdir(iso_dir) if f.endswith('.iso')]
  275. return {"isos": isos}
  276. return {"isos": []}
  277. @app.post("/upload-iso")
  278. async def upload_iso(file: UploadFile = File(...)):
  279. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  280. iso_dir = f"{images_path}/isos"
  281. os.makedirs(iso_dir, exist_ok=True)
  282. file_path = os.path.join(iso_dir, file.filename)
  283. with open(file_path, "wb") as f:
  284. while chunk := await file.read(1024 * 1024): # Read in 1MB chunks
  285. f.write(chunk)
  286. return {"message": f"ISO {file.filename} uploaded successfully", "path": file_path}
  287. @app.delete("/isos/{iso_name}")
  288. async def delete_iso(iso_name: str):
  289. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  290. iso_dir = f"{images_path}/isos"
  291. file_path = os.path.join(iso_dir, iso_name)
  292. if os.path.exists(file_path) and iso_name.endswith('.iso'):
  293. os.remove(file_path)
  294. return {"message": f"ISO {iso_name} deleted successfully"}
  295. else:
  296. return {"error": "ISO not found or invalid"}
  297. @app.post("/disks")
  298. async def create_disk(name: str, size: int):
  299. images_path = os.environ.get('LIBVIRT_IMAGES_PATH', '/var/lib/libvirt/images')
  300. disk_path = f"{images_path}/{name}.qcow2"
  301. try:
  302. result = subprocess.run(['qemu-img', 'create', '-f', 'qcow2', disk_path, f'{size}G'], capture_output=True, text=True)
  303. if result.returncode == 0:
  304. return {"message": f"Disk {name}.qcow2 created with size {size}G"}
  305. else:
  306. return {"error": result.stderr}
  307. except Exception as e:
  308. return {"error": str(e)}