Selaa lähdekoodia

Lista snapshots

Celestino Rey 6 kuukautta sitten
vanhempi
sitoutus
85a3997718
4 muutettua tiedostoa jossa 228 lisäystä ja 2 poistoa
  1. 13 0
      README.md
  2. 78 1
      app/main.py
  3. 136 0
      static/dashboard.js
  4. 1 1
      static/index.html

+ 13 - 0
README.md

@@ -8,6 +8,7 @@ Una API REST y interfaz web para gestionar máquinas virtuales en Ubuntu usando
 - **Configuración de arranque**: Cambiar el orden de arranque de las VMs (disco duro, CD-ROM, etc.).
 - **Subida de ISOs**: Sube imágenes ISO directamente desde la interfaz web para usar en la creación de VMs.
 - **Gestión de ISOs**: Lista y elimina ISOs subidas.
+- **Snapshots de VMs**: Crear, listar y eliminar snapshots para backup y recuperación.
 - **Interfaz Responsive**: Diseño moderno con sidebar que se adapta a dispositivos móviles, tablets y escritorio.
 - **Descarga de configuración**: Exporta la configuración XML de cualquier VM para backup o transferencia.
 
@@ -143,6 +144,14 @@ export LIBVIRT_IMAGES_PATH=/ruta/a/imagenes
 - `PUT /vms/{vm_name}/boot-order`: Cambia el orden de arranque.
   - Body JSON: `{"order": ["hd", "cdrom"]}` (ejemplo).
 
+### Snapshots
+
+- `POST /vms/{vm_name}/snapshot?snapshot_name={name}&description={desc}`: Crea un snapshot de una VM.
+  - `snapshot_name`: Nombre del snapshot (obligatorio).
+  - `description`: Descripción del snapshot (opcional).
+- `GET /vms/{vm_name}/snapshots`: Lista todos los snapshots de una VM con detalles (nombre, fecha, descripción, estado).
+- `DELETE /vms/{vm_name}/snapshots/{snapshot_name}`: Elimina un snapshot específico.
+
 ### Discos
 
 - `POST /disks?name={name}&size={size}`: Crea una imagen de disco.
@@ -183,6 +192,10 @@ La interfaz web utiliza un diseño moderno con las siguientes características:
   - Subir nuevas imágenes ISO
   - Ver lista de ISOs disponibles
   - Eliminar ISOs ya no necesarias
+- **Snapshots**: 
+  - **Crear Snapshot**: Guardar el estado actual de una VM con nombre y descripción
+  - **Ver Snapshots**: Listar todos los snapshots disponibles con fecha, descripción y estado
+  - **Eliminar Snapshot**: Borrar snapshots específicos con confirmación
 - **Acciones rápidas por VM**:
   - Iniciar / Detener
   - Acceder a consola VNC

+ 78 - 1
app/main.py

@@ -519,4 +519,81 @@ async def create_snapshot(vm_name: str, snapshot_name: str, description: str = "
     except Exception as e:
         error_msg = str(e)
         print(f"[SNAPSHOT] Unexpected error: {error_msg}")
-        return {"error": f"Error: {error_msg}"}
+        return {"error": f"Error: {error_msg}"}
+
+@app.get("/vms/{vm_name}/snapshots")
+async def list_snapshots(vm_name: str):
+    """List all snapshots of a VM"""
+    try:
+        conn = libvirt.open(None)
+        domain = conn.lookupByName(vm_name)
+        
+        snapshots = []
+        try:
+            # Get all snapshots
+            snapshot_list = domain.listAllSnapshots()
+            
+            for snapshot in snapshot_list:
+                snap_name = snapshot.getName()
+                
+                # Get snapshot info using available methods
+                try:
+                    # Try to get current status
+                    is_current = snapshot.isCurrent()
+                    state = "current" if is_current else "available"
+                except:
+                    state = "available"
+                
+                # Try to get creation time from snapshot info
+                created_time = 0
+                try:
+                    # getInfo returns: (type, hypervisor, creationTime, state_flags, dom_name, parent)
+                    snap_info = snapshot.getInfo()
+                    if snap_info and len(snap_info) >= 3:
+                        created_time = snap_info[2]  # creationTime is at index 2
+                except:
+                    created_time = 0
+                
+                snapshots.append({
+                    "name": snap_name,
+                    "created": created_time,
+                    "description": "",
+                    "state": state
+                })
+        except libvirt.libvirtError as e:
+            # No snapshots available
+            print(f"[SNAPSHOT] No snapshots found: {str(e)}")
+            pass
+        
+        conn.close()
+        
+        return {
+            "vm_name": vm_name,
+            "snapshots": snapshots
+        }
+    except libvirt.libvirtError as e:
+        error_msg = str(e)
+        print(f"[SNAPSHOT] Error listing snapshots: {error_msg}")
+        return {"error": f"Error al listar snapshots: {error_msg}"}
+
+@app.delete("/vms/{vm_name}/snapshots/{snapshot_name}")
+async def delete_snapshot(vm_name: str, snapshot_name: str):
+    """Delete a snapshot of a VM"""
+    try:
+        conn = libvirt.open(None)
+        domain = conn.lookupByName(vm_name)
+        
+        print(f"[SNAPSHOT] Deleting snapshot '{snapshot_name}' from VM '{vm_name}'")
+        snapshot = domain.snapshotLookupByName(snapshot_name)
+        snapshot.delete(0)
+        conn.close()
+        
+        print(f"[SNAPSHOT] Snapshot '{snapshot_name}' deleted successfully")
+        return {"message": f"Snapshot '{snapshot_name}' eliminado exitosamente"}
+    except libvirt.libvirtError as e:
+        error_msg = str(e)
+        print(f"[SNAPSHOT] Error deleting snapshot: {error_msg}")
+        return {"error": f"Error al eliminar snapshot: {error_msg}"}
+    except Exception as e:
+        error_msg = str(e)
+        print(f"[SNAPSHOT] Unexpected error: {error_msg}")

+ 136 - 0
static/dashboard.js

@@ -201,6 +201,9 @@ function displayVMDetails(vm, vmName) {
                     <button class="btn btn-sm btn-warning btn-action" onclick="openSnapshotModal('${vmName}')">
                         <i class="bi bi-camera"></i> Crear Snapshot
                     </button>
+                    <button class="btn btn-sm btn-secondary btn-action" onclick="openSnapshotsListModal('${vmName}')">
+                        <i class="bi bi-collection"></i> Ver Snapshots
+                    </button>
                     <button class="btn btn-sm btn-secondary btn-action" onclick="downloadVMConfig('${vmName}')">
                         <i class="bi bi-download"></i> Descargar XML
                     </button>
@@ -821,3 +824,136 @@ async function createSnapshot(vmName) {
     }
 }
 
+// Open snapshots list modal
+function openSnapshotsListModal(vmName) {
+    const modal = document.createElement('div');
+    modal.classList.add('modal', 'fade');
+    modal.innerHTML = `
+        <div class="modal-dialog modal-lg">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">Snapshots - ${vmName}</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+                </div>
+                <div class="modal-body">
+                    <div id="snapshotsList" class="text-center">
+                        <p class="text-muted">Cargando snapshots...</p>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
+                </div>
+            </div>
+        </div>
+    `;
+    
+    document.body.appendChild(modal);
+    const bsModal = new bootstrap.Modal(modal);
+    bsModal.show();
+    
+    // Limpiar modal al cerrarlo
+    modal.addEventListener('hidden.bs.modal', () => {
+        modal.remove();
+    });
+    
+    // Cargar snapshots
+    loadSnapshotsList(vmName);
+}
+
+// Load and display snapshots list
+async function loadSnapshotsList(vmName) {
+    try {
+        const response = await fetch(`${API_BASE}/vms/${vmName}/snapshots`);
+        const data = await response.json();
+        
+        if (data.error) {
+            document.getElementById('snapshotsList').innerHTML = `<p class="text-danger">Error: ${data.error}</p>`;
+            return;
+        }
+        
+        const snapshots = data.snapshots;
+        
+        if (!snapshots || snapshots.length === 0) {
+            document.getElementById('snapshotsList').innerHTML = '<p class="text-muted">No hay snapshots disponibles</p>';
+            return;
+        }
+        
+        let html = `
+            <div class="table-responsive">
+                <table class="table table-sm table-hover">
+                    <thead>
+                        <tr>
+                            <th>Nombre</th>
+                            <th>Creado</th>
+                            <th>Descripción</th>
+                            <th>Estado</th>
+                            <th>Acciones</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+        `;
+        
+        snapshots.forEach(snap => {
+            let created = 'Desconocida';
+            if (snap.created && snap.created > 0) {
+                created = new Date(snap.created * 1000).toLocaleString('es-ES');
+            }
+            html += `
+                <tr>
+                    <td><strong>${snap.name}</strong></td>
+                    <td>${created}</td>
+                    <td>${snap.description || '-'}</td>
+                    <td><span class="badge bg-info">${snap.state}</span></td>
+                    <td>
+                        <button class="btn btn-sm btn-danger" onclick="deleteSnapshotConfirm('${vmName}', '${snap.name}')">
+                            <i class="bi bi-trash"></i> Eliminar
+                        </button>
+                    </td>
+                </tr>
+            `;
+        });
+        
+        html += `
+                    </tbody>
+                </table>
+            </div>
+        `;
+        
+        document.getElementById('snapshotsList').innerHTML = html;
+    } catch (error) {
+        console.error('Error:', error);
+        document.getElementById('snapshotsList').innerHTML = `<p class="text-danger">Error: ${error.message}</p>`;
+    }
+}
+
+// Delete snapshot with confirmation
+function deleteSnapshotConfirm(vmName, snapshotName) {
+    if (confirm(`¿Estás seguro de que deseas eliminar el snapshot "${snapshotName}"?`)) {
+        deleteSnapshot(vmName, snapshotName);
+    }
+}
+
+// Delete snapshot
+async function deleteSnapshot(vmName, snapshotName) {
+    try {
+        const encodedSnapshotName = encodeURIComponent(snapshotName);
+        const response = await fetch(`${API_BASE}/vms/${vmName}/snapshots/${encodedSnapshotName}`, {
+            method: 'DELETE'
+        });
+        const data = await response.json();
+        
+        if (data.error) {
+            alert(`Error: ${data.error}`);
+            return;
+        }
+        
+        alert(`✓ Snapshot "${snapshotName}" eliminado exitosamente`);
+        
+        // Recargar lista de snapshots
+        loadSnapshotsList(vmName);
+    } catch (error) {
+        console.error('Error:', error);
+        alert(`Error al eliminar snapshot: ${error.message}`);
+    }
+}
+

+ 1 - 1
static/index.html

@@ -701,6 +701,6 @@
     </div>
 
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
-    <script src="/static/dashboard.js?v=4"></script>
+    <script src="/static/dashboard.js?v=7"></script>
 </body>
 </html>