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/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 930b3ae..873c65e 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; /* @@ -26,7 +26,7 @@ opens tg.cyberlabmanager.controller to javafx.fxml; opens tg.cyberlabmanager.ui to javafx.fxml; - // ── Exports publics ────────────────────────────────────────────────── + // Exports publics exports tg.cyberlabmanager.app; exports tg.cyberlabmanager.controller; exports tg.cyberlabmanager.model; diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 7228bab..bc81b5a 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -1,6 +1,7 @@ package tg.cyberlabmanager.controller; import tg.cyberlabmanager.data.DatabaseManager; +import tg.cyberlabmanager.hypervisor.HypervisorException; import tg.cyberlabmanager.hypervisor.IHypervisor; import tg.cyberlabmanager.model.AppConfig; import tg.cyberlabmanager.model.Lab; @@ -12,7 +13,7 @@ import java.util.List; /** - * Controleur principal coordonnant l'interface, la persistance et l'hyperviseur. + * Contrôleur principal coordonnant l'interface, la persistance et l'hyperviseur. */ public class LabController { private IHypervisor hypervisor; @@ -23,13 +24,13 @@ public class LabController { private Label statusLabel; /** - * Cree un controleur vide utilise par JavaFX lors du chargement FXML. + * Cree un contrôleur vide utilise par JavaFX lors du chargement FXML. */ public LabController() { } /** - * Cree un controleur avec ses dependances injectees. + * Cree un contrôleur avec ses dépendances injectées. * * @param hypervisor service de pilotage de l'hyperviseur * @param databaseManager service de persistance @@ -49,7 +50,7 @@ private void initialize() { } /** - * Declenche le demarrage d'une machine virtuelle. + * Déclenche le démarrage d'une machine virtuelle. * * @param vm machine virtuelle cible */ @@ -58,7 +59,7 @@ public void onStartVMClicked(VirtualMachine vm) { } /** - * Declenche l'arret d'une machine virtuelle. + * Déclenche l'arrêt d'une machine virtuelle. * * @param vm machine virtuelle cible */ @@ -67,7 +68,7 @@ public void onStopVMClicked(VirtualMachine vm) { } /** - * Declenche la sauvegarde d'etat d'une machine virtuelle. + * Déclenche la sauvegarde d'etat d'une machine virtuelle. * * @param vm machine virtuelle cible */ @@ -76,7 +77,7 @@ public void onSaveStateClicked(VirtualMachine vm) { } /** - * Declenche la creation d'un snapshot pour une machine virtuelle. + * Déclenche la creation d'un snapshot pour une machine virtuelle. * * @param vm machine virtuelle cible * @param name nom du snapshot @@ -87,7 +88,7 @@ public void onTakeSnapshotClicked(VirtualMachine vm, String name, String descrip } /** - * Declenche la restauration d'un snapshot pour une machine virtuelle. + * Déclenche la restauration d'un snapshot pour une machine virtuelle. * * @param vm machine virtuelle cible * @param snapshotName nom ou identifiant du snapshot @@ -158,11 +159,16 @@ public List getOrphanVMs() { } /** - * Persiste la configuration mise a jour. + * Persiste la configuration mise à jour dans la base et dans l'appel. * * @param newConfig nouvelle configuration applicative */ - public void onConfigUpdated(AppConfig newConfig) { + public void onConfigUpdated(AppConfig newConfig) throws HypervisorException { + try { + hypervisor.applyConfig(newConfig); + } catch (HypervisorException e) { + throw new RuntimeException(e); + } databaseManager.saveConfig(newConfig); } } 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/test/java/tg/cyberlabmanager/hypervisor/VBoxExceptionTest.java b/src/test/java/tg/cyberlabmanager/hypervisor/VBoxExceptionTest.java new file mode 100644 index 0000000..8f3227a --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/hypervisor/VBoxExceptionTest.java @@ -0,0 +1,22 @@ +package tg.cyberlabmanager.hypervisor; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class VBoxExceptionTest { + @Test + void shouldStoreExitCodeAndOutput() { + VBoxException exception = new VBoxException("Erreur VBoxManage", 1, "VBOX_E_INVALID_OBJECT_STATE"); + + assertEquals(1, exception.getExitCode()); + assertEquals("VBOX_E_INVALID_OBJECT_STATE", exception.getOutput()); + } + + @Test + void shouldBeInstanceOfHypervisorException() { + VBoxException exception = new VBoxException("message", 2, "output"); + + assertEquals(HypervisorException.class, exception.getClass().getSuperclass()); + } +} \ No newline at end of file diff --git a/src/test/java/tg/cyberlabmanager/hypervisor/VBoxOrchestratorTest.java b/src/test/java/tg/cyberlabmanager/hypervisor/VBoxOrchestratorTest.java new file mode 100644 index 0000000..b2dcb07 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/hypervisor/VBoxOrchestratorTest.java @@ -0,0 +1,143 @@ +package tg.cyberlabmanager.hypervisor; + +import org.junit.jupiter.api.Test; +import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.VMDescriptor; +import tg.cyberlabmanager.model.VMStatus; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class VBoxOrchestratorTest { + + @Test + void shouldParseVBoxManageListVMsOutputLine() { + List vms = VBoxOrchestrator.parseListVMsOutput( + "\"Cyberlab\" {b9c987d2-cc84-40c5-b523-748d401e7d7e}"); + + assertEquals(List.of( + new VMDescriptor("Cyberlab", "b9c987d2-cc84-40c5-b523-748d401e7d7e") + ), vms); + } + + @Test + void shouldParseSeveralVBoxManageListVMsOutputLines() { + List vms = VBoxOrchestrator.parseListVMsOutput(""" + "Cyberlab" {b9c987d2-cc84-40c5-b523-748d401e7d7e} + "Windows 11 Lab" {1A2B3C4D-1234-4567-89AB-ABCDEF123456} + """); + + assertEquals(List.of( + new VMDescriptor("Cyberlab", "b9c987d2-cc84-40c5-b523-748d401e7d7e"), + new VMDescriptor("Windows 11 Lab", "1A2B3C4D-1234-4567-89AB-ABCDEF123456") + ), vms); + } + + @Test + void shouldReturnEmptyListForBlankVBoxManageListVMsOutput() { + assertEquals(List.of(), VBoxOrchestrator.parseListVMsOutput("")); + } + + @Test + void shouldReturnEmptyListForNullVBoxManageListVMsOutput() { + assertEquals(List.of(), VBoxOrchestrator.parseListVMsOutput(null)); + } + + @Test + void shouldIgnoreInvalidVBoxManageListVMsOutputLines() { + List vms = VBoxOrchestrator.parseListVMsOutput(""" + invalid line + "Missing uuid" + "Bad uuid" {abc---test} + "Valid VM" {b9c987d2-cc84-40c5-b523-748d401e7d7e} + """); + + assertEquals(List.of( + new VMDescriptor("Valid VM", "b9c987d2-cc84-40c5-b523-748d401e7d7e") + ), vms); + } + + @Test + void shouldParseVmNamesContainingSpaces() { + List vms = VBoxOrchestrator.parseListVMsOutput( + "\"Windows 11 Analysis Lab\" {b9c987d2-cc84-40c5-b523-748d401e7d7e}"); + + assertEquals(List.of( + new VMDescriptor("Windows 11 Analysis Lab", "b9c987d2-cc84-40c5-b523-748d401e7d7e") + ), vms); + } + + @Test + void shouldMapRunningVBoxStatesToRunningStatus() { + assertEquals(VMStatus.RUNNING, VBoxOrchestrator.parseVMStatus("running")); + assertEquals(VMStatus.RUNNING, VBoxOrchestrator.parseVMStatus("resuming")); + } + + @Test + void shouldMapPoweredOffVBoxStatesToPoweredOffStatus() { + assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("poweroff")); + assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("poweredoff")); + assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("aborted")); + assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("stopping")); + } + + @Test + void shouldMapSavedVBoxStatesToSavedStatus() { + assertEquals(VMStatus.SAVED, VBoxOrchestrator.parseVMStatus("saved")); + assertEquals(VMStatus.SAVED, VBoxOrchestrator.parseVMStatus("saving")); + assertEquals(VMStatus.SAVED, VBoxOrchestrator.parseVMStatus("suspended")); + } + + @Test + void shouldMapPausedVBoxStatesToPausedStatus() { + assertEquals(VMStatus.PAUSED, VBoxOrchestrator.parseVMStatus("paused")); + } + + @Test + void shouldMapUnknownVBoxStatesToUnknownStatus() { + assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseVMStatus(null)); + assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseVMStatus("starting")); + assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseVMStatus("teleporting")); + assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseVMStatus("unexpected")); + } + + @Test + void shouldIgnoreVBoxStatusCase() { + assertEquals(VMStatus.RUNNING, VBoxOrchestrator.parseVMStatus("RUNNING")); + assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("PowerOff")); + assertEquals(VMStatus.SAVED, VBoxOrchestrator.parseVMStatus("Saved")); + } + + @Test + void shouldNotBeReadyWhenVBoxManagePathIsNotConfigured() { + VBoxOrchestrator orchestrator = new VBoxOrchestrator(new AppConfig()); + // AppConfig vide → vboxManagePath null après résolution + assertFalse(orchestrator.isReady()); + } + + @Test + void shouldParseRunningStatusFromShowVmInfoOutput() { + String output = """ + name="Cyberlab" + VMState="running" + memory=2048 + """; + assertEquals(VMStatus.RUNNING, VBoxOrchestrator.parseShowVmInfoOutput(output)); + } + + @Test + void shouldReturnUnknownStatusWhenVMStateLineIsAbsent() { + String output = """ + name="Cyberlab" + memory=2048 + """; + assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseShowVmInfoOutput(output)); + } + + @Test + void shouldReturnUnknownStatusForNullShowVmInfoOutput() { + assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseShowVmInfoOutput(null)); + } +}