runv-server

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

commit 02586fc84e1a4c13408396ac48808aa676baeddd
parent d3b6022f7a9654e916bd138ed5c2f0f06f76012f
Author: Pablo Murad <pablo@pablomurad.com>
Date:   Sat, 21 Mar 2026 15:46:08 -0300

chat, gemini and gopher

Diffstat:
Mscripts/admin/setup_alt_protocols.py | 102++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mscripts/docs/alt_protocols.md | 15+++++++++++----
2 files changed, 70 insertions(+), 47 deletions(-)

diff --git a/scripts/admin/setup_alt_protocols.py b/scripts/admin/setup_alt_protocols.py @@ -7,7 +7,7 @@ Infraestrutura Gopher (gophernicus) e Gemini (molly-brown) para runv.club. Idempotente, dry-run, subprocess sem shell. Executar como root no Debian. -Versão 0.04 — runv.club +Versão 0.05 — runv.club """ from __future__ import annotations @@ -30,7 +30,7 @@ from typing import Any, Final # Constantes # --------------------------------------------------------------------------- -VERSION: Final[str] = "0.04" +VERSION: Final[str] = "0.05" DEFAULT_USERS_JSON: Final[Path] = Path("/var/lib/runv/users.json") DEFAULT_HOMES_ROOT: Final[Path] = Path("/home") @@ -47,7 +47,17 @@ GOPHER_SYSTEMD_SERVICE: Final[Path] = Path("/lib/systemd/system/gophernicus@.ser MOLLY_CONF_DIR: Final[Path] = Path("/etc/molly-brown") MOLLY_INSTANCE: Final[str] = "runv.club" # molly-brown@runv.club.service MOLLY_LOG_DIR: Final[Path] = Path("/var/log/molly-brown") -MOLLY_SERVICE_USER_FALLBACK: Final[str] = "molly-brown" +MOLLY_SYSTEMD_DROPIN_DIR: Final[Path] = Path( + "/etc/systemd/system/molly-brown@.service.d" +) +MOLLY_LOGS_DROPIN_PATH: Final[Path] = ( + MOLLY_SYSTEMD_DROPIN_DIR / "50-runv-logs.conf" +) +MOLLY_LOGS_DROPIN_BODY: Final[str] = ( + "# runv.club — gerido por setup_alt_protocols.py\n" + "[Service]\n" + "LogsDirectory=molly-brown\n" +) PACKAGES_GOPHER: Final[tuple[str, ...]] = ("gophernicus",) PACKAGES_GEMINI: Final[tuple[str, ...]] = ("molly-brown",) @@ -178,23 +188,37 @@ def molly_log_paths(instance: str) -> tuple[Path, Path]: ) -def molly_service_user(instance: str, log: logging.Logger) -> str: - """User= do unit systemd molly-brown@instance (fallback Debian: molly-brown).""" - unit = f"molly-brown@{instance}.service" - try: - r = subprocess.run( - ["systemctl", "show", "-p", "User", "--value", unit], - capture_output=True, - text=True, - timeout=30, +def write_molly_brown_logs_dropin( + *, + dry_run: bool, + log: logging.Logger, + force: bool, +) -> None: + """ + Garante LogsDirectory=molly-brown no unit molly-brown@. + + O pacote Debian usa DynamicUser=yes; o systemd cria /var/log/molly-brown com + o dono correcto em cada arranque — não usar chown estático nos logs. + """ + if dry_run: + log.info( + "[dry-run] gravaria %s (LogsDirectory=molly-brown)", + MOLLY_LOGS_DROPIN_PATH, ) - if r.returncode == 0: - name = (r.stdout or "").strip() - if name and name != "(null)": - return name - except (OSError, subprocess.TimeoutExpired) as e: - log.debug("systemctl User para %s: %s", unit, e) - return MOLLY_SERVICE_USER_FALLBACK + return + if MOLLY_LOGS_DROPIN_PATH.is_file() and not force: + existing = MOLLY_LOGS_DROPIN_PATH.read_text(encoding="utf-8", errors="replace") + if existing.strip() == MOLLY_LOGS_DROPIN_BODY.strip(): + log.debug("drop-in Molly logs já presente: %s", MOLLY_LOGS_DROPIN_PATH) + return + log.info("drop-in Molly logs existe com conteúdo diferente — use --force") + return + if MOLLY_LOGS_DROPIN_PATH.is_file() and force: + backup_if_exists(MOLLY_LOGS_DROPIN_PATH, log, dry_run=False) + MOLLY_SYSTEMD_DROPIN_DIR.mkdir(parents=True, exist_ok=True) + MOLLY_LOGS_DROPIN_PATH.write_text(MOLLY_LOGS_DROPIN_BODY, encoding="utf-8") + os.chmod(MOLLY_LOGS_DROPIN_PATH, 0o644) + log.info("systemd drop-in Molly: %s", MOLLY_LOGS_DROPIN_PATH) def ensure_molly_log_files( @@ -204,44 +228,31 @@ def ensure_molly_log_files( log: logging.Logger, ) -> tuple[Path, Path]: """ - Cria /var/log/molly-brown e ficheiros de log com dono = User do serviço. - Molly-brown não aceita AccessLog/ErrorLog = \"-\" (interpreta como path /- e falha). + Garante caminhos de log sob /var/log/molly-brown/ (criados vazios se faltarem). + + O dono correcto fica a cargo do systemd via drop-in LogsDirectory=molly-brown + (DynamicUser no Debian). Molly-brown não aceita AccessLog/ErrorLog = \"-\" + (interpreta como path /- e falha). """ access_p, error_p = molly_log_paths(instance) - user_name = molly_service_user(instance, log) if dry_run: log.info( - "[dry-run] criaria %s e %s, %s (dono: %s)", + "[dry-run] criaria %s, %s, %s (dono ajustado pelo systemd no arranque)", MOLLY_LOG_DIR, access_p, error_p, - user_name, ) return access_p, error_p MOLLY_LOG_DIR.mkdir(parents=True, exist_ok=True) - os.chmod(MOLLY_LOG_DIR, 0o755) - - try: - pw = pwd.getpwnam(user_name) - uid, gid = pw.pw_uid, pw.pw_gid - except KeyError: - log.warning( - "Utilizador «%s» inexistente — logs com dono root; o serviço pode falhar ao escrever", - user_name, - ) - uid, gid = 0, 0 - for p in (access_p, error_p): if not p.exists(): p.touch(exist_ok=True) - try: - os.chown(p, uid, gid) - os.chmod(p, 0o644) - except OSError as e: - log.warning("chown/chmod %s: %s", p, e) - - log.info("logs Molly: %s, %s (dono %s)", access_p, error_p, user_name) + log.info( + "logs Molly: %s, %s (requer drop-in LogsDirectory; systemd ajusta dono)", + access_p, + error_p, + ) return access_p, error_p @@ -671,6 +682,11 @@ def main(argv: list[str] | None = None) -> int: key, ) else: + write_molly_brown_logs_dropin( + dry_run=args.dry_run, + log=log, + force=args.force, + ) access_p, error_p = ensure_molly_log_files( MOLLY_INSTANCE, dry_run=args.dry_run, diff --git a/scripts/docs/alt_protocols.md b/scripts/docs/alt_protocols.md @@ -26,10 +26,17 @@ Script em **`scripts/admin/setup_alt_protocols.py`**: instala e configura **goph O **molly-brown** trata `AccessLog` e `ErrorLog` como **caminhos de ficheiro**. Valores como `"-"` (estilo «stdout» noutros programas) são interpretados de forma errada e o processo tenta abrir `/-`, falhando de imediato. -- **Comportamento actual do script (v0.03+):** cria `/var/log/molly-brown/`, ficheiros `runv.club-access.log` e `runv.club-error.log`, ajusta o dono ao `User=` do unit (`systemctl show`, fallback `molly-brown`), e grava esses caminhos em `/etc/molly-brown/runv.club.conf`. -- **Servidor já provisionado com conf antiga:** o script só reescreve o `.conf` se o ficheiro não existir ou se correr com **`--force`** (faz backup com timestamp). Exemplo: +- **Comportamento actual do script (v0.05+):** instala o drop-in systemd **`/etc/systemd/system/molly-brown@.service.d/50-runv-logs.conf`** com `LogsDirectory=molly-brown`, para o systemd criar/ajustar **`/var/log/molly-brown`** com o dono correcto em cada arranque (necessário porque o pacote Debian usa **`DynamicUser=yes`** — um `chown` baseado em `getpwnam` ou no nome `User=` **não** coincide com o UID dinâmico real). Cria também os ficheiros `runv.club-access.log` e `runv.club-error.log` se faltarem, e grava os caminhos absolutos em `/etc/molly-brown/runv.club.conf`. +- **Servidor já provisionado com conf antiga:** o script só reescreve o `.conf` (e o drop-in, se já existir com outro conteúdo) se correr com **`--force`** (faz backup com timestamp onde aplicável). Exemplo: `sudo python3 scripts/admin/setup_alt_protocols.py --verbose --force` -- **Correcção manual rápida:** `sudo mkdir -p /var/log/molly-brown`; criar os dois `.log`; `sudo chown` para o utilizador do serviço (`systemctl show -p User --value molly-brown@runv.club.service`); editar o `.conf` e substituir `AccessLog` / `ErrorLog` pelos caminhos absolutos; depois `sudo systemctl reset-failed molly-brown@runv.club.service` e `sudo systemctl start molly-brown@runv.club.service`. +- **Correcção manual rápida (só `.conf`):** editar `AccessLog` / `ErrorLog` para caminhos absolutos sob `/var/log/molly-brown/`; garantir o drop-in `LogsDirectory=molly-brown` como acima; `sudo systemctl daemon-reload`; `sudo systemctl reset-failed molly-brown@runv.club.service` e `sudo systemctl start molly-brown@runv.club.service`. + +## Erro `permission denied` em `/var/log/molly-brown/…-error.log` + +Aparece quando os ficheiros de log ficaram com dono **root** ou outro UID que **não** é o do processo molly-brown. No Debian, o unit **`molly-brown@.service`** usa **`DynamicUser=yes`**: o utilizador de runtime é gerido pelo systemd, por isso **`sudo chown molly-brown:molly-brown`** (utilizador estático em `/etc/passwd`, se existir) **não** resolve de forma fiável. + +- **Solução suportada:** o script **v0.05+** instala o drop-in com **`LogsDirectory=molly-brown`**; no arranque, o systemd corrige a propriedade de `/var/log/molly-brown`. Depois de actualizar o repo: `sudo python3 scripts/admin/setup_alt_protocols.py --verbose --force`, `sudo systemctl daemon-reload`, `sudo systemctl reset-failed molly-brown@runv.club.service`, `sudo systemctl start molly-brown@runv.club.service`. +- **Verificação:** `systemctl cat molly-brown@runv.club.service` deve mostrar o fragmento `50-runv-logs.conf` com `LogsDirectory=molly-brown`. ## Checklist rápido (conf antiga, UFW, «activating») @@ -67,7 +74,7 @@ sudo python3 scripts/admin/setup_alt_protocols.py --verbose |------|--------| | `--dry-run` | Simula; não grava (validação de root ignorada em alguns passos só se documentado). | | `--verbose` | Log detalhado. | -| `--force` | Sobrescreve configs de sistema (com backup com timestamp) e ficheiros modelo no backfill. Necessário para **regravar** `/etc/molly-brown/runv.club.conf` após correcções (ex. logs Molly). | +| `--force` | Sobrescreve configs de sistema (com backup com timestamp) e ficheiros modelo no backfill. Necessário para **regravar** `/etc/molly-brown/runv.club.conf` ou o drop-in **`50-runv-logs.conf`** após correcções (ex. logs Molly / `DynamicUser`). | | `--skip-install` | Não corre `apt-get`. | | `--skip-gopher` / `--skip-gemini` | Ignora pacote, config e serviço desse protocolo. | | `--skip-firewall` | Não altera UFW. |