starthere.py (23422B)
1 #!/usr/bin/env python3 2 """ 3 starthere.py - bootstrap seguro para runv.club em Debian/ext4 4 5 O que faz: 6 - atualiza índices APT 7 - instala um conjunto conservador de pacotes úteis para o projeto 8 - faz limpeza segura (autoremove + autoclean) 9 - enable/start apache2; UFW inativo → allow SSH/80/443 e enable 10 - descobre automaticamente o filesystem que contém /home (pode ser / ou /home, etc.) 11 - habilita usrquota nesse mountpoint ext4 no /etc/fstab 12 - remount + quotacheck + quotaon nesse mesmo ponto 13 - roda quotacheck (-cu, depois -cuM, depois variantes com -f se quotas já ativas no mount) 14 - quotaon -vu trata EBUSY (quotas já ativas após remount) como sucesso com aviso 15 - ativa quotas de usuário 16 17 O que NÃO faz: 18 - não purga pacotes arbitrariamente 19 - não mexe em SSH 20 - não mexe no Apache além de instalar o pacote se faltar 21 - não cria usuários 22 - não instala stack de email 23 """ 24 25 from __future__ import annotations 26 27 import argparse 28 import os 29 import shlex 30 import shutil 31 import subprocess 32 import sys 33 from dataclasses import dataclass 34 from datetime import datetime 35 from pathlib import Path 36 from typing import Final 37 38 # Com python3 -P ou PYTHONSAFEPATH=1 o diretório deste script deixa de ir para sys.path; 39 # sem isto, «from runv_mount» falha mesmo com runv_mount.py ao lado. 40 _SCRIPT_DIR = Path(__file__).resolve().parent 41 if str(_SCRIPT_DIR) not in sys.path: 42 sys.path.insert(0, str(_SCRIPT_DIR)) 43 44 from admin_guard import ensure_admin_cli 45 46 try: 47 from runv_mount import MountLookupError, find_mount_triple 48 except ModuleNotFoundError: 49 print( 50 "Erro: módulo 'runv_mount' não encontrado. " 51 "O ficheiro runv_mount.py tem de estar no mesmo diretório que starthere.py.\n" 52 f" Esperado: {_SCRIPT_DIR / 'runv_mount.py'}", 53 file=sys.stderr, 54 ) 55 raise SystemExit(1) from None 56 57 DEFAULT_QUOTA_PROBE: Final[Path] = Path("/home") 58 59 VERSION: Final[str] = "0.02" 60 61 FSTAB = Path("/etc/fstab") 62 BACKUP_DIR = Path("/root/runv-fstab-backups") 63 64 BASE_PACKAGES = [ 65 "apache2", 66 "openssh-server", 67 "sudo", 68 "ufw", 69 "quota", 70 "curl", 71 "wget", 72 "git", 73 "rsync", 74 "tmux", 75 "htop", 76 "vim", 77 "nano", 78 "tree", 79 "jq", 80 "acl", 81 "zip", 82 "unzip", 83 "less", 84 "ca-certificates", 85 "man-db", 86 "build-essential", 87 "python3-venv", 88 "python3-pip", 89 "ripgrep", 90 "shellcheck", 91 "e2fsprogs", 92 ] 93 94 @dataclass 95 class CmdResult: 96 cmd: list[str] 97 returncode: int 98 stdout: str 99 stderr: str 100 101 102 class BootstrapError(RuntimeError): 103 pass 104 105 106 def eprint(msg: str) -> None: 107 print(msg, file=sys.stderr) 108 109 110 def require_root() -> None: 111 if os.geteuid() != 0: 112 raise BootstrapError("Este script precisa rodar como root (use sudo).") 113 114 115 def run( 116 cmd: list[str], 117 *, 118 dry_run: bool = False, 119 verbose: bool = False, 120 check: bool = True, 121 env: dict[str, str] | None = None, 122 ) -> CmdResult: 123 if verbose or dry_run: 124 eprint("$ " + " ".join(shlex.quote(part) for part in cmd)) 125 if dry_run: 126 return CmdResult(cmd, 0, "", "") 127 proc = subprocess.run( 128 cmd, 129 text=True, 130 capture_output=True, 131 env=env, 132 check=False, 133 ) 134 if verbose and proc.stdout: 135 eprint(proc.stdout.rstrip()) 136 if verbose and proc.stderr: 137 eprint(proc.stderr.rstrip()) 138 if check and proc.returncode != 0: 139 raise BootstrapError( 140 f"Comando falhou ({proc.returncode}): {' '.join(cmd)}\n" 141 f"STDOUT:\n{proc.stdout}\nSTDERR:\n{proc.stderr}" 142 ) 143 return CmdResult(cmd, proc.returncode, proc.stdout, proc.stderr) 144 145 146 def command_exists(name: str) -> bool: 147 return shutil.which(name) is not None 148 149 150 def apt_env() -> dict[str, str]: 151 env = os.environ.copy() 152 env["DEBIAN_FRONTEND"] = "noninteractive" 153 return env 154 155 156 def apt_update(verbose: bool, dry_run: bool) -> None: 157 run(["apt-get", "update"], verbose=verbose, dry_run=dry_run, env=apt_env()) 158 159 160 def apt_install(packages: list[str], verbose: bool, dry_run: bool) -> None: 161 run( 162 ["apt-get", "install", "-y", *packages], 163 verbose=verbose, 164 dry_run=dry_run, 165 env=apt_env(), 166 ) 167 168 169 def apt_cleanup(verbose: bool, dry_run: bool) -> None: 170 run(["apt-get", "autoremove", "-y"], verbose=verbose, dry_run=dry_run, env=apt_env()) 171 run(["apt-get", "autoclean", "-y"], verbose=verbose, dry_run=dry_run, env=apt_env()) 172 173 174 def get_mount_kernel_view(mountpoint: str, *, verbose: bool) -> tuple[str, str, list[str]]: 175 """Lê TARGET,FSTYPE,OPTIONS do kernel para um mountpoint (ex. ``/`` ou ``/home``).""" 176 res = run( 177 ["findmnt", "-no", "TARGET,FSTYPE,OPTIONS", mountpoint], 178 verbose=verbose, 179 dry_run=False, 180 ) 181 line = res.stdout.strip() 182 if not line: 183 raise BootstrapError( 184 f"Não consegui obter informações do mount {mountpoint!r} com findmnt." 185 ) 186 parts = line.split(maxsplit=2) 187 if len(parts) != 3: 188 raise BootstrapError(f"Saída inesperada do findmnt: {line!r}") 189 target, fstype, options = parts 190 opts_list = [opt.strip() for opt in options.split(",") if opt.strip()] 191 return target, fstype, opts_list 192 193 194 def mount_options_indicate_user_quota(options: list[str]) -> bool: 195 """usrquota explícito ou journaled (usrjquota=...).""" 196 blob = ",".join(options) 197 return "usrquota" in blob or "usrjquota" in blob 198 199 200 def discover_quota_mountpoint(home_probe: Path, verbose: bool) -> str: 201 """ 202 O mesmo critério que create_runv_user / setquota: filesystem que contém o path de sonda 203 (por omissão ``/home``). Pode ser ``/`` ou ``/home`` (volume dedicado), etc. 204 """ 205 try: 206 tgt, fst, opts_csv = find_mount_triple(home_probe) 207 except MountLookupError as e: 208 raise BootstrapError( 209 f"Não foi possível descobrir em que filesystem {home_probe} está montado. " 210 f"Detalhe: {e}" 211 ) from e 212 if verbose: 213 eprint( 214 f"Deteção automática: {home_probe} → mount {tgt!r}, fstype {fst}, opções {opts_csv!r}" 215 ) 216 if fst != "ext4": 217 raise BootstrapError( 218 f"O filesystem que contém {home_probe} está em {tgt!r} com tipo {fst!r}. " 219 "Só configuramos quotas automaticamente para ext4 (alinhado a create_runv_user.py). " 220 "Noutro tipo de FS, configure quotas manualmente ou use uma VPS com /home em ext4." 221 ) 222 return tgt 223 224 225 def backup_fstab(verbose: bool, dry_run: bool) -> Path: 226 timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") 227 backup_path = BACKUP_DIR / f"fstab.{timestamp}.bak" 228 if verbose or dry_run: 229 eprint(f"Backup do fstab: {backup_path}") 230 if not dry_run: 231 BACKUP_DIR.mkdir(parents=True, exist_ok=True) 232 shutil.copy2(FSTAB, backup_path) 233 return backup_path 234 235 236 def ensure_usrquota_in_fstab(mountpoint: str, *, dry_run: bool, verbose: bool) -> bool: 237 """ 238 Garante usrquota na linha do fstab que monta ``mountpoint`` como ext4. 239 Retorna True se o arquivo foi alterado. 240 """ 241 content = FSTAB.read_text(encoding="utf-8").splitlines(keepends=True) 242 changed = False 243 new_lines: list[str] = [] 244 245 for line in content: 246 stripped = line.strip() 247 if not stripped or stripped.startswith("#"): 248 new_lines.append(line) 249 continue 250 251 parts = line.split() 252 if len(parts) < 4: 253 new_lines.append(line) 254 continue 255 256 _device, mp, fstype, options = parts[:4] 257 if mp == mountpoint and fstype == "ext4": 258 opts = [o for o in options.split(",") if o] 259 if "usrquota" not in opts: 260 opts.append("usrquota") 261 parts[3] = ",".join(opts) 262 newline = "\t".join(parts) 263 if not newline.endswith("\n"): 264 newline += "\n" 265 new_lines.append(newline) 266 changed = True 267 continue 268 new_lines.append(line) 269 270 if changed: 271 backup_fstab(verbose, dry_run) 272 if verbose or dry_run: 273 eprint(f"Atualizando /etc/fstab para incluir usrquota em {mountpoint!r}") 274 if not dry_run: 275 FSTAB.write_text("".join(new_lines), encoding="utf-8") 276 else: 277 if verbose: 278 eprint(f"/etc/fstab já contém usrquota para {mountpoint!r} (ext4)") 279 return changed 280 281 282 def remount_with_usrquota(mountpoint: str, verbose: bool, dry_run: bool) -> None: 283 run( 284 ["mount", "-o", "remount,usrquota", mountpoint], 285 verbose=verbose, 286 dry_run=dry_run, 287 ) 288 if dry_run or mount_has_user_quota(mountpoint, verbose): 289 return 290 if verbose: 291 eprint( 292 f"Aviso: usrquota ainda não aparece em {mountpoint!r}; " 293 "tentando remount genérico..." 294 ) 295 run(["mount", "-o", "remount", mountpoint], verbose=verbose, dry_run=dry_run) 296 297 298 def mount_has_user_quota(mountpoint: str, verbose: bool = False) -> bool: 299 _, _, options = get_mount_kernel_view(mountpoint, verbose=verbose) 300 return mount_options_indicate_user_quota(options) 301 302 303 def dry_run_assume_quota_active( 304 *, 305 dry_run: bool, 306 fstab_changed: bool, 307 skip_remount: bool, 308 ) -> bool: 309 """ 310 Em --dry-run não escrevemos fstab nem remontamos de verdade; o findmnt 311 continua sem usrquota. Assumimos sucesso só para completar o plano de quotas. 312 """ 313 if not dry_run: 314 return False 315 if skip_remount and fstab_changed: 316 eprint( 317 "AVISO (dry-run): com --skip-remount e fstab a alterar, " 318 "em execução real seria preciso remount ou reboot antes de quotacheck." 319 ) 320 return True 321 322 323 def quota_mount_ready( 324 mountpoint: str, 325 verbose: bool, 326 *, 327 dry_run: bool, 328 dry_run_trust: bool, 329 ) -> bool: 330 if dry_run_trust: 331 return True 332 return mount_has_user_quota(mountpoint, verbose) 333 334 335 def quota_tools_present() -> list[str]: 336 required = ["quotacheck", "quotaon", "setquota", "quota"] 337 return [tool for tool in required if command_exists(tool)] 338 339 340 def run_quotacheck_escalation( 341 mountpoint: str, 342 *, 343 verbose: bool, 344 dry_run: bool, 345 allow_live_scan: bool, 346 ) -> None: 347 """ 348 Cria/atualiza aquota.* com quotacheck. 349 350 Após ``remount,usrquota``, o kernel pode já reportar quotas de utilizador 351 ativas; nesse caso o quotacheck recusa-se sem ``-f`` («use -f to force»). 352 Tentamos primeiro varreduras normais e só depois variantes com ``-f``. 353 """ 354 if allow_live_scan: 355 sequences: list[tuple[list[str], str]] = [ 356 (["quotacheck", "-cuM", mountpoint], "quotacheck -cuM"), 357 (["quotacheck", "-cuM", "-f", mountpoint], "quotacheck -cuM -f"), 358 ] 359 else: 360 sequences = [ 361 (["quotacheck", "-cu", mountpoint], "quotacheck -cu"), 362 (["quotacheck", "-cuM", mountpoint], "quotacheck -cuM"), 363 (["quotacheck", "-cuM", "-f", mountpoint], "quotacheck -cuM -f"), 364 (["quotacheck", "-cu", "-f", mountpoint], "quotacheck -cu -f"), 365 ] 366 367 last_exc: BootstrapError | None = None 368 for i, (cmd, label) in enumerate(sequences): 369 try: 370 run(cmd, verbose=verbose, dry_run=dry_run) 371 return 372 except BootstrapError as exc: 373 last_exc = exc 374 if dry_run: 375 raise 376 if i + 1 < len(sequences): 377 eprint(f"{label} falhou; a tentar método seguinte...") 378 379 assert last_exc is not None 380 tried = ", ".join(label for _cmd, label in sequences) 381 raise BootstrapError( 382 f"quotacheck falhou após tentar: {tried}.\nÚltimo erro:\n{last_exc}\n" 383 "Se a mensagem falar de quotas nativas ext4 (tune2fs -O quota) vs ficheiros " 384 "aquota.*, veja a secção de quotas em starthere.md; em muitos casos " 385 "-f resolve após remount com usrquota já ativo." 386 ) from last_exc 387 388 389 def _quotaon_stderr_implies_already_active(text: str) -> bool: 390 """EBUSY típico quando usrquota já ficou ativo no remount e aquota.* está em uso.""" 391 t = text.lower() 392 return "device or resource busy" in t or ( 393 "resource busy" in t and "quotaon" in t 394 ) 395 396 397 def run_quotaon_user_vu(mountpoint: str, *, verbose: bool, dry_run: bool) -> None: 398 """ 399 Executa ``quotaon -vu``. Se falhar com EBUSY, assume quotas de utilizador 400 já ativas (comum após ``remount,usrquota`` + quotacheck) e continua. 401 """ 402 res = run( 403 ["quotaon", "-vu", mountpoint], 404 verbose=verbose, 405 dry_run=dry_run, 406 check=False, 407 ) 408 if dry_run: 409 return 410 if res.returncode == 0: 411 return 412 combined = (res.stderr or "") + (res.stdout or "") 413 if _quotaon_stderr_implies_already_active(combined): 414 qmp = shlex.quote(mountpoint) 415 eprint( 416 "quotaon: quotas de utilizador já ativas neste mount (Device or resource busy); " 417 f"a continuar. Confirme com «quota -vs» ou «sudo repquota -s {qmp}»." 418 ) 419 return 420 raise BootstrapError( 421 f"Comando falhou ({res.returncode}): quotaon -vu {shlex.quote(mountpoint)}\n" 422 f"STDOUT:\n{res.stdout}\nSTDERR:\n{res.stderr}" 423 ) 424 425 426 def init_quotas( 427 mountpoint: str, 428 verbose: bool, 429 dry_run: bool, 430 *, 431 allow_live_scan: bool, 432 ) -> None: 433 present = quota_tools_present() 434 missing = [tool for tool in ["quotacheck", "quotaon", "setquota", "quota"] if tool not in present] 435 if missing: 436 raise BootstrapError( 437 "Ferramentas de quota ausentes mesmo após instalar o pacote quota: " 438 + ", ".join(missing) 439 ) 440 441 if not dry_run and not mount_has_user_quota(mountpoint, False): 442 raise BootstrapError( 443 f"O mount {mountpoint!r} ainda não mostra usrquota ativo. " 444 "Reinicie a VM ou confirme o remount antes de prosseguir." 445 ) 446 447 run_quotacheck_escalation( 448 mountpoint, 449 verbose=verbose, 450 dry_run=dry_run, 451 allow_live_scan=allow_live_scan, 452 ) 453 454 run_quotaon_user_vu(mountpoint, verbose=verbose, dry_run=dry_run) 455 456 457 def block_device_for_mount(mountpoint: str) -> str | None: 458 res = run( 459 ["findmnt", "-no", "SOURCE", mountpoint], 460 verbose=False, 461 dry_run=False, 462 check=False, 463 ) 464 if res.returncode != 0: 465 return None 466 dev = res.stdout.strip() 467 if not dev or dev == "none": 468 return None 469 return dev 470 471 472 def ext4_has_internal_quota_feature(device: str) -> bool | None: 473 """True se `tune2fs -l` lista a feature «quota» (quotas nativas ext4).""" 474 proc = subprocess.run( 475 ["tune2fs", "-l", device], 476 text=True, 477 capture_output=True, 478 check=False, 479 timeout=60, 480 ) 481 if proc.returncode != 0: 482 return None 483 for line in proc.stdout.splitlines(): 484 if line.startswith("Filesystem features:"): 485 _label, _, rest = line.partition(":") 486 parts = rest.split() 487 return "quota" in parts 488 return False 489 490 491 def note_ext4_quota_deprecation_context(mountpoint: str) -> None: 492 """ 493 Explica os avisos «external quota files» / tune2fs -O quota após sucesso. 494 """ 495 dev = block_device_for_mount(mountpoint) 496 if not dev: 497 return 498 internal = ext4_has_internal_quota_feature(dev) 499 if internal is True: 500 return 501 eprint( 502 "Nota (ext4): os avisos de quotacheck/quotaon sobre «external quota files» " 503 "e «tune2fs -O quota» aparecem quando a feature interna «quota» do ext4 " 504 "ainda não está ligada no dispositivo — o script usa o modo clássico " 505 "(usrquota + aquota.*), que continua válido; confirme com «quota -vs». " 506 "Migrar para o modo recomendado pelo kernel exige janela de manutenção " 507 f"(desmontar {dev}, «tune2fs -O quota», remontar); ver starthere.md." 508 ) 509 510 511 def ufw_status_text() -> str: 512 proc = subprocess.run( 513 ["ufw", "status"], 514 text=True, 515 capture_output=True, 516 check=False, 517 ) 518 return (proc.stdout or "") + (proc.stderr or "") 519 520 521 def configure_ufw(verbose: bool, dry_run: bool) -> None: 522 """Habilita UFW só se estiver inativo; preserva SSH antes de fechar.""" 523 if not command_exists("ufw"): 524 eprint("AVISO: comando ufw ausente; instale o pacote ufw ou não use --no-install.") 525 return 526 txt = ufw_status_text().lower() 527 if "inactive" not in txt: 528 if verbose: 529 eprint("UFW já está ativo ou estado não reconhecido; não altero regras.") 530 return 531 if verbose or dry_run: 532 eprint("UFW inativo: permitindo SSH, HTTP, HTTPS e ativando.") 533 run(["ufw", "allow", "OpenSSH"], verbose=verbose, dry_run=dry_run) 534 run(["ufw", "allow", "80/tcp"], verbose=verbose, dry_run=dry_run) 535 run(["ufw", "allow", "443/tcp"], verbose=verbose, dry_run=dry_run) 536 run(["ufw", "--force", "enable"], verbose=verbose, dry_run=dry_run) 537 538 539 def configure_apache(verbose: bool, dry_run: bool) -> None: 540 if not command_exists("systemctl"): 541 eprint("AVISO: systemctl ausente; não configurei Apache.") 542 return 543 run(["systemctl", "enable", "apache2"], verbose=verbose, dry_run=dry_run) 544 run(["systemctl", "start", "apache2"], verbose=verbose, dry_run=dry_run) 545 546 547 def build_parser() -> argparse.ArgumentParser: 548 p = argparse.ArgumentParser( 549 description="Bootstrap seguro do servidor runv.club (Debian/ext4 + quotas)." 550 ) 551 p.add_argument("--dry-run", action="store_true", help="Mostra o plano sem executar.") 552 p.add_argument("--verbose", action="store_true", help="Mostra mais detalhes.") 553 p.add_argument( 554 "--packages", 555 nargs="*", 556 default=BASE_PACKAGES, 557 help="Lista de pacotes a instalar (padrão conservador incluído).", 558 ) 559 p.add_argument( 560 "--no-cleanup", 561 action="store_true", 562 help="Pula apt autoremove/autoclean.", 563 ) 564 p.add_argument( 565 "--no-install", 566 action="store_true", 567 help="Não instala pacotes; só verifica/configura quotas.", 568 ) 569 p.add_argument( 570 "--no-quota", 571 action="store_true", 572 help="Não mexe em quotas; só instala pacotes e faz limpeza segura.", 573 ) 574 p.add_argument( 575 "--quota-probe", 576 type=Path, 577 default=DEFAULT_QUOTA_PROBE, 578 metavar="PATH", 579 help=( 580 "Caminho para descobrir o filesystem de quotas (deve refletir onde ficam as homes " 581 f"runv; predefinido: {DEFAULT_QUOTA_PROBE})." 582 ), 583 ) 584 p.add_argument( 585 "--skip-remount", 586 action="store_true", 587 help="Não tenta remount após editar /etc/fstab.", 588 ) 589 p.add_argument( 590 "--allow-live-scan", 591 action="store_true", 592 help=( 593 "Usa só quotacheck -cuM (sem tentar antes -cu). " 594 "Por omissão o script já tenta -cu e cai para -cuM se necessário." 595 ), 596 ) 597 p.add_argument( 598 "--no-services", 599 action="store_true", 600 help="Não ativa Apache nem configura/ativa UFW (pacotes podem ser instalados).", 601 ) 602 p.add_argument( 603 "--version", 604 action="version", 605 version=f"%(prog)s {VERSION} — runv.club", 606 ) 607 return p 608 609 610 def main() -> int: 611 parser = build_parser() 612 args = parser.parse_args() 613 ensure_admin_cli( 614 script_name=Path(__file__).name, 615 dry_run=bool(args.dry_run), 616 ) 617 618 try: 619 require_root() 620 621 quota_mp: str | None = None 622 if not args.no_quota: 623 quota_mp = discover_quota_mountpoint(args.quota_probe, args.verbose) 624 625 print(f"== runv.club / starthere.py v{VERSION} ==") 626 print("Bootstrap conservador para Debian/ext4.") 627 if quota_mp is not None: 628 print( 629 f"Quotas: mount detetado para {args.quota_probe} → {quota_mp!r} (ext4, alinhado a create_runv_user)." 630 ) 631 print() 632 633 print("[1/6] Atualizando índices APT...") 634 if not args.no_install: 635 apt_update(args.verbose, args.dry_run) 636 637 print("[2/6] Instalando pacotes-base...") 638 if not args.no_install: 639 apt_install(args.packages, args.verbose, args.dry_run) 640 641 print("[3/6] Limpeza segura...") 642 if not args.no_cleanup and not args.no_install: 643 apt_cleanup(args.verbose, args.dry_run) 644 else: 645 print("Pulando limpeza segura.") 646 647 print("[4/6] Serviços (Apache, UFW)...") 648 if not args.no_services: 649 configure_apache(args.verbose, args.dry_run) 650 configure_ufw(args.verbose, args.dry_run) 651 else: 652 print("Pulado por --no-services.") 653 654 if not args.no_quota: 655 assert quota_mp is not None 656 print(f"[5/6] Ajustando /etc/fstab para usrquota em {quota_mp!r} ...") 657 changed = ensure_usrquota_in_fstab( 658 quota_mp, 659 dry_run=args.dry_run, 660 verbose=args.verbose, 661 ) 662 663 if changed and not args.skip_remount: 664 print(f"Tentando remount de {quota_mp!r} com usrquota ...") 665 try: 666 remount_with_usrquota(quota_mp, args.verbose, args.dry_run) 667 except BootstrapError as exc: 668 raise BootstrapError( 669 f"Não consegui remount de {quota_mp!r} com usrquota.\n{exc}\n" 670 "Caminho recomendado: reinicie a VM e rode o script novamente." 671 ) from exc 672 673 dry_trust = dry_run_assume_quota_active( 674 dry_run=args.dry_run, 675 fstab_changed=changed, 676 skip_remount=args.skip_remount, 677 ) 678 679 if quota_mount_ready( 680 quota_mp, 681 args.verbose, 682 dry_run=args.dry_run, 683 dry_run_trust=dry_trust, 684 ): 685 print("[6/6] Inicializando e ativando quotas...") 686 init_quotas( 687 quota_mp, 688 args.verbose, 689 args.dry_run, 690 allow_live_scan=args.allow_live_scan, 691 ) 692 print(f"Quotas de usuário ativadas em {quota_mp!r}.") 693 if not args.dry_run: 694 note_ext4_quota_deprecation_context(quota_mp) 695 else: 696 raise BootstrapError( 697 f"usrquota ainda não aparece ativo em {quota_mp!r}. " 698 "Reinicie a VM e rode o script novamente." 699 ) 700 else: 701 print("[5/6] Quotas puladas por --no-quota") 702 print("[6/6] Nada a fazer em quotas") 703 704 print() 705 print("Concluído.") 706 print("Próximos passos:") 707 if quota_mp is not None: 708 print(f"- Confirmar mount de quotas: mount | grep ' on {quota_mp} '") 709 else: 710 print("- Confirmar mounts e usrquota conforme a sua configuração") 711 print("- Checar quotas: quota -vs") 712 print("- Contas: usar create_runv_user.py (setquota) conforme create_runv_user.md") 713 print("- Reinício (se precisar): sudo reboot ou /sbin/reboot") 714 return 0 715 716 except BootstrapError as exc: 717 eprint(f"ERRO: {exc}") 718 return 2 719 except KeyboardInterrupt: 720 eprint("Interrompido pelo usuário.") 721 return 130 722 723 724 if __name__ == "__main__": 725 raise SystemExit(main())