tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1181Shell 82Rust 62Markdown 521515 Definiciones

⎇ main scripts/mudanza/formatos/systemd.py Python · 203 líneas · 9.4 KB Historia Quién tocó qué crudo

traducir: familia «servicio» — systemd → tarjeta de arje, y el pivote pasa a ser uno POR FAMILIA fefa92ec5 · Sergio ·

1"""Lector de units de systemd → modelo pivote «servicio» (SDD 29).
2
3systemd es el init de la mayoría de los servidores de los que uno se muda, así que éste es el lector
4que más entradas abre. Pero un unit dice MUCHO más de lo que cualquier otro init sabe expresar, y
5ahí está todo el riesgo: traducir sólo `ExecStart=` y callarse el resto produce un servicio que
6arranca y **hace otra cosa**.
7
8⚠ Los tres campos donde una elisión silenciosa no pierde una opción sino que CAMBIA el sistema:
9
10  · `User=` / `Group=` — sin esto el servicio corre como root. No es «una opción menos»: es
11    escalación de privilegios en un fichero generado, que nadie vuelve a leer.
12  · `EnvironmentFile=` — el fichero no existe en la máquina donde se traduce, así que su contenido
13    no se puede ni mirar. Un servicio al que le falta medio entorno arranca y falla tarde.
14  · Los especificadores (`%i`, `%h`, `%n`…) y las plantillas `foo@.service` — no se resuelven sin
15    saber la instancia. Expandirlos «por lo más probable» es adivinar en silencio.
16
17Los tres van al acta como SIN-TRADUCIR, con su línea.
18"""
19import sys, os
20sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21from modelo import Servicio, Sistema                       # noqa: E402
22from formatos import lector                                # noqa: E402
23
24# `Restart=` de systemd → el vocabulario del pivote. `on-abnormal`/`on-watchdog`/`on-abort` son
25# matices de «cuándo cuenta como fallo» que ningún otro init distingue: se acercan a `si-falla` y
26# eso se ANOTA, no se hace callado.
27_REINICIO = {"no": "no", "always": "siempre", "on-failure": "si-falla", "on-success": "si-falla",
28             "on-abnormal": "si-falla", "on-watchdog": "si-falla", "on-abort": "si-falla"}
29_MATIZ = {"on-abnormal", "on-watchdog", "on-abort", "on-success"}
30
31# Prefijos de `ExecStart=`. Cada uno cambia la semántica del arranque y ninguno es parte del comando.
32_PREFIJOS = {"-": "ignorar el fallo de arranque", "@": "argv[0] distinto del ejecutable",
33             "+": "correr con privilegios completos", "!": "saltar la restricción de usuario",
34             ":": "no expandir variables de entorno"}
35
36
37def _seg(val):
38    """`RestartSec=` admite `5`, `5s`, `500ms`, `1min`. Devuelve milisegundos."""
39    v = val.strip().lower()
40    for suf, mult in (("ms", 1), ("min", 60000), ("s", 1000), ("m", 60000)):
41        if v.endswith(suf):
42            try:
43                return int(float(v[:-len(suf)]) * mult)
44            except ValueError:
45                return 0
46    try:
47        return int(float(v) * 1000)
48    except ValueError:
49        return 0
50
51
52def _entorno(val):
53    """`Environment="A=1" B=2` — comillas opcionales, varios pares por línea."""
54    import shlex
55    d = {}
56    try:
57        piezas = shlex.split(val)
58    except ValueError:
59        piezas = val.split()
60    for p in piezas:
61        if "=" in p:
62            k, _, v = p.partition("=")
63            d[k] = v
64    return d
65
66
67@lector("systemd", familia="servicio")
68def leer(texto, nombre=""):
69    sis = Sistema(formato_origen="systemd")
70    acta = sis.acta
71    base = os.path.basename(nombre or "unidad.service")
72    srv = Servicio(nombre=base[:-8] if base.endswith(".service") else base)
73    srv.plantilla = "@." in base
74    if srv.plantilla:
75        acta.anota(0, "SIN-TRADUCIR", base,
76                   "es una PLANTILLA (`nombre@.service`): sin saber la instancia, `%i` no se resuelve "
77                   "y el servicio traducido arrancaría con otro argumento")
78
79    seccion, arranques, extras = "", [], []
80    lineas, i = texto.splitlines(), 0
81    while i < len(lineas):
82        n = i + 1
83        cruda = lineas[i]
84        # Continuación de línea: systemd la usa muchísimo en ExecStart largos, y cortar ahí deja un
85        # comando TRUNCADO que además parece válido.
86        while cruda.rstrip().endswith("\\") and i + 1 < len(lineas):
87            i += 1
88            cruda = cruda.rstrip()[:-1] + " " + lineas[i].strip()
89        i += 1
90        ln = cruda.strip()
91        if not ln or ln[0] in "#;":
92            continue
93        if ln.startswith("[") and ln.endswith("]"):
94            seccion = ln[1:-1]
95            continue
96        if "=" not in ln:
97            acta.anota(n, "SIN-TRADUCIR", ln, "línea sin `clave=valor` y fuera de toda sección")
98            continue
99        k, _, v = ln.partition("=")
100        k, v = k.strip(), v.strip()
101
102        if k == "Description":
103            srv.descripcion = v
104            acta.anota(n, "traducido", ln)
105        elif k == "Type":
106            srv.tipo = v
107            srv.lineas["tipo"] = n
108            acta.anota(n, "traducido", ln)
109        elif k == "ExecStart":
110            pref = ""
111            while v and v[0] in _PREFIJOS:
112                pref += v[0]
113                v = v[1:].lstrip()
114            if pref:
115                acta.anota(n, "SIN-TRADUCIR", ln,
116                           "prefijo `%s` en ExecStart (%s): el pivote no lo modela y quitarlo "
117                           "cambia el arranque" % (pref, "; ".join(_PREFIJOS[c] for c in pref)))
118            arranques.append((n, v))
119        elif k in ("ExecStartPre", "ExecStartPost", "ExecStop", "ExecStopPost", "ExecReload",
120                   "ExecCondition"):
121            extras.append((n, k, v))
122        elif k == "WorkingDirectory":
123            srv.cwd = v
124            srv.lineas["cwd"] = n
125            acta.anota(n, "traducido", ln)
126        elif k in ("User", "Group"):
127            campo = "usuario" if k == "User" else "grupo"
128            setattr(srv, campo, v)
129            srv.lineas[campo] = n
130            # Sin anotación: si el destino sabe bajar de privilegio esto es `traducido`, y si no es
131            # SIN-TRADUCIR. Lo sabe el ESCRITOR. Que el lector opine acá duplica la entrada y la
132            # contradice — y un acta que se contradice se deja de leer.
133        elif k == "Environment":
134            srv.entorno.update(_entorno(v))
135            acta.anota(n, "traducido", ln)
136        elif k == "EnvironmentFile":
137            srv.entorno_ficheros.append(v)
138            srv.lineas["entorno_ficheros"] = n
139            acta.anota(n, "SIN-TRADUCIR", ln,
140                       "el fichero de entorno NO está en esta máquina: su contenido no se puede "
141                       "leer ni copiar, y el servicio arrancaría con medio entorno")
142        elif k == "Restart":
143            if v in _MATIZ:
144                acta.anota(n, "decision", ln,
145                           f"`{v}` es un matiz de «cuándo cuenta como fallo» que ningún otro init "
146                           "distingue; se traduce como «si-falla»")
147            elif v not in _REINICIO:
148                acta.anota(n, "SIN-TRADUCIR", ln, f"valor de Restart no reconocido: `{v}`")
149            else:
150                acta.anota(n, "traducido", ln)
151            srv.reinicio = _REINICIO.get(v, "si-falla")
152            srv.lineas["reinicio"] = n
153        elif k == "RestartSec":
154            srv.reinicio_ms = _seg(v)
155            acta.anota(n, "traducido", ln)
156        elif k == "After":
157            srv.tras += v.split()
158            acta.anota(n, "traducido", ln)
159        elif k in ("Requires", "BindsTo"):
160            srv.necesita += v.split()
161            acta.anota(n, "traducido", ln)
162        elif k == "WantedBy":
163            srv.arranque_auto = True
164            acta.anota(n, "traducido", ln)
165        elif k in ("Wants", "Before", "Conflicts", "PartOf", "RequiredBy", "Also",
166                   "DefaultDependencies", "StartLimitIntervalSec", "StartLimitBurst"):
167            acta.anota(n, "no-aplica", ln, "orden/relación que el destino resuelve por el grafo")
168        elif k in ("StandardOutput", "StandardError", "SyslogIdentifier"):
169            acta.anota(n, "no-aplica", ln, "el destino captura la salida por su propio journal")
170        elif k in ("Documentation", "RemainAfterExit", "KillMode", "KillSignal", "TimeoutStopSec",
171                   "TimeoutStartSec", "PIDFile", "Alias"):
172            acta.anota(n, "no-aplica", ln, f"`{k}` es específico de systemd")
173        elif seccion == "Service":
174            # Todo el endurecimiento (`ProtectSystem`, `NoNewPrivileges`, `CapabilityBoundingSet`…)
175            # cae acá. Callarlo sería entregar un servicio MENOS confinado que el original.
176            acta.anota(n, "SIN-TRADUCIR", ln,
177                       f"`{k}` no se modela; si es confinamiento, el servicio traducido corre con "
178                       "MÁS permisos que el original")
179        else:
180            acta.anota(n, "SIN-TRADUCIR", ln, f"`{k}` en [{seccion or 'sin sección'}] no reconocida")
181
182    if not arranques:
183        acta.anota(0, "SIN-TRADUCIR", base, "el unit no tiene `ExecStart=`: no hay qué arrancar")
184    else:
185        srv.cmd = arranques[0][1]
186        srv.origen = arranques[0][0]
187        acta.anota(srv.origen, "traducido", f"ExecStart={srv.cmd}")
188        for n, v in arranques[1:]:
189            acta.anota(n, "SIN-TRADUCIR", f"ExecStart={v}",
190                       "systemd admite varios ExecStart en `Type=oneshot`; el pivote modela UNO")
191    for n, k, v in extras:
192        acta.anota(n, "SIN-TRADUCIR", f"{k}={v}",
193                   f"`{k}` es una fase del ciclo de vida aparte del arranque; hay que portarla a mano")
194
195    for especificador in ("%i", "%I", "%n", "%N", "%h", "%H", "%f", "%p", "%t"):
196        if especificador in srv.cmd:
197            acta.anota(0, "SIN-TRADUCIR", srv.cmd,
198                       f"el comando usa el especificador `{especificador}`, que systemd expande en "
199                       "tiempo de arranque; resolverlo acá sería adivinar")
200            break
201
202    sis.servicios.append(srv)
203    return sis