mudanza: el perfil — el software del servidor nuevo se DECLARA, no se instala a mano
`declarar.py --perfil-out` emite un `[perfil.<label>]` listo para `targets.toml`, generado desde el censo: cada raíz está ahí porque un servicio que CORRÍA la necesita y takana tiene receta. Por qué un perfil y no una lista de `install`: mudar servicio por servicio a una caja viva va contra el diseño —el software de una máquina se declara y viene en la imagen, reproducible y firmado— y un servidor armado a fuerza de instalaciones sueltas no se puede volver a construir. Que es justo el problema que esta mudanza existe para no repetir: el `caddy` de gioser no tiene dueño ni receta y se pierde con la máquina. Y hay un impedimento medido, no estético: `takana install` REPRODUCE desde fuente y eso exige el lab entero en el cliente (SDD 28 §5.4), que una caja de destino no tiene. El perfil lo resuelve por el lado correcto: el software entra al armar la imagen, en el hub, que sí tiene lab. Del censo de gioser salen `caddy`, `gitea`, `python3` (agrupado: lo piden python3 y uvicorn). Verificado que el instrumental lo acepta: `targets.py gioser-mudado` expande a 33 raíces. ⚠ Y dice lo que NO puede cubrir, uno por uno con su motivo: 34 servicios entre `suelto`, `paquete-ajeno` y `desconocido`. Un perfil que se calla lo que le falta sale N/N describiendo un servidor incompleto — la lección de `foot` en escritorio-sway. Con esto la mudanza produce TRES documentos declarativos que reconstruyen el servidor desde cero: el perfil (qué software), la semilla (qué corre y cómo) y el plan (qué datos, en qué orden). Ninguno es un log de lo hecho: los tres son entradas que se vuelven a ejecutar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -251,6 +251,56 @@ recomendación misma.
|
||||
Si se elige un equivalente, queda en el censo (`alternativa = "…"`) y **el plan lo dice en su paso**,
|
||||
con la advertencia arriba de todo: la configuración del origen no sirve tal cual.
|
||||
|
||||
### 4.0 quater · El perfil: el software del servidor nuevo se DECLARA, no se instala
|
||||
|
||||
`declarar.py --perfil-out` emite además un `[perfil.<label>]` listo para `docs/state/targets.toml`.
|
||||
|
||||
**Por qué un perfil y no una lista de `install`.** Mudar servicio por servicio a una caja viva es lo
|
||||
que uno haría en cualquier distro, y en takana va contra el diseño: el software de una máquina se
|
||||
**declara** y viene en la imagen, reproducible y firmado. Un servidor armado a fuerza de
|
||||
instalaciones sueltas no se puede volver a construir — que es exactamente el problema que esta
|
||||
mudanza existe para no repetir: **el `caddy` de gioser no tiene dueño ni receta, y se pierde con la
|
||||
máquina**.
|
||||
|
||||
Y hay un impedimento medido, no estético: `takana install` **reproduce desde fuente**, y eso exige el
|
||||
LAB entero en el cliente ([SDD 28 §5.4](28-servidor-de-produccion.md)). Una caja de destino no lo
|
||||
tiene. El perfil esquiva el problema por el lado correcto: el software entra **al armar la imagen**,
|
||||
en el hub, que sí tiene lab.
|
||||
|
||||
Generado desde el censo de gioser:
|
||||
|
||||
```toml
|
||||
[perfil.gioser-mudado]
|
||||
descripcion = "servicios mudados desde momento — generado por scripts/mudanza/declarar.py"
|
||||
hereda = ["base"]
|
||||
paquetes = [
|
||||
"caddy",
|
||||
"gitea",
|
||||
"python3", # python3, uvicorn
|
||||
]
|
||||
```
|
||||
|
||||
Verificado que el instrumental lo acepta: `scripts/targets.py gioser-mudado` expande a **33 raíces**.
|
||||
|
||||
⚠ **Y dice lo que NO puede cubrir**, uno por uno con su motivo: 34 servicios entre `suelto`,
|
||||
`paquete-ajeno` y `desconocido`. *Un perfil que se calla lo que le falta sale N/N describiendo un
|
||||
servidor incompleto* — la lección de `foot` en `escritorio-sway`, que llegó a 121/121 sellado y sin
|
||||
emulador de terminal.
|
||||
|
||||
### 4.1.bis Los tres artefactos declarativos
|
||||
|
||||
Con esto la mudanza deja de ser una secuencia de comandos y pasa a producir **tres documentos que
|
||||
reconstruyen el servidor desde cero**:
|
||||
|
||||
| artefacto | qué declara | lo consume |
|
||||
|---|---|---|
|
||||
| `[perfil.<x>]` → `targets.toml` | **qué software** lleva | `servidor-image.sh` al armar la imagen |
|
||||
| `seed.card.json` | **qué corre y cómo** | `arje-zero` al arrancar |
|
||||
| `plan.toml` | **qué datos y en qué orden** | `aplicar.py`, o un humano línea por línea |
|
||||
|
||||
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.
|
||||
|
||||
### 4.1 El aplicador *(implementado: `scripts/mudanza/aplicar.py`)*
|
||||
|
||||
`aplicar.py --plan plan.toml [--dry-run] [--only <clase>] [--paso N] [--hecho N]`
|
||||
|
||||
@@ -31,7 +31,10 @@ variables de entorno va a necesitar que alguien las escriba. Se avisa por nombre
|
||||
Uso:
|
||||
scripts/mudanza/declarar.py --censo censo.toml [--label mi-servidor] --out seed.card.json
|
||||
"""
|
||||
import argparse, hashlib, json, shlex, sys, tomllib
|
||||
import argparse, hashlib, json, os, shlex, sys, tomllib
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from planear import origen_binario, alternativas, _REPO # noqa: E402
|
||||
|
||||
CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" # sin I, L, O, U
|
||||
|
||||
@@ -85,11 +88,63 @@ def tarjeta(nombre, cmdline, cwd, puertos):
|
||||
}
|
||||
|
||||
|
||||
def perfil(censo, label, repo):
|
||||
"""Emite un `[perfil.<label>]` para `docs/state/targets.toml`.
|
||||
|
||||
── POR QUÉ UN PERFIL Y NO UNA LISTA DE `install` ────────────────────────────────────────────
|
||||
Mudar servicio por servicio a una caja viva es lo que uno haría en cualquier distro, y en takana
|
||||
es ir contra el diseño: **el software de una máquina se DECLARA en `targets.toml` y viene en la
|
||||
imagen**, reproducible y firmado. Un servidor armado a fuerza de instalaciones sueltas no se
|
||||
puede volver a construir — que es exactamente el problema que esta mudanza existe para no repetir
|
||||
(el caddy puesto a mano en gioser no tiene dueño ni receta: se pierde con la máquina).
|
||||
|
||||
Además hay un impedimento medido: `takana install` REPRODUCE desde fuente, y eso exige el LAB
|
||||
entero en el cliente (SDD 28 §5.4). Una caja de destino no lo tiene. El perfil esquiva el
|
||||
problema por el lado correcto: el software entra al armar la imagen, en el hub, que sí tiene lab.
|
||||
|
||||
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`."""
|
||||
raices, sin_receta = {}, []
|
||||
for s_ in censo.get("servicio", []):
|
||||
if s_.get("decision") != "muda":
|
||||
continue
|
||||
elegido = s_.get("alternativa") or s_["name"]
|
||||
clase, detalle = origen_binario(dict(s_, name=elegido), repo)
|
||||
if clase == "receta-takana":
|
||||
import re as _re
|
||||
m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
|
||||
# Se agrupa POR RECETA, no por servicio: dos servicios pueden necesitar la misma
|
||||
# (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.
|
||||
receta = m.group(1) if m else elegido
|
||||
raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
|
||||
else:
|
||||
sin_receta.append((s_["name"], clase, detalle))
|
||||
|
||||
L = [f"[perfil.{label}]",
|
||||
f'descripcion = "servicios mudados desde {censo.get("maquina",{}).get("hostname","?")} '
|
||||
f'— generado por scripts/mudanza/declarar.py"',
|
||||
'hereda = ["base"]',
|
||||
"# Generado desde un CENSO, no escrito a mano: cada raíz está acá porque un servicio que",
|
||||
"# CORRÍA en la máquina vieja la necesita, y takana tiene receta para construirla.",
|
||||
"paquetes = ["]
|
||||
for receta in sorted(raices):
|
||||
quienes = raices[receta]
|
||||
nombres = ", ".join(sorted({q[0] for q in quienes}))
|
||||
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 "")
|
||||
nota = f" # {det}" if det and det != receta else ""
|
||||
L.append(f' "{receta}",{nota}')
|
||||
L.append("]")
|
||||
return "\n".join(L), sin_receta
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Genera una Semilla de arje desde un censo decidido.")
|
||||
ap.add_argument("--censo", required=True)
|
||||
ap.add_argument("--label", default="takana-servidor", help="etiqueta de la Semilla raíz")
|
||||
ap.add_argument("--out", help="fichero de salida (default: stdout)")
|
||||
ap.add_argument("--perfil-out", help="además, escribir un [perfil.<label>] para targets.toml")
|
||||
a = ap.parse_args()
|
||||
|
||||
with open(a.censo, "rb") as f:
|
||||
@@ -150,6 +205,22 @@ def main():
|
||||
print(" al primer arranque, la rota deja el servicio caído sin que nada avise.")
|
||||
print(" ⚠ El entorno (`envp`) va VACÍO: el censo no lee /proc/<pid>/environ porque trae")
|
||||
print(" tokens. Un servicio que dependa de variables va a necesitar que alguien las escriba.")
|
||||
if a.perfil_out:
|
||||
bloque, sin_receta = perfil(censo, a.label, _REPO)
|
||||
open(a.perfil_out, "w").write(bloque + "\n")
|
||||
n_r = len([l for l in bloque.splitlines() if l.strip().startswith('"')])
|
||||
print(f"\n══ PERFIL '{a.label}' ══")
|
||||
print(f" {n_r} receta(s) que takana YA construye → {a.perfil_out}")
|
||||
print(" Pegalo en docs/state/targets.toml y armá la imagen: el software del servidor nuevo")
|
||||
print(" se DECLARA, no se instala a mano. Además `takana install` reproduce desde fuente y")
|
||||
print(" eso exige el lab entero en el cliente — el perfil lo resuelve en el hub, que sí lo tiene.")
|
||||
if sin_receta:
|
||||
print(f"\n ⚠ {len(sin_receta)} servicio(s) que el perfil NO puede cubrir:")
|
||||
for nom, cl, det in sin_receta:
|
||||
print(f" {nom:22} [{cl}] {det[:84]}")
|
||||
print(" Un perfil que se calla lo que le falta sale N/N describiendo un servidor")
|
||||
print(" incompleto. Para cada uno: escribir la receta, elegir un equivalente que sí")
|
||||
print(" esté, o llevar el binario a mano sabiendo que no se reproduce.")
|
||||
if a.out:
|
||||
open(a.out, "w").write(txt)
|
||||
print(f"\n semilla → {a.out}")
|
||||
|
||||
Reference in New Issue
Block a user