`openrc → arje` sin escribir ningún traductor de ese par: cuarto plugin, cuarto par. OpenRC es el init de gioser, la máquina que esta mudanza termina borrando. **El hecho que manda acá: un servicio de OpenRC es un PROGRAMA, no una declaración.** Un unit de systemd se lee; un script de OpenRC puede hacer cualquier cosa antes de arrancar nada. Medido sobre el `/etc/init.d` real: **60 de 121 definen su propia `start()`/`stop()`**. En ésos no hay `command=` que valga — leer las variables y emitir tarjeta daría un servicio que arranca OTRA COSA. Regla al revés que en systemd: `start()` propia ⇒ SIN-TRADUCIR y NO se emite tarjeta. Los números del corpus real cierran solos: 121 − 1 (no es openrc-run) − 60 (start propia) − 1 (sin `command=`) = 59, y el lector emitió exactamente 59, con 59 ids únicos. Dos cosas que OpenRC obliga a ir a buscar fuera del script: `/etc/conf.d/<x>`, donde viven los argumentos de verdad (a diferencia del `EnvironmentFile=` de systemd, éste SÍ está en la máquina: se lee y se aplica), y `/etc/runlevels/`, que es lo único que dice si el servicio arranca solo. **Tres defectos que sólo aparecieron corriendo contra las 121 de verdad**, no sobre un ejemplo mío: - `name=` no es un identificador sino un rótulo humano: en gioser vale «Aura Backend», con espacio, y se iba al id de la tarjeta y al path del cgroup. El identificador es el nombre del fichero. - Ids repetidos: `/etc/init.d` guarda copias `*.bak-FECHA` junto a los servicios vivos y son scripts válidos; dos con el mismo nombre dan el MISMO id determinista ⇒ semilla con dos cards homónimas. El escritor lo detecta y no emite la segunda. - La tarjeta decía `"desde": "systemd"` viniendo de OpenRC: el campo que existe para saber de dónde salió algo era justo el que mentía. Estaba cableado. Y el centro deja de filtrar la entrada por extensión: los servicios de OpenRC no tienen ninguna, y filtrar en el centro es que lo que no entra se pierda EN SILENCIO. Filtra el lector, que sabe, y lo anota — así un `.bak` en `/etc/init.d` se REPORTA en vez de desaparecer. Controles: dos corridas dan el fichero byte a byte idéntico; los tres pares previos sin regresión (`nginx → caddy` sigue dando `Valid configuration`, `systemd → arje` sigue emitiendo sus 3 tarjetas). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
157 lines
8.0 KiB
Python
Executable File
157 lines
8.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""traducir.py — CENTRO de traducción de configuraciones, por modelo pivote y plugins (SDD 29).
|
||
|
||
Mudar de un servicio a su equivalente no es cambiar un binario: es **reescribir la configuración**.
|
||
Esto hace la parte mecánica y —sobre todo— **dice con número de línea lo que NO pudo traducir**.
|
||
|
||
── POR QUÉ UN CENTRO Y NO TRADUCTORES DE A PARES ───────────────────────────────────────────────
|
||
De a pares el trabajo crece en **N×M**: con nginx, apache, caddy, haproxy y lighttpd son 20
|
||
traductores, y cada formato nuevo agrega 2N. Con un **modelo pivote** son **N lectores + M
|
||
escritores**: un formato nuevo cuesta uno o dos plugins y estrena todos los pares de golpe.
|
||
|
||
Probado, no argumentado: el lector de apache se agregó **sin tocar el escritor de Caddy**, y con eso
|
||
`apache → caddy` funciona sin que exista ningún traductor apache→caddy.
|
||
|
||
── LOS PLUGINS, Y LA FAMILIA ───────────────────────────────────────────────────────────────────
|
||
Un formato se agrega dejando un fichero en `formatos/` con `@lector("x")` y/o `@escritor("y")`. El
|
||
centro los DESCUBRE: no hay una lista que editar, porque una lista que hay que mantener a mano se
|
||
desincroniza y el formato nuevo «no existe» sin que nada falle.
|
||
|
||
⚠ El pivote es **uno por FAMILIA**, y eso no es taxonomía: un unit de systemd no es un sitio web, y
|
||
un par cruzado produciría basura *plausible*. El centro lo rechaza con un error, no con un intento.
|
||
Hoy hay dos familias — `web` (nginx, apache → caddy) y `servicio` (systemd → arje) — y agregar una
|
||
tercera no toca nada de lo escrito.
|
||
|
||
── DOCTRINA HEREDADA DE `soltar`/`paskaq` (tawasuyu) ───────────────────────────────────────────
|
||
`soltar` desencadena datos presos en formatos cautivos. Otro dominio, mismos principios:
|
||
**elisión honesta** (lo no traducido se reporta con línea y motivo), **no adivinar en silencio**,
|
||
**procedencia total** y **determinismo**. Y una consecuencia de diseño propia: **el acta viaja DENTRO
|
||
del modelo**, no al costado — si fuera de cada traductor, lo que el LECTOR no entendió se perdería
|
||
antes de llegar al escritor, y el fichero que uno despliega no lo diría.
|
||
|
||
Uso:
|
||
scripts/mudanza/traducir.py --list
|
||
scripts/mudanza/traducir.py --from nginx --to caddy --in nginx.conf --out Caddyfile
|
||
scripts/mudanza/traducir.py --from apache --to caddy --in httpd.conf --out Caddyfile
|
||
scripts/mudanza/traducir.py --from systemd --to arje --in /etc/systemd/system --out seed.card.json
|
||
"""
|
||
import argparse, os, sys
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import formatos # noqa: E402
|
||
|
||
|
||
def imprimir_acta(acta):
|
||
por = acta.por_estado()
|
||
print("\n══ ACTA DE TRADUCCIÓN ══")
|
||
print(f" {len(acta.entradas)} directiva(s) leídas\n")
|
||
titulo = {"SIN-TRADUCIR": "⚠ NO SE PUDO TRADUCIR — hay que escribirlas a mano",
|
||
"decision": "◆ traducidas con una DECISIÓN — revisalas",
|
||
"traducido": "✓ traducidas",
|
||
"no-aplica": "· no aplican en el destino"}
|
||
for e in ("SIN-TRADUCIR", "decision", "traducido", "no-aplica"):
|
||
if e not in por:
|
||
continue
|
||
print(f"── {titulo[e]} ({len(por[e])}) " + "─" * 22)
|
||
detalle = e in ("SIN-TRADUCIR", "decision")
|
||
for a in (por[e] if detalle else por[e][:5]):
|
||
print(f" línea {a['linea']:>4} {a['directiva'][:64]}")
|
||
if detalle and a["motivo"]:
|
||
print(f" └─ {a['motivo'][:98]}")
|
||
if not detalle and len(por[e]) > 5:
|
||
print(f" … y {len(por[e])-5} más")
|
||
print()
|
||
n = len(acta.sin_traducir())
|
||
if n:
|
||
print(f" ⚠ {n} sin traducir. Desplegar sin resolverlas es desplegar un servidor INCOMPLETO")
|
||
print(" que además parece funcionar — el modo de fallo más caro.")
|
||
return n
|
||
|
||
|
||
def _fusionar(a, b):
|
||
"""Junta dos modelos de la MISMA familia: extiende cada campo lista y concatena el acta.
|
||
|
||
Genérico a propósito — vale para `Config` y para `Sistema` sin que el centro sepa de ninguno de
|
||
los dos. Un directorio de units es la entrada NORMAL de systemd, no un caso raro."""
|
||
import dataclasses
|
||
if a is None:
|
||
return b
|
||
for f in dataclasses.fields(a):
|
||
va, vb = getattr(a, f.name), getattr(b, f.name)
|
||
if isinstance(va, list):
|
||
va.extend(vb)
|
||
a.acta.entradas.extend(b.acta.entradas)
|
||
return a
|
||
|
||
|
||
def _entradas(ruta):
|
||
"""Un fichero, o todos los `*.service`/`*.conf` de un directorio, en orden determinista."""
|
||
if os.path.isdir(ruta):
|
||
# TODOS los ficheros regulares no ocultos, no sólo los de cierta extensión: los servicios de
|
||
# OpenRC no tienen ninguna. Filtrar acá por nombre sería el centro decidiendo qué es de cada
|
||
# formato — y lo que no entra se pierde EN SILENCIO. Filtra el lector, que sabe, y lo DICE.
|
||
hs = sorted(f for f in os.listdir(ruta)
|
||
if not f.startswith(".") and os.path.isfile(os.path.join(ruta, f)))
|
||
if not hs:
|
||
sys.exit(f"✗ {ruta} no tiene ningún fichero regular")
|
||
return [os.path.join(ruta, h) for h in hs]
|
||
return [ruta]
|
||
|
||
|
||
def main():
|
||
lectores, escritores = formatos.cargar()
|
||
fams = formatos.familias()
|
||
ap = argparse.ArgumentParser(description="Centro de traducción de configuraciones (pivote + plugins).")
|
||
ap.add_argument("--list", action="store_true", help="listar formatos disponibles y los pares que habilitan")
|
||
ap.add_argument("--from", dest="desde", choices=sorted(lectores))
|
||
ap.add_argument("--to", dest="hacia", choices=sorted(escritores))
|
||
ap.add_argument("--in", dest="entrada", help="fichero, o directorio (se leen sus *.service/*.conf)")
|
||
ap.add_argument("--out", dest="salida")
|
||
a = ap.parse_args()
|
||
|
||
if a.list:
|
||
print("\n══ formatos registrados ══")
|
||
tot_l, tot_e, pares = 0, 0, 0
|
||
for fam, (le, es) in fams.items():
|
||
n, m = len(le), len(es)
|
||
tot_l, tot_e, pares = tot_l + n, tot_e + m, pares + n * m
|
||
print(f"\n── familia «{fam}» " + "─" * 40)
|
||
print(f" lectores ({n}): {', '.join(le) or '—'}")
|
||
print(f" escritores ({m}): {', '.join(es) or '—'}")
|
||
for x in le:
|
||
for y in es:
|
||
if x != y:
|
||
print(f" {x} → {y}")
|
||
print(f"\n {pares} par(es) habilitados con {tot_l + tot_e} plugin(s).")
|
||
print(f" De a pares harían falta {pares} traductores, y cada formato nuevo agregaría "
|
||
f"tantos como formatos tenga ya su familia.")
|
||
print(" ⚠ Los pares se cuentan DENTRO de cada familia: un lector de servicios contra un")
|
||
print(" escritor de web no es un par, es basura plausible. El centro lo rechaza.")
|
||
return
|
||
|
||
if not (a.desde and a.hacia and a.entrada):
|
||
ap.error("hacen falta --from, --to y --in (o --list)")
|
||
|
||
fl, fe = lectores[a.desde][1], escritores[a.hacia][1]
|
||
if fl != fe:
|
||
sys.exit(f"✗ `{a.desde}` es de la familia «{fl}» y `{a.hacia}` de la familia «{fe}».\n"
|
||
f" No es un par: traducir entre familias distintas daría un fichero que PARECE\n"
|
||
f" correcto. Mirá `--list` para los pares que sí existen.")
|
||
|
||
modelo = None
|
||
for ruta in _entradas(a.entrada):
|
||
with open(ruta) as fh:
|
||
modelo = _fusionar(modelo, lectores[a.desde][0](fh.read(), ruta))
|
||
texto = escritores[a.hacia][0](modelo) # el escritor puede AÑADIR al acta: se imprime después
|
||
n = imprimir_acta(modelo.acta)
|
||
if a.salida:
|
||
open(a.salida, "w").write(texto)
|
||
print(f"\n {a.hacia} → {a.salida}")
|
||
else:
|
||
print(texto)
|
||
sys.exit(1 if n else 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|