#!/usr/bin/env python3 """modelo.py — el MODELO PIVOTE de configuración de servicio, y el acta que lo acompaña (SDD 29). ── POR QUÉ UN PIVOTE Y NO PARES ──────────────────────────────────────────────────────────────── Traducir de a pares crece en N×M: con nginx, apache, caddy, haproxy y lighttpd son 20 traductores, y cada formato nuevo agrega 2N. Con un modelo intermedio son **N lectores + M escritores**: un formato nuevo cuesta UNO o DOS plugins, y estrena todos los pares de golpe. Es la diferencia entre un centro de traducción y una pila de conversores. ── EL ACTA VIAJA DENTRO DEL MODELO, NO AL COSTADO ────────────────────────────────────────────── Ésta es la decisión que hace que el pivote no mienta. Si el acta fuera un efecto de cada traductor, lo que un LECTOR no entendió se perdería antes de llegar al escritor. Viajando en el modelo, el escritor puede emitirlo como comentario y el humano lo ve en el fichero que va a desplegar. Doctrina heredada de `soltar`/`paskaq` (tawasuyu): **elisión honesta** — lo que no se pudo traducir se reporta con su línea y su motivo, nunca se descarta en silencio. """ from dataclasses import dataclass, field @dataclass class Accion: """Qué hace una ruta. `tipo` ∈ {proxy, archivos, redir, responder, intentar}.""" tipo: str valor: str = "" extra: dict = field(default_factory=dict) @dataclass class Ruta: """Un camino dentro de un sitio. `clase` ∈ {prefijo, exacto, regex}. `regex` se conserva a propósito aunque casi ningún destino lo soporte igual: **perder la distinción sería traducir mal en silencio**. El escritor decide si sabe emitirla o la manda al acta.""" patron: str clase: str = "prefijo" acciones: list = field(default_factory=list) origen: int = 0 @dataclass class Sitio: nombres: list = field(default_factory=list) puertos: list = field(default_factory=list) # "80", "443" tls: dict = field(default_factory=dict) # {cert, key} raiz: str = "" indices: list = field(default_factory=list) rutas: list = field(default_factory=list) acciones: list = field(default_factory=list) # a nivel de sitio origen: int = 0 @dataclass class Acta: """Registro de procedencia. Cuatro estados, y el que importa es el primero.""" entradas: list = field(default_factory=list) def anota(self, linea, estado, texto, motivo=""): # estado ∈ {SIN-TRADUCIR, decision, traducido, no-aplica} self.entradas.append({"linea": linea, "estado": estado, "directiva": texto, "motivo": motivo}) def sin_traducir(self): return [e for e in self.entradas if e["estado"] == "SIN-TRADUCIR"] def por_estado(self): d = {} for e in self.entradas: d.setdefault(e["estado"], []).append(e) return d @dataclass class Config: """Lo que un lector produce y un escritor consume.""" formato_origen: str = "" sitios: list = field(default_factory=list) acta: Acta = field(default_factory=Acta) # ── FAMILIA «servicio» ────────────────────────────────────────────────────────────────────────── # ⚠ El pivote NO es uno solo: es uno POR FAMILIA. Un unit de systemd no es un sitio web, y meterlo a # la fuerza en `Sitio`/`Ruta` sería justo el «parecerse» que el acta existe para evitar. Cada familia # tiene su modelo, y el registro sólo empareja lectores con escritores de la MISMA familia — un par # cruzado se rechaza ruidosamente en vez de producir basura plausible. @dataclass class Servicio: """Un proceso supervisado. El denominador común de systemd, OpenRC, runit, s6 y arje. `usuario` se conserva aunque el destino no siempre sepa emitirlo: es el campo donde una elisión silenciosa **escala privilegios** en vez de sólo perder una opción.""" nombre: str = "" descripcion: str = "" tipo: str = "simple" # simple | forking | oneshot | notify | dbus cmd: str = "" # la línea de arranque, tal cual cwd: str = "" usuario: str = "" grupo: str = "" entorno: dict = field(default_factory=dict) entorno_ficheros: list = field(default_factory=list) # no se pueden leer al traducir tras: list = field(default_factory=list) # orden (After=) necesita: list = field(default_factory=list) # dependencia dura (Requires=) reinicio: str = "no" # no | siempre | si-falla reinicio_ms: int = 0 arranque_auto: bool = False # ¿lo levanta el arranque? (WantedBy=) plantilla: bool = False # `foo@.service`: no instanciable a ciegas origen: int = 0 # {campo: línea} — el lector anota DÓNDE vio cada cosa para que el escritor pueda señalar la # línea real en vez de un 0. Un acta que dice «línea 0» obliga a buscar a mano lo que ya se sabía. lineas: dict = field(default_factory=dict) @dataclass class Sistema: """Lo que produce un lector de la familia «servicio» y consume su escritor.""" formato_origen: str = "" servicios: list = field(default_factory=list) acta: Acta = field(default_factory=Acta)