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…
traducir: familia «servicio» — systemd → tarjeta de arje, y el pivote pasa a ser uno POR FAMILIA
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