yetgg.py (5537B)
1 #!/usr/bin/env python3 2 """ 3 runv.club — backfill Gopher/Gemini para utilizadores já registados. 4 5 Cria ``~/public_gopher``, ``~/public_gemini`` (modelos) e bind mounts em 6 ``/var/gemini/users/<user>``, usando a **mesma lista de contas** que o IRC 7 (união ``users.json`` + ``/home``, filtro ``IRC_PATCH_SKIP_USERS``). 8 9 Não instala pacotes nem serviços; ver ``scripts/admin/setup_alt_protocols.py``. 10 11 Executar como root em produção. Ver ``--help``. 12 """ 13 14 from __future__ import annotations 15 16 import argparse 17 import importlib.util 18 import logging 19 import os 20 import sys 21 from pathlib import Path 22 from typing import Any, Final 23 24 VERSION: Final[str] = "0.02" 25 26 GEMINI_ROOT: Final[Path] = Path("/var/gemini") 27 GEMINI_USERS: Final[Path] = GEMINI_ROOT / "users" 28 29 30 def eprint(msg: str) -> None: 31 print(msg, file=sys.stderr) 32 33 34 def repo_root() -> Path: 35 return Path(__file__).resolve().parent.parent 36 37 38 _ADMIN_DIR = repo_root() / "scripts" / "admin" 39 if str(_ADMIN_DIR) not in sys.path: 40 sys.path.insert(0, str(_ADMIN_DIR)) 41 42 from admin_guard import ensure_admin_cli 43 44 45 def load_script_module(name: str, path: Path) -> Any: 46 spec = importlib.util.spec_from_file_location(name, path) 47 if spec is None or spec.loader is None: 48 raise ImportError(f"não foi possível carregar {path}") 49 mod = importlib.util.module_from_spec(spec) 50 spec.loader.exec_module(mod) 51 return mod 52 53 54 def setup_logging(verbose: bool) -> logging.Logger: 55 logging.basicConfig( 56 level=logging.DEBUG if verbose else logging.INFO, 57 format="%(levelname)s: %(message)s", 58 ) 59 return logging.getLogger("yetgg") 60 61 62 def require_root(log: logging.Logger) -> None: 63 if os.geteuid() != 0: 64 log.error("Execute como root (sudo).") 65 raise SystemExit(1) 66 67 68 def ensure_gemini_users_tree(*, dry_run: bool, log: logging.Logger) -> None: 69 if GEMINI_USERS.is_dir(): 70 return 71 log.warning("%s inexistente — criar antes dos bind mounts Gemini", GEMINI_USERS) 72 if dry_run: 73 log.info("[dry-run] mkdir -p %s %s (755 root:root)", GEMINI_ROOT, GEMINI_USERS) 74 return 75 GEMINI_ROOT.mkdir(parents=True, exist_ok=True) 76 GEMINI_USERS.mkdir(parents=True, exist_ok=True) 77 os.chmod(GEMINI_ROOT, 0o755) 78 os.chmod(GEMINI_USERS, 0o755) 79 try: 80 os.chown(GEMINI_ROOT, 0, 0) 81 os.chown(GEMINI_USERS, 0, 0) 82 except OSError as e: 83 log.warning("chown em %s / %s: %s", GEMINI_ROOT, GEMINI_USERS, e) 84 log.info("criado: %s e %s", GEMINI_ROOT, GEMINI_USERS) 85 86 87 def parse_args(argv: list[str] | None) -> argparse.Namespace: 88 p = argparse.ArgumentParser( 89 description="Backfill Gopher/Gemini por utilizador (lista como patch_irc).", 90 ) 91 p.add_argument("--dry-run", action="store_true", help="só simular") 92 p.add_argument("--verbose", action="store_true", help="log detalhado") 93 p.add_argument( 94 "--force", 95 action="store_true", 96 help="sobrescrever gophermap / bind Gemini (como setup_alt_protocols); index.gmi existente mantém-se", 97 ) 98 p.add_argument( 99 "--users-json", 100 type=Path, 101 default=Path("/var/lib/runv/users.json"), 102 metavar="PATH", 103 ) 104 p.add_argument( 105 "--homes-root", 106 type=Path, 107 default=Path("/home"), 108 metavar="PATH", 109 ) 110 p.add_argument("--version", action="version", version=f"%(prog)s {VERSION}") 111 return p.parse_args(argv) 112 113 114 def main(argv: list[str] | None = None) -> int: 115 args = parse_args(argv) 116 ensure_admin_cli( 117 script_name=Path(__file__).name, 118 dry_run=bool(args.dry_run), 119 ) 120 log = setup_logging(args.verbose) 121 122 if not args.dry_run: 123 require_root(log) 124 else: 125 log.info("dry-run: não grava alterações.") 126 127 root = repo_root() 128 patch_irc_path = root / "patches" / "patch_irc.py" 129 alt_path = root / "scripts" / "admin" / "setup_alt_protocols.py" 130 if not patch_irc_path.is_file(): 131 log.error("ficheiro em falta: %s", patch_irc_path) 132 return 1 133 if not alt_path.is_file(): 134 log.error("ficheiro em falta: %s", alt_path) 135 return 1 136 137 patch_irc = load_script_module("patch_irc_dynamic", patch_irc_path) 138 setup_alt = load_script_module("setup_alt_protocols_dynamic", alt_path) 139 140 resolve_all_users = patch_irc.resolve_all_users 141 ensure_user_public_dirs = setup_alt.ensure_user_public_dirs 142 ensure_gemini_bind_mount = setup_alt.ensure_gemini_bind_mount 143 144 users = resolve_all_users(args.users_json, args.homes_root, log) 145 ensure_gemini_users_tree(dry_run=args.dry_run, log=log) 146 147 failures = 0 148 for username in users: 149 try: 150 ensure_user_public_dirs( 151 username, 152 args.homes_root, 153 force=args.force, 154 dry_run=args.dry_run, 155 log=log, 156 ) 157 ensure_gemini_bind_mount( 158 username, 159 args.homes_root, 160 force=args.force, 161 dry_run=args.dry_run, 162 log=log, 163 ) 164 except OSError as e: 165 log.error("%s: %s", username, e) 166 failures += 1 167 168 print() 169 print("========== yetgg — resumo ==========") 170 print(f"Modo: {'DRY-RUN' if args.dry_run else 'aplicação'}") 171 print(f"Utilizadores na lista: {len(users)} falhas: {failures}") 172 print(f"JSON: {args.users_json} homes: {args.homes_root}") 173 print("====================================") 174 175 return 1 if failures else 0 176 177 178 if __name__ == "__main__": 179 raise SystemExit(main())