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,223 @@
|
||||
"""Interface en ligne de commande de JoinDomainGUI.
|
||||
|
||||
N'importe volontairement pas PySide6 : la CLI doit pouvoir tourner sur un
|
||||
poste sans environnement graphique (déploiement scripté, SSH, etc.).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
from typing import Sequence
|
||||
|
||||
from . import __version__, diagnostics, privileges
|
||||
from .backend import get_backend
|
||||
|
||||
DEFAULT_DOMAIN = "h3adm.lan"
|
||||
|
||||
_STATUS_PREFIX = {
|
||||
diagnostics.Status.OK: "[OK] ",
|
||||
diagnostics.Status.WARN: "[!!] ",
|
||||
diagnostics.Status.FAIL: "[XX] ",
|
||||
}
|
||||
|
||||
|
||||
class _Color:
|
||||
RESET = "\x1b[0m"
|
||||
BOLD = "\x1b[1m"
|
||||
DIM = "\x1b[2m"
|
||||
RED = "\x1b[31m"
|
||||
GREEN = "\x1b[32m"
|
||||
YELLOW = "\x1b[33m"
|
||||
CYAN = "\x1b[36m"
|
||||
|
||||
|
||||
_STATUS_COLOR = {
|
||||
diagnostics.Status.OK: _Color.GREEN,
|
||||
diagnostics.Status.WARN: _Color.YELLOW,
|
||||
diagnostics.Status.FAIL: _Color.RED,
|
||||
}
|
||||
|
||||
|
||||
def _enable_windows_ansi() -> None:
|
||||
"""Active l'interprétation des séquences ANSI dans les consoles Windows classiques."""
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
enable_virtual_terminal_processing = 0x0004
|
||||
for std_handle in (-11, -12): # STD_OUTPUT_HANDLE, STD_ERROR_HANDLE
|
||||
handle = kernel32.GetStdHandle(std_handle)
|
||||
mode = ctypes.c_uint32()
|
||||
if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
||||
kernel32.SetConsoleMode(handle, mode.value | enable_virtual_terminal_processing)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _color_enabled(stream) -> bool:
|
||||
if os.environ.get("NO_COLOR") is not None:
|
||||
return False
|
||||
if os.environ.get("FORCE_COLOR") is not None:
|
||||
return True
|
||||
try:
|
||||
return stream.isatty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _colorize(text: str, color: str, stream) -> str:
|
||||
if not _color_enabled(stream):
|
||||
return text
|
||||
return f"{color}{text}{_Color.RESET}"
|
||||
|
||||
|
||||
def _print_check(result: diagnostics.CheckResult) -> None:
|
||||
line = f"{_STATUS_PREFIX[result.status]}{result.name} — {result.detail}"
|
||||
print(_colorize(line, _STATUS_COLOR[result.status], sys.stdout))
|
||||
|
||||
|
||||
def _confirm(prompt: str, assume_yes: bool) -> bool:
|
||||
if assume_yes:
|
||||
return True
|
||||
reply = input(f"{prompt} [o/N] : ").strip().lower()
|
||||
return reply in ("o", "oui", "y", "yes")
|
||||
|
||||
|
||||
def _resolve_password(args: argparse.Namespace) -> str:
|
||||
if args.password_stdin:
|
||||
return sys.stdin.readline().rstrip("\n")
|
||||
if args.password:
|
||||
print(
|
||||
"Attention : --password expose le mot de passe dans l'historique du shell "
|
||||
"et la liste des processus. Préférez --password-stdin.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return args.password
|
||||
return getpass.getpass("Mot de passe : ")
|
||||
|
||||
|
||||
def _print_error(message: str) -> None:
|
||||
print(_colorize(f"Erreur : {message}", _Color.RED, sys.stderr), file=sys.stderr)
|
||||
|
||||
|
||||
def _print_result(result) -> None:
|
||||
color = _Color.GREEN if result.success else _Color.RED
|
||||
print(_colorize(result.message, color, sys.stdout))
|
||||
|
||||
|
||||
def _require_elevation() -> int | None:
|
||||
if privileges.is_elevated():
|
||||
return None
|
||||
_print_error(privileges.elevation_hint())
|
||||
return 2
|
||||
|
||||
|
||||
def _cmd_diagnose(args: argparse.Namespace) -> int:
|
||||
results = list(diagnostics.run_diagnostics(args.domain))
|
||||
for result in results:
|
||||
_print_check(result)
|
||||
|
||||
oks = sum(1 for r in results if r.status == diagnostics.Status.OK)
|
||||
warns = sum(1 for r in results if r.status == diagnostics.Status.WARN)
|
||||
fails = sum(1 for r in results if r.status == diagnostics.Status.FAIL)
|
||||
summary = (
|
||||
f"\nRésumé : {_colorize(f'{oks} OK', _Color.GREEN, sys.stdout)}, "
|
||||
f"{_colorize(f'{warns} avertissement(s)', _Color.YELLOW, sys.stdout)}, "
|
||||
f"{_colorize(f'{fails} échec(s)', _Color.RED, sys.stdout)}."
|
||||
)
|
||||
print(summary)
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
def _cmd_join(args: argparse.Namespace) -> int:
|
||||
if (code := _require_elevation()) is not None:
|
||||
return code
|
||||
|
||||
backend = get_backend()
|
||||
available, reason = backend.is_available()
|
||||
if not available:
|
||||
_print_error(reason)
|
||||
return 2
|
||||
|
||||
password = _resolve_password(args)
|
||||
|
||||
if not _confirm(
|
||||
f"Rejoindre le domaine « {args.domain} » avec l'utilisateur « {args.username} » ?", args.yes
|
||||
):
|
||||
print(_colorize("Annulé.", _Color.YELLOW, sys.stdout))
|
||||
return 1
|
||||
|
||||
result = backend.join(
|
||||
args.domain,
|
||||
args.username,
|
||||
password,
|
||||
ou=args.ou,
|
||||
computer_name=args.computer_name,
|
||||
on_output=print,
|
||||
)
|
||||
_print_result(result)
|
||||
if result.success and result.needs_reboot:
|
||||
print(_colorize("Un redémarrage est nécessaire pour finaliser la jointure.", _Color.YELLOW, sys.stdout))
|
||||
return 0 if result.success else 1
|
||||
|
||||
|
||||
def _cmd_install_prereqs(args: argparse.Namespace) -> int:
|
||||
if (code := _require_elevation()) is not None:
|
||||
return code
|
||||
|
||||
backend = get_backend()
|
||||
if not backend.can_install_prerequisites():
|
||||
_print_error("Ce backend ne prend pas en charge l'installation automatique des prérequis.")
|
||||
return 2
|
||||
|
||||
if not _confirm("Installer les prérequis système (realmd, sssd, adcli, samba-common-bin, packagekit) ?", args.yes):
|
||||
print(_colorize("Annulé.", _Color.YELLOW, sys.stdout))
|
||||
return 1
|
||||
|
||||
result = backend.install_prerequisites(on_output=print)
|
||||
_print_result(result)
|
||||
return 0 if result.success else 1
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="JoinDomainGUI",
|
||||
description="Diagnostic réseau et jointure de domaine Active Directory (Linux/Windows).",
|
||||
)
|
||||
parser.add_argument("--version", action="version", version=f"JoinDomainGUI {__version__}")
|
||||
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_diag = sub.add_parser("diagnose", help="Lance le diagnostic réseau AD.")
|
||||
p_diag.add_argument("--domain", default=DEFAULT_DOMAIN, help=f"Domaine à diagnostiquer (défaut : {DEFAULT_DOMAIN}).")
|
||||
p_diag.set_defaults(func=_cmd_diagnose)
|
||||
|
||||
p_join = sub.add_parser("join", help="Rejoint le domaine (nécessite les droits root/administrateur).")
|
||||
p_join.add_argument("--domain", default=DEFAULT_DOMAIN, help=f"Domaine à rejoindre (défaut : {DEFAULT_DOMAIN}).")
|
||||
p_join.add_argument("--username", required=True, help="Compte autorisé à joindre le domaine.")
|
||||
p_join.add_argument("--password", help="Mot de passe (déconseillé : visible dans l'historique/ps). Préférez --password-stdin.")
|
||||
p_join.add_argument("--password-stdin", action="store_true", help="Lit le mot de passe depuis l'entrée standard.")
|
||||
p_join.add_argument("--ou", default=None, help="Unité d'organisation cible (optionnel).")
|
||||
p_join.add_argument("--computer-name", default=None, help="Nom de machine (optionnel).")
|
||||
p_join.add_argument("-y", "--yes", action="store_true", help="Ne pas demander de confirmation.")
|
||||
p_join.set_defaults(func=_cmd_join)
|
||||
|
||||
p_install = sub.add_parser(
|
||||
"install-prereqs", help="Installe les outils système requis pour la jointure (Linux/apt uniquement)."
|
||||
)
|
||||
p_install.add_argument("-y", "--yes", action="store_true", help="Ne pas demander de confirmation.")
|
||||
p_install.set_defaults(func=_cmd_install_prereqs)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def run_cli(argv: Sequence[str]) -> int:
|
||||
_enable_windows_ansi()
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
return args.func(args)
|
||||
Reference in New Issue
Block a user