planear: buscar la receta por el PAQUETE del origen — y el intérprete no es el programa
`origen_binario` buscaba la receta por el nombre del SERVICIO, y ése casi nunca es el nombre del paquete: `sshd` lo trae `openssh`, `crond` lo trae `cronie`. El censo ya le preguntó al gestor de paquetes quién posee cada binario, así que ese nombre también se prueba. El perfil pasó de 4 recetas a 7, y los 27 servicios quedan: receta-takana 9 · suelto 9 · paquete-ajeno 13 · interprete 4 · borrado 4. **La trampa, que es la que haría mentir al perfil.** `openclaw` lo posee el paquete `nodejs`; `fail2ban-server`, `glances` y `uvicorn` los posee `python`. Contarlos como cubiertos porque existe `recipes/nodejs.toml` haría salir el perfil N/N describiendo un servidor al que le faltan CUATRO programas. Tienen clase propia (`interprete`) y cuentan las DOS cosas a la vez, porque las dos son ciertas: su runtime entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo que pase— y el servicio sigue listado como NO cubierto. Meterlo sólo en las raíces miente; dejarlo sólo en los faltantes arma una imagen sin runtime y el programa, cuando llegue, no arranca. Y el paquete del origen no se llama igual que la receta ni para el mismo intérprete: Artix empaqueta `python` y el catálogo tiene `python3.toml`. Sin ese alias tres servicios decían «hace falta una receta takana» teniendo el runtime sellado — dos trabajos muy distintos. **Sellada y sin sellar tampoco son lo mismo**, y el perfil las listaba igual: una sellada se INSTALA del repo firmado, una sin sellar hay que CONSTRUIRLA. Salió con un caso real — `recipes/qdrant.toml` entró al catálogo desde otro frente mientras se escribía esto y no tiene artefacto. Ahora se marca en la línea y se resume al pie. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -301,6 +301,42 @@ reconstruyen el servidor desde cero**:
|
|||||||
Ninguno de los tres es un log de lo que se hizo: los tres son entradas que se vuelven a ejecutar.
|
Ninguno de los tres es un log de lo que se hizo: los tres son entradas que se vuelven a ejecutar.
|
||||||
Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez.
|
Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez.
|
||||||
|
|
||||||
|
### 4.0 duodecies · La cuenta de lo que falta, bien hecha
|
||||||
|
|
||||||
|
`origen_binario` buscaba la receta por el **nombre del servicio**, y ése casi nunca es el nombre del
|
||||||
|
paquete: `sshd` lo trae `openssh`, `crond` lo trae `cronie`. El censo ya le preguntó al gestor de
|
||||||
|
paquetes quién posee cada binario, así que ese nombre también se prueba. Con eso el perfil pasó de
|
||||||
|
**4 recetas a 7**, y la clasificación de los 27 servicios quedó así:
|
||||||
|
|
||||||
|
```
|
||||||
|
receta-takana 9 paquete-ajeno 13
|
||||||
|
suelto 9 interprete 4 borrado 4
|
||||||
|
```
|
||||||
|
|
||||||
|
#### La trampa: el intérprete NO es el programa
|
||||||
|
|
||||||
|
`openclaw` lo posee el paquete `nodejs`, y `fail2ban-server`, `glances` y `uvicorn` los posee
|
||||||
|
`python`. Contarlos como cubiertos porque existe `recipes/nodejs.toml` haría salir el perfil **N/N
|
||||||
|
describiendo un servidor al que le faltan cuatro programas**. Tienen clase propia, `interprete`, y
|
||||||
|
cuentan **las dos cosas a la vez**, porque las dos son ciertas:
|
||||||
|
|
||||||
|
- su **runtime entra al perfil** — `openclaw` necesita `nodejs` en la imagen pase lo que pase;
|
||||||
|
- el **servicio sigue listado como no cubierto**, con su motivo.
|
||||||
|
|
||||||
|
Meterlo sólo en las raíces haría un perfil que miente; dejarlo sólo en los faltantes armaría una
|
||||||
|
imagen sin runtime, y el programa, cuando llegue, no arrancaría.
|
||||||
|
|
||||||
|
(Y el paquete del origen no se llama igual que la receta ni para el mismo intérprete: Artix
|
||||||
|
empaqueta `python`, el catálogo tiene `python3.toml`. Sin ese alias, tres servicios decían «hace
|
||||||
|
falta una receta takana» teniendo el runtime sellado — dos trabajos muy distintos.)
|
||||||
|
|
||||||
|
#### Sellada y sin sellar no son lo mismo
|
||||||
|
|
||||||
|
El perfil las listaba igual. Una sellada se **instala** del repo firmado; una sin sellar hay que
|
||||||
|
**construirla**, y eso puede ser una tarde. Salió con un caso real: `recipes/qdrant.toml` entró al
|
||||||
|
catálogo desde otro frente **mientras se escribía esto** y no tiene artefacto. Ahora el perfil lo
|
||||||
|
marca en la línea y lo resume al pie.
|
||||||
|
|
||||||
### 4.0 undecies · Cuatro binarios que ya no existen — el paso que CADUCA
|
### 4.0 undecies · Cuatro binarios que ya no existen — el paso que CADUCA
|
||||||
|
|
||||||
El más urgente de todo el censo, y lo encontró la lista de lo que el perfil no puede cubrir:
|
El más urgente de todo el censo, y lo encontró la lista de lo que el perfil no puede cubrir:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def perfil(censo, label, repo):
|
|||||||
|
|
||||||
Lo que el perfil NO puede cubrir se lista aparte, con su motivo. Un perfil que se calla lo que le
|
Lo que el perfil NO puede cubrir se lista aparte, con su motivo. Un perfil que se calla lo que le
|
||||||
falta sale N/N y describe un servidor incompleto — la lección de `foot` en `escritorio-sway`."""
|
falta sale N/N y describe un servidor incompleto — la lección de `foot` en `escritorio-sway`."""
|
||||||
raices, sin_receta = {}, []
|
raices, sin_receta, sin_sellar = {}, [], set()
|
||||||
for s_ in censo.get("servicio", []):
|
for s_ in censo.get("servicio", []):
|
||||||
if s_.get("decision") != "muda":
|
if s_.get("decision") != "muda":
|
||||||
continue
|
continue
|
||||||
@@ -66,8 +66,25 @@ def perfil(censo, label, repo):
|
|||||||
# (uvicorn y python3 son los dos `python3`), y repetir una raíz en un perfil no la
|
# (uvicorn y python3 son los dos `python3`), y repetir una raíz en un perfil no la
|
||||||
# duplica — sólo ensucia y hace creer que son dos cosas distintas.
|
# duplica — sólo ensucia y hace creer que son dos cosas distintas.
|
||||||
receta = m.group(1) if m else elegido
|
receta = m.group(1) if m else elegido
|
||||||
|
# ⚠ Sellada y sin sellar no son lo mismo, y el perfil las listaba igual. Una sellada se
|
||||||
|
# INSTALA del repo firmado; una sin sellar hay que CONSTRUIRLA antes, y eso puede ser una
|
||||||
|
# tarde. Medido: `qdrant` entró al catálogo ayer desde otro frente y no tiene artefacto.
|
||||||
|
if "sin sellar" in detalle:
|
||||||
|
sin_sellar.add(receta)
|
||||||
raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
|
raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
|
||||||
else:
|
else:
|
||||||
|
# ⚠ Un servicio de clase `interprete` cuenta las DOS cosas a la vez, y hay que hacer las
|
||||||
|
# dos: su runtime SÍ entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo
|
||||||
|
# que pase— y el servicio sigue listado como NO CUBIERTO, porque el intérprete no es el
|
||||||
|
# programa. Meterlo sólo en las raíces haría salir el perfil N/N con la app faltando;
|
||||||
|
# dejarlo sólo en la lista de faltantes armaría una imagen sin runtime y el programa,
|
||||||
|
# cuando llegue, no arrancaría.
|
||||||
|
if clase == "interprete":
|
||||||
|
import re as _re
|
||||||
|
m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
|
||||||
|
if m:
|
||||||
|
raices.setdefault(m.group(1), []).append(
|
||||||
|
(f"{s_['name']} (sólo el runtime)", tuple(s_.get("ports", []))))
|
||||||
sin_receta.append((s_["name"], clase, detalle))
|
sin_receta.append((s_["name"], clase, detalle))
|
||||||
|
|
||||||
L = [f"[perfil.{label}]",
|
L = [f"[perfil.{label}]",
|
||||||
@@ -82,9 +99,14 @@ def perfil(censo, label, repo):
|
|||||||
nombres = ", ".join(sorted({q[0] for q in quienes}))
|
nombres = ", ".join(sorted({q[0] for q in quienes}))
|
||||||
puertos = sorted({p for q in quienes for p in q[1]})
|
puertos = sorted({p for q in quienes for p in q[1]})
|
||||||
det = nombres + (f" · puertos {','.join(str(x) for x in puertos)}" if puertos else "")
|
det = nombres + (f" · puertos {','.join(str(x) for x in puertos)}" if puertos else "")
|
||||||
|
if receta in sin_sellar:
|
||||||
|
det = ("⚠ SIN SELLAR: hay que construirla antes de armar la imagen — " + det).strip(" —")
|
||||||
nota = f" # {det}" if det and det != receta else ""
|
nota = f" # {det}" if det and det != receta else ""
|
||||||
L.append(f' "{receta}",{nota}')
|
L.append(f' "{receta}",{nota}')
|
||||||
L.append("]")
|
L.append("]")
|
||||||
|
if sin_sellar:
|
||||||
|
L.append(f"# ⚠ {len(sin_sellar)} sin artefacto en el store: {', '.join(sorted(sin_sellar))}.")
|
||||||
|
L.append("# El perfil es correcto, pero armar la imagen las va a CONSTRUIR, no instalar.")
|
||||||
return "\n".join(L), sin_receta
|
return "\n".join(L), sin_receta
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,17 @@ def _no_aplica(nombre):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Paquetes que proveen un RUNTIME, no un programa. Que exista `recipes/nodejs.toml` no significa que
|
||||||
|
# `openclaw` esté cubierto: el intérprete se resuelve y el programa sigue faltando.
|
||||||
|
INTERPRETES = {"python", "python3", "nodejs", "node", "perl", "ruby", "php", "lua", "java", "jre"}
|
||||||
|
|
||||||
|
# El paquete del origen y la receta del catálogo no siempre se llaman igual ni siquiera para el mismo
|
||||||
|
# intérprete: Artix empaqueta `python` y el catálogo tiene `python3.toml`. Sin esto, tres servicios
|
||||||
|
# salían «hace falta una receta takana» cuando el runtime YA está sellado y lo que falta es el
|
||||||
|
# programa — dos trabajos muy distintos.
|
||||||
|
ALIAS_INTERPRETE = {"python": "python3", "node": "nodejs", "jre": "java"}
|
||||||
|
|
||||||
|
|
||||||
# ── familias de equivalentes funcionales ────────────────────────────────────────────────────────
|
# ── familias de equivalentes funcionales ────────────────────────────────────────────────────────
|
||||||
# Agrupa cosas que hacen EL MISMO trabajo, para poder ofrecer alternativas. Es conocimiento de
|
# Agrupa cosas que hacen EL MISMO trabajo, para poder ofrecer alternativas. Es conocimiento de
|
||||||
# dominio, no gusto: que nginx y caddy sirvan HTTP es un hecho. Lo que NO se decide acá es cuál es
|
# dominio, no gusto: que nginx y caddy sirvan HTTP es un hecho. Lo que NO se decide acá es cuál es
|
||||||
@@ -285,14 +296,33 @@ def origen_binario(e, repo=None):
|
|||||||
# contestar `suelto` a todo sería una respuesta falsa con forma de respuesta.
|
# contestar `suelto` a todo sería una respuesta falsa con forma de respuesta.
|
||||||
raise SystemExit(f"!! no encuentro el catálogo de recetas en {repo}/recipes — "
|
raise SystemExit(f"!! no encuentro el catálogo de recetas en {repo}/recipes — "
|
||||||
f"pasá --repo o corré desde el repo")
|
f"pasá --repo o corré desde el repo")
|
||||||
for cand in (nombre, nombre.replace("_", "-"), os.path.basename(exe or "")):
|
# ⚠ El nombre del SERVICIO no es el nombre del paquete: `sshd` lo trae `openssh`, `crond` lo trae
|
||||||
if cand and os.path.exists(os.path.join(repo, "recipes", f"{cand}.toml")):
|
# `cronie`. Buscar sólo por el nombre del servicio daba «no hay receta» para cinco que el catálogo
|
||||||
import glob
|
# SÍ construye y tiene selladas. El censo ya le preguntó al gestor de paquetes quién posee el
|
||||||
|
# binario, así que ese nombre también se prueba.
|
||||||
|
import glob
|
||||||
|
for cand in (nombre, nombre.replace("_", "-"), os.path.basename(exe or ""),
|
||||||
|
pkg, (pkg or "").removesuffix("-bin"), ALIAS_INTERPRETE.get(pkg or "", "")):
|
||||||
|
if not cand or not os.path.exists(os.path.join(repo, "recipes", f"{cand}.toml")):
|
||||||
|
continue
|
||||||
|
# ⚠⚠ Y la trampa, que es la que haría mentir al perfil: un paquete INTÉRPRETE provee el
|
||||||
|
# runtime, no el programa. `openclaw` lo posee `nodejs` y `fail2ban-server` lo posee
|
||||||
|
# `python`; tener esas recetas NO te da esos servicios. Contarlos como cubiertos haría que el
|
||||||
|
# perfil saliera N/N describiendo un servidor al que le faltan cuatro programas.
|
||||||
|
if pkg in INTERPRETES and cand in (pkg, ALIAS_INTERPRETE.get(pkg or "", "")) and cand != nombre:
|
||||||
sellado = bool(glob.glob(os.path.join(repo, "store", f"*-{cand}")))
|
sellado = bool(glob.glob(os.path.join(repo, "store", f"*-{cand}")))
|
||||||
return ("receta-takana",
|
return ("interprete",
|
||||||
f"takana YA lo construye: `recipes/{cand}.toml`"
|
f"lo corre el intérprete `{pkg}`, que takana SÍ construye (`recipes/{cand}.toml`"
|
||||||
+ (" (sellado en el store)" if sellado else " (sin sellar: hay que construirlo)")
|
+ (", sellada)" if sellado else ", sin sellar)")
|
||||||
+ f" ⇒ instalarlo del repo firmado, no copiar el binario de {exe or '?'}")
|
+ f" — pero el intérprete NO es el programa: `{nombre}` es código que hay que "
|
||||||
|
f"llevar y declarar aparte. El perfil resuelve el runtime y nada más")
|
||||||
|
sellado = bool(glob.glob(os.path.join(repo, "store", f"*-{cand}")))
|
||||||
|
via = "" if cand in (nombre, nombre.replace("_", "-")) else f" (por su paquete `{pkg}`)" \
|
||||||
|
if cand in (pkg, (pkg or "").removesuffix("-bin")) else f" (por su binario)"
|
||||||
|
return ("receta-takana",
|
||||||
|
f"takana YA lo construye{via}: `recipes/{cand}.toml`"
|
||||||
|
+ (" (sellado en el store)" if sellado else " (sin sellar: hay que construirlo)")
|
||||||
|
+ f" ⇒ instalarlo del repo firmado, no copiar el binario de {exe or '?'}")
|
||||||
if pkg:
|
if pkg:
|
||||||
return ("paquete-ajeno",
|
return ("paquete-ajeno",
|
||||||
f"lo provee el gestor del origen: paquete `{pkg}`. En el destino hace falta una "
|
f"lo provee el gestor del origen: paquete `{pkg}`. En el destino hace falta una "
|
||||||
|
|||||||
Reference in New Issue
Block a user