planear: el preflight medía contra / — y los datos de gioser no entran ahí
La cadena `censar → planear → aplicar` corrió entera contra gioser por primera vez: 59 pasos, 20
ejecutables y 39 manuales, bien separados. El ensayo en seco destapó lo que ninguna prueba de juguete
iba a mostrar: **33,6 G de datos contra una raíz con 3,5 G libres** (el sitio está en `/work`, 66 G).
Dos cosas estaban mal a la vez:
· **El plan copiaba ruta → MISMA ruta**, sin forma de decir dónde cae cada árbol en el destino. Ahora
`[[datos]]` tiene `destino` (vacío = la misma ruta). El fallo que evita es caro: aparecía a mitad
de un rsync de 22 G.
· **El preflight sumaba todo y lo comparaba contra `/`.** Acierta POR CASUALIDAD mientras todo caiga
en `/`, y da un veredicto completamente falso en cuanto una ruta va a otro montaje — en las dos
direcciones. Ahora mide por sistema de ficheros, agrupa los destinos, nombra qué rutas caen en cada
uno, y el veredicto lo saca el propio comando en vez de un humano leyendo una columna.
Los dos controles, contra la caja de verdad:
✗ / necesita 34386 MiB · libres 3596 ⇐ /var/www /var/lib /srv /opt /home exit 1
✓ /work necesita 34386 MiB · libres 66192 ⇐ /work/var/www … /work/home exit 0
Además, el paso `muere` ahora cumple la regla 2 ENTERA («por su nombre Y CON SU TAMAÑO»). Un dominio
fósil no pesa nada por sí mismo: pesa lo que dejó en disco, y `terapeuta.ec` —que no resuelve en
ningún DNS— tiene 279 M en `/var/www/terapeuta`. Que eso se supiera dependía de que alguien se
acordara. Se lista un renglón POR DIRECTORIO (siete dominios `*.gioser.net` reclaman el mismo
`gioser.bak`; repetirlo siete veces convierte el aviso en ruido) y se dice «podría ser de», nunca
«es»: el directorio se llama `terapeuta` y el dominio `terapeuta.ec`, y emparejar «casi» acierta casi
siempre y el resto de las veces manda a borrar lo que no era. Los directorios que no reclama nadie se
listan aparte: son los que nadie recuerda.
⚠ Y un bug que me hice yo y que el control cazó: empalmar por `str.index('# ── 1. datos ───')` cuando
ese marcador existe en DOS funciones. Tomó el corte al revés (j < i) y DUPLICÓ `def pasos` entero;
Python se queda con la última definición, así que el fichero importaba bien y generaba planes con el
preflight viejo. Se vio porque el comando del plan no era el que yo acababa de escribir. Un marcador
de empalme que no es único no es un marcador.
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,40 @@ 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.
|
||||
Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez.
|
||||
|
||||
### 4.0 nonies · La cadena entera, y el preflight que medía contra el sistema de ficheros equivocado
|
||||
|
||||
`censar → planear → aplicar` corrió de punta a punta contra gioser: **59 pasos**, 20 ejecutables y 39
|
||||
manuales, correctamente separados — el aplicador marca los manuales `pendiente-humano` y la corrida
|
||||
no se considera completa.
|
||||
|
||||
Y ahí el ensayo en seco destapó lo que ninguna prueba de juguete iba a mostrar: **33,6 G de datos
|
||||
contra una raíz de 3,5 G libres.**
|
||||
|
||||
```
|
||||
/dev/root 3,5 G libres /
|
||||
/dev/sdb 19,0 G libres /store
|
||||
/dev/sda4 66,2 G libres /work
|
||||
```
|
||||
|
||||
Dos cosas estaban mal a la vez:
|
||||
|
||||
- **El plan copiaba ruta → misma ruta**, sin forma de decir dónde cae cada árbol en el destino. Ahora
|
||||
`[[datos]]` tiene `destino` (vacío = la misma ruta), y el fallo que evita es caro: aparecía a mitad
|
||||
de un `rsync` de 22 G.
|
||||
- **El preflight sumaba todo y lo comparaba contra `/`.** Eso acierta *por casualidad* mientras todo
|
||||
caiga en `/`, y da un veredicto **completamente falso** en cuanto una ruta va a otro montaje — en
|
||||
las dos direcciones. Ahora mide **por sistema de ficheros**, agrupando los destinos, y el veredicto
|
||||
lo saca el propio comando en vez de un humano leyendo una columna:
|
||||
|
||||
```
|
||||
✗ / necesita 34386 MiB · libres 3596 ⇐ /var/www /var/lib /srv /opt /home
|
||||
NO ENTRA en /: faltan 30790 MiB (exit 1)
|
||||
|
||||
✓ /work necesita 34386 MiB · libres 66192 ⇐ /work/var/www …/work/home (exit 0)
|
||||
```
|
||||
|
||||
Los dos controles corridos contra la caja de verdad: el negativo falla y el positivo pasa.
|
||||
|
||||
### 4.0 octies · `/proc` es un formato de origen más — y había DOS emisores de tarjetas
|
||||
|
||||
`declarar.py` tenía su propio molde de card de arje, y `formatos/arje.py` el suyo. Dos emisores del
|
||||
|
||||
@@ -408,6 +408,45 @@ def fuera_de_git(t):
|
||||
return res
|
||||
|
||||
|
||||
def contenido_web(t, raices=("/var/www", "/srv")):
|
||||
"""Qué hay en las raíces de contenido web, con su tamaño. Un nivel, sin juicio.
|
||||
|
||||
── POR QUÉ EXISTE ──────────────────────────────────────────────────────────────────────────
|
||||
La regla 2 del plan dice que lo que muere se nombra «por su nombre Y CON SU TAMAÑO». Los
|
||||
servicios y los datos ya lo cumplían; los DOMINIOS no — se listaban pelados, y un dominio fósil
|
||||
no pesa nada por sí mismo: pesa lo que dejó en disco. `terapeuta.ec` no resuelve en ningún DNS y
|
||||
tiene 279 M en `/var/www/terapeuta`. Que eso se supiera dependía de que alguien se acordara.
|
||||
|
||||
⚠ **No se afirma la correspondencia, se OFRECE.** El directorio se llama `terapeuta` y el
|
||||
dominio `terapeuta.ec`: no coinciden. Una regla difusa que empareje «casi» acierta la mayoría de
|
||||
las veces y el resto de las veces manda a borrar lo que no era. Se listan los candidatos con su
|
||||
tamaño y decide el humano — y se listan TAMBIÉN los directorios que no se parecen a ningún
|
||||
dominio, que son los que nadie recuerda (`gioser.bak.20260501`, `ogisoer`)."""
|
||||
res = []
|
||||
for raiz in raices:
|
||||
out, ok = t.run(f"du -sh {raiz}/* 2>/dev/null | sort -rh | head -40")
|
||||
if not ok or not out.strip():
|
||||
continue
|
||||
for l in out.splitlines():
|
||||
p = l.split(None, 1)
|
||||
if len(p) == 2 and p[1].startswith("/"):
|
||||
res.append({"path": p[1], "size": p[0], "raiz": raiz})
|
||||
return res
|
||||
|
||||
|
||||
def candidatos_de(dominio, contenido):
|
||||
"""Directorios que PODRÍAN ser de este dominio. Por etiqueta principal, no por nombre completo."""
|
||||
d = dominio.lower()
|
||||
etiquetas = [x for x in d.split(".") if x not in ("www", "com", "net", "org", "ec", "io", "dev")]
|
||||
principal = etiquetas[-1] if etiquetas else d
|
||||
out = []
|
||||
for c in contenido:
|
||||
base = os.path.basename(c["path"]).lower()
|
||||
if principal and (principal in base or base.split(".")[0] in etiquetas):
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
|
||||
# ── clasificación ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Nombres que NO son servicios: son el andamiaje del sistema o hijos de un servicio. Contarlos como
|
||||
@@ -539,6 +578,11 @@ def emitir_toml(c):
|
||||
L.append(f"paquete = {toml_escape(s.get('paquete',''))}")
|
||||
L.append('decision = "" # muda | muere | (vacío = sin decidir)')
|
||||
L.append("")
|
||||
for x in c.get("contenido", []):
|
||||
L.append("[[contenido]]")
|
||||
for k, v in x.items():
|
||||
L.append(f"{k} = {toml_escape(v)}")
|
||||
L.append("")
|
||||
for d in c["dominios"]:
|
||||
L.append("[[dominio]]")
|
||||
for k, v in d.items():
|
||||
@@ -549,6 +593,12 @@ def emitir_toml(c):
|
||||
L.append("[[datos]]")
|
||||
for k, v in d.items():
|
||||
L.append(f"{k} = {toml_escape(v)}")
|
||||
# ⚠ `destino` existe porque la ruta de origen NO tiene por qué caber en la misma ruta del
|
||||
# destino. Medido contra la caja takana: `/` tiene 3,5 G libres y los datos de gioser son
|
||||
# 33,6 G — el sitio está en `/work` (66 G) y en `/store` (19 G). Sin este campo el plan
|
||||
# copiaba ruta→misma ruta y la mudanza no entraba, con el detalle de que el fallo aparece a
|
||||
# mitad de un rsync de 22 G.
|
||||
L.append('destino = "" # vacío = la MISMA ruta en el destino. Ej: "/work/home"')
|
||||
L.append('decision = "" # muda | muere | (vacío = sin decidir)')
|
||||
L.append("")
|
||||
for f in c["fuera_de_git"]:
|
||||
@@ -649,6 +699,7 @@ def main():
|
||||
"el proceso reescribió su argv[0] y no se pudo leer /proc/<pid>/exe")
|
||||
pts = puertos(t)
|
||||
nombres = dominios(t)
|
||||
cont = contenido_web(t)
|
||||
sondear = (a.probe or a.probe_dns or bool(a.host)) and not a.no_probe
|
||||
http_ok = not a.probe_dns
|
||||
if nombres and not sondear:
|
||||
@@ -670,7 +721,7 @@ def main():
|
||||
censo = {
|
||||
"fecha": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"identidad": ident, "servicios": svc, "huerfanos": huerfanos, "puertos": pts,
|
||||
"dominios": doms, "datos": dat, "montajes": montajes(t),
|
||||
"dominios": doms, "datos": dat, "contenido": cont, "montajes": montajes(t),
|
||||
"fuera_de_git": fuera_de_git(t),
|
||||
}
|
||||
resumen(censo)
|
||||
|
||||
@@ -36,6 +36,7 @@ import argparse, os, sys, shlex, time, tomllib
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import formatos # noqa: E402
|
||||
from censar import candidatos_de as candidatos # noqa: E402
|
||||
|
||||
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
_AQUI = os.path.join("scripts", "mudanza", "traducir.py")
|
||||
@@ -532,27 +533,60 @@ def pasos(censo, target, key):
|
||||
svc_muda = [s for s in censo.get("servicio", []) if s.get("decision") == "muda"]
|
||||
doms_muda = [d for d in censo.get("dominio", []) if d.get("decision") == "muda"]
|
||||
muere = [(t, n, e) for t, n, e in entradas(censo) if e.get("decision") == "muere"]
|
||||
contenido = censo.get("contenido", [])
|
||||
|
||||
# ── 0. preflight ────────────────────────────────────────────────────────────────────────────
|
||||
# ⚠ El sitio se mide POR SISTEMA DE FICHEROS, no contra `/`. Medido en la caja takana: `/` tiene
|
||||
# 3,5 G libres, `/store` 19 G y `/work` 66 G — un preflight que sume 33,6 G y los compare contra
|
||||
# `/` da un veredicto correcto por casualidad si todo cae en `/`, y COMPLETAMENTE FALSO en cuanto
|
||||
# una ruta va a otro montaje: diría que no entra cuando entra, o al revés. Y además: aquí el
|
||||
# veredicto lo saca el propio comando (exit ≠0 si falta sitio), no un humano leyendo una columna.
|
||||
need = sum(x for x in (a_mib(d.get("size_copy") or d.get("size")) for d in datos_muda)
|
||||
if x is not None)
|
||||
ilegibles = [d["path"] for d in datos_muda
|
||||
if a_mib(d.get("size_copy") or d.get("size")) is None]
|
||||
paso("preflight", f"el destino responde y tiene sitio (hacen falta {humano(need)})",
|
||||
f"{ssh} 'df -Pm / | tail -1'",
|
||||
verifica=f"la columna 'Available' debe ser > {int(need)} MiB",
|
||||
verifica_tipo="humano",
|
||||
chks = []
|
||||
for d in datos_muda:
|
||||
mib = a_mib(d.get("size_copy") or d.get("size"))
|
||||
if mib is None:
|
||||
continue
|
||||
dst = (d.get("destino") or "").strip() or d["path"]
|
||||
chks.append(f"chk {shlex.quote(dst)} {int(mib)}")
|
||||
guion = "\n".join([
|
||||
"set -u",
|
||||
"chk() {",
|
||||
' t="$1"',
|
||||
' while [ ! -d "$t" ] && [ "$t" != "/" ]; do t=$(dirname "$t"); done',
|
||||
' df -Pm "$t" | tail -1 | awk -v n="$2" -v r="$1" \'{print $6, $4, n, r}\'',
|
||||
"}",
|
||||
"{",
|
||||
*[" " + c for c in chks],
|
||||
"} | awk '",
|
||||
" { libre[$1]=$2; falta[$1]+=$3; rutas[$1]=rutas[$1] \" \" $4 }",
|
||||
" END { mal=0",
|
||||
' for (f in falta) {',
|
||||
' printf " %-16s necesita %8d MiB · libres %8d ⇐%s\\n", f, falta[f], libre[f], rutas[f]',
|
||||
' if (falta[f] > libre[f]) { mal=1; printf " ✗ NO ENTRA en %s: faltan %d MiB\\n", f, falta[f]-libre[f] }',
|
||||
" }",
|
||||
' if (mal==0) printf " ✓ entra en todos los sistemas de ficheros\\n"',
|
||||
" exit mal }'",
|
||||
])
|
||||
paso("preflight", f"el destino responde y tiene sitio ({humano(need)} en total)",
|
||||
f"{ssh} 'sh -s' <<'FIN'\n{guion}\nFIN",
|
||||
verifica=f"{ssh} 'sh -s' <<'FIN'\n{guion}\nFIN",
|
||||
nota=("⚠ tamaños ilegibles, NO contados en el total: " + ", ".join(ilegibles)) if ilegibles else
|
||||
"Dimensionado con el tamaño de COPIA (hardlinks expandidos), no con `du`.")
|
||||
"Dimensionado con el tamaño de COPIA (hardlinks expandidos), no con `du`, y medido "
|
||||
"contra el sistema de ficheros de CADA destino.")
|
||||
|
||||
# ── 1. datos ────────────────────────────────────────────────────────────────────────────────
|
||||
for d in datos_muda:
|
||||
p = d["path"]
|
||||
paso("datos", f"copiar {p} ({d.get('size','?')})",
|
||||
dst = (d.get("destino") or "").strip() or p
|
||||
paso("datos", f"copiar {p} → {dst} ({d.get('size','?')})",
|
||||
f"rsync -aH --partial --info=stats2 --exclude='.dmerge' "
|
||||
f"-e {shlex.quote('ssh ' + ssh_opts)} {shlex.quote(p + '/')} {target}:{shlex.quote(p + '/')}",
|
||||
f"-e {shlex.quote('ssh ' + ssh_opts)} {shlex.quote(p + '/')} {target}:{shlex.quote(dst + '/')}",
|
||||
verifica=(f"src=$(find {shlex.quote(p)} -type f | wc -l); "
|
||||
f"dst=$({ssh} 'find {shlex.quote(p)} -type f | wc -l'); "
|
||||
f"dst=$({ssh} 'find {shlex.quote(dst)} -type f | wc -l'); "
|
||||
f'echo "ficheros: origen=$src destino=$dst"; [ "$src" = "$dst" ]'),
|
||||
nota="`-H` obligatorio: sin él los hardlinks se expanden (60 G medidos → 85 G). "
|
||||
"Y la verificación COMPARA, no informa: cuenta los ficheros de los dos lados y "
|
||||
@@ -609,10 +643,36 @@ def pasos(censo, target, key):
|
||||
|
||||
# ── 4. lo que muere ─────────────────────────────────────────────────────────────────────────
|
||||
if muere:
|
||||
# ⚠ Un dominio fósil no pesa nada POR SÍ MISMO: pesa lo que dejó en disco. Listarlo pelado
|
||||
# cumple la mitad de la regla 2 («por su nombre Y CON SU TAMAÑO») y deja lo caro invisible:
|
||||
# `terapeuta.ec` no resuelve en ningún DNS y tiene 279 M en `/var/www/terapeuta`. Que eso se
|
||||
# supiera dependía de que alguien se acordara.
|
||||
det = []
|
||||
for t, n, e in muere:
|
||||
tam = f" ({e.get('size')})" if e.get("size") else ""
|
||||
det.append(f"# {t:9} {n}{tam}")
|
||||
|
||||
# ⚠ Un dominio fósil no pesa nada POR SÍ MISMO: pesa lo que dejó en disco. Listarlo pelado
|
||||
# cumple la mitad de la regla 2 («por su nombre Y CON SU TAMAÑO») y deja lo caro invisible:
|
||||
# `terapeuta.ec` no resuelve en ningún DNS y tiene 279 M en `/var/www/terapeuta`. Que eso se
|
||||
# supiera dependía de que alguien se acordara.
|
||||
#
|
||||
# Se lista UN RENGLÓN POR DIRECTORIO, no uno por dominio: siete dominios `*.gioser.net` que
|
||||
# mueren reclaman el mismo `gioser.bak`, y repetirlo siete veces convierte el aviso en ruido.
|
||||
# Y se dice «podría», nunca «es»: el directorio se llama `terapeuta` y el dominio
|
||||
# `terapeuta.ec`. Emparejar «casi» acierta casi siempre, y el resto de las veces manda a
|
||||
# borrar lo que no era.
|
||||
if contenido:
|
||||
doms_muertos = [n for t, n, _ in muere if t == "dominio"]
|
||||
det.append("#")
|
||||
det.append("# CONTENIDO EN LAS RAÍCES WEB — esto NO lo mueve ningún paso de este plan.")
|
||||
for c in contenido:
|
||||
quien = [d for d in doms_muertos if c in candidatos(d, [c])]
|
||||
marca = ("podría ser de: " + ", ".join(quien[:4]) +
|
||||
(f" (+{len(quien)-4})" if len(quien) > 4 else "")) if quien else \
|
||||
"no corresponde a NINGÚN dominio que muera: nadie lo recuerda, miralo"
|
||||
det.append(f"# {c['path']:34} {c['size']:>6} ← {marca}")
|
||||
|
||||
paso("muere", f"{len(muere)} cosa(s) NO se mudan y mueren con la máquina vieja",
|
||||
"# NO SE BORRA NADA ACÁ. Esta lista existe para leerse antes de apagar el origen:\n"
|
||||
+ "\n".join(det),
|
||||
|
||||
Reference in New Issue
Block a user