diff --git a/docs/29-mudanza.md b/docs/29-mudanza.md index 2d598893..4308b9f3 100644 --- a/docs/29-mudanza.md +++ b/docs/29-mudanza.md @@ -301,6 +301,35 @@ 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 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: +`/proc//exe` termina en `(deleted)` para **cuatro servicios de gioser**. El fichero ya no está +en disco; sólo vive el inodo que sostiene su proceso. + +Y en tres de los cuatro hay **ahora otro fichero en la misma ruta, de distinto tamaño**: + +| servicio | corriendo | en su ruta hoy | +|---|---|---| +| `tejido` | 12 750 368 B | 13 815 864 B | +| `shuma-gateway` | 8 431 896 B | 10 363 504 B | +| `pacha-secretos` | 8 634 240 B | 8 647 456 B | +| `puerta-f6e393ff…` | 197 262 440 B | *no hay nada* | + +Copiar la ruta **no falla**: muda otra cosa, y el servicio nuevo no es el que estaba andando. Es el +modo de fallo más caro que hay en este documento — todo verde, todo distinto. + +Se recuperan leyendo `/proc//exe`, **y sólo mientras el proceso viva**. En una mudanza que +termina *borrando el origen*, un reinicio de gioser antes de este paso pierde esos binarios para +siempre. Por eso el rescate va **antes del preflight**: es el único paso del plan que puede volverse +imposible mientras se piensa el resto. + +Su verificación **compara tamaños** contra el que corre, y no por celo: un `cat` de un `/proc` que ya +no existe crea un fichero **vacío** y devuelve 0. Probado en los dos sentidos — el paso real pasa, y +con un fichero vacío a propósito el guardián dice `FALTA tejido` y sale 1. + +Los cuatro ya están rescatados en `work/mudanza/rescate/`, con su `SHA256SUMS`. + ### 4.0 decies · Leer la config del servidor: un fósil se detecta SIN tocar la red La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Eso lo dice el diff --git a/scripts/mudanza/censar.py b/scripts/mudanza/censar.py index 9a06142a..4badc9cb 100755 --- a/scripts/mudanza/censar.py +++ b/scripts/mudanza/censar.py @@ -101,6 +101,24 @@ def invocaciones(t, pids): # quedaban en «no se pudo leer su binario» por esto, y no por ser realmente desconocidos. exe, _ = t.run(f"readlink /proc/{pid}/exe 2>/dev/null") cmd, cwd, exe = cmd.strip(), cwd.strip(), exe.strip() + # ⚠⚠ `/proc//exe` termina en « (deleted)» cuando el fichero YA NO ESTÁ en disco: el + # proceso sostiene el inodo y nadie más lo puede abrir por su ruta. Medido en gioser: CUATRO + # servicios están así, y en dos de ellos hay AHORA otro fichero en la misma ruta, de distinto + # tamaño — `tejido` corre 12 750 368 bytes y en su ruta hay 13 815 864. Copiar la ruta muda + # OTRA COSA, en silencio, y el servicio nuevo no es el que estaba andando. + # + # Se recupera leyendo `/proc//exe`, y SÓLO mientras el proceso viva: al reiniciar gioser + # —o al morirse ese pid— ese binario desaparece para siempre. En una mudanza que termina + # BORRANDO el origen, esto es lo más urgente del censo. + borrado = exe.endswith(" (deleted)") + if borrado: + exe = exe[:-len(" (deleted)")] + vivo_b = disco_b = "" + if borrado: + v, _ = t.run(f"stat -L -c %s /proc/{pid}/exe 2>/dev/null") + d, _ = t.run(f"stat -c %s {shlex.quote(exe)} 2>/dev/null") + vivo_b = v.strip() if v.strip().isdigit() else "" + disco_b = d.strip() if d.strip().isdigit() else "" for v in ("cmd", "cwd", "exe"): if locals()[v].startswith("(sonda falló"): cmd, cwd, exe = (("" if v == "cmd" else cmd), ("" if v == "cwd" else cwd), @@ -111,7 +129,9 @@ def invocaciones(t, pids): if cmd or cwd or exe: out[nombre] = {"cmdline": cmd, "cwd": cwd, "exe": exe if exe.startswith("/") else "", - "usuario": (sup or {}).get("usuario", "")} + "usuario": (sup or {}).get("usuario", ""), + "exe_borrado": borrado, "pid": pid, + "bytes_vivo": vivo_b, "bytes_disco": disco_b} return out @@ -642,6 +662,11 @@ def emitir_toml(c): L.append(f"exec = {toml_escape(s.get('exec',''))}") if s.get('usuario'): L.append(f"usuario = {toml_escape(s['usuario'])}") + if s.get('exec_borrado'): + L.append("exec_borrado = true") + L.append(f"pid_vivo = {int(s.get('pid_vivo') or 0)}") + L.append(f"bytes_vivo = {toml_escape(s.get('bytes_vivo',''))}") + L.append(f"bytes_disco = {toml_escape(s.get('bytes_disco',''))}") if s.get('exec_motivo'): L.append(f"exec_motivo = {toml_escape(s['exec_motivo'])}") L.append(f"paquete = {toml_escape(s.get('paquete',''))}") @@ -750,6 +775,11 @@ def main(): e['cmdline'] = i.get('cmdline', '') e['cwd'] = i.get('cwd', '') e['exe_real'] = i.get('exe', '') + if i.get('exe_borrado'): + e['exec_borrado'] = True + e['pid_vivo'] = i.get('pid', 0) + e['bytes_vivo'] = i.get('bytes_vivo', '') + e['bytes_disco'] = i.get('bytes_disco', '') if i.get('usuario'): e['usuario'] = i['usuario'] # `/proc//exe` MANDA sobre `argv[0]`: es el binario de verdad y no lo puede reescribir el diff --git a/scripts/mudanza/planear.py b/scripts/mudanza/planear.py index b1dcb8ef..ab406277 100755 --- a/scripts/mudanza/planear.py +++ b/scripts/mudanza/planear.py @@ -297,6 +297,19 @@ def origen_binario(e, repo=None): return ("paquete-ajeno", f"lo provee el gestor del origen: paquete `{pkg}`. En el destino hace falta una " f"receta takana, o `qorpa` si se acepta una jaula con la distro ajena (ADR 0015)") + if exe and e.get("exec_borrado"): + # Su propia clase, y la más urgente del censo: el fichero YA NO EXISTE. No es «hay que + # llevarlo», es «se pierde en el próximo reinicio y el que está en su ruta NO es el mismo». + pid = e.get("pid_vivo") or (e.get("pids") or [0])[0] + bv, bd = e.get("bytes_vivo", ""), e.get("bytes_disco", "") + dif = (f" El que hay AHORA en esa ruta pesa {bd} bytes y el que corre {bv}: NO SON EL MISMO." + if bv and bd and bv != bd else + (" En esa ruta ya no hay nada." if not bd else "")) + return ("borrado", + f"⚠ el binario que CORRE fue borrado o reemplazado: `{exe}` ya no existe en disco." + f"{dif} Sólo vive mientras viva el pid {pid}; al reiniciar el origen se pierde PARA " + f"SIEMPRE. Rescatalo ya: sudo cat /proc/{pid}/exe > {os.path.basename(exe)} " + f"&& chmod +x {os.path.basename(exe)}") if exe: return ("suelto", f"NADIE lo provee: binario puesto a mano en {exe}. Hay que LLEVARLO (con lo que " @@ -544,6 +557,33 @@ def pasos(censo, target, key): muere = [(t, n, e) for t, n, e in entradas(censo) if e.get("decision") == "muere"] contenido = censo.get("contenido", []) + # ── 0 bis. RESCATE, antes que nada ────────────────────────────────────────────────────────── + # Va PRIMERO, antes del preflight, porque es el único paso del plan que puede volverse + # IMPOSIBLE mientras se piensa el resto: estos binarios ya no existen en disco y sólo viven en el + # inodo que sostiene su proceso. Un reinicio del origen —o que se caiga ese pid— y se pierden + # para siempre. Y en dos de los cuatro hay AHORA otro fichero en la misma ruta, de distinto + # tamaño: copiar la ruta no falla, muda otra cosa. + borrados = [s for s in censo.get("servicio", []) + if s.get("exec_borrado") and s.get("decision") == "muda"] + if borrados: + lineas, ver = [], [] + for s in borrados: + pid = s.get("pid_vivo") or (s.get("pids") or [0])[0] + nom = os.path.basename(s.get("exec") or s["name"]) + lineas.append(f"sudo cat /proc/{pid}/exe > rescate/{shlex.quote(nom)} && " + f"chmod +x rescate/{shlex.quote(nom)}") + if s.get("bytes_vivo"): + # La verificación COMPARA contra el tamaño del que CORRE. Un `cat` de un /proc que ya + # no existe crea un fichero VACÍO y devuelve 0: sin comparar, el rescate «pasa». + ver.append(f"[ \"$(stat -c %s rescate/{shlex.quote(nom)} 2>/dev/null)\" = " + f"\"{s['bytes_vivo']}\" ] || {{ echo 'FALTA {nom}'; exit 1; }}") + paso("rescate", f"rescatar {len(borrados)} binario(s) BORRADOS del disco (sólo mientras corran)", + "mkdir -p rescate\n" + "\n".join(lineas), + verifica="\n".join(ver) + "\necho ' ✓ los binarios rescatados coinciden con los que corren'", + nota="⚠ ESTE PASO CADUCA. `/proc//exe` deja de existir cuando muere el proceso, y " + "estos ficheros ya no están en su ruta. Si el origen se reinicia antes de correr " + "esto, el binario que estaba andando NO se puede recuperar de ninguna manera.") + # ── 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