runv-server

server tooling for runv.club
Log | Files | Refs | README

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