app.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. const API_BASE = window.location.origin;
  2. function getStateClass(state) {
  3. switch (state) {
  4. case 'running': return 'bg-success';
  5. case 'shut off': return 'bg-secondary';
  6. case 'paused': return 'bg-warning text-dark';
  7. case 'crashed': return 'bg-danger';
  8. default: return 'bg-light text-dark';
  9. }
  10. }
  11. async function loadISOs() {
  12. try {
  13. const response = await fetch(`${API_BASE}/isos`);
  14. const data = await response.json();
  15. const tbody = document.getElementById('isoTableBody');
  16. tbody.innerHTML = '';
  17. data.isos.forEach(iso => {
  18. const row = document.createElement('tr');
  19. row.innerHTML = `
  20. <td>${iso.name}</td>
  21. <td>
  22. <button class="btn btn-sm btn-danger" onclick="deleteISO('${iso.name}')">Delete</button>
  23. </td>
  24. `;
  25. tbody.appendChild(row);
  26. });
  27. } catch (error) {
  28. console.error('Error loading ISOs:', error);
  29. alert('Error loading ISOs');
  30. }
  31. }
  32. async function deleteISO(name) {
  33. if (confirm(`Delete ISO ${name}?`)) {
  34. try {
  35. const response = await fetch(`${API_BASE}/isos/${name}`, { method: 'DELETE' });
  36. const result = await response.json();
  37. if (response.ok) {
  38. alert(result.message);
  39. loadISOs();
  40. loadISOsForSelect(); // Refresh the select
  41. } else {
  42. alert(result.error);
  43. }
  44. } catch (error) {
  45. console.error('Error deleting ISO:', error);
  46. alert('Error deleting ISO');
  47. }
  48. }
  49. }
  50. async function loadISOsForSelect() {
  51. try {
  52. const response = await fetch(`${API_BASE}/isos`);
  53. const data = await response.json();
  54. const select = document.getElementById('iso');
  55. select.innerHTML = '<option value="">Select ISO</option>';
  56. data.isos.forEach(iso => {
  57. select.innerHTML += `<option value="${iso.path}">${iso.name}</option>`;
  58. });
  59. } catch (error) {
  60. console.error('Error loading ISOs:', error);
  61. }
  62. }
  63. /* Load VMs and populate the table */
  64. async function loadVMs() {
  65. try {
  66. const response = await fetch(`${API_BASE}/vms`);
  67. const data = await response.json();
  68. const tbody = document.getElementById('vmTableBody');
  69. tbody.innerHTML = '';
  70. const select = document.getElementById('bootVM');
  71. select.innerHTML = '<option value="">Select VM</option>';
  72. const diskSelect = document.getElementById('diskVM');
  73. diskSelect.innerHTML = '<option value="">Select VM</option>';
  74. data.vms.forEach(vm => {
  75. const row = document.createElement('tr');
  76. row.innerHTML = `
  77. <td>${vm.id}</td>
  78. <td>${vm.name}</td>
  79. <td><span class="badge ${getStateClass(vm.state)}">${vm.state}</span></td>
  80. <td>
  81. <button class="btn btn-sm btn-info" onclick="showVMInfo('${vm.name}')">Info</button>
  82. <button class="btn btn-sm btn-success" onclick="startVM('${vm.name}')">Lanzar</button>
  83. <button class="btn btn-sm btn-warning" onclick="stopVM('${vm.name}')">Stop</button>
  84. <button class="btn btn-sm btn-danger" onclick="deleteVM('${vm.name}')">Delete</button>
  85. ${vm.vnc_port ? `<button class="btn btn-sm btn-primary" onclick="openVNC('${vm.name}')">Console</button>` : ''}
  86. </td>
  87. `;
  88. tbody.appendChild(row);
  89. select.innerHTML += `<option value="${vm.name}">${vm.name}</option>`;
  90. diskSelect.innerHTML += `<option value="${vm.name}">${vm.name}</option>`;
  91. });
  92. loadISOsForSelect(); // Load ISOs after loading VMs
  93. } catch (error) {
  94. console.error('Error loading VMs:', error);
  95. alert('Error loading VMs');
  96. }
  97. }
  98. async function showVMInfo(name) {
  99. try {
  100. const response = await fetch(`${API_BASE}/vms/${name}/info`);
  101. const data = await response.json();
  102. if (!response.ok) {
  103. alert(data.error || 'Error loading VM information');
  104. return;
  105. }
  106. const modalBody = document.getElementById('infoModalBody');
  107. modalBody.innerHTML = `
  108. <div class="row mb-3">
  109. <div class="col-md-6">
  110. <h6><strong>Nombre:</strong></h6>
  111. <p>${data.name}</p>
  112. </div>
  113. <div class="col-md-6">
  114. <h6><strong>ID:</strong></h6>
  115. <p>${data.id}</p>
  116. </div>
  117. </div>
  118. <div class="row mb-3">
  119. <div class="col-md-6">
  120. <h6><strong>Estado:</strong></h6>
  121. <p><span class="badge ${getStateClass(data.state)}">${data.state}</span></p>
  122. </div>
  123. <div class="col-md-6">
  124. <h6><strong>Memoria:</strong></h6>
  125. <p>${data.memory} MiB</p>
  126. </div>
  127. </div>
  128. <div class="row mb-3">
  129. <div class="col-md-6">
  130. <h6><strong>CPUs:</strong></h6>
  131. <p>${data.cpus}</p>
  132. </div>
  133. <div class="col-md-6">
  134. <h6><strong>Puerto VNC:</strong></h6>
  135. <p>${data.vnc_port || 'No asignado'}</p>
  136. </div>
  137. </div>
  138. <div class="mb-3">
  139. <h6><strong>Discos:</strong></h6>
  140. <table class="table table-sm">
  141. <thead>
  142. <tr>
  143. <th>Dispositivo</th>
  144. <th>Tipo</th>
  145. <th>Ruta</th>
  146. </tr>
  147. </thead>
  148. <tbody>
  149. ${data.disks.map(disk => `
  150. <tr>
  151. <td>${disk.target}</td>
  152. <td>${disk.type}</td>
  153. <td>${disk.source}</td>
  154. </tr>
  155. `).join('')}
  156. </tbody>
  157. </table>
  158. </div>
  159. <div class="mb-3">
  160. <h6><strong>Interfaces de Red:</strong></h6>
  161. <table class="table table-sm">
  162. <thead>
  163. <tr>
  164. <th>MAC</th>
  165. <th>Red</th>
  166. <th>Modelo</th>
  167. </tr>
  168. </thead>
  169. <tbody>
  170. ${data.interfaces.map(iface => `
  171. <tr>
  172. <td>${iface.mac}</td>
  173. <td>${iface.network}</td>
  174. <td>${iface.model}</td>
  175. </tr>
  176. `).join('')}
  177. </tbody>
  178. </table>
  179. </div>
  180. `;
  181. const modal = new bootstrap.Modal(document.getElementById('infoModal'));
  182. modal.show();
  183. } catch (error) {
  184. console.error('Error loading VM information:', error);
  185. alert('Error loading VM information');
  186. }
  187. }
  188. async function startVM(name) {
  189. try {
  190. await fetch(`${API_BASE}/vms/${name}/start`);
  191. loadVMs();
  192. } catch (error) {
  193. console.error('Error starting VM:', error);
  194. alert('Error starting VM');
  195. }
  196. }
  197. async function stopVM(name) {
  198. try {
  199. await fetch(`${API_BASE}/vms/${name}/stop`);
  200. loadVMs();
  201. } catch (error) {
  202. console.error('Error stopping VM:', error);
  203. alert('Error stopping VM');
  204. }
  205. }
  206. async function deleteVM(name) {
  207. if (confirm(`Delete VM ${name}?`)) {
  208. try {
  209. await fetch(`${API_BASE}/vms/${name}`, { method: 'DELETE' });
  210. loadVMs();
  211. } catch (error) {
  212. console.error('Error deleting VM:', error);
  213. alert('Error deleting VM');
  214. }
  215. }
  216. }
  217. function openVNC(vm_name) {
  218. fetch(`${API_BASE}/console/${vm_name}`)
  219. .then(response => response.json())
  220. .then(data => {
  221. if (data.url) {
  222. window.open(data.url);
  223. } else {
  224. alert(data.error || 'Error opening console');
  225. }
  226. })
  227. .catch(error => {
  228. console.error('Error:', error);
  229. alert('Error opening console');
  230. });
  231. }
  232. document.getElementById('createVMForm').addEventListener('submit', async (e) => {
  233. e.preventDefault();
  234. const name = document.getElementById('name').value;
  235. const memory = document.getElementById('memory').value;
  236. const disk_size = document.getElementById('disk_size').value;
  237. const cpus = document.getElementById('cpus').value;
  238. const iso = document.getElementById('iso').value;
  239. if (!iso) {
  240. alert('Por favor seleccione una imagen ISO');
  241. return;
  242. }
  243. try {
  244. const response = await fetch(`${API_BASE}/vms?name=${name}&memory=${memory}&disk_size=${disk_size}&cpus=${cpus}&iso=${iso}`, {
  245. method: 'POST'
  246. });
  247. let result;
  248. try {
  249. result = await response.json();
  250. } catch {
  251. result = { error: `Server error: ${response.status} ${response.statusText}` };
  252. }
  253. if (response.ok) {
  254. alert(result.message || 'VM creada correctamente');
  255. loadVMs();
  256. document.getElementById('createVMForm').reset();
  257. } else {
  258. console.error('Error response:', result);
  259. alert(`Error: ${result.error || 'Error desconocido al crear VM'}`);
  260. }
  261. } catch (error) {
  262. console.error('Error creating VM:', error);
  263. alert(`Error de conexión: ${error.message}`);
  264. }
  265. });
  266. if (document.getElementById('setBootOrderForm')) {
  267. document.getElementById('setBootOrderForm').addEventListener('submit', async (e) => {
  268. e.preventDefault();
  269. const vm_name = document.getElementById('bootVM').value;
  270. const boot_order_str = document.getElementById('bootOrder').value;
  271. const order = boot_order_str.split(',').map(s => s.trim());
  272. try {
  273. const response = await fetch(`${API_BASE}/vms/${vm_name}/boot-order`, {
  274. method: 'PUT',
  275. headers: {
  276. 'Content-Type': 'application/json'
  277. },
  278. body: JSON.stringify({ order: order })
  279. });
  280. const result = await response.json();
  281. if (response.ok) {
  282. alert(result.message);
  283. loadVMs();
  284. document.getElementById('setBootOrderForm').reset();
  285. } else {
  286. alert(result.error);
  287. }
  288. } catch (error) {
  289. console.error('Error setting boot order:', error);
  290. alert('Error setting boot order');
  291. }
  292. });
  293. }
  294. if (document.getElementById('addDiskForm')) {
  295. document.getElementById('addDiskForm').addEventListener('submit', async (e) => {
  296. e.preventDefault();
  297. const vm_name = document.getElementById('diskVM').value;
  298. const disk_name = document.getElementById('diskName').value;
  299. const disk_size = document.getElementById('diskSize').value;
  300. if (!vm_name) {
  301. alert('Por favor selecciona una máquina virtual');
  302. return;
  303. }
  304. if (!disk_name) {
  305. alert('Por favor ingresa un nombre para el disco');
  306. return;
  307. }
  308. if (!disk_size || disk_size <= 0) {
  309. alert('Por favor ingresa un tamaño de disco válido (> 0 GB)');
  310. return;
  311. }
  312. try {
  313. const response = await fetch(`${API_BASE}/vms/${vm_name}/add-disk?disk_name=${disk_name}&disk_size=${disk_size}`, {
  314. method: 'POST'
  315. });
  316. let result;
  317. try {
  318. result = await response.json();
  319. } catch {
  320. result = { error: `Server error: ${response.status} ${response.statusText}` };
  321. }
  322. if (response.ok) {
  323. alert(result.message || 'Disco agregado correctamente');
  324. loadVMs();
  325. document.getElementById('addDiskForm').reset();
  326. } else {
  327. console.error('Add disk error:', result);
  328. alert(result.error || 'Error al agregar disco');
  329. }
  330. } catch (error) {
  331. console.error('Error adding disk:', error);
  332. alert(`Error de conexión: ${error.message}`);
  333. }
  334. });
  335. }
  336. if (document.getElementById('uploadIsoForm')) {
  337. document.getElementById('uploadIsoForm').addEventListener('submit', (e) => {
  338. e.preventDefault();
  339. const fileInput = document.getElementById('isoFile');
  340. const file = fileInput.files[0];
  341. if (!file) {
  342. alert('Please select a file');
  343. return;
  344. }
  345. const formData = new FormData();
  346. formData.append('file', file);
  347. const progressContainer = document.getElementById('progressContainer');
  348. const progressBar = document.getElementById('uploadProgress');
  349. const uploadBtn = document.getElementById('uploadBtn');
  350. // Show progress bar
  351. progressContainer.style.display = 'block';
  352. uploadBtn.disabled = true;
  353. progressBar.style.width = '0%';
  354. progressBar.textContent = '0%';
  355. const xhr = new XMLHttpRequest();
  356. xhr.upload.addEventListener('progress', (e) => {
  357. if (e.lengthComputable) {
  358. const percentComplete = Math.round((e.loaded / e.total) * 100);
  359. progressBar.style.width = percentComplete + '%';
  360. progressBar.textContent = percentComplete + '%';
  361. }
  362. });
  363. xhr.addEventListener('load', () => {
  364. if (xhr.status === 200) {
  365. const result = JSON.parse(xhr.responseText);
  366. alert(result.message);
  367. loadISOsForSelect();
  368. document.getElementById('uploadIsoForm').reset();
  369. progressContainer.style.display = 'none';
  370. uploadBtn.disabled = false;
  371. } else {
  372. try {
  373. const result = JSON.parse(xhr.responseText);
  374. alert(result.error || 'Error uploading ISO');
  375. } catch {
  376. alert('Error uploading ISO');
  377. }
  378. progressContainer.style.display = 'none';
  379. uploadBtn.disabled = false;
  380. }
  381. });
  382. xhr.addEventListener('error', () => {
  383. alert('Error uploading ISO');
  384. progressContainer.style.display = 'none';
  385. uploadBtn.disabled = false;
  386. });
  387. xhr.open('POST', `${API_BASE}/upload-iso`);
  388. xhr.send(formData);
  389. });
  390. }
  391. // Initialize event listeners and load data when DOM is ready
  392. document.addEventListener('DOMContentLoaded', function() {
  393. loadVMs();
  394. loadISOs();
  395. });