"""Interface graphique PySide6 : diagnostic réseau AD + jointure de domaine.""" from __future__ import annotations import platform import socket from PySide6.QtCore import Qt from PySide6.QtGui import QColor from PySide6.QtWidgets import ( QApplication, QCheckBox, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QMainWindow, QMessageBox, QPlainTextEdit, QPushButton, QTabWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, ) from . import __version__, diagnostics, privileges from .backend import get_backend from .backend.linux_realmd import REMOVABLE_PACKAGES from .worker import DiagnosticsWorker, InstallPrerequisitesWorker, JoinWorker, LeaveWorker DEFAULT_DOMAIN = "h3adm.lan" _STATUS_COLORS = { diagnostics.Status.OK: QColor("#1e7e34"), diagnostics.Status.WARN: QColor("#b8860b"), diagnostics.Status.FAIL: QColor("#c0392b"), } class DiagnosticsTab(QWidget): def __init__(self, parent=None): super().__init__(parent) self._worker: DiagnosticsWorker | None = None self._results: list[diagnostics.CheckResult] = [] layout = QVBoxLayout(self) form = QFormLayout() self.domain_edit = QLineEdit(DEFAULT_DOMAIN) form.addRow("Domaine :", self.domain_edit) layout.addLayout(form) buttons = QHBoxLayout() self.run_button = QPushButton("Lancer le diagnostic") self.run_button.clicked.connect(self._start_diagnostics) buttons.addWidget(self.run_button) self.copy_button = QPushButton("Copier le rapport") self.copy_button.clicked.connect(self._copy_report) self.copy_button.setEnabled(False) buttons.addWidget(self.copy_button) buttons.addStretch() layout.addLayout(buttons) self.tree = QTreeWidget() self.tree.setHeaderLabels(["Vérification", "Statut", "Détail"]) self.tree.setColumnWidth(0, 260) self.tree.setColumnWidth(1, 130) layout.addWidget(self.tree) self.summary_label = QLabel("") layout.addWidget(self.summary_label) def _start_diagnostics(self) -> None: domain = self.domain_edit.text().strip() if not domain: QMessageBox.warning(self, "Domaine manquant", "Veuillez saisir un domaine à diagnostiquer.") return self.tree.clear() self._results = [] self.run_button.setEnabled(False) self.copy_button.setEnabled(False) self.summary_label.setText("Diagnostic en cours...") self._worker = DiagnosticsWorker(domain) self._worker.check_ready.connect(self._on_check_ready) self._worker.finished_all.connect(self._on_finished) self._worker.start() def _on_check_ready(self, result: diagnostics.CheckResult) -> None: self._results.append(result) item = QTreeWidgetItem([result.name, result.status.value, result.detail]) color = _STATUS_COLORS.get(result.status) if color: item.setForeground(1, color) self.tree.addTopLevelItem(item) def _on_finished(self) -> None: self.run_button.setEnabled(True) self.copy_button.setEnabled(True) ok = sum(1 for r in self._results if r.status == diagnostics.Status.OK) warn = sum(1 for r in self._results if r.status == diagnostics.Status.WARN) fail = sum(1 for r in self._results if r.status == diagnostics.Status.FAIL) self.summary_label.setText(f"Terminé : {ok} OK, {warn} avertissement(s), {fail} échec(s).") def _copy_report(self) -> None: lines = [f"Diagnostic réseau pour le domaine {self.domain_edit.text().strip()}", ""] for r in self._results: lines.append(f"[{r.status.value}] {r.name} — {r.detail}") QApplication.clipboard().setText("\n".join(lines)) class JoinTab(QWidget): def __init__(self, parent=None): super().__init__(parent) self._worker: JoinWorker | None = None self._leave_worker: LeaveWorker | None = None self.backend = get_backend() layout = QVBoxLayout(self) form = QFormLayout() self.domain_edit = QLineEdit(DEFAULT_DOMAIN) self.username_edit = QLineEdit() self.username_edit.setPlaceholderText("ex : administrateur@h3adm.lan") self.password_edit = QLineEdit() self.password_edit.setEchoMode(QLineEdit.EchoMode.Password) self.ou_edit = QLineEdit() self.ou_edit.setPlaceholderText("optionnel, ex : OU=Postes,DC=h3adm,DC=lan") # Le nom de machine ne se change pas depuis cette fenetre : sous Linux # `realm join` reprend le hostname systeme, et sous Windows le renommage # releve des parametres systeme. Le champ reste affiche, en lecture # seule, pour montrer le nom sous lequel le poste apparaitra. self.computer_name_edit = QLineEdit(socket.gethostname()) self.computer_name_edit.setEnabled(False) self.computer_name_edit.setToolTip( "Le nom de la machine ne peut pas être modifié depuis cette fenêtre.\n" + ( "Renommez le poste depuis les paramètres système de Windows, puis redémarrez." if platform.system() == "Windows" else "Changez le nom d'hôte avec : sudo hostnamectl set-hostname " ) ) form.addRow("Domaine :", self.domain_edit) form.addRow("Utilisateur :", self.username_edit) form.addRow("Mot de passe :", self.password_edit) form.addRow("Unité d'organisation :", self.ou_edit) form.addRow("Nom de la machine :", self.computer_name_edit) self.remove_packages_check = QCheckBox( "Désinstaller aussi les paquets système du domaine en quittant" ) self.remove_packages_check.setVisible(platform.system() != "Windows") self.remove_packages_check.setToolTip( "Retire " + ", ".join(REMOVABLE_PACKAGES) + ".\n" "packagekit est conservé : il sert aussi au gestionnaire de logiciels." ) layout.addLayout(form) layout.addWidget(self.remove_packages_check) self.prereq_label = QLabel("") self.prereq_label.setWordWrap(True) self.prereq_label.setVisible(False) layout.addWidget(self.prereq_label) buttons = QHBoxLayout() self.join_button = QPushButton("Rejoindre le domaine") self.join_button.clicked.connect(self._start_join) buttons.addWidget(self.join_button) self.install_button = QPushButton("Installer les prérequis") self.install_button.clicked.connect(self._start_install) self.install_button.setVisible(False) buttons.addWidget(self.install_button) self.leave_button = QPushButton("Quitter le domaine") self.leave_button.clicked.connect(self._start_leave) buttons.addWidget(self.leave_button) buttons.addStretch() layout.addLayout(buttons) self.log = QPlainTextEdit() self.log.setReadOnly(True) layout.addWidget(self.log) self._refresh_backend_state() def _refresh_backend_state(self) -> None: """Met à jour l'état des boutons selon les privilèges et la disponibilité du backend.""" elevated = privileges.is_elevated() available, reason = self.backend.is_available() self.install_button.setVisible(not available and self.backend.can_install_prerequisites()) self.install_button.setEnabled(elevated) if not elevated: self.install_button.setToolTip( "Droits insuffisants pour installer les prérequis.\n" + privileges.elevation_hint() ) else: self.install_button.setToolTip("") self.prereq_label.setVisible(not available) if not available: self.prereq_label.setText("⚠ " + reason) if not elevated: self.join_button.setEnabled(False) self.join_button.setToolTip( "Droits insuffisants pour rejoindre le domaine.\n" + privileges.elevation_hint() ) elif not available: self.join_button.setEnabled(False) self.join_button.setToolTip("Prérequis manquants — installez-les d'abord.") else: self.join_button.setEnabled(True) self.join_button.setToolTip("") can_leave = self.backend.can_leave() self.leave_button.setVisible(can_leave) self.remove_packages_check.setVisible(can_leave and platform.system() != "Windows") if not elevated: self.leave_button.setEnabled(False) self.leave_button.setToolTip( "Droits insuffisants pour quitter le domaine.\n" + privileges.elevation_hint() ) elif not available: self.leave_button.setEnabled(False) self.leave_button.setToolTip("Prérequis manquants — installez-les d'abord.") else: self.leave_button.setEnabled(True) self.leave_button.setToolTip("") def _start_install(self) -> None: confirm = QMessageBox.question( self, "Confirmer l'installation", "Installer les paquets système requis pour la jointure de domaine " "(realmd, sssd, adcli, samba-common-bin, packagekit) ?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if confirm != QMessageBox.StandardButton.Yes: return self.log.clear() self.install_button.setEnabled(False) self.join_button.setEnabled(False) self._install_worker = InstallPrerequisitesWorker(self.backend) self._install_worker.output_line.connect(self.log.appendPlainText) self._install_worker.finished_install.connect(self._on_install_finished) self._install_worker.start() def _on_install_finished(self, result) -> None: if result.success: QMessageBox.information(self, "Installation terminée", result.message) else: QMessageBox.critical(self, "Échec de l'installation", result.message) self._refresh_backend_state() def _start_join(self) -> None: domain = self.domain_edit.text().strip() username = self.username_edit.text().strip() password = self.password_edit.text() ou = self.ou_edit.text().strip() or None if not domain or not username or not password: QMessageBox.warning(self, "Champs manquants", "Domaine, utilisateur et mot de passe sont requis.") return available, reason = self.backend.is_available() if not available: QMessageBox.critical(self, "Backend indisponible", reason) return confirm = QMessageBox.question( self, "Confirmer la jointure", f"Rejoindre le domaine « {domain} » avec l'utilisateur « {username} » ?\n\n" "Cette action modifie la configuration système du poste et nécessitera " "probablement un redémarrage.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if confirm != QMessageBox.StandardButton.Yes: return self.log.clear() self.join_button.setEnabled(False) self._worker = JoinWorker(self.backend, domain, username, password, ou, None) self._worker.output_line.connect(self.log.appendPlainText) self._worker.finished_join.connect(self._on_join_finished) self._worker.start() def _start_leave(self) -> None: domain = self.domain_edit.text().strip() username = self.username_edit.text().strip() password = self.password_edit.text() remove_packages = self.remove_packages_check.isChecked() and self.remove_packages_check.isVisible() if not domain: QMessageBox.warning(self, "Champ manquant", "Le domaine est requis.") return # Sous Windows, Remove-Computer exige un compte autorise a retirer # l'objet ordinateur ; sous Linux, realm leave s'en passe. if platform.system() == "Windows" and (not username or not password): QMessageBox.warning( self, "Identifiants requis", "Sous Windows, quitter le domaine demande un compte du domaine " "autorisé à retirer cette machine de l'annuaire.", ) return available, reason = self.backend.is_available() if not available: QMessageBox.critical(self, "Backend indisponible", reason) return details = ( f"Faire quitter le domaine « {domain} » à ce poste ?\n\n" "Les comptes du domaine ne pourront plus s'y connecter et un " "redémarrage sera nécessaire." ) if remove_packages: details += ( "\n\nLes paquets suivants seront également désinstallés :\n" + ", ".join(REMOVABLE_PACKAGES) + "\n\nAssurez-vous qu'aucune session de compte du domaine n'est ouverte." ) confirm = QMessageBox.question( self, "Confirmer la sortie du domaine", details, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if confirm != QMessageBox.StandardButton.Yes: return self.log.clear() self.join_button.setEnabled(False) self.leave_button.setEnabled(False) self._leave_worker = LeaveWorker(self.backend, domain, username, password, remove_packages) self._leave_worker.output_line.connect(self.log.appendPlainText) self._leave_worker.finished_leave.connect(self._on_leave_finished) self._leave_worker.start() def _on_leave_finished(self, result) -> None: self._refresh_backend_state() if result.success: msg = result.message if result.needs_reboot: msg += "\n\nUn redémarrage est nécessaire pour finaliser la sortie." QMessageBox.information(self, "Sortie du domaine réussie", msg) else: QMessageBox.critical(self, "Échec de la sortie du domaine", result.message) def _on_join_finished(self, result) -> None: self._refresh_backend_state() if result.success: msg = result.message if result.needs_reboot: msg += "\n\nUn redémarrage est nécessaire pour finaliser la jointure." QMessageBox.information(self, "Jointure réussie", msg) else: QMessageBox.critical(self, "Échec de la jointure", result.message) class AboutTab(QWidget): def __init__(self, parent=None): super().__init__(parent) layout = QVBoxLayout(self) layout.setAlignment(Qt.AlignmentFlag.AlignTop) title = QLabel("JoinDomainGUI") title.setStyleSheet("font-size: 18pt; font-weight: bold;") layout.addWidget(title) version = QLabel(f"Version {__version__}") layout.addWidget(version) description = QLabel( "Diagnostic réseau et jointure de domaine Active Directory, " "pour postes Linux (realmd) et Windows (Add-Computer)." ) description.setWordWrap(True) layout.addWidget(description) backend = get_backend() elevated = "élevé (root/administrateur)" if privileges.is_elevated() else "standard (non élevé)" state = QLabel( f"Plateforme détectée : {platform.system()} {platform.release()}\n" f"Backend de jointure : {type(backend).__name__}\n" f"Droits actuels : {elevated}" ) layout.addWidget(state) cli_title = QLabel("Utilisation en ligne de commande") cli_title.setStyleSheet("font-weight: bold; margin-top: 12px;") layout.addWidget(cli_title) cli_hint = QPlainTextEdit( "python main.py diagnose --domain h3adm.lan\n" "python main.py join --domain h3adm.lan --username administrateur --password-stdin\n" "python main.py install-prereqs\n" "python main.py --help" ) cli_hint.setReadOnly(True) cli_hint.setMaximumHeight(100) layout.addWidget(cli_hint) layout.addStretch() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Diagnostic réseau & Jointure de domaine") self.resize(760, 620) central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) if not privileges.is_elevated(): banner = QLabel( "⚠ L'application ne tourne pas avec les droits administrateur/root. " "Le diagnostic fonctionne, mais la jointure au domaine sera indisponible. " + privileges.elevation_hint() ) banner.setWordWrap(True) banner.setStyleSheet( "background-color:#fff3cd; color:#664d03; padding:8px; border:1px solid #ffe69c;" ) layout.addWidget(banner) tabs = QTabWidget() tabs.addTab(DiagnosticsTab(), "Diagnostic réseau") tabs.addTab(JoinTab(), "Rejoindre le domaine") tabs.addTab(AboutTab(), "À propos") layout.addWidget(tabs)