diff --git a/docs/Diagrammes/Arreter-VM.drawio b/docs/Diagrammes/Arreter-VM.drawio new file mode 100644 index 0000000..cc37d4e --- /dev/null +++ b/docs/Diagrammes/Arreter-VM.drawio @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/Diagrammes/Creer-Labo.drawio b/docs/Diagrammes/Creer-Labo.drawio new file mode 100644 index 0000000..847d739 --- /dev/null +++ b/docs/Diagrammes/Creer-Labo.drawio @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Diagrammes/Exporter-PDF.drawio b/docs/Diagrammes/Exporter-PDF.drawio new file mode 100644 index 0000000..85d2a17 --- /dev/null +++ b/docs/Diagrammes/Exporter-PDF.drawio @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/Diagrammes/Importer-VM.drawio b/docs/Diagrammes/Importer-VM.drawio new file mode 100644 index 0000000..6cd5329 --- /dev/null +++ b/docs/Diagrammes/Importer-VM.drawio @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/Diagrammes/Prendre-Snapshot.drawio b/docs/Diagrammes/Prendre-Snapshot.drawio new file mode 100644 index 0000000..2d5942f --- /dev/null +++ b/docs/Diagrammes/Prendre-Snapshot.drawio @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Diagrammes/Restaurer-Snapshot.drawio b/docs/Diagrammes/Restaurer-Snapshot.drawio new file mode 100644 index 0000000..9300d5c --- /dev/null +++ b/docs/Diagrammes/Restaurer-Snapshot.drawio @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/Diagrammes/SEQ1-Demarrer-VM.drawio b/docs/Diagrammes/SEQ1-Demarrer-VM.drawio new file mode 100644 index 0000000..e9099ae --- /dev/null +++ b/docs/Diagrammes/SEQ1-Demarrer-VM.drawio @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/Diagrammes/UML.drawio b/docs/Diagrammes/UML.drawio new file mode 100644 index 0000000..ed744ba --- /dev/null +++ b/docs/Diagrammes/UML.drawio @@ -0,0 +1,463 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Diagrammes/UML.drawio.png b/docs/Diagrammes/UML.drawio.png new file mode 100644 index 0000000..1ea7079 Binary files /dev/null and b/docs/Diagrammes/UML.drawio.png differ diff --git "a/docs/Images/Image coll\303\251e.png" "b/docs/Images/Image coll\303\251e.png" new file mode 100644 index 0000000..7352491 Binary files /dev/null and "b/docs/Images/Image coll\303\251e.png" differ diff --git a/nbactions.xml b/nbactions.xml new file mode 100644 index 0000000..186cf9c --- /dev/null +++ b/nbactions.xml @@ -0,0 +1,19 @@ + + + + run + + jar + + + javafx:run + + + + ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} + + ${packageClassName} + java + + + diff --git a/pom.xml b/pom.xml index 00d3d36..fd49685 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ - + org.openjfx javafx-controls @@ -31,21 +31,21 @@ ${javafx.version} - + org.xerial sqlite-jdbc 3.49.1.0 - + org.apache.pdfbox pdfbox 3.0.4 - + org.junit.jupiter @@ -80,7 +80,7 @@ - + org.apache.maven.plugins maven-compiler-plugin @@ -88,7 +88,7 @@ + org.openjfx javafx-maven-plugin diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 74c3ae1..505ba0e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -3,18 +3,18 @@ */ module tg.cyberlabmanager { - // ── JavaFX ────────────────────────────────────────────────────────── + // JavaFX requires javafx.controls; requires javafx.fxml; - // ── JDBC + SQLite ──────────────────────────────────────────────────── + // JDBC + SQLite requires java.sql; requires org.xerial.sqlitejdbc; - // ── Export PDF ─────────────────────────────────────────────────────── + // Export PDF requires org.apache.pdfbox; - // ── Logging (java.util.logging) ────────────────────────────────────── + // Logging requires java.logging; /* @@ -34,4 +34,4 @@ exports tg.cyberlabmanager.hypervisor; exports tg.cyberlabmanager.pdf; exports tg.cyberlabmanager.ui; -} +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 6074c08..bdc61aa 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -6,15 +6,22 @@ import java.io.File; import tg.cyberlabmanager.data.DatabaseManager; +import tg.cyberlabmanager.hypervisor.HypervisorException; import tg.cyberlabmanager.hypervisor.IHypervisor; +import tg.cyberlabmanager.hypervisor.VBoxOrchestrator; import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.AuditEntry; import tg.cyberlabmanager.model.Lab; import tg.cyberlabmanager.model.VirtualMachine; +import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.model.VMDescriptor; import tg.cyberlabmanager.pdf.PdfExporter; import tg.cyberlabmanager.model.VMStatus; import tg.cyberlabmanager.ui.LabListView; import tg.cyberlabmanager.ui.VmTableView; +import tg.cyberlabmanager.model.JournalEntry; +import java.time.LocalDateTime; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -26,8 +33,12 @@ import static tg.cyberlabmanager.model.VMStatus.SAVED; /** - * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). Il - * gère les données de la maquette et agit comme relais visuel. + * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). + * + *

+ * Coordonne l'UI, la persistance ({@link DatabaseManager}, Rôle 2) et le + * pilotage de l'hyperviseur VirtualBox ({@link VBoxOrchestrator}, Rôle 3). + *

*/ public class LabController { @@ -37,9 +48,9 @@ public class LabController { private Lab selectedLab; private VirtualMachine selectedVM; private boolean auditExpanded = false; + private boolean viewingOrphans = false; private final ExecutorService executor = Executors.newCachedThreadPool(); - private final java.util.Map> vmsByLab = new java.util.HashMap<>(); @FXML private Label statusLabel; @@ -74,6 +85,8 @@ public class LabController { @FXML private javafx.scene.control.Button btnTakeSnapshot; @FXML + private javafx.scene.control.Button btnAssignLab; + @FXML private Label controlsPlaceholder; @FXML private javafx.scene.control.ComboBox snapshotCombo; @@ -90,40 +103,31 @@ public class LabController { @FXML private void initialize() { - // TODO (Rôle 3) : remplacer par la vraie implémentation dès qu'elle est livrée - this.hypervisor = new tg.cyberlabmanager.hypervisor.MockHypervisor(); - mockLabs(); - initMockVMs(); - refreshLabList(); + this.databaseManager = new DatabaseManager(); + AppConfig config = databaseManager.getConfig(); - } + VBoxOrchestrator orchestrator = new VBoxOrchestrator(config); + this.hypervisor = orchestrator; - // ── Données mock temporaires ────────────────────────────────────────── - private List mockLabs() { - List labs = new ArrayList<>(); - labs.add(new Lab(1, "APT29 Analysis", "Analyse APT", "Malware")); - labs.add(new Lab(2, "Red Team Ops", "Pentest réseau", "Pentest")); - labs.add(new Lab(3, "Network Traffic Analysis", "Analyse trafic", "Network")); - labs.add(new Lab(4, "Memory Forensics Lab", "Forensique mémoire", "Forensics")); - labs.add(new Lab(5, "Ransomware Sandbox", "Sandbox malware", "Malware")); - return labs; - } - - private void initMockVMs() { - for (int labId = 1; labId <= 5; labId++) { - vmsByLab.put(labId, mockVMsForLab(labId)); + if (!orchestrator.isReady()) { + showError("VirtualBox introuvable", + "VBoxManage n'a pas été détecté automatiquement. " + + "Vous pourrez configurer son chemin dans les paramètres."); } + + this.pdfExporter = new PdfExporter(); + + refreshLabList(); } -// ── Affichage de la liste des labos ────────────────────────────────── + // ── Affichage de la liste des labos ────────────────────────────────── private void refreshLabList() { labListContainer.getChildren().clear(); - // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() - List labs = mockLabs(); + List labs = databaseManager.getAllLabs(); for (Lab lab : labs) { - boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId(); + boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId() && !viewingOrphans; String badgeStyle = switch (lab.getCategory().toLowerCase()) { case "malware" -> @@ -139,14 +143,21 @@ private void refreshLabList() { }; javafx.scene.layout.VBox card = LabListView.createLabCard( - lab.getTitle(), lab.getCategory(), badgeStyle, isSelected - ); + lab.getTitle(), lab.getCategory(), badgeStyle, isSelected); - // Listener de clic — Jour 2 card.setOnMouseClicked(e -> onLabCardClicked(lab)); labListContainer.getChildren().add(card); } + + // ── Carte « VMs sans labo » ────────────────────────────────────── + List orphans = databaseManager.getOrphanVMs(); + if (!orphans.isEmpty()) { + javafx.scene.layout.VBox orphanCard = LabListView.createOrphanCard( + orphans.size(), viewingOrphans); + orphanCard.setOnMouseClicked(e -> onOrphanCardClicked(orphans)); + labListContainer.getChildren().add(orphanCard); + } } /** @@ -170,20 +181,18 @@ public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, Pd } public void initView() { - } private List getVMsForLab(int labId) { - // TODO (Rôle 2) : remplacer par databaseManager.getVMsForLab(labId) - return vmsByLab.getOrDefault(labId, new ArrayList<>()); + return databaseManager.getVMsForLab(labId); } public List getAllLabs() { - throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); + return databaseManager.getAllLabs(); } public List getOrphanVMs() { - throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); + return databaseManager.getOrphanVMs(); } public void onStartVMClicked(VirtualMachine vm, @@ -198,6 +207,17 @@ public void onStartVMClicked(VirtualMachine vm, onSuccess.run(); }); } catch (Exception e) { + try { + VMStatus liveStatus = hypervisor.getStatus(vm.getUuid()); + if (liveStatus == VMStatus.RUNNING) { + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.RUNNING); + onSuccess.run(); + }); + return; + } + } catch (Exception ignored) { + } javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); } }); @@ -269,81 +289,288 @@ public void onRestoreSnapshotClicked(VirtualMachine vm, } public void onAddJournalEntry(VirtualMachine vm, String text) { - + if (vm == null || text == null || text.isBlank()) { + return; + } + JournalEntry entry = new JournalEntry(); + entry.setContent(text); + entry.setTimestamp(LocalDateTime.now()); + databaseManager.addJournalEntry(entry, vm.getId()); } - public void onImportVMsClicked(Consumer> onSuccess, - Consumer onError) { + // ── Import de VM déjà présentes dans VirtualBox ────────────────────── + /** + * Déclenche le listage des machines virtuelles disponibles dans + * l'hyperviseur, en vue de leur import (rattachement en base). + */ + public void onImportVMsClicked() { executor.submit(() -> { try { - Thread.sleep(800); // simule le temps d'import - javafx.application.Platform.runLater(() -> { - VirtualMachine imported = new VirtualMachine(); - imported.setName("Imported-VM-" + (System.currentTimeMillis() % 1000)); - imported.setUuid(java.util.UUID.randomUUID().toString()); - imported.setStatus(VMStatus.POWERED_OFF); - - List result = new ArrayList<>(); - result.add(imported); - onSuccess.accept(result); - }); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - javafx.application.Platform.runLater(() -> onError.accept("Import interrompu")); + List descriptors = hypervisor.listVMs(); + javafx.application.Platform.runLater(() -> showImportSelectionDialog(descriptors)); + } catch (HypervisorException e) { + javafx.application.Platform.runLater(() -> showError("Erreur listage VM", e.getMessage())); + } + }); + } + + private void showImportSelectionDialog(List descriptors) { + if (descriptors.isEmpty()) { + showError("Aucune VM trouvée", "Aucune machine virtuelle détectée dans VirtualBox."); + return; + } + + // Exclure les VM dont l'UUID est déjà connu en base (tous labos + orphelines + // confondus) + List knownUuids = new ArrayList<>(); + for (Lab lab : databaseManager.getAllLabs()) { + for (VirtualMachine vm : databaseManager.getVMsForLab(lab.getId())) { + knownUuids.add(vm.getUuid()); + } + } + for (VirtualMachine vm : databaseManager.getOrphanVMs()) { + knownUuids.add(vm.getUuid()); + } + + List importable = descriptors.stream() + .filter(d -> !knownUuids.contains(d.uuid())) + .toList(); + + if (importable.isEmpty()) { + showError("Aucune VM à importer", + "Toutes les VM VirtualBox détectées sont déjà rattachées dans l'application."); + return; + } + + javafx.scene.control.ListView listView = new javafx.scene.control.ListView<>(); + listView.getItems().addAll(importable); + listView.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + listView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { + @Override + protected void updateItem(VMDescriptor item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null : item.name() + " (" + item.uuid() + ")"); + } + }); + + javafx.scene.control.Dialog> dialog = new javafx.scene.control.Dialog<>(); + dialog.setTitle("Importer des VM"); + dialog.setHeaderText("Sélectionnez les VM à rattacher" + + (selectedLab != null ? " au labo \"" + selectedLab.getTitle() + "\"" : " (sans labo)")); + dialog.getDialogPane().setContent(listView); + dialog.getDialogPane().getButtonTypes().addAll( + javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL); + + dialog.setResultConverter(button -> button == javafx.scene.control.ButtonType.OK + ? new ArrayList<>(listView.getSelectionModel().getSelectedItems()) + : null); + + dialog.showAndWait().ifPresent(selectedDescriptors -> { + if (selectedDescriptors.isEmpty()) { + return; + } + + List selectedVMs = new ArrayList<>(); + for (VMDescriptor d : selectedDescriptors) { + VirtualMachine vm = new VirtualMachine(); + vm.setName(d.name()); + vm.setUuid(d.uuid()); + vm.setStatus(VMStatus.UNKNOWN); + selectedVMs.add(vm); + } + + Integer labId = (selectedLab != null) ? selectedLab.getId() : null; + onImportConfirm(selectedVMs, labId); + }); + } + + /** + * Persiste en base les machines virtuelles sélectionnées pour import, + * rattachées (ou non) au laboratoire indiqué. + * + * @param selectedVMs machines virtuelles sélectionnées + * @param labId identifiant du laboratoire cible, ou {@code null} + */ + public void onImportConfirm(List selectedVMs, Integer labId) { + executor.submit(() -> { + List alreadyImported = new ArrayList<>(); + List importedOk = new ArrayList<>(); + + for (VirtualMachine vm : selectedVMs) { + try { + databaseManager.saveVirtualMachine(vm, labId); + importedOk.add(vm); + } catch (RuntimeException e) { + // Filet de sécurité : la VM a pu être importée entre le listage et la + // confirmation + if (e.getMessage() != null && e.getMessage().contains("UNIQUE constraint")) { + alreadyImported.add(vm.getName()); + } else { + javafx.application.Platform.runLater(() -> showError("Erreur import VM", e.getMessage())); + return; + } + } } + + javafx.application.Platform.runLater(() -> { + if (labId != null && selectedLab != null && labId.equals(selectedLab.getId())) { + refreshVmTable(getVMsForLab(selectedLab.getId())); + } + if (!importedOk.isEmpty()) { + appendJournalLine(importedOk.size() + " VM importée(s)"); + persistAuditEntry(null, "Import VM", + importedOk.size() + " VM importée(s)" + + (labId == null ? " (orphelines)" : ""), + selectedLab != null ? selectedLab.getTitle() : null); + } + if (!alreadyImported.isEmpty()) { + showError("VM déjà importées", + "Ces VM étaient déjà connues et n'ont pas été réimportées : " + + String.join(", ", alreadyImported)); + } + }); }); } public void onExportLabClicked(Lab lab, String filePath) { + executor.submit(() -> { + try { + List vms = databaseManager.getVMsForLab(lab.getId()); + + List allSnapshots = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allSnapshots.addAll(databaseManager.getSnapshotsForVM(vm.getId())); + } + + List allJournals = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allJournals.addAll(databaseManager.getJournalEntriesForVM(vm.getId())); + } + + pdfExporter.exportLabToPdf(lab, vms, allSnapshots, allJournals, filePath); + + javafx.application.Platform.runLater(() -> { + appendJournalLine("✅ Export PDF terminé : " + filePath); + persistAuditEntry(null, "Export PDF", + "Export PDF du labo \"" + lab.getTitle() + "\"", lab.getTitle()); + }); + + } catch (Exception e) { + javafx.application.Platform.runLater(() -> { + appendJournalLine("❌ Erreur export PDF : " + e.getMessage()); + showError("Erreur export PDF", e.getMessage()); + }); + } + }); } public void onExportOrphansClicked(String filePath) { } /** - * Persiste la configuration mise a jour. + * Persiste la configuration mise à jour dans la base et l'applique à + * l'hyperviseur déjà instancié. * * @param newConfig nouvelle configuration applicative */ public void onConfigUpdated(AppConfig newConfig) { + try { + hypervisor.applyConfig(newConfig); + databaseManager.saveConfig(newConfig); + appendJournalLine("Configuration mise à jour"); + } catch (HypervisorException e) { + showError("Erreur configuration hyperviseur", e.getMessage()); + } } @FXML - private void handleImportVM() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Importer une VM"); - fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("VirtualBox Appliance", "*.ova"), - new FileChooser.ExtensionFilter("Disque virtuel", "*.vdi") - ); + private void handleNewLab() { + javafx.scene.control.Dialog dialog = new javafx.scene.control.Dialog<>(); + dialog.setTitle("Nouveau laboratoire"); + dialog.setHeaderText("Créer un nouveau laboratoire"); + + javafx.scene.control.ButtonType createButtonType = new javafx.scene.control.ButtonType("Créer", + javafx.scene.control.ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().addAll( + createButtonType, javafx.scene.control.ButtonType.CANCEL); + + javafx.scene.control.TextField titleField = new javafx.scene.control.TextField(); + titleField.setPromptText("Titre du labo"); + + javafx.scene.control.TextArea descriptionField = new javafx.scene.control.TextArea(); + descriptionField.setPromptText("Description"); + descriptionField.setPrefRowCount(3); + + javafx.scene.control.ComboBox categoryCombo = new javafx.scene.control.ComboBox<>(); + categoryCombo.getItems().addAll("Malware", "Pentest", "Network", "Forensics"); + categoryCombo.setPromptText("Catégorie"); + categoryCombo.setMaxWidth(Double.MAX_VALUE); + + javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(10, + new javafx.scene.control.Label("Titre :"), titleField, + new javafx.scene.control.Label("Description :"), descriptionField, + new javafx.scene.control.Label("Catégorie :"), categoryCombo); + content.setPadding(new javafx.geometry.Insets(10)); + dialog.getDialogPane().setContent(content); + + javafx.scene.Node createButton = dialog.getDialogPane().lookupButton(createButtonType); + createButton.setDisable(true); + titleField.textProperty() + .addListener((obs, oldVal, newVal) -> createButton.setDisable(newVal == null || newVal.isBlank())); + + dialog.setResultConverter(button -> { + if (button != createButtonType) { + return null; + } + Lab lab = new Lab(); + lab.setTitle(titleField.getText().trim()); + lab.setDescription(descriptionField.getText()); + lab.setCategory(categoryCombo.getValue() != null ? categoryCombo.getValue() : "Malware"); + return lab; + }); - File selectedFile = fileChooser.showOpenDialog( - labListContainer.getScene().getWindow() - ); + dialog.showAndWait().ifPresent(lab -> { + try { + databaseManager.saveLab(lab); + selectedLab = lab; + viewingOrphans = false; + refreshLabList(); + appendJournalLine("Labo \"" + lab.getTitle() + "\" créé"); + + // Proposition d'importer des VM immédiatement (cf. diagramme séquence «Créer + // labo») + javafx.scene.control.Alert prompt = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION); + prompt.setTitle("Labo créé"); + prompt.setHeaderText("Labo \"" + lab.getTitle() + "\" créé avec succès !"); + prompt.setContentText("Voulez-vous importer des VM dans ce labo maintenant ?"); + + javafx.scene.control.ButtonType btnOui = new javafx.scene.control.ButtonType("Oui, importer des VM", + javafx.scene.control.ButtonBar.ButtonData.YES); + javafx.scene.control.ButtonType btnNon = new javafx.scene.control.ButtonType("Non, plus tard", + javafx.scene.control.ButtonBar.ButtonData.NO); + prompt.getButtonTypes().setAll(btnOui, btnNon); + + prompt.showAndWait().ifPresent(response -> { + if (response == btnOui) { + onImportVMsClicked(); + } + }); - if (selectedFile == null) { - return; - } + } catch (Exception e) { + showError("Erreur création labo", e.getMessage()); + } + }); + } - // TODO (Rôle 3) : remplacer par un appel réel à hypervisor.importVM() - onImportVMsClicked( - vms -> { - if (selectedLab != null) { - getVMsForLab(selectedLab.getId()).addAll(vms); - refreshVmTable(getVMsForLab(selectedLab.getId())); - } - appendJournalLine("Import terminé : " + selectedFile.getName()); - appendAuditLine("VM importée depuis " + selectedFile.getName()); - }, - errorMsg -> showError("Erreur d'import", errorMsg) - ); + @FXML + private void handleImportVM() { + onImportVMsClicked(); } @FXML private void handleExportPDF() { - // Le bouton Export PDF n'est visible que si un labo est sélectionné, - // mais on vérifie par précaution. if (selectedLab == null) { return; } @@ -351,26 +578,17 @@ private void handleExportPDF() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Exporter le rapport PDF"); fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("PDF Document", "*.pdf") - ); - // Suggestion de nom de fichier basé sur le titre du labo + new FileChooser.ExtensionFilter("PDF Document", "*.pdf")); fileChooser.setInitialFileName(selectedLab.getTitle().replace(" ", "_") + ".pdf"); File selectedFile = fileChooser.showSaveDialog( - labListContainer.getScene().getWindow() - ); + labListContainer.getScene().getWindow()); if (selectedFile == null) { - return; // L'utilisateur a annulé + return; } - // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() - // Pour l'instant, on appelle le stub métier (déjà présent dans LabController) onExportLabClicked(selectedLab, selectedFile.getAbsolutePath()); - - // Ajout des traces immédiates (le stub ne fait rien pour l'instant) - appendJournalLine("Export PDF demandé pour " + selectedLab.getTitle()); - appendAuditLine("Export PDF demandé pour " + selectedLab.getTitle()); } @FXML @@ -384,16 +602,17 @@ private void handleStartVM() { onStartVMClicked(selectedVM, () -> { btnStart.setDisable(false); + selectedVM.setStatus(VMStatus.RUNNING); + persisterStatutVM(selectedVM); updateControlButtons(VMStatus.RUNNING); refreshVmTable(getVMsForLab(selectedLab.getId())); - appendJournalLine("VM started successfully"); - appendAuditLine("VM démarrée"); + appendJournalLine("VM démarrée avec succès"); + persistAuditEntry(selectedVM.getId(), "Start VM", "VM démarrée", selectedLab.getTitle()); }, errorMsg -> { btnStart.setDisable(false); showError("Erreur démarrage VM", errorMsg); - } - ); + }); } @FXML @@ -407,16 +626,17 @@ private void handleStopVM() { onStopVMClicked(selectedVM, () -> { btnStop.setDisable(false); + selectedVM.setStatus(VMStatus.POWERED_OFF); + persisterStatutVM(selectedVM); updateControlButtons(VMStatus.POWERED_OFF); refreshVmTable(getVMsForLab(selectedLab.getId())); - appendJournalLine("VM stopped"); - appendAuditLine("VM arrêtée"); + appendJournalLine("VM arrêtée"); + persistAuditEntry(selectedVM.getId(), "Stop VM", "VM arrêtée", selectedLab.getTitle()); }, errorMsg -> { btnStop.setDisable(false); showError("Erreur arrêt VM", errorMsg); - } - ); + }); } @FXML @@ -430,16 +650,30 @@ private void handleSaveState() { onSaveStateClicked(selectedVM, () -> { btnSaveState.setDisable(false); + selectedVM.setStatus(VMStatus.SAVED); + persisterStatutVM(selectedVM); updateControlButtons(VMStatus.SAVED); refreshVmTable(getVMsForLab(selectedLab.getId())); - appendJournalLine("VM state saved"); - appendAuditLine("État sauvegardé"); + appendJournalLine("État de la VM sauvegardé"); + persistAuditEntry(selectedVM.getId(), "Save State", "État sauvegardé", selectedLab.getTitle()); }, errorMsg -> { btnSaveState.setDisable(false); showError("Erreur sauvegarde état", errorMsg); - } - ); + }); + } + + /** + * Persiste le statut courant d'une VM en base sans modifier son labo de + * rattachement. + */ + private void persisterStatutVM(VirtualMachine vm) { + try { + Integer labId = (selectedLab != null) ? selectedLab.getId() : null; + databaseManager.saveVirtualMachine(vm, labId); + } catch (Exception e) { + appendJournalLine("⚠️ Impossible de sauvegarder le statut en base : " + e.getMessage()); + } } @FXML @@ -460,20 +694,22 @@ private void handleTakeSnapshot() { onTakeSnapshotClicked(selectedVM, name, "", () -> { - tg.cyberlabmanager.model.Snapshot snapshot = new tg.cyberlabmanager.model.Snapshot(); + Snapshot snapshot = new Snapshot(); snapshot.setName(name); snapshot.setDescription(""); - snapshot.setCreatedAt(java.time.LocalDateTime.now()); + snapshot.setCreatedAt(LocalDateTime.now()); snapshot.setOnline(selectedVM.getStatus() == VMStatus.RUNNING); selectedVM.addSnapshot(snapshot); + databaseManager.saveSnapshot(snapshot, selectedVM.getId()); + snapshotCombo.getItems().add(name); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("Snapshot \"" + name + "\" created"); - appendAuditLine("Snapshot \"" + name + "\" créé"); + persistAuditEntry(selectedVM.getId(), "Take Snapshot", + "Snapshot \"" + name + "\" créé", selectedLab.getTitle()); }, - errorMsg -> showError("Erreur snapshot", errorMsg) - ); + errorMsg -> showError("Erreur snapshot", errorMsg)); }); } @@ -499,18 +735,46 @@ private void handleRestoreSnapshot() { return; } - onRestoreSnapshotClicked(selectedVM, snapshotName, + final VirtualMachine vmToRestore = selectedVM; + final String labTitle = selectedLab != null ? selectedLab.getTitle() : ""; + + onRestoreSnapshotClicked(vmToRestore, snapshotName, () -> { - appendJournalLine("Snapshot \"" + snapshotName + "\" restored"); - appendAuditLine("Snapshot \"" + snapshotName + "\" restauré"); + // Après restauration, la VM est éteinte + vmToRestore.setStatus(VMStatus.POWERED_OFF); + persisterStatutVM(vmToRestore); + updateControlButtons(VMStatus.POWERED_OFF); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("Snapshot \"" + snapshotName + "\" restauré"); + persistAuditEntry(vmToRestore.getId(), "Restore Snapshot", + "Snapshot \"" + snapshotName + "\" restauré", labTitle); }, - errorMsg -> showError("Erreur restauration snapshot", errorMsg) - ); + errorMsg -> { + // Fallback: la restauration peut échouer si la session VBox est encore + // verrouillée mais la VM est en fait dans l'état attendu + executor.submit(() -> { + try { + VMStatus live = hypervisor.getStatus(vmToRestore.getUuid()); + if (live == VMStatus.POWERED_OFF || live == VMStatus.SAVED) { + javafx.application.Platform.runLater(() -> { + vmToRestore.setStatus(live); + persisterStatutVM(vmToRestore); + updateControlButtons(live); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("Snapshot restauré (état: " + live.name() + ")"); + }); + return; + } + } catch (Exception ignored) { + } + javafx.application.Platform.runLater( + () -> showError("Erreur restauration", errorMsg)); + }); + }); }); } @FXML - private void handleAddJournalEntry() { String text = journalInputField.getText(); if (text == null || text.isBlank()) { @@ -520,7 +784,22 @@ private void handleAddJournalEntry() { appendJournalLine(text); journalInputField.clear(); - // TODO (Rôle 2) : databaseManager.addJournalEntry(selectedLab, selectedVM, text) + if (selectedVM != null) { + try { + JournalEntry entry = new JournalEntry(); + entry.setContent(text); + entry.setTimestamp(LocalDateTime.now()); + + databaseManager.addJournalEntry(entry, selectedVM.getId()); + + appendJournalLine("✅ Note sauvegardée en base"); + } catch (Exception e) { + appendJournalLine("❌ Erreur sauvegarde: " + e.getMessage()); + showError("Erreur journal", e.getMessage()); + } + } else { + appendJournalLine("⚠️ Aucune VM sélectionnée - note non sauvegardée"); + } } @FXML @@ -535,38 +814,6 @@ public void shutdown() { executor.shutdownNow(); } - private List mockVMsForLab(int labId) { - List vms = new ArrayList<>(); - - if (labId == 1) { // APT29 Analysis - VirtualMachine vm = new VirtualMachine(); - vm.setName("Kali-APT29"); - vm.setUuid("a1b2c3d4-1111-2222-3333-444455556666"); - vm.setStatus(VMStatus.RUNNING); - vms.add(vm); - } else if (labId == 2) { // Red Team Ops - VirtualMachine vm = new VirtualMachine(); - vm.setName("Metasploitable3"); - vm.setUuid("b2c3d4e5-2222-3333-4444-555566667777"); - vm.setStatus(VMStatus.POWERED_OFF); - vms.add(vm); - } else if (labId == 3) { // Network Traffic Analysis - VirtualMachine vm = new VirtualMachine(); - vm.setName("Wireshark-Station"); - vm.setUuid("f8c9a7b6-3333-4444-5555-666677778888"); - vm.setStatus(VMStatus.RUNNING); - vms.add(vm); - } else if (labId == 4) { // Memory Forensics - VirtualMachine vm = new VirtualMachine(); - vm.setName("REMnux"); - vm.setUuid("c3d4e5f6-4444-5555-6666-777788889999"); - vm.setStatus(VMStatus.SAVED); - vms.add(vm); - } - // labId == 5 (Ransomware Sandbox) → liste vide intentionnellement - return vms; - } - private void refreshVmTable(List vms) { vmListContainer.getChildren().clear(); @@ -582,6 +829,14 @@ private void refreshVmTable(List vms) { vmEmptyLabel.setVisible(false); vmEmptyLabel.setManaged(false); + // Charger les snapshots depuis la DB en premier pour que les stats soient + // correctes + for (VirtualMachine vm : vms) { + if (vm.getSnapshots().isEmpty() && vm.getId() != 0) { + databaseManager.getSnapshotsForVM(vm.getId()).forEach(vm::addSnapshot); + } + } + long runningCount = vms.stream() .filter(v -> v.getStatus() == VMStatus.RUNNING) .count(); @@ -612,23 +867,50 @@ private void refreshVmTable(List vms) { }; javafx.scene.layout.HBox row = VmTableView.createVmRow( - vm.getName(), shortUuid, statusText, "" - ); + vm.getName(), shortUuid, statusText, ""); - // Listener clic VM → Jour 3 row.setOnMouseClicked(e -> onVmRowClicked(vm)); vmListContainer.getChildren().add(row); } } private void onLabCardClicked(Lab lab) { + // Double-clic sur le labo déjà sélectionné → désélection (retour à l'état + // neutre) + if (!viewingOrphans && selectedLab != null && selectedLab.getId() == lab.getId()) { + selectedLab = null; + selectedVM = null; + viewingOrphans = false; + labTitleLabel.setText("Sélectionnez un laboratoire"); + btnExportPdf.setVisible(false); + btnExportPdf.setManaged(false); + statsBar.setVisible(false); + statsBar.setManaged(false); + vmTableHeader.setVisible(false); + vmTableHeader.setManaged(false); + vmListContainer.getChildren().clear(); + controlsPlaceholder.setVisible(true); + controlsPlaceholder.setManaged(true); + btnStart.setVisible(false); + btnStart.setManaged(false); + btnStop.setVisible(false); + btnStop.setManaged(false); + btnSaveState.setVisible(false); + btnSaveState.setManaged(false); + btnTakeSnapshot.setVisible(false); + btnTakeSnapshot.setManaged(false); + btnAssignLab.setVisible(false); + btnAssignLab.setManaged(false); + refreshLabList(); + return; + } + this.selectedLab = lab; this.selectedVM = null; + this.viewingOrphans = false; - // Mettre à jour le titre labTitleLabel.setText(lab.getTitle()); - // Afficher les éléments cachés btnExportPdf.setVisible(true); btnExportPdf.setManaged(true); statsBar.setVisible(true); @@ -636,7 +918,6 @@ private void onLabCardClicked(Lab lab) { vmTableHeader.setVisible(true); vmTableHeader.setManaged(true); - // Remettre le panneau droit en état vide selectedVmNameLabel.setText(""); controlsPlaceholder.setVisible(true); controlsPlaceholder.setManaged(true); @@ -648,39 +929,175 @@ private void onLabCardClicked(Lab lab) { btnSaveState.setManaged(false); btnTakeSnapshot.setVisible(false); btnTakeSnapshot.setManaged(false); + btnAssignLab.setVisible(false); + btnAssignLab.setManaged(false); - // Rafraîchir la liste et le tableau refreshLabList(); - - // TODO (Rôle 2) : remplacer mockVMsForLab par databaseManager.getVMsForLab refreshVmTable(getVMsForLab(lab.getId())); } + private void onOrphanCardClicked(List orphans) { + this.selectedLab = null; + this.selectedVM = null; + this.viewingOrphans = true; + + labTitleLabel.setText("VMs sans labo"); + + btnExportPdf.setVisible(false); + btnExportPdf.setManaged(false); + statsBar.setVisible(true); + statsBar.setManaged(true); + vmTableHeader.setVisible(true); + vmTableHeader.setManaged(true); + + selectedVmNameLabel.setText(""); + controlsPlaceholder.setVisible(true); + controlsPlaceholder.setManaged(true); + btnStart.setVisible(false); + btnStart.setManaged(false); + btnStop.setVisible(false); + btnStop.setManaged(false); + btnSaveState.setVisible(false); + btnSaveState.setManaged(false); + btnTakeSnapshot.setVisible(false); + btnTakeSnapshot.setManaged(false); + btnAssignLab.setVisible(false); + btnAssignLab.setManaged(false); + + refreshLabList(); + refreshVmTable(orphans); + } + private void onVmRowClicked(VirtualMachine vm) { this.selectedVM = vm; selectedVmNameLabel.setText(vm.getName()); - // Cacher le placeholder "sélectionnez une VM" controlsPlaceholder.setVisible(false); controlsPlaceholder.setManaged(false); - // Activer les bons boutons selon le statut de la VM updateControlButtons(vm.getStatus()); + + // Interroger l'hyperviseur en arrière-plan pour synchroniser l'état réel + executor.submit(() -> { + try { + VMStatus liveStatus = hypervisor.getStatus(vm.getUuid()); + if (liveStatus != null && liveStatus != VMStatus.UNKNOWN) { + javafx.application.Platform.runLater(() -> { + if (selectedVM == vm) { + vm.setStatus(liveStatus); + updateControlButtons(liveStatus); + } + }); + } + } catch (Exception ignored) { + } + }); } private void updateControlButtons(VMStatus status) { boolean isRunning = status == VMStatus.RUNNING; boolean isStopped = status == VMStatus.POWERED_OFF || status == VMStatus.SAVED; + boolean isUnknown = status == VMStatus.UNKNOWN || status == null; - btnStart.setVisible(isStopped); - btnStart.setManaged(isStopped); - btnStop.setVisible(isRunning); - btnStop.setManaged(isRunning); + btnStart.setVisible(isStopped || isUnknown); + btnStart.setManaged(isStopped || isUnknown); + btnStop.setVisible(isRunning || isUnknown); + btnStop.setManaged(isRunning || isUnknown); btnSaveState.setVisible(isRunning); btnSaveState.setManaged(isRunning); btnTakeSnapshot.setVisible(true); btnTakeSnapshot.setManaged(true); + + // Le bouton d'assignation change de texte selon le contexte + if (viewingOrphans) { + btnAssignLab.setText("🔗 Assigner au labo"); + } else { + btnAssignLab.setText("🔗 Changer de labo / Détacher"); + } + btnAssignLab.setVisible(true); + btnAssignLab.setManaged(true); + } + + /** + * Ouvre un dialog de sélection de laboratoire pour rattacher la VM + * sélectionnée à un labo existant, ou la détacher (orpheline). + */ + @FXML + private void handleAssignLabToVM() { + if (selectedVM == null) { + return; + } + + List labs = databaseManager.getAllLabs(); + if (labs.isEmpty()) { + showError("Aucun labo disponible", + "Créez d'abord un laboratoire avant d'assigner une VM."); + return; + } + + // Options : tous les labs + option « Détacher (orpheline) » + javafx.scene.control.ChoiceDialog dialog = new javafx.scene.control.ChoiceDialog<>(); + dialog.setTitle("Assigner au labo"); + dialog.setHeaderText("VM : " + selectedVM.getName()); + dialog.setContentText("Choisissez le laboratoire de destination :"); + + for (Lab lab : labs) { + dialog.getItems().add(lab.getId() + " — " + lab.getTitle()); + } + dialog.getItems().add("— Détacher (orpheline)"); + dialog.setSelectedItem(dialog.getItems().get(0)); + + dialog.showAndWait().ifPresent(choice -> { + if (choice == null) { + return; + } + + Integer targetLabId = null; + String targetLabName = null; + + if (!choice.startsWith("—")) { + // Extraire l'ID depuis le format "ID — Titre" + try { + int sepIdx = choice.indexOf(" — "); + int labId = Integer.parseInt(choice.substring(0, sepIdx).trim()); + for (Lab lab : labs) { + if (lab.getId() == labId) { + targetLabId = labId; + targetLabName = lab.getTitle(); + break; + } + } + } catch (NumberFormatException ignored) { + return; + } + } + + final Integer finalLabId = targetLabId; + final String finalLabName = targetLabName; + + try { + databaseManager.saveVirtualMachine(selectedVM, finalLabId); + + // Rafraîchir l'affichage selon le contexte actif + if (viewingOrphans) { + refreshVmTable(databaseManager.getOrphanVMs()); + } else if (selectedLab != null) { + refreshVmTable(databaseManager.getVMsForLab(selectedLab.getId())); + } + refreshLabList(); + + String msg = finalLabName != null + ? "VM \"" + selectedVM.getName() + "\" assignée au labo \"" + finalLabName + "\"" + : "VM \"" + selectedVM.getName() + "\" détachée (orpheline)"; + appendJournalLine(msg); + persistAuditEntry(selectedVM.getId(), "Assign Lab", msg, + finalLabName != null ? finalLabName : null); + + } catch (Exception e) { + showError("Erreur assignation", e.getMessage()); + } + }); } private void appendJournalLine(String text) { @@ -698,10 +1115,29 @@ private void showError(String title, String msg) { alert.showAndWait(); } - private void appendAuditLine(String action) { - String time = java.time.LocalTime.now() - .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); - String vmName = selectedVM != null ? selectedVM.getName() : "?"; - auditArea.appendText("[" + time + "] [" + vmName + "] " + action + "\n"); + /** + * Trace une action dans la zone d'audit visuelle ET la persiste en base + * via {@link DatabaseManager#addAuditEntry(AuditEntry)}. + * + * @param vmId identifiant local de la VM concernée, ou {@code null} + * @param action action auditée (ex: "Start VM") + * @param details détails complémentaires de l'action + * @param labName nom du laboratoire concerné, ou {@code null} + */ + private void persistAuditEntry(Integer vmId, String action, String details, String labName) { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vmId); + entry.setAction(action); + entry.setDetails(details); + entry.setLabName(labName); + + auditArea.appendText(entry.getLog() + "\n"); + + try { + databaseManager.addAuditEntry(entry); + } catch (Exception e) { + appendJournalLine("⚠️ Erreur persistance audit : " + e.getMessage()); + } } -} +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak b/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak new file mode 100644 index 0000000..d343847 --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak @@ -0,0 +1,765 @@ +package tg.cyberlabmanager.controller; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.stage.FileChooser; +import java.io.File; + +import tg.cyberlabmanager.data.DatabaseManager; +import tg.cyberlabmanager.hypervisor.IHypervisor; +import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.Lab; +import tg.cyberlabmanager.model.VirtualMachine; +import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.pdf.PdfExporter; +import tg.cyberlabmanager.model.VMStatus; +import tg.cyberlabmanager.ui.LabListView; +import tg.cyberlabmanager.ui.VmTableView; +import tg.cyberlabmanager.model.JournalEntry; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.ArrayList; +import static tg.cyberlabmanager.model.VMStatus.PAUSED; +import static tg.cyberlabmanager.model.VMStatus.POWERED_OFF; +import static tg.cyberlabmanager.model.VMStatus.RUNNING; +import static tg.cyberlabmanager.model.VMStatus.SAVED; + +/** + * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). Il + * gère les données de la maquette et agit comme relais visuel. + */ +public class LabController { + + private IHypervisor hypervisor; + private DatabaseManager databaseManager; + private PdfExporter pdfExporter; + private Lab selectedLab; + private VirtualMachine selectedVM; + private boolean auditExpanded = false; + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @FXML + private Label statusLabel; + @FXML + private javafx.scene.layout.VBox labListContainer; + @FXML + private Label labTitleLabel; + @FXML + private javafx.scene.control.Button btnExportPdf; + @FXML + private javafx.scene.layout.HBox statsBar; + @FXML + private Label statTotal; + @FXML + private Label statRunning; + @FXML + private Label statSnapshots; + @FXML + private javafx.scene.layout.HBox vmTableHeader; + @FXML + private javafx.scene.layout.VBox vmListContainer; + @FXML + private Label vmEmptyLabel; + @FXML + private Label selectedVmNameLabel; + @FXML + private javafx.scene.control.Button btnStart; + @FXML + private javafx.scene.control.Button btnStop; + @FXML + private javafx.scene.control.Button btnSaveState; + @FXML + private javafx.scene.control.Button btnTakeSnapshot; + @FXML + private Label controlsPlaceholder; + @FXML + private javafx.scene.control.ComboBox snapshotCombo; + @FXML + private javafx.scene.control.TextArea journalArea; + @FXML + private javafx.scene.layout.VBox snapshotSection; + @FXML + private javafx.scene.control.TextField journalInputField; + @FXML + private javafx.scene.control.TextArea auditArea; + @FXML + private Label auditArrow; + + @FXML + private void initialize() { + // TODO (Rôle 3) : remplacer par la vraie implémentation dès qu'elle est livrée + this.hypervisor = new tg.cyberlabmanager.hypervisor.MockHypervisor(); + + // Initialiser le DatabaseManager + this.databaseManager = new DatabaseManager(); + + // Initialiser le PdfExporter + this.pdfExporter = new PdfExporter(); + + mockLabs(); + refreshLabList(); + + } + + // ── Données mock temporaires ────────────────────────────────────────── + private List mockLabs() { + List labs = new ArrayList<>(); + labs.add(new Lab(1, "APT29 Analysis", "Analyse APT", "Malware")); + labs.add(new Lab(2, "Red Team Ops", "Pentest réseau", "Pentest")); + labs.add(new Lab(3, "Network Traffic Analysis", "Analyse trafic", "Network")); + labs.add(new Lab(4, "Memory Forensics Lab", "Forensique mémoire", "Forensics")); + labs.add(new Lab(5, "Ransomware Sandbox", "Sandbox malware", "Malware")); + return labs; + } + + + +// ── Affichage de la liste des labos ────────────────────────────────── + private void refreshLabList() { + labListContainer.getChildren().clear(); + + // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() + List labs = databaseManager.getAllLabs(); + + for (Lab lab : labs) { + boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId(); + + String badgeStyle = switch (lab.getCategory().toLowerCase()) { + case "malware" -> + "badge-malware"; + case "pentest" -> + "badge-pentest"; + case "forensics" -> + "badge-forensics"; + case "network" -> + "badge-network"; + default -> + "badge-default"; + }; + + javafx.scene.layout.VBox card = LabListView.createLabCard( + lab.getTitle(), lab.getCategory(), badgeStyle, isSelected + ); + + // Listener de clic — Jour 2 + card.setOnMouseClicked(e -> onLabCardClicked(lab)); + + labListContainer.getChildren().add(card); + } + } + + /** + * Constructeur par défaut initialisant le contrôleur du laboratoire. + */ + public LabController() { + } + + public void setDependencies(IHypervisor hypervisor, + DatabaseManager databaseManager, + PdfExporter pdfExporter) { + this.hypervisor = hypervisor; + this.databaseManager = databaseManager; + this.pdfExporter = pdfExporter; + } + + public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { + this.hypervisor = hypervisor; + this.databaseManager = databaseManager; + this.pdfExporter = pdfExporter; + } + + public void initView() { + + } + + private List getVMsForLab(int labId) { + // TODO (Rôle 2) : remplacer par databaseManager.getVMsForLab(labId) + List vms = databaseManager.getVMsForLab(labId); + + // Fallback vers les mocks si la base est vide + if (vms.isEmpty()) { + vms = mockVMsForLab(labId); + } + + return vms; +} + + public List getAllLabs() { + throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); + } + + public List getOrphanVMs() { + throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); + } + + public void onStartVMClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.startVM(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.RUNNING); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onStopVMClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.stopVM(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.POWERED_OFF); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onSaveStateClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.saveState(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.SAVED); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onTakeSnapshotClicked(VirtualMachine vm, + String name, + String description, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.takeSnapshot(vm.getUuid(), name, description); + javafx.application.Platform.runLater(onSuccess); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onRestoreSnapshotClicked(VirtualMachine vm, + String snapshotName, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.restoreSnapshot(vm.getUuid(), snapshotName); + javafx.application.Platform.runLater(onSuccess); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onAddJournalEntry(VirtualMachine vm, String text) { + + } + + public void onImportVMsClicked(Consumer> onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + Thread.sleep(800); // simule le temps d'import + javafx.application.Platform.runLater(() -> { + VirtualMachine imported = new VirtualMachine(); + imported.setName("Imported-VM-" + (System.currentTimeMillis() % 1000)); + imported.setUuid(java.util.UUID.randomUUID().toString()); + imported.setStatus(VMStatus.POWERED_OFF); + + List result = new ArrayList<>(); + result.add(imported); + onSuccess.accept(result); + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + javafx.application.Platform.runLater(() -> onError.accept("Import interrompu")); + } + }); + } + + public void onExportLabClicked(Lab lab, String filePath) { + // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() + executor.submit(() -> { + try { + // Récupérer les données depuis la base + List vms = databaseManager.getVMsForLab(lab.getId()); + + // Récupérer les snapshots pour chaque VM + List allSnapshots = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allSnapshots.addAll(databaseManager.getSnapshotsForVM(vm.getId())); + } + + // Récupérer les journaux pour chaque VM + List allJournals = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allJournals.addAll(databaseManager.getJournalEntriesForVM(vm.getId())); + } + + // Exporter le PDF + pdfExporter.exportLabToPdf(lab, vms, allSnapshots, allJournals, filePath); + + // Succès - mise à jour de l'UI + javafx.application.Platform.runLater(() -> { + appendJournalLine("✅ Export PDF terminé : " + filePath); + appendAuditLine("Export PDF du labo \"" + lab.getTitle() + "\""); + }); + + } catch (Exception e) { + // Erreur - affichage dans l'UI + javafx.application.Platform.runLater(() -> { + appendJournalLine("❌ Erreur export PDF : " + e.getMessage()); + showError("Erreur export PDF", e.getMessage()); + }); + } + }); +} + + public void onExportOrphansClicked(String filePath) { + } + + /** + * Persiste la configuration mise a jour. + * + * @param newConfig nouvelle configuration applicative + */ + public void onConfigUpdated(AppConfig newConfig) { + } + + @FXML + private void handleImportVM() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Importer une VM"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("VirtualBox Appliance", "*.ova"), + new FileChooser.ExtensionFilter("Disque virtuel", "*.vdi") + ); + + File selectedFile = fileChooser.showOpenDialog( + labListContainer.getScene().getWindow() + ); + + if (selectedFile == null) { + return; + } + + // TODO (Rôle 3) : remplacer par un appel réel à hypervisor.importVM() + onImportVMsClicked( + vms -> { + if (selectedLab != null) { + getVMsForLab(selectedLab.getId()).addAll(vms); + refreshVmTable(getVMsForLab(selectedLab.getId())); + } + appendJournalLine("Import terminé : " + selectedFile.getName()); + appendAuditLine("VM importée depuis " + selectedFile.getName()); + }, + errorMsg -> showError("Erreur d'import", errorMsg) + ); + } + + @FXML + private void handleExportPDF() { + // Le bouton Export PDF n'est visible que si un labo est sélectionné, + // mais on vérifie par précaution. + if (selectedLab == null) { + return; + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Exporter le rapport PDF"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("PDF Document", "*.pdf") + ); + // Suggestion de nom de fichier basé sur le titre du labo + fileChooser.setInitialFileName(selectedLab.getTitle().replace(" ", "_") + ".pdf"); + + File selectedFile = fileChooser.showSaveDialog( + labListContainer.getScene().getWindow() + ); + + if (selectedFile == null) { + return; // L'utilisateur a annulé + } + + // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() + // Pour l'instant, on appelle le stub métier (déjà présent dans LabController) + onExportLabClicked(selectedLab, selectedFile.getAbsolutePath()); + + } + + @FXML + private void handleStartVM() { + if (selectedVM == null) { + return; + } + + btnStart.setDisable(true); + + onStartVMClicked(selectedVM, + () -> { + btnStart.setDisable(false); + updateControlButtons(VMStatus.RUNNING); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM started successfully"); + appendAuditLine("VM démarrée"); + }, + errorMsg -> { + btnStart.setDisable(false); + showError("Erreur démarrage VM", errorMsg); + } + ); + } + + @FXML + private void handleStopVM() { + if (selectedVM == null) { + return; + } + + btnStop.setDisable(true); + + onStopVMClicked(selectedVM, + () -> { + btnStop.setDisable(false); + updateControlButtons(VMStatus.POWERED_OFF); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM stopped"); + appendAuditLine("VM arrêtée"); + }, + errorMsg -> { + btnStop.setDisable(false); + showError("Erreur arrêt VM", errorMsg); + } + ); + } + + @FXML + private void handleSaveState() { + if (selectedVM == null) { + return; + } + + btnSaveState.setDisable(true); + + onSaveStateClicked(selectedVM, + () -> { + btnSaveState.setDisable(false); + updateControlButtons(VMStatus.SAVED); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM state saved"); + appendAuditLine("État sauvegardé"); + }, + errorMsg -> { + btnSaveState.setDisable(false); + showError("Erreur sauvegarde état", errorMsg); + } + ); + } + + @FXML + private void handleTakeSnapshot() { + if (selectedVM == null) { + return; + } + + javafx.scene.control.TextInputDialog dialog = new javafx.scene.control.TextInputDialog(); + dialog.setTitle("Nouveau snapshot"); + dialog.setHeaderText("Nom du snapshot"); + dialog.setContentText("Nom :"); + + dialog.showAndWait().ifPresent(name -> { + if (name.isBlank()) { + return; + } + + onTakeSnapshotClicked(selectedVM, name, "", + () -> { + tg.cyberlabmanager.model.Snapshot snapshot = new tg.cyberlabmanager.model.Snapshot(); + snapshot.setName(name); + snapshot.setDescription(""); + snapshot.setCreatedAt(java.time.LocalDateTime.now()); + snapshot.setOnline(selectedVM.getStatus() == VMStatus.RUNNING); + + selectedVM.addSnapshot(snapshot); + snapshotCombo.getItems().add(name); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("Snapshot \"" + name + "\" created"); + appendAuditLine("Snapshot \"" + name + "\" créé"); + }, + errorMsg -> showError("Erreur snapshot", errorMsg) + ); + }); + } + + @FXML + private void handleRestoreSnapshot() { + if (selectedVM == null) { + return; + } + + String snapshotName = snapshotCombo.getValue(); + if (snapshotName == null) { + return; + } + + javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirmer la restauration"); + confirm.setHeaderText(null); + confirm.setContentText("Restaurer le snapshot \"" + snapshotName + "\" ?\nL'état actuel de la VM sera perdu."); + + confirm.showAndWait().ifPresent(response -> { + if (response != javafx.scene.control.ButtonType.OK) { + return; + } + + onRestoreSnapshotClicked(selectedVM, snapshotName, + () -> { + appendJournalLine("Snapshot \"" + snapshotName + "\" restored"); + appendAuditLine("Snapshot \"" + snapshotName + "\" restauré"); + }, + errorMsg -> showError("Erreur restauration snapshot", errorMsg) + ); + }); + } + + @FXML +private void handleAddJournalEntry() { + String text = journalInputField.getText(); + if (text == null || text.isBlank()) { + return; + } + + appendJournalLine(text); + journalInputField.clear(); + + // TODO (Rôle 2) : databaseManager.addJournalEntry(selectedLab, selectedVM, text) + if (selectedVM != null) { + try { + // Créer une entrée de journal + tg.cyberlabmanager.model.JournalEntry entry = new tg.cyberlabmanager.model.JournalEntry(); + entry.setContent(text); + entry.setTimestamp(java.time.LocalDateTime.now()); + + // Sauvegarder en base + databaseManager.addJournalEntry(entry, selectedVM.getId()); + + appendJournalLine("✅ Note sauvegardée en base"); + } catch (Exception e) { + appendJournalLine("❌ Erreur sauvegarde: " + e.getMessage()); + showError("Erreur journal", e.getMessage()); + } + } else { + appendJournalLine("⚠️ Aucune VM sélectionnée - note non sauvegardée"); + } +} + @FXML + private void handleToggleAuditLogs() { + auditExpanded = !auditExpanded; + auditArea.setVisible(auditExpanded); + auditArea.setManaged(auditExpanded); + auditArrow.setText(auditExpanded ? "⌄" : "›"); + } + + public void shutdown() { + executor.shutdownNow(); + } + + private List mockVMsForLab(int labId) { + List vms = new ArrayList<>(); + + if (labId == 1) { // APT29 Analysis + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali-APT29"); + vm.setUuid("a1b2c3d4-1111-2222-3333-444455556666"); + vm.setStatus(VMStatus.RUNNING); + vms.add(vm); + } else if (labId == 2) { // Red Team Ops + VirtualMachine vm = new VirtualMachine(); + vm.setName("Metasploitable3"); + vm.setUuid("b2c3d4e5-2222-3333-4444-555566667777"); + vm.setStatus(VMStatus.POWERED_OFF); + vms.add(vm); + } else if (labId == 3) { // Network Traffic Analysis + VirtualMachine vm = new VirtualMachine(); + vm.setName("Wireshark-Station"); + vm.setUuid("f8c9a7b6-3333-4444-5555-666677778888"); + vm.setStatus(VMStatus.RUNNING); + vms.add(vm); + } else if (labId == 4) { // Memory Forensics + VirtualMachine vm = new VirtualMachine(); + vm.setName("REMnux"); + vm.setUuid("c3d4e5f6-4444-5555-6666-777788889999"); + vm.setStatus(VMStatus.SAVED); + vms.add(vm); + } + // labId == 5 (Ransomware Sandbox) → liste vide intentionnellement + return vms; + } + + private void refreshVmTable(List vms) { + vmListContainer.getChildren().clear(); + + if (vms.isEmpty()) { + vmEmptyLabel.setVisible(true); + vmEmptyLabel.setManaged(true); + statTotal.setText("0"); + statRunning.setText("0"); + statSnapshots.setText("0"); + return; + } + + vmEmptyLabel.setVisible(false); + vmEmptyLabel.setManaged(false); + + long runningCount = vms.stream() + .filter(v -> v.getStatus() == VMStatus.RUNNING) + .count(); + long snapCount = vms.stream() + .mapToLong(v -> v.getSnapshots().size()) + .sum(); + + statTotal.setText(String.valueOf(vms.size())); + statRunning.setText(String.valueOf(runningCount)); + statSnapshots.setText(String.valueOf(snapCount)); + + for (VirtualMachine vm : vms) { + String shortUuid = vm.getUuid() != null && vm.getUuid().length() > 8 + ? vm.getUuid().substring(0, 8) + "..." + : vm.getUuid(); + + String statusText = switch (vm.getStatus()) { + case RUNNING -> + "Running"; + case POWERED_OFF -> + "Stopped"; + case SAVED -> + "Saved"; + case PAUSED -> + "Paused"; + default -> + "Unknown"; + }; + + javafx.scene.layout.HBox row = VmTableView.createVmRow( + vm.getName(), shortUuid, statusText, "" + ); + + // Listener clic VM → Jour 3 + row.setOnMouseClicked(e -> onVmRowClicked(vm)); + vmListContainer.getChildren().add(row); + } + } + + private void onLabCardClicked(Lab lab) { + this.selectedLab = lab; + this.selectedVM = null; + + // Mettre à jour le titre + labTitleLabel.setText(lab.getTitle()); + + // Afficher les éléments cachés + btnExportPdf.setVisible(true); + btnExportPdf.setManaged(true); + statsBar.setVisible(true); + statsBar.setManaged(true); + vmTableHeader.setVisible(true); + vmTableHeader.setManaged(true); + + // Remettre le panneau droit en état vide + selectedVmNameLabel.setText(""); + controlsPlaceholder.setVisible(true); + controlsPlaceholder.setManaged(true); + btnStart.setVisible(false); + btnStart.setManaged(false); + btnStop.setVisible(false); + btnStop.setManaged(false); + btnSaveState.setVisible(false); + btnSaveState.setManaged(false); + btnTakeSnapshot.setVisible(false); + btnTakeSnapshot.setManaged(false); + + // Rafraîchir la liste et le tableau + refreshLabList(); + + // TODO (Rôle 2) : remplacer mockVMsForLab par databaseManager.getVMsForLab + refreshVmTable(getVMsForLab(lab.getId())); + } + + private void onVmRowClicked(VirtualMachine vm) { + this.selectedVM = vm; + + selectedVmNameLabel.setText(vm.getName()); + + // Cacher le placeholder "sélectionnez une VM" + controlsPlaceholder.setVisible(false); + controlsPlaceholder.setManaged(false); + + // Activer les bons boutons selon le statut de la VM + updateControlButtons(vm.getStatus()); + } + + private void updateControlButtons(VMStatus status) { + boolean isRunning = status == VMStatus.RUNNING; + boolean isStopped = status == VMStatus.POWERED_OFF || status == VMStatus.SAVED; + + btnStart.setVisible(isStopped); + btnStart.setManaged(isStopped); + btnStop.setVisible(isRunning); + btnStop.setManaged(isRunning); + btnSaveState.setVisible(isRunning); + btnSaveState.setManaged(isRunning); + btnTakeSnapshot.setVisible(true); + btnTakeSnapshot.setManaged(true); + } + + private void appendJournalLine(String text) { + String time = java.time.LocalTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); + journalArea.appendText("[" + time + "] " + text + "\n"); + } + + private void showError(String title, String msg) { + javafx.scene.control.Alert alert = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(msg); + alert.showAndWait(); + } + + private void appendAuditLine(String action) { + String time = java.time.LocalTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); + String vmName = selectedVM != null ? selectedVM.getName() : "?"; + auditArea.appendText("[" + time + "] [" + vmName + "] " + action + "\n"); + } +} diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java b/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java index 99ad3cf..7b54baa 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java @@ -1,5 +1,7 @@ package tg.cyberlabmanager.hypervisor; +import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.VMDescriptor; import tg.cyberlabmanager.model.VMStatus; import java.util.List; @@ -8,13 +10,21 @@ * Contrat minimal pour piloter un hyperviseur depuis l'application. */ public interface IHypervisor { + /** + * Applique une nouvelle configuration à l'hyperviseur déjà instancié. + * + * @param config nouvelle configuration applicative + * @throws HypervisorException si la configuration hyperviseur est invalide + */ + void applyConfig(AppConfig config) throws HypervisorException; + /** * Liste les machines virtuelles disponibles dans l'hyperviseur. * - * @return liste de couples ou tableaux de donnees representant les VM - * @throws HypervisorException si la recuperation echoue + * @return descripteurs des machines virtuelles disponibles + * @throws HypervisorException si la recuperation échoue */ - List listVMs() throws HypervisorException; + List listVMs() throws HypervisorException; /** * Retourne le statut courant d'une machine virtuelle. diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java index 9851744..3ae1587 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java @@ -1,16 +1,16 @@ package tg.cyberlabmanager.hypervisor; /** - * Exception specifique aux commandes VirtualBox executees via VBoxManage. + * Exception specifique aux commandes VirtualBox exécutées via VBoxManage. */ public class VBoxException extends HypervisorException { - /** Code de sortie retourne par VBoxManage. */ + /** Code de sortie retourné par VBoxManage. */ private final int exitCode; - /** Sortie standard ou erreur retournee par VBoxManage. */ + /** Sortie standard ou erreur retournée par VBoxManage. */ private final String output; /** - * Cree une exception VirtualBox avec le code de sortie et la sortie texte. + * Crée une exception VirtualBox avec le code de sortie et la sortie texte. * * @param message message decrivant l'erreur * @param exitCode code de sortie de VBoxManage diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java index b8f432d..e1ca5db 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java @@ -1,58 +1,436 @@ package tg.cyberlabmanager.hypervisor; +import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.VMDescriptor; import tg.cyberlabmanager.model.VMStatus; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** - * Implementation de {@link IHypervisor} pour VirtualBox via VBoxManage. + * Implémentation de {@link IHypervisor} pour VirtualBox via VBoxManage. + * + *

À la construction, l'orchestrateur résout le chemin de VBoxManage en + * trois étapes :

+ *
    + *
  1. Utilisation du chemin présent dans {@link AppConfig} s'il est valide.
  2. + *
  3. Auto-détection dans les emplacements standards de l'OS.
  4. + *
  5. Si introuvable, {@link #vboxManagePath} reste {@code null} ; toute + * opération lève une {@link HypervisorException} que l'UI intercepte + * pour inviter l'utilisateur à saisir le chemin manuellement.
  6. + *
*/ public class VBoxOrchestrator implements IHypervisor { + + /** Regex pour analyser une ligne de {@code VBoxManage list vms} : {@code "Nom" {uuid}}. */ + private static final Pattern LIST_VMS_PATTERN = + Pattern.compile( + "^\"([^\"]+)\"\\s+\\{([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-" + + "[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\\}\\s*$"); + + /** Chemin résolu vers VBoxManage, ou {@code null} si introuvable. */ + private String vboxManagePath; + + /** + * Crée un orchestrateur VirtualBox. + * + *

Tente de résoudre VBoxManage depuis la configuration fournie, + * puis par auto-détection si nécessaire.

+ * + * @param config configuration de l'application (peut être {@code null}) + */ + public VBoxOrchestrator(AppConfig config) { + this.vboxManagePath = resolveVboxManagePath(config); + } + /** - * Cree un orchestrateur VirtualBox. + * Indique si VBoxManage a été résolu et est prêt à l'emploi. + * + *

L'UI appelle cette méthode au démarrage pour décider si elle doit + * afficher une boîte de dialogue de configuration.

+ * + * @return {@code true} si VBoxManage est disponible */ - public VBoxOrchestrator() { + public boolean isReady() { + return vboxManagePath != null; } - /** {@inheritDoc} */ + /** + * Applique une nouvelle configuration à l'orchestrateur déjà instancié. + * + *

Cette méthode permet au contrôleur de synchroniser l'orchestrateur + * avec les paramètres courants sans exposer les détails VirtualBox à l'UI.

+ * + * @param config nouvelle configuration applicative + * @throws HypervisorException si le chemin configuré est invalide + */ @Override - public List listVMs() throws HypervisorException { - throw new UnsupportedOperationException("Not implemented yet"); + public void applyConfig(AppConfig config) throws HypervisorException { + String configured = (config != null) ? config.getVboxManagePath() : null; + + if (configured == null || configured.isBlank()) { + this.vboxManagePath = detectVBoxManage(); + return; + } + + setVboxManagePath(configured); + } + + /** + * Met à jour le chemin de VBoxManage après saisie par l'utilisateur. + * + *

Utilisée par {@link #applyConfig(AppConfig)} pour appliquer un chemin + * saisi dans les paramètres.

+ * + * @param path chemin vers l'exécutable VBoxManage + * @throws HypervisorException si le chemin est vide ou non exécutable + */ + public void setVboxManagePath(String path) throws HypervisorException { + if (path == null || path.isBlank()) { + throw new HypervisorException("Le chemin de VBoxManage ne peut pas être vide."); + } + if (!isVBoxManageExecutable(path)) { + throw new HypervisorException( + "Impossible d'exécuter VBoxManage au chemin indiqué : " + path); + } + this.vboxManagePath = path; + } + + /** + * {@inheritDoc} + * + *

Exécute {@code VBoxManage list vms} et retourne les paires + * {@code [nom, uuid]} pour chaque VM trouvée.

+ */ + @Override + public List listVMs() throws HypervisorException { + return parseListVMsOutput(runCommand("list", "vms")); + } + + static List parseListVMsOutput(String output) { + List vms = new ArrayList<>(); + + if (output != null) { + for (String line : output.split("\\R")) { + Matcher matcher = LIST_VMS_PATTERN.matcher(line.trim()); + if (matcher.matches()) { + vms.add(new VMDescriptor(matcher.group(1), matcher.group(2))); + } + } + } + + return vms; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + *

Exécute {@code VBoxManage showvminfo --machinereadable} et + * extrait le champ {@code VMState}.

+ */ @Override public VMStatus getStatus(String uuid) throws HypervisorException { - throw new UnsupportedOperationException("Not implemented yet"); + String output = runCommand("showvminfo", uuid, "--machinereadable"); + + return parseShowVmInfoOutput(output); + } + + /** + * Extrait le statut d'une VM depuis la sortie de {@code VBoxManage showvminfo --machinereadable}. + * + * @param output sortie brute de la commande, peut être {@code null} + * @return statut extrait, ou {@link VMStatus#UNKNOWN} si absent ou non reconnu + */ + static VMStatus parseShowVmInfoOutput(String output) { + if (output == null) return VMStatus.UNKNOWN; + for (String line : output.split("\\R")) { + if (line.startsWith("VMState=")) { + String state = line.substring("VMState=".length()) + .replace("\"", "").trim(); + return parseVMStatus(state); + } + } + return VMStatus.UNKNOWN; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + *

Exécute {@code VBoxManage startvm --type headless}.

+ */ @Override public void startVM(String uuid) throws HypervisorException { - throw new UnsupportedOperationException("Not implemented yet"); + runCommand("startvm", uuid, "--type", "headless"); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + *

Exécute {@code VBoxManage controlvm poweroff}.

+ */ @Override public void stopVM(String uuid) throws HypervisorException { - throw new UnsupportedOperationException("Not implemented yet"); + runCommand("controlvm", uuid, "poweroff"); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + *

Exécute {@code VBoxManage controlvm savestate}.

+ */ @Override public void saveState(String uuid) throws HypervisorException { - throw new UnsupportedOperationException("Not implemented yet"); + runCommand("controlvm", uuid, "savestate"); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + *

Exécute {@code VBoxManage snapshot take } + * avec {@code --description} si une description est fournie.

+ */ @Override - public void takeSnapshot(String uuid, String name, String description) throws HypervisorException { - throw new UnsupportedOperationException("Not implemented yet"); + public void takeSnapshot(String uuid, String name, String description) + throws HypervisorException { + if (description != null && !description.isBlank()) { + runCommand("snapshot", uuid, "take", name, "--description", description); + } else { + runCommand("snapshot", uuid, "take", name); + } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + *

Exécute {@code VBoxManage snapshot restore }.

+ */ @Override public void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException { - throw new UnsupportedOperationException("Not implemented yet"); + runCommand("snapshot", uuid, "restore", snapshotName); + } + + /** + * Exécute une commande VBoxManage et retourne la sortie standard. + * + * @param args arguments passés à VBoxManage (sans le chemin du binaire) + * @return sortie texte produite par VBoxManage + * @throws HypervisorException si VBoxManage n'est pas configuré, si la + * commande échoue ou si une erreur I/O survient + */ + private String runCommand(String... args) throws HypervisorException { + String executable = requireConfigured(); + + String[] command = new String[args.length + 1]; + command[0] = executable; + System.arraycopy(args, 0, command, 1, args.length); + + try { + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); + Process process = pb.start(); + + // Thread de lecture pour ne pas bloquer + CompletableFuture outputFuture = CompletableFuture.supplyAsync(() -> { + try { + return new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + return ""; + } + }); + + // waitFor() avec timeout + boolean completed = process.waitFor(60, TimeUnit.SECONDS); + + // Renvoie exception après les 60 secondes accordées + if (!completed) { + process.destroyForcibly(); + throw new HypervisorException("La commande a expiré (Timeout)."); + } + + String output = outputFuture.get(1, TimeUnit.SECONDS); // déjà terminé, récupération immédiate + + // Commande échouée + int exitCode = process.exitValue(); + if (exitCode != 0) { + throw new VBoxException( + "VBoxManage a retourné une erreur (code " + exitCode + ")", + exitCode, + output + ); + } + + return output; + + } catch (IOException e) { + throw new HypervisorException("Erreur I/O lors de l'exécution de VBoxManage.", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HypervisorException("Exécution de VBoxManage interrompue.", e); + } catch (ExecutionException e) { + throw new HypervisorException( + "Erreur lors de la lecture de la sortie de VBoxManage.", e.getCause()); + } catch (TimeoutException e) { + // Ne dois pas arriver (process déjà terminé), mais on couvre le cas qd même + throw new HypervisorException( + "Délai dépassé lors de la récupération de la sortie de VBoxManage.", e); + } + } + + /** + * Vérifie que VBoxManage est configuré avant toute commande. + * + * @return chemin VBoxManage prêt à être utilisé + * @throws HypervisorException si le chemin n'a pas été résolu + */ + private String requireConfigured() throws HypervisorException { + if (vboxManagePath == null) { + throw new HypervisorException( + "VBoxManage est introuvable. " + + "Veuillez configurer son chemin dans les paramètres de l'application."); + } + return vboxManagePath; + } + + /** + * Parcourt les emplacements standards de l'OS pour trouver VBoxManage. + * + * @return premier chemin valide trouvé, ou {@code null} + */ + private static String detectVBoxManage() { + String os = System.getProperty("os.name", "").toLowerCase(); + + for (String candidate : buildCandidatePaths(os)) { + if (isVBoxManageExecutable(candidate)) { + return candidate; + } + } + + return null; + } + + /** + * Résout le chemin d'accès à VBoxManage depuis la configuration fournie, + * ou par auto-détection si le chemin configuré est absent ou invalide. + * + * @param config configuration applicative, peut être {@code null} + * @return chemin résolu vers VBoxManage, ou {@code null} si introuvable + */ + private static String resolveVboxManagePath(AppConfig config) { + String configured = (config != null) ? config.getVboxManagePath() : null; + + if (configured != null && !configured.isBlank() && isVBoxManageExecutable(configured)) { + return configured; + } + + return detectVBoxManage(); + } + + /** + * Construit la liste des chemins candidats selon l'OS. + * + * @param osName nom de l'OS en minuscules + * @return chemins à tester, du plus probable au moins probable + */ + private static List buildCandidatePaths(String osName) { + List candidates = new ArrayList<>(); + + if (osName.contains("win")) { + // Commande nue d'abord (si VirtualBox est dans le PATH Windows) + candidates.add("VBoxManage.exe"); + candidates.add("C:\\Program Files\\Oracle\\VirtualBox\\VBoxManage.exe"); + candidates.add("C:\\Program Files (x86)\\Oracle\\VirtualBox\\VBoxManage.exe"); + } else { + // Linux / macOS (PATH d'abord, puis chemins typiques) + candidates.add("VBoxManage"); + candidates.add("vboxmanage"); + candidates.add("/usr/bin/VBoxManage"); + candidates.add("/usr/local/bin/VBoxManage"); + candidates.add("/opt/VirtualBox/VBoxManage"); + } + + return candidates; + } + + /** + * Vérifie qu'un chemin pointe vers un VBoxManage fonctionnel en exécutant + * {@code --version}. + * + *

La sortie est drainée pour éviter tout blocage du sous-processus.

+ * + * @param path chemin à tester + * @return {@code true} si VBoxManage répond avec un code de sortie 0 + */ + private static boolean isVBoxManageExecutable(String path) { + if (path == null || path.isBlank()) { + return false; + } + try { + ProcessBuilder pb = new ProcessBuilder(path, "--version"); + pb.redirectErrorStream(true); + Process process = pb.start(); + // Drainer la sortie pour ne pas bloquer le processus + process.getInputStream().transferTo(OutputStream.nullOutputStream()); + // Pour éviter les freezes , on attend 5 secondes ou on tue le process + boolean complete = process.waitFor(5, TimeUnit.SECONDS); + if (!complete) { + process.destroyForcibly(); + return false; + } + return process.exitValue() == 0; + + } catch (Exception e) { + return false; + } + } + + /** + * Convertit un état VirtualBox brut en {@link VMStatus}. + * + * @param state valeur du champ {@code VMState} dans la sortie machinereadable + * @return statut correspondant, ou {@link VMStatus#UNKNOWN} si non reconnu + */ + static VMStatus parseVMStatus(String state) { + if (state == null) { + return VMStatus.UNKNOWN; + } + + // Normalisation pour éviter les problèmes de casse + String s = state.toLowerCase(); + + return switch (s) { + // ÉTATS ACTIFS + case "running", + "resuming" -> VMStatus.RUNNING; + + // En pause + case "paused" -> VMStatus.PAUSED; + + // ÉTATS ÉTEINTS (ou en transition vers éteint) + case "poweroff", + "poweredoff", + "aborted", + "stopping" -> VMStatus.POWERED_OFF; + + // ÉTATS SAUVEGARDÉS (Hibernation) + case "saved", + "saving", + "suspended" -> VMStatus.SAVED; + + // "starting" : La VM n'est pas encore utilisable. + case "starting" -> VMStatus.UNKNOWN; + + // Tout le reste + // Inclut: restoring, snapshotting, teleporting, etc. + default -> VMStatus.UNKNOWN; + }; } } diff --git a/src/main/java/tg/cyberlabmanager/model/VMDescriptor.java b/src/main/java/tg/cyberlabmanager/model/VMDescriptor.java new file mode 100644 index 0000000..e6232c1 --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/model/VMDescriptor.java @@ -0,0 +1,13 @@ +package tg.cyberlabmanager.model; + +/** + * Descripteur minimal d'une machine virtuelle découverte auprès de VirtualBox. + * + *

Ce record sert à transporter l'identité affichable d'une VM sans charger + * le modèle métier complet.

+ * + * @param name nom de la machine virtuelle + * @param uuid UUID VirtualBox de la machine virtuelle + */ +public record VMDescriptor(String name, String uuid) { +} diff --git a/src/main/java/tg/cyberlabmanager/ui/LabListView.java b/src/main/java/tg/cyberlabmanager/ui/LabListView.java index 930ea8f..cc6ee36 100644 --- a/src/main/java/tg/cyberlabmanager/ui/LabListView.java +++ b/src/main/java/tg/cyberlabmanager/ui/LabListView.java @@ -3,6 +3,7 @@ import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.layout.VBox; +import tg.cyberlabmanager.ui.LabListView; /** * Vue chargee d'afficher la liste des laboratoires et leurs cartes. @@ -32,4 +33,26 @@ public static VBox createLabCard(String name, String badgeText, String badgeStyl card.getChildren().addAll(title, badge); return card; } + + /** + * Crée la carte "VMs sans labo" affichée en bas de la sidebar. + * + * @param count nombre de VMs orphelines + * @param selected si la carte est actuellement sélectionnée + * @return le VBox pré-configuré avec les styles orphelin + */ + public static VBox createOrphanCard(int count, boolean selected) { + VBox card = new VBox(5); + card.setPadding(new Insets(10, 12, 10, 12)); + card.getStyleClass().add(selected ? "lab-card-orphan-selected" : "lab-card-orphan"); + + Label title = new Label("VMs sans labo"); + title.getStyleClass().add("lab-card-title-orphan"); + + Label badge = new Label(count + " VM" + (count > 1 ? "s" : "")); + badge.getStyleClass().addAll("badge", "badge-orphan"); + + card.getChildren().addAll(title, badge); + return card; + } } diff --git a/src/main/java/tg/cyberlabmanager/ui/VmTableView.java b/src/main/java/tg/cyberlabmanager/ui/VmTableView.java index cd0dcce..ce05837 100644 --- a/src/main/java/tg/cyberlabmanager/ui/VmTableView.java +++ b/src/main/java/tg/cyberlabmanager/ui/VmTableView.java @@ -35,7 +35,14 @@ public static HBox createVmRow(String name, String uuid, String status, String o Label lStatus = new Label(status); lStatus.setPrefWidth(150); - lStatus.getStyleClass().addAll("status-badge", "status-running"); + String statusCss = switch (status == null ? "" : status.toLowerCase()) { + case "running" -> "status-running"; + case "stopped", "powered_off" -> "status-stopped"; + case "saved" -> "status-saved"; + case "paused" -> "status-paused"; + default -> "status-unknown"; + }; + lStatus.getStyleClass().addAll("status-badge", statusCss); Label lOs = new Label(os); lOs.getStyleClass().add("vm-os"); diff --git a/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css index 2651f8a..967ba7c 100644 --- a/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css +++ b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css @@ -647,4 +647,64 @@ -fx-pref-width: 1; -fx-min-width: 1; -fx-max-width: 1; +} + +/* ── CARTE ORPHELINES (VMs sans labo) ──────────────────────────────────── */ +.lab-card-orphan { + -fx-background-color: #1E1818; + -fx-background-radius: 5; + -fx-border-color: #78350F; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-cursor: hand; + -fx-padding: 10 12 10 12; +} + +.lab-card-orphan:hover { + -fx-background-color: #2A1F10; + -fx-border-color: #B45309; +} + +.lab-card-orphan-selected { + -fx-background-color: #2A1F10; + -fx-border-color: #F59E0B; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-padding: 10 12 10 12; +} + +.lab-card-title-orphan { + -fx-text-fill: #FCD34D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 13; + -fx-font-weight: 500; + -fx-wrap-text: true; +} + +/* Badge orphelines */ +.badge-orphan { + -fx-background-color: rgba(120, 53, 15, 0.6); + -fx-text-fill: #FCD34D; + -fx-border-color: #B45309; +} + +/* ── BOUTON ASSIGNER AU LABO ────────────────────────────────────────────── */ +.btn-assign-lab { + -fx-background-color: rgba(76, 29, 149, 0.35); + -fx-text-fill: #C4B5FD; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; + -fx-border-color: #6D28D9; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-pref-height: 38; +} + +.btn-assign-lab:hover { + -fx-background-color: rgba(76, 29, 149, 0.65); + -fx-text-fill: #EDE9FE; + -fx-border-color: #8B5CF6; } \ No newline at end of file diff --git a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml index 2f307af..25544af 100644 --- a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml +++ b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml @@ -49,8 +49,16 @@ +