diff --git a/scripts/mudanza/censar.py b/scripts/mudanza/censar.py index ddb5f84c..5d20522f 100755 --- a/scripts/mudanza/censar.py +++ b/scripts/mudanza/censar.py @@ -1226,7 +1226,13 @@ def main(): if i and http_ok: time.sleep(0.5) # no parecer un escaneo ante el propio fail2ban del objetivo doms.append(sondear_dominio(d, set(ident["ips"]), http_ok)) - raices = a.data_root or ["/var/www", "/var/lib", "/srv", "/opt", "/home", "/store"] + # ⚠ `/mnt/vvv` ENTRA (2026-09-16). No estaba, y ahí vive el trabajo: los repos de la persona, el + # monorepo, el store y `work/sources`. El censo listaba 169 cosas y ninguna era el árbol donde + # está todo — el agujero apareció cuando el usuario preguntó por `humanoid` y el censo sólo + # conocía `/home/sergio/humanoid`, que son 200 M de `build/` viejo: el proyecto de verdad, con + # su `.git`, estaba en `/mnt/vvv/humanoid`, fuera de toda raíz censada. + # (`repos_git` sí lo miraba: una parte del censo conocía el árbol y la otra no.) + raices = a.data_root or ["/var/www", "/var/lib", "/srv", "/opt", "/home", "/store", "/mnt/vvv"] # Dos vistas y no una: el TOTAL por raíz es lo que dimensiona la mudanza (y lo que mira el # preflight), y el DETALLE por cosa es lo que se puede decidir. Dar sólo el total obliga a # decidir 25 G de una; dar sólo el detalle pierde el número que dice si entra en el destino. diff --git a/scripts/mudanza/planear.py b/scripts/mudanza/planear.py index 85e410fc..6685e0ed 100755 --- a/scripts/mudanza/planear.py +++ b/scripts/mudanza/planear.py @@ -601,6 +601,62 @@ def revisar(censo, interactivo=True): return censo +def decidir_desde_fichero(censo, ruta_dec): + """Decidir EN LOTE desde un fichero de texto: ` ` por línea. + + ── POR QUÉ EXISTE (pedido del usuario, 2026-09-16) ───────────────────────────────────────── + «no sé cómo activar o desactivar cosas aquí desde shuma llimphi remoto, que las opciones son + con teclas unitarias». El modo interactivo de `--decide` lee una tecla por entrada, y hay + terminales —la consola remota de shuma, entre otras— donde eso no se puede usar. Una + herramienta cuya única forma de decidir exige un tipo de terminal no es una herramienta: es una + herramienta *para esa terminal*. El fichero funciona en cualquiera, se revisa antes de + aplicarlo, se versiona y se vuelve a correr — que es lo mismo que ya se pide del plan. + + Formato, deliberadamente pobre (`#` comenta, se ignoran líneas vacías): + + /home/sergio/mapas muda + terapeuta respalda # basta con que el nombre sea único + /var/lib/swap muere + + ⚠ Una clave que no empareja con NADA es un ERROR RUIDOSO, no un aviso: significa que alguien + decidió algo que no se aplicó, y el silencio ahí es exactamente la clase de fallo que este + programa existe para no tener. Lo mismo si empareja con varias: se nombran todas y no se toca + ninguna, porque elegir una por su cuenta sería adivinar. + """ + lineas = [] + with open(ruta_dec, encoding="utf-8") as fh: + for i, l in enumerate(fh, 1): + l = l.split("#", 1)[0].strip() + if l: + lineas.append((i, l)) + todos = entradas(censo) + aplicadas, problemas = 0, [] + for i, l in lineas: + partes = l.rsplit(None, 1) + if len(partes) != 2 or partes[1] not in DECISIONES: + problemas.append(f"línea {i}: «{l}» — falta la decisión o no es una de " + f"{'/'.join(DECISIONES)}") + continue + clave, dec = partes + cands = [(t, n, e) for t, n, e in todos + if n == clave or os.path.basename(str(n)) == clave] + if not cands: + problemas.append(f"línea {i}: «{clave}» no empareja con NADA del censo") + continue + if len(cands) > 1: + problemas.append(f"línea {i}: «{clave}» empareja con {len(cands)}: " + + ", ".join(n for _t, n, _e in cands[:5])) + continue + cands[0][2]["decision"] = dec + aplicadas += 1 + if problemas: + print("\n!! decisiones NO aplicadas:", file=sys.stderr) + for x in problemas: + print(f" {x}", file=sys.stderr) + sys.exit("\n No se escribe nada: un lote a medias deja decidido lo que nadie revisó.") + return aplicadas + + def guardar_censo(censo, ruta, respaldar=True): """Vuelve a escribir el censo con las decisiones puestas, usando el MISMO emisor que lo creó. @@ -1149,6 +1205,9 @@ def main(): ap.add_argument("--revisar", action="store_true", help="sólo mostrar la revisión con recomendaciones, sin preguntar") ap.add_argument("--out", help="fichero del plan (default: stdout)") + ap.add_argument("--decidir", metavar="FICHERO", + help="decidir EN LOTE desde un fichero ` ` por línea. " + "Para terminales donde no se puede pulsar una tecla por entrada") ap.add_argument("--censo-out", help="dónde escribir el censo con las decisiones tomadas " "(default: sobre el mismo censo, con copia `.bak`)") ap.add_argument("--respaldo", help="corral donde va lo marcado `respalda` (host:ruta o ruta " @@ -1161,6 +1220,11 @@ def main(): if a.revisar: revisar(censo, interactivo=False) return + if a.decidir: + n = decidir_desde_fichero(censo, a.decidir) + destino_censo = a.censo_out or a.censo + guardar_censo(censo, destino_censo, respaldar=(destino_censo == a.censo)) + print(f" {n} decisión(es) aplicadas → {destino_censo}") if a.decide: censo = revisar(censo, interactivo=True) # ── LAS DECISIONES SE GUARDAN, O NO EXISTIERON ────────────────────────────────────────── diff --git a/scripts/mudanza/rutas-fuera-de-git.txt b/scripts/mudanza/rutas-fuera-de-git.txt index 666a1eb3..0e51de73 100644 --- a/scripts/mudanza/rutas-fuera-de-git.txt +++ b/scripts/mudanza/rutas-fuera-de-git.txt @@ -29,3 +29,34 @@ /var/lib/gitea # los repos servidos, la sqlite y las claves del gitea /etc/arje/cards.d # las tarjetas de arje: qué arranca esta máquina /ente # la Semilla de arje-zero + +# ── AMPLIACIÓN 2026-09-16: el barrido del frente tawasuyu ─────────────────────────────────────── +# El agente de tawasuyu barrió SU home y encontró 572 KB de secretos que ninguna de las 15 rutas de +# arriba miraba. Son identidades y semillas: no se «vuelven a generar», porque generar otras +# significa ser OTRA máquina para el resto de la flota. Van acá para que el censo las vea sin que +# nadie tenga que acordarse — que es el modo de fallo que esta lista existe para cerrar. +~/.wawa/seeds # ⚠ el anillo de la ceremonia de capacidades del génesis: sin él NINGUNA app nueva de wawa nace +~/.config/wawa # las tres llaves del operador de wawa (primaria, secundaria, recuperación) +~/.tejido # la IDENTIDAD de esta máquina en tejido + el padrón de teléfonos dados de alta +~/.local/share/agora # el grafo de confianza de ágora: el mismo padrón que usa `minga listen --admission` +~/.config/minga # la frase del espejo (16 B): sin ella el post-commit se aparta EN SILENCIO +~/.config/thasnuna # el token de telegram y el roster: INSTALAR.md dice que esto no va al repo a propósito +~/.config/shuma # identidad x25519, token del gateway y pares conocidos de shuma +~/.config/mirada # el escritorio afinado a mano: `perfiles-escritorio.ron` (178 KB) no lo genera nada +~/.config/hcloud # credenciales de Hetzner Cloud +~/.config/hammer # ⚠ ruta VIEJA del renombre, pero acá viven los `storagebox*.env` del respaldo +~/.rumi/seeds # semillas de rumi +~/keys # ⚠ claves de FIRMA DE APPS (`rizoma.p12`): perderlas impide ACTUALIZAR lo ya instalado +~/fdroid # ⚠ `keystore.p12` firma el índice del repo F-Droid, y `config.yml` trae su contraseña +~/.gnupg # el llavero GPG +~/.pgpass # contraseñas de postgres +~/.local/share/keyrings # llavero del escritorio +~/.local/share/pki # certificados de cliente +~/kkk # la credencial de deepseek (46 B): sin ella `scripts/agentes/deepseek` deja de existir +~/.zshrc # el entorno de trabajo de la persona, afinado a mano +~/.zshenv # ídem — y define rutas de las que dependen los guiones sueltos +/etc/wireguard # ⚠ `server.key` y `wg0.conf`: la VPN de la flota. Otra clave = otros pares +/etc/shuma # `gateway.env` del gateway de shuma +/etc/sandokan # `cerebro.json` +/etc/tawasuyu/agente # las autoridades del agente +/etc/local.d # los `*.start` de OpenRC escritos a mano (tejido-relay, arje-boot-guard)