+ * 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 :
+ *
+ *
Utilisation du chemin présent dans {@link AppConfig} s'il est valide.
+ *
Auto-détection dans les emplacements standards de l'OS.
+ *
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.
+ *
*/
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}
+ *
+ *