Version initiale : diagnostic réseau AD + jointure de domaine
Application PySide6 (GUI + CLI) fonctionnant sous Linux et Windows : - Diagnostic réseau complet AD (DNS, SRV, ports, NTP, outils requis) - Jointure de domaine via realmd (Linux) ou Add-Computer (Windows) - Installation automatique des prérequis Linux (apt) - CLI colorée (diagnose/join/install-prereqs), sans dépendance PySide6 - Scripts de packaging PyInstaller pour Linux et Windows Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
"""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,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QTabWidget,
|
||||
QTreeWidget,
|
||||
QTreeWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from . import __version__, diagnostics, privileges
|
||||
from .backend import get_backend
|
||||
from .worker import DiagnosticsWorker, InstallPrerequisitesWorker, JoinWorker
|
||||
|
||||
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.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")
|
||||
self.computer_name_edit = QLineEdit(socket.gethostname())
|
||||
|
||||
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)
|
||||
layout.addLayout(form)
|
||||
|
||||
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)
|
||||
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("")
|
||||
|
||||
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
|
||||
computer_name = self.computer_name_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, computer_name)
|
||||
self._worker.output_line.connect(self.log.appendPlainText)
|
||||
self._worker.finished_join.connect(self._on_join_finished)
|
||||
self._worker.start()
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user