"""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)