tools.py (22740B)
1 #!/usr/bin/env python3 2 """ 3 runv.club — ferramentas globais, MOTD, comandos em /usr/local/bin e /etc/skel. 4 5 Debian 13 · Python 3 stdlib apenas · sem shell=True. 6 Execute como root. Ver docs/05-tools-and-system-experience.md no repositório. 7 """ 8 9 from __future__ import annotations 10 11 import argparse 12 import filecmp 13 import logging 14 import os 15 import shutil 16 import subprocess 17 import sys 18 from dataclasses import dataclass, field 19 from pathlib import Path 20 21 TOOL_ROOT: Path = Path(__file__).resolve().parent 22 ADMIN_TOOLS_DIR: Path = TOOL_ROOT.parent / "scripts" / "admin" 23 if str(ADMIN_TOOLS_DIR) not in sys.path: 24 sys.path.insert(0, str(ADMIN_TOOLS_DIR)) 25 26 from admin_guard import ensure_admin_cli 27 28 MANIFEST_PATH: Path = TOOL_ROOT / "manifests" / "apt_packages.txt" 29 30 # Nome no manifesto → pacote apt real ("chat" = IRC no terminal; Debian usa o pacote weechat). 31 _APT_PACKAGE_ALIASES: dict[str, str] = { 32 "chat": "weechat", 33 } 34 BIN_DIR: Path = TOOL_ROOT / "bin" 35 LIB_DIR: Path = TOOL_ROOT / "lib" 36 MOTD_SRC: Path = TOOL_ROOT / "motd" / "60-runv" 37 SKEL_DIR: Path = TOOL_ROOT / "skel" 38 SUDOERS_ADMIN_SRC: Path = TOOL_ROOT / "sudoers" / "90-runv-pmurad-admin" 39 40 DEST_BIN_DIR: Path = Path("/usr/local/bin") 41 DEST_COMMUNITY_LIB_DIR: Path = Path("/usr/local/share/runv/lib") 42 DEST_MOTD: Path = Path("/etc/update-motd.d/60-runv") 43 DEST_SKEL: Path = Path("/etc/skel") 44 DEST_SSHD_DROPIN: Path = Path("/etc/ssh/sshd_config.d/90-runv-jailed.conf") 45 DEST_SUDOERS_ADMIN: Path = Path("/etc/sudoers.d/90-runv-pmurad-admin") 46 PATCH_IRC_PATH: Path = TOOL_ROOT.parent / "patches" / "patch_irc.py" 47 REMOVE_JAILS_PATH: Path = TOOL_ROOT.parent / "scripts" / "admin" / "remove_runv_jails.py" 48 49 50 @dataclass 51 class RunSummary: 52 """Acumula ações para o resumo final.""" 53 54 dry_run: bool = False 55 apt_updated: bool = False 56 apt_install_attempted: bool = False 57 packages_requested: list[str] = field(default_factory=list) 58 copied: list[str] = field(default_factory=list) 59 skipped: list[str] = field(default_factory=list) 60 errors: list[str] = field(default_factory=list) 61 62 63 def setup_logging(verbose: bool) -> logging.Logger: 64 level = logging.DEBUG if verbose else logging.INFO 65 logging.basicConfig( 66 level=level, 67 format="%(levelname)s: %(message)s", 68 ) 69 return logging.getLogger("runv-tools") 70 71 72 def require_root(log: logging.Logger) -> None: 73 if os.geteuid() != 0: 74 log.error("Este script precisa ser executado como root (sudo).") 75 sys.exit(1) 76 77 78 def run_subprocess( 79 cmd: list[str], 80 *, 81 dry_run: bool, 82 log: logging.Logger, 83 env: dict[str, str] | None = None, 84 ) -> subprocess.CompletedProcess[str] | None: 85 """Executa comando sem shell; em dry-run apenas registra.""" 86 log.debug("exec: %s", " ".join(cmd)) 87 if dry_run: 88 log.info("[dry-run] %s", " ".join(cmd)) 89 return None 90 e = os.environ.copy() 91 if env: 92 e.update(env) 93 return subprocess.run( 94 cmd, 95 check=False, 96 capture_output=True, 97 text=True, 98 env=e, 99 timeout=3600, 100 ) 101 102 103 def read_apt_manifest(path: Path, log: logging.Logger) -> list[str]: 104 if not path.is_file(): 105 log.error("Manifesto não encontrado: %s", path) 106 sys.exit(1) 107 packages: list[str] = [] 108 for raw in path.read_text(encoding="utf-8").splitlines(): 109 line = raw.strip() 110 if not line or line.startswith("#"): 111 continue 112 packages.append(_APT_PACKAGE_ALIASES.get(line, line)) 113 return packages 114 115 116 def install_apt_packages( 117 packages: list[str], 118 *, 119 dry_run: bool, 120 log: logging.Logger, 121 summary: RunSummary, 122 ) -> None: 123 if not packages: 124 log.info("Nenhum pacote listado no manifesto; etapa apt ignorada.") 125 return 126 summary.packages_requested = list(packages) 127 env_apt = { 128 "DEBIAN_FRONTEND": "noninteractive", 129 "LC_ALL": "C", 130 } 131 log.info("Atualizando índice apt (apt-get update)...") 132 r = run_subprocess( 133 ["apt-get", "update", "-qq"], 134 dry_run=dry_run, 135 log=log, 136 env=env_apt, 137 ) 138 if dry_run: 139 summary.apt_updated = True 140 elif r is not None: 141 if r.returncode != 0: 142 err = (r.stderr or r.stdout or "").strip() 143 msg = f"apt-get update falhou (código {r.returncode})" + (f": {err}" if err else "") 144 summary.errors.append(msg) 145 log.error("%s", msg) 146 return 147 summary.apt_updated = True 148 149 log.info("Instalando pacotes: %s", ", ".join(packages)) 150 summary.apt_install_attempted = True 151 cmd = ["apt-get", "install", "-y", "--no-install-recommends", *packages] 152 r = run_subprocess(cmd, dry_run=dry_run, log=log, env=env_apt) 153 if dry_run: 154 return 155 if r is None: 156 return 157 if r.returncode != 0: 158 err = (r.stderr or r.stdout or "").strip() 159 msg = f"apt-get install falhou (código {r.returncode})" + (f": {err}" if err else "") 160 summary.errors.append(msg) 161 log.error("%s", msg) 162 else: 163 log.info("Pacotes instalados ou já presentes (apt idempotente).") 164 165 166 def ensure_parent(path: Path, log: logging.Logger) -> None: 167 path.parent.mkdir(parents=True, exist_ok=True) 168 169 170 def copy_one( 171 src: Path, 172 dst: Path, 173 mode: int, 174 *, 175 force: bool, 176 dry_run: bool, 177 log: logging.Logger, 178 summary: RunSummary, 179 ) -> None: 180 if not src.is_file(): 181 summary.errors.append(f"origem inexistente: {src}") 182 log.error("Origem inexistente: %s", src) 183 return 184 185 def same_content() -> bool: 186 if not dst.is_file(): 187 return False 188 try: 189 return filecmp.cmp(src, dst, shallow=False) 190 except OSError: 191 return False 192 193 # Sem --force: só pula se o ficheiro já for byte-a-byte igual à origem (reexecução actualiza mudanças do repo). 194 if not force and dst.exists() and same_content(): 195 log.info("Destino já coincide com a origem, pulando: %s", dst) 196 summary.skipped.append(str(dst)) 197 return 198 if dry_run: 199 log.info("[dry-run] copiaria %s -> %s (modo %o)", src, dst, mode) 200 summary.copied.append(f"{src} -> {dst} (simulado)") 201 return 202 ensure_parent(dst, log) 203 shutil.copy2(src, dst) 204 os.chmod(dst, mode) 205 try: 206 os.chown(dst, 0, 0) 207 except OSError as e: 208 log.warning("chown root:root em %s: %s", dst, e) 209 log.info("Instalado: %s", dst) 210 summary.copied.append(str(dst)) 211 212 213 def install_bin_scripts( 214 *, 215 force: bool, 216 dry_run: bool, 217 log: logging.Logger, 218 summary: RunSummary, 219 ) -> None: 220 if not dry_run: 221 DEST_BIN_DIR.mkdir(parents=True, exist_ok=True) 222 for name in ( 223 "runv-help", 224 "runv-links", 225 "runv-status", 226 "runv-games", 227 "runvers", 228 "chat", 229 "runv-profile", 230 "runv-finger", 231 "runv-who", 232 "runv-bulletin", 233 "runv-email-alias", 234 "runv-admin-email-alias", 235 ): 236 copy_one( 237 BIN_DIR / name, 238 DEST_BIN_DIR / name, 239 0o755, 240 force=force, 241 dry_run=dry_run, 242 log=log, 243 summary=summary, 244 ) 245 246 247 def install_community_lib( 248 *, 249 force: bool, 250 dry_run: bool, 251 log: logging.Logger, 252 summary: RunSummary, 253 ) -> None: 254 """Copia bibliotecas partilhadas para /usr/local/share/runv/lib/.""" 255 if not dry_run: 256 DEST_COMMUNITY_LIB_DIR.mkdir(parents=True, exist_ok=True) 257 for src in sorted(LIB_DIR.glob("*.py")): 258 copy_one( 259 src, 260 DEST_COMMUNITY_LIB_DIR / src.name, 261 0o644, 262 force=force, 263 dry_run=dry_run, 264 log=log, 265 summary=summary, 266 ) 267 268 269 def install_motd( 270 *, 271 force: bool, 272 dry_run: bool, 273 log: logging.Logger, 274 summary: RunSummary, 275 ) -> None: 276 copy_one( 277 MOTD_SRC, 278 DEST_MOTD, 279 0o755, 280 force=force, 281 dry_run=dry_run, 282 log=log, 283 summary=summary, 284 ) 285 286 287 def install_admin_sudoers( 288 *, 289 force: bool, 290 dry_run: bool, 291 log: logging.Logger, 292 summary: RunSummary, 293 ) -> None: 294 copy_one( 295 SUDOERS_ADMIN_SRC, 296 DEST_SUDOERS_ADMIN, 297 0o440, 298 force=force, 299 dry_run=dry_run, 300 log=log, 301 summary=summary, 302 ) 303 if dry_run or summary.errors: 304 return 305 check = subprocess.run( 306 ["visudo", "-cf", str(DEST_SUDOERS_ADMIN)], 307 capture_output=True, 308 text=True, 309 timeout=30, 310 ) 311 if check.returncode != 0: 312 err = (check.stderr or check.stdout or "").strip() 313 msg = f"visudo -cf falhou para {DEST_SUDOERS_ADMIN}: {err}" 314 summary.errors.append(msg) 315 log.error("%s", msg) 316 return 317 log.info("Sudoers validado para pmurad-admin: %s", DEST_SUDOERS_ADMIN) 318 319 320 def remove_obsolete_skel_readme( 321 *, 322 dry_run: bool, 323 log: logging.Logger, 324 summary: RunSummary, 325 ) -> None: 326 stale = DEST_SKEL / "README.md" 327 if not stale.is_file(): 328 return 329 if dry_run: 330 log.info("[dry-run] removeria %s", stale) 331 summary.copied.append(f"rm {stale} (simulado)") 332 return 333 try: 334 stale.unlink() 335 log.info("Removido (política skel): %s", stale) 336 summary.copied.append(f"removido {stale}") 337 except OSError as e: 338 summary.errors.append(f"remover {stale}: {e}") 339 log.error("Não foi possível remover %s: %s", stale, e) 340 341 342 def remove_jail_ssh_baseline( 343 *, 344 dry_run: bool, 345 log: logging.Logger, 346 summary: RunSummary, 347 ) -> None: 348 """Remove o drop-in antigo de ChrootDirectory para membros runv.""" 349 if not DEST_SSHD_DROPIN.exists(): 350 log.info("Drop-in SSH runv-jailed ausente: %s", DEST_SSHD_DROPIN) 351 summary.skipped.append(str(DEST_SSHD_DROPIN)) 352 return 353 if dry_run: 354 log.info("[dry-run] removeria %s; testaria sshd -t; recarregaria ssh", DEST_SSHD_DROPIN) 355 summary.copied.append(f"remover {DEST_SSHD_DROPIN} (simulado)") 356 return 357 try: 358 DEST_SSHD_DROPIN.unlink() 359 except OSError as e: 360 msg = f"remover {DEST_SSHD_DROPIN}: {e}" 361 summary.errors.append(msg) 362 log.error("%s", msg) 363 return 364 summary.copied.append(f"removido {DEST_SSHD_DROPIN}") 365 366 test = subprocess.run( 367 ["sshd", "-t"], 368 capture_output=True, 369 text=True, 370 timeout=30, 371 ) 372 if test.returncode != 0: 373 err = (test.stderr or test.stdout or "").strip() 374 msg = f"sshd -t falhou após remover drop-in runv-jailed: {err}" 375 summary.errors.append(msg) 376 log.error("%s", msg) 377 return 378 379 reloaded = False 380 for unit in ("ssh", "sshd"): 381 rr = subprocess.run( 382 ["systemctl", "reload", unit], 383 capture_output=True, 384 text=True, 385 timeout=60, 386 ) 387 if rr.returncode == 0: 388 log.info("systemctl reload %s concluído", unit) 389 reloaded = True 390 break 391 log.debug("systemctl reload %s: %s", unit, (rr.stderr or rr.stdout or "").strip()) 392 if not reloaded: 393 msg = "systemctl reload ssh/sshd falhou — recarregue o sshd manualmente" 394 summary.errors.append(msg) 395 log.error("%s", msg) 396 397 398 def install_skel( 399 *, 400 force: bool, 401 dry_run: bool, 402 log: logging.Logger, 403 summary: RunSummary, 404 ) -> None: 405 """Copia apenas arquivos modelo; não instala pacotes.""" 406 if not dry_run: 407 DEST_SKEL.mkdir(parents=True, exist_ok=True) 408 409 remove_obsolete_skel_readme(dry_run=dry_run, log=log, summary=summary) 410 411 skel_files: list[tuple[Path, Path, int]] = [ 412 (SKEL_DIR / ".bash_aliases", DEST_SKEL / ".bash_aliases", 0o644), 413 (SKEL_DIR / ".plan", DEST_SKEL / ".plan", 0o644), 414 (SKEL_DIR / ".project", DEST_SKEL / ".project", 0o644), 415 ] 416 for src, dst, mode in skel_files: 417 copy_one(src, dst, mode, force=force, dry_run=dry_run, log=log, summary=summary) 418 419 runv_skel_dir = DEST_SKEL / ".runv" 420 profile_src = SKEL_DIR / ".runv" / "profile.json" 421 profile_dst = runv_skel_dir / "profile.json" 422 if not profile_src.is_file(): 423 summary.errors.append(f"origem inexistente: {profile_src}") 424 log.error("Origem inexistente: %s", profile_src) 425 else: 426 if not dry_run: 427 runv_skel_dir.mkdir(parents=True, exist_ok=True) 428 os.chmod(runv_skel_dir, 0o755) 429 try: 430 os.chown(runv_skel_dir, 0, 0) 431 except OSError as e: 432 log.warning("chown em %s: %s", runv_skel_dir, e) 433 elif not runv_skel_dir.exists(): 434 log.info("[dry-run] criaria diretório %s (755)", runv_skel_dir) 435 copy_one( 436 profile_src, 437 profile_dst, 438 0o644, 439 force=force, 440 dry_run=dry_run, 441 log=log, 442 summary=summary, 443 ) 444 445 pub_dir = DEST_SKEL / "public_html" 446 index_src = SKEL_DIR / "public_html" / "index.html" 447 index_dst = pub_dir / "index.html" 448 449 if not index_src.is_file(): 450 summary.errors.append(f"origem inexistente: {index_src}") 451 log.error("Origem inexistente: %s", index_src) 452 return 453 454 if not dry_run: 455 pub_dir.mkdir(parents=True, exist_ok=True) 456 os.chmod(pub_dir, 0o755) 457 try: 458 os.chown(pub_dir, 0, 0) 459 except OSError as e: 460 log.warning("chown em %s: %s", pub_dir, e) 461 elif not pub_dir.exists() and dry_run: 462 log.info("[dry-run] criaria diretório %s (755)", pub_dir) 463 464 copy_one( 465 index_src, 466 index_dst, 467 0o644, 468 force=force, 469 dry_run=dry_run, 470 log=log, 471 summary=summary, 472 ) 473 474 if not dry_run and pub_dir.is_dir(): 475 os.chmod(pub_dir, 0o755) 476 try: 477 os.chown(pub_dir, 0, 0) 478 except OSError: 479 pass 480 481 # public_gopher / public_gemini (Gopher / Gemini — mesmo critério que public_html) 482 gopher_dir = DEST_SKEL / "public_gopher" 483 gopher_src = SKEL_DIR / "public_gopher" / "gophermap" 484 gopher_dst = gopher_dir / "gophermap" 485 gemini_dir = DEST_SKEL / "public_gemini" 486 gemini_src = SKEL_DIR / "public_gemini" / "index.gmi" 487 gemini_dst = gemini_dir / "index.gmi" 488 489 if not gopher_src.is_file() or not gemini_src.is_file(): 490 for p in (gopher_src, gemini_src): 491 if not p.is_file(): 492 summary.errors.append(f"origem inexistente: {p}") 493 log.error("Origem inexistente: %s", p) 494 else: 495 if not dry_run: 496 gopher_dir.mkdir(parents=True, exist_ok=True) 497 os.chmod(gopher_dir, 0o755) 498 gemini_dir.mkdir(parents=True, exist_ok=True) 499 os.chmod(gemini_dir, 0o755) 500 try: 501 os.chown(gopher_dir, 0, 0) 502 os.chown(gemini_dir, 0, 0) 503 except OSError as e: 504 log.warning("chown em skel gopher/gemini: %s", e) 505 else: 506 if not gopher_dir.exists(): 507 log.info("[dry-run] criaria diretório %s (755)", gopher_dir) 508 if not gemini_dir.exists(): 509 log.info("[dry-run] criaria diretório %s (755)", gemini_dir) 510 511 copy_one( 512 gopher_src, 513 gopher_dst, 514 0o644, 515 force=force, 516 dry_run=dry_run, 517 log=log, 518 summary=summary, 519 ) 520 if gemini_dst.is_file(): 521 log.info("Destino já existe, mantido (index.gmi em skel): %s", gemini_dst) 522 summary.skipped.append(str(gemini_dst)) 523 else: 524 copy_one( 525 gemini_src, 526 gemini_dst, 527 0o644, 528 force=force, 529 dry_run=dry_run, 530 log=log, 531 summary=summary, 532 ) 533 534 if not dry_run: 535 if gopher_dir.is_dir(): 536 os.chmod(gopher_dir, 0o755) 537 if gemini_dir.is_dir(): 538 os.chmod(gemini_dir, 0o755) 539 540 541 def apply_irc_patch( 542 *, 543 dry_run: bool, 544 log: logging.Logger, 545 summary: RunSummary, 546 ) -> None: 547 if not PATCH_IRC_PATH.is_file(): 548 msg = f"patch IRC não encontrado: {PATCH_IRC_PATH}" 549 summary.errors.append(msg) 550 log.error("%s", msg) 551 return 552 553 cmd = [sys.executable, str(PATCH_IRC_PATH), "--all-users", "--force"] 554 if dry_run: 555 log.info("[dry-run] %s", " ".join(cmd)) 556 summary.copied.append(f"patch IRC (simulado): {' '.join(cmd)}") 557 return 558 559 r = run_subprocess(cmd, dry_run=False, log=log) 560 assert r is not None 561 if r.returncode != 0: 562 err = (r.stderr or r.stdout or "").strip() 563 msg = f"patch_irc.py falhou (código {r.returncode})" + (f": {err}" if err else "") 564 summary.errors.append(msg) 565 log.error("%s", msg) 566 return 567 568 summary.copied.append("patch IRC aplicado a todos os utilizadores (--force)") 569 if r.stdout.strip(): 570 log.info("patch IRC: %s", r.stdout.strip().splitlines()[-1]) 571 572 573 def remove_existing_jails( 574 *, 575 dry_run: bool, 576 log: logging.Logger, 577 summary: RunSummary, 578 ) -> None: 579 if not REMOVE_JAILS_PATH.is_file(): 580 msg = f"remove_runv_jails.py não encontrado: {REMOVE_JAILS_PATH}" 581 summary.errors.append(msg) 582 log.error("%s", msg) 583 return 584 585 cmd = [sys.executable, str(REMOVE_JAILS_PATH)] 586 if dry_run: 587 cmd.append("--dry-run") 588 if log.isEnabledFor(logging.DEBUG): 589 cmd.append("--verbose") 590 591 r = run_subprocess(cmd, dry_run=False if not dry_run else True, log=log) 592 if dry_run: 593 summary.copied.append(f"remoção de jails existentes (simulado): {' '.join(cmd)}") 594 return 595 596 assert r is not None 597 if r.returncode != 0: 598 err = (r.stderr or r.stdout or "").strip() 599 msg = f"remove_runv_jails.py falhou (código {r.returncode})" + (f": {err}" if err else "") 600 summary.errors.append(msg) 601 log.error("%s", msg) 602 return 603 604 summary.copied.append("jails SSH removidas de utilizadores existentes") 605 if r.stdout.strip(): 606 log.info("remove_runv_jails.py: %s", r.stdout.strip().splitlines()[-1]) 607 608 609 def print_summary(summary: RunSummary, log: logging.Logger) -> None: 610 print() 611 print("========== runv-tools — resumo ==========") 612 if summary.dry_run: 613 print("Modo: DRY-RUN (nenhuma alteração no sistema)") 614 print(f"apt-get update executado/simulado: {summary.apt_updated}") 615 if summary.packages_requested: 616 print(f"Pacotes (manifesto): {', '.join(summary.packages_requested)}") 617 print(f"Instalação apt tentada/simulada: {summary.apt_install_attempted}") 618 if summary.copied: 619 print("Copiados / simulados:") 620 for c in summary.copied: 621 print(f" + {c}") 622 if summary.skipped: 623 print("Sem alteração (destino já idêntico à origem no repositório):") 624 for s in summary.skipped: 625 print(f" = {s}") 626 if summary.errors: 627 print("Erros:") 628 for e in summary.errors: 629 print(f" ! {e}") 630 print("==========================================") 631 sys.exit(1) 632 print("Concluído sem erros fatais registrados pelo script.") 633 print("==========================================") 634 635 636 def parse_args(argv: list[str] | None) -> argparse.Namespace: 637 p = argparse.ArgumentParser( 638 description="Instala pacotes globais, comandos runv, MOTD e arquivos em /etc/skel.", 639 ) 640 p.add_argument( 641 "--dry-run", 642 action="store_true", 643 help="não altera o sistema; mostra o que seria feito", 644 ) 645 p.add_argument( 646 "--verbose", 647 action="store_true", 648 help="log detalhado", 649 ) 650 p.add_argument( 651 "--force", 652 action="store_true", 653 help="sobrescreve sempre (mesmo conteúdo idêntico); sem isto, só copia se origem e destino diferirem", 654 ) 655 p.add_argument( 656 "--skip-apt", 657 action="store_true", 658 help="não executa apt-get (útil para reaplicar só arquivos/MOTD/skel)", 659 ) 660 p.add_argument( 661 "--reconcile-existing-users", 662 action="store_true", 663 help="reaplica/verifica patch IRC em utilizadores já existentes", 664 ) 665 p.add_argument( 666 "--remove-existing-jails", 667 action="store_true", 668 help="remove runv-jailed, binds/fstab e /srv/jail de membros existentes", 669 ) 670 return p.parse_args(argv) 671 672 673 def main(argv: list[str] | None = None) -> int: 674 args = parse_args(argv) 675 ensure_admin_cli( 676 script_name=Path(__file__).name, 677 dry_run=bool(args.dry_run), 678 ) 679 log = setup_logging(args.verbose) 680 summary = RunSummary(dry_run=args.dry_run) 681 682 if not args.dry_run: 683 require_root(log) 684 else: 685 log.info("Dry-run: validação de root ignorada (nada será gravado).") 686 687 if not args.skip_apt: 688 pkgs = read_apt_manifest(MANIFEST_PATH, log) 689 install_apt_packages(pkgs, dry_run=args.dry_run, log=log, summary=summary) 690 else: 691 log.info("Etapa apt ignorada (--skip-apt).") 692 693 log.info("Instalando scripts em %s", DEST_BIN_DIR) 694 install_bin_scripts(force=args.force, dry_run=args.dry_run, log=log, summary=summary) 695 696 log.info("Instalando biblioteca comunitária em %s", DEST_COMMUNITY_LIB_DIR) 697 install_community_lib(force=args.force, dry_run=args.dry_run, log=log, summary=summary) 698 699 log.info("Instalando MOTD em %s", DEST_MOTD) 700 install_motd(force=args.force, dry_run=args.dry_run, log=log, summary=summary) 701 702 log.info("Garantindo sudo administrativo para pmurad-admin") 703 install_admin_sudoers( 704 force=args.force, 705 dry_run=args.dry_run, 706 log=log, 707 summary=summary, 708 ) 709 710 log.info("Removendo baseline antigo de SSH runv-jailed (sem jail por padrão)") 711 remove_jail_ssh_baseline( 712 dry_run=args.dry_run, 713 log=log, 714 summary=summary, 715 ) 716 717 log.info("Sincronizando skel em %s", DEST_SKEL) 718 install_skel(force=args.force, dry_run=args.dry_run, log=log, summary=summary) 719 720 if args.remove_existing_jails: 721 log.info("Removendo jails SSH de utilizadores existentes") 722 remove_existing_jails(dry_run=args.dry_run, log=log, summary=summary) 723 else: 724 log.info("Jails existentes não serão removidas (sem --remove-existing-jails).") 725 726 if args.reconcile_existing_users: 727 log.info("Aplicando patch IRC (chat / WeeChat) aos utilizadores existentes") 728 apply_irc_patch(dry_run=args.dry_run, log=log, summary=summary) 729 else: 730 log.info("Patch IRC em utilizadores existentes ignorado (sem --reconcile-existing-users).") 731 732 print_summary(summary, log) 733 return 0 734 735 736 if __name__ == "__main__": 737 raise SystemExit(main())