runv-server

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

commit 42f7676f4f34c395279a7e40212c29ac7f196557
parent e2e74bc0461b9fcaf1b180a0c5eba1d35d0c288d
Author: Pablo Murad <pablo@pablomurad.com>
Date:   Sat, 21 Mar 2026 20:49:17 -0300

fixed a lot of stuff

Diffstat:
M.gitignore | 3+++
Mterminal/README.md | 5+++--
Mterminal/config.example.toml | 3++-
Mterminal/docs/INSTALL.md | 11++++++++++-
Aterminal/gen_config_toml.py | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mterminal/setup_entre.py | 29+++++++++++++++++++++--------
6 files changed, 144 insertions(+), 12 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -8,6 +8,9 @@ venv/ .env.local guide.md +# config do módulo entre: gerar com terminal/gen_config_toml.py (não versionar) +terminal/config.toml + # Gerados no servidor / localmente (evita conflitos em git pull) site/public/news/data/news.json site/public/news/feed.rss diff --git a/terminal/README.md b/terminal/README.md @@ -11,7 +11,8 @@ Módulo **runv.club** para quem se liga por SSH ao utilizador Unix **`entre`**: | `entre_app.py` | Programa principal (ForceCommand SSH). | | `entre_core.py` | Validação, fila JSON, log, email. | | `setup_entre.py` | Instalação no servidor (root): utilizador `entre`, shell `/bin/sh`, `--auth-mode` (`shared-password` \| `key-only` \| `empty-password` estilo tilde.town), PAM opcional, drop-in SSH, `sshd -t` + `sshd -T -C`, reload. | -| `config.example.toml` | Modelo de configuração → copiar para `config.toml`. | +| `config.example.toml` | Modelo versionado; **não** editar como `config.toml` no git. | +| `gen_config_toml.py` | Gera `config.toml` a partir do example (evita conflitos em `git pull`). | | `templates/*.txt` | Textos da experiência e do email ao admin. | | `docs/USO.md` | **Instalação + uso** (admin, visitante, testes, checklist). | | `docs/INSTALL.md` | Guia de instalação detalhado (Debian 13). | @@ -26,7 +27,7 @@ Em linhas: 1. Como root: `python3 setup_entre.py` (ou `scripts/install.sh`) — por omissão `--auth-mode shared-password`, shell `/bin/sh`, validação `sshd -T -C …`. 2. **Onboarding sem senha (estilo tilde.town):** `sudo python3 setup_entre.py --auth-mode empty-password` — **PAM** por omissão; SSH por omissão **keyboard-interactive** (melhor no **OpenSSH do Windows**). Teste: `ssh entre@runv.club`. Se a sessão fechar, veja PAM e logs em [INSTALL.md](docs/INSTALL.md). Para o modo README tilde (**password** + senha vazia), use **`--empty-password-tilde-password-auth`** (Linux/Git Bash). -3. Editar `/opt/runv/terminal/config.toml` (`admin_email`, etc.). +3. Gerar ou ajustar `/opt/runv/terminal/config.toml` com `python3 gen_config_toml.py --install-root /opt/runv/terminal` (ou `--force` para repor o example). O ficheiro está em `.gitignore` no clone; só o **example** é versionado. 4. Modo default: `sudo passwd entre`. Modo `key-only`: `authorized_keys`. 5. Visitante: `ssh entre@runv.club` e seguir o fluxo até à despedida. diff --git a/terminal/config.example.toml b/terminal/config.example.toml @@ -1,4 +1,5 @@ -# Copie para config.toml ao instalar (ex.: /opt/runv/terminal/config.toml). +# Não edite como config.toml em produção: gere com gen_config_toml.py ou setup_entre.py. +# Ex.: python3 gen_config_toml.py --install-root /opt/runv/terminal # Valores por omissão da fila e log alinham com setup_entre.py. queue_dir = "/var/lib/runv/entre-queue" diff --git a/terminal/docs/INSTALL.md b/terminal/docs/INSTALL.md @@ -63,7 +63,16 @@ Opções úteis: ## 4. Configuração (`config.toml`) -Edite **`/opt/runv/terminal/config.toml`**: +O **`config.toml`** não deve ser versionado no repositório (está em **`.gitignore`** em `terminal/config.toml` no clone). Gere-o a partir do modelo: + +```bash +sudo python3 /opt/runv/src/terminal/gen_config_toml.py --install-root /opt/runv/terminal +``` + +- **`--force`** — sobrescreve um `config.toml` já existente (perde edições locais nesse ficheiro). +- O **`setup_entre.py`** chama a mesma lógica na primeira instalação (ou com **`--force-config`**). + +Edite **`/opt/runv/terminal/config.toml`** quando precisar de valores que não vêm do example: - **`admin_email`** — endereço para notificações. Pode ficar vazio no TOML se **`admin_email`** estiver definido em **`/etc/runv-email.json`** (fallback usado pelo `entre_app.py`). Se ambos estiverem vazios, só fila + log. - **`mail_from`** — remetente do email (cabeçalho `From`); por omissão **`noreply@runv.club`** (não use **`entre@runv.club`**: essa conta é só SSH). Valores antigos `entre@runv.club` no TOML são normalizados para noreply. Para outro remetente verificado no Mailgun, defina explicitamente no TOML. diff --git a/terminal/gen_config_toml.py b/terminal/gen_config_toml.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Gera ``config.toml`` a partir de ``config.example.toml`` (sem editar o example no git). + +Uso típico no servidor após ``git pull`` (evita conflitos se ``config.toml`` não for versionado): + + sudo python3 /opt/runv/src/terminal/gen_config_toml.py --install-root /opt/runv/terminal + +No clone local (ficheiro em ``terminal/config.toml``, ignorado pelo git): + + python3 terminal/gen_config_toml.py + +O ``setup_entre.py`` chama a mesma função ao instalar o módulo. +""" + +from __future__ import annotations + +import argparse +import shutil +import sys +from pathlib import Path +from typing import Final, Literal + +SCRIPT_DIR: Final[Path] = Path(__file__).resolve().parent + + +def write_terminal_config_toml( + *, + example: Path, + out: Path, + force: bool, + dry_run: bool, +) -> Literal["wrote", "skipped", "dry_run"]: + """ + Copia ``example`` para ``out`` se ``out`` não existir ou ``force``. + + Returns: + ``wrote``, ``skipped`` (já existia e não force), ou ``dry_run``. + """ + if not example.is_file(): + raise FileNotFoundError(f"modelo em falta: {example}") + if dry_run: + return "dry_run" + if out.is_file() and not force: + return "skipped" + out.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(example, out) + try: + out.chmod(0o640) + except OSError: + pass + return "wrote" + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Gera config.toml do módulo entre a partir de config.example.toml.", + ) + parser.add_argument( + "--install-root", + type=Path, + default=SCRIPT_DIR, + help="directório do módulo (default: pasta deste script)", + ) + parser.add_argument( + "--example", + type=Path, + default=None, + help="caminho explícito do config.example.toml (default: <install-root>/config.example.toml)", + ) + parser.add_argument( + "--force", + action="store_true", + help="sobrescrever config.toml existente", + ) + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + root = args.install_root.resolve() + example = args.example.resolve() if args.example else root / "config.example.toml" + out = root / "config.toml" + + try: + result = write_terminal_config_toml( + example=example, + out=out, + force=bool(args.force), + dry_run=bool(args.dry_run), + ) + except FileNotFoundError as e: + print(e, file=sys.stderr) + return 1 + + if result == "dry_run": + print(f"[dry-run] escreveria {out} a partir de {example}") + return 0 + if result == "skipped": + print(f"Mantido {out} (use --force para substituir pelo example).") + return 0 + print(f"Escrito {out} a partir de {example}.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/terminal/setup_entre.py b/terminal/setup_entre.py @@ -59,6 +59,8 @@ import time from pathlib import Path from typing import Final +from gen_config_toml import write_terminal_config_toml + VERSION: Final[str] = "0.11" ENTRE_USER: Final[str] = "entre" INSTALL_ROOT: Final[Path] = Path("/opt/runv/terminal") @@ -634,6 +636,7 @@ def copy_module(dest: Path, *, dry_run: bool) -> None: "entre_app.py", "entre_core.py", "config.example.toml", + "gen_config_toml.py", "README.md", ] subdirs = ["templates", "docs", "systemd", "scripts", "data", "examples"] @@ -659,16 +662,22 @@ def install_config(dest: Path, *, dry_run: bool, force: bool) -> None: cfg = dest / "config.toml" example = dest / "config.example.toml" if dry_run: - print(f"[dry-run] config em {cfg}") + print(f"[dry-run] config em {cfg} (gen_config_toml)") + return + if not example.is_file(): + eprint(f"Aviso: {example} não encontrado.") return - if cfg.is_file() and not force: - print(f"Mantido {cfg} existente (use --force-config para sobrescrever do example).") + try: + result = write_terminal_config_toml( + example=example, out=cfg, force=force, dry_run=False + ) + except FileNotFoundError as e: + eprint(str(e)) return - if example.is_file(): - shutil.copy2(example, cfg) - print(f"Instalado {cfg} a partir do example.") + if result == "skipped": + print(f"Mantido {cfg} existente (use --force-config para regenerar do example).") else: - eprint(f"Aviso: {example} não encontrado.") + print(f"Instalado {cfg} (gen_config_toml a partir do example).") def chmod_tree_templates(root: Path) -> None: @@ -690,7 +699,11 @@ def print_final_instructions( ) -> None: print() print("== Concluído ==") - print(f"1. Editar {install_root / 'config.toml'} (admin_email, etc.).") + print( + f"1. Opcional: {install_root / 'config.toml'} — regenere com " + f"python3 {install_root / 'gen_config_toml.py'} --install-root {install_root} " + "(ou --force para repor o example). Com /etc/runv-email.json, admin_email pode ficar vazio no TOML." + ) if auth_mode == AUTH_SHARED: print("2. Acesso por palavra-passe Unix partilhada (definida só pelo root):")