censar: 26 repositorios existen SÓLO en la máquina que se va a borrar — el paso 1 del plan

Buscando cómo escribir la receta de `tawasuyu` apareció que su ÚNICO remoto es el gitea de gioser
(`ssh://gitea@git.tawasuyu.net:2345/…`, y ese nombre resuelve a 204.168.193.248 = gioser). Al mirar
el resto: **28 repositorios con remoto en esta máquina, 26 SIN NINGUNA copia fuera**. Sólo `takana` y
`llimphi-standalone` tienen espejo externo.

Apagar el origen no borra unos servicios: borra EL CÓDIGO CON EL QUE SE VOLVERÍAN A CONSTRUIR, y los
clones de trabajo están en el mismo disco que también muere. Entre los 26 está `tawasuyu`, que
produce 10 de los 13 binarios que nadie más provee — la dependencia circular completa.

No se ve desde ninguna otra parte del censo: un repo no es un proceso, ni un puerto, ni un dominio.
Se descubre cuando ya no hay de dónde sacarlo.

· `censar.py` lo mira ahora (`[[repo]]`), y reporta NO «tiene remoto» sino si alguno de sus remotos
  NO es esta máquina: un remoto que apunta afuera es la prueba de que el código sobrevive. La lista
  de «esta máquina» sale del censo mismo (sus IPs + los dominios que sirve), no de nombres cableados.
· `planear.py` lo emite como PASO 1, por delante del rescate de binarios: aquello pierde un servicio,
  esto pierde la posibilidad de reconstruirlo. El arreglo ya está escrito en el repo —takana usa
  `pushurl` doble por `scripts/espejo-setup.sh`.

Y una corrección de algo que dije antes: la perilla por receta que vi en `recipe.rs` es `strip_debug`,
no una versión de rust. NO existe `rust_version`; sólo `zig_version`. Pinear rustc por receta para
honrar el `rust-toolchain.toml` de tawasuyu (1.96.0, contra 1.97.0 del lab) sería código nuevo en
takana-core, no una opción ya disponible.

De paso, medido el subárbol de los diez binarios por separado: cuatro (`willay-daemon`,
`sandokan-seguridad-core`, `pacha-secretos`, `tupu-cli`, entre 84 y 153 deps) NO arrastran
criptografía en C; cuatro traen `ring` y dos `aws-lc-sys`. O sea que hay un escalón por donde
empezar sin pelear con cmake.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
Sergio
2026-09-12 11:06:52 +00:00
co-authored by Claude Opus 5
parent 35afe8260d
commit 0b5dae7812
3 changed files with 98 additions and 1 deletions
+53 -1
View File
@@ -322,6 +322,50 @@ _CONFS = [("/etc/caddy/Caddyfile", "caddy"),
("/etc/httpd/conf.d/*", "apache")]
def repos_git(t, raices, ips, dominios):
"""Repositorios git bajo los datos, y si su remoto vive EN ESTA MÁQUINA.
── LA DEPENDENCIA CIRCULAR QUE MATA LA MUDANZA AL FINAL ─────────────────────────────────────
Medido en gioser: **25 repositorios cuyo único remoto es el gitea de la propia máquina** — entre
ellos `tawasuyu`, que produce 10 de los 13 binarios que nadie más provee. Apagar el origen no
borra sólo unos servicios: borra **el código con el que se volverían a construir**, y los clones
de trabajo están en el mismo disco que también muere.
Es un fallo que no se ve desde ninguna otra parte del censo —el repo no es un proceso, ni un
puerto, ni un dominio— y que sólo se descubre cuando ya no hay de dónde sacarlo.
Un remoto que apunta AFUERA es la prueba de que el código sobrevive. Por eso lo que se reporta no
es «tiene remoto» sino **si alguno de sus remotos NO es esta máquina**."""
if not raices:
return []
busca = " ".join(shlex.quote(r) for r in raices)
out, ok = t.run(f"find {busca} -maxdepth 4 -name .git -prune 2>/dev/null | head -300")
if not ok or not out.strip():
return []
mios = set(dominios) | set(ips) | {"localhost", "127.0.0.1"}
res = []
for g in out.split():
r = g[:-5] if g.endswith("/.git") else g
urls, ok2 = t.run(f"git -C {shlex.quote(r)} config --get-regexp "
f"'remote\\..*\\.(url|pushurl)' 2>/dev/null")
if not ok2 or not urls.strip() or urls.startswith("(sonda falló"):
continue
dentro, fuera = [], []
for l in urls.splitlines():
p = l.split(None, 1)
if len(p) != 2:
continue
u = p[1]
# `ssh://gitea@host:2345/x.git`, `gitea@host:x.git`, `https://host/x.git`
h = u.split("://", 1)[-1].split("/", 1)[0].split("@")[-1].split(":")[0]
(dentro if h in mios else fuera).append(u)
if dentro:
res.append({"path": r, "dentro": " ".join(dentro),
"fuera": " ".join(fuera),
"solo_aqui": "si" if not fuera else "no"})
return res
def sitios_web(t):
"""Los sitios declarados en la config, con su raíz y si esa raíz EXISTE.
@@ -694,6 +738,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("repos", []):
L.append("[[repo]]")
for k, v in x.items():
L.append(f"{k} = {toml_escape(v)}")
L.append("")
for x in c.get("sitios", []):
L.append("[[sitio]]")
for k, v in x.items():
@@ -845,10 +894,13 @@ def main():
doms.append(sondear_dominio(d, set(ident["ips"]), http_ok))
raices = a.data_root or ["/var/www", "/var/lib", "/srv", "/opt", "/home", "/store"]
dat = datos(t, raices)
# Después de `raices`, que es de dónde salen los repos a mirar. (Estaba antes y `raices` todavía
# no existía: habría reventado con NameError en la primera corrida con datos.)
repos = repos_git(t, raices + ["/mnt/vvv"], ident.get("ips", []), nombres)
censo = {
"fecha": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"identidad": ident, "servicios": svc, "huerfanos": huerfanos, "puertos": pts,
"dominios": doms, "datos": dat, "contenido": cont, "sitios": sitios, "montajes": montajes(t),
"dominios": doms, "datos": dat, "contenido": cont, "sitios": sitios, "repos": repos, "montajes": montajes(t),
"fuera_de_git": fuera_de_git(t),
}
resumen(censo)
+24
View File
@@ -638,6 +638,30 @@ 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. CÓDIGO QUE SÓLO EXISTE ACÁ ───────────────────────────────────────────────────────────
# El primer paso del plan entero, por delante incluso del rescate de binarios: aquello es un
# servicio que se pierde, esto es EL CÓDIGO CON EL QUE SE VOLVERÍA A CONSTRUIR. Y no se ve desde
# ninguna otra parte del censo —un repo no es un proceso, ni un puerto, ni un dominio—, así que
# se descubre cuando ya no hay de dónde sacarlo.
solos = [r for r in censo.get("repo", []) if r.get("solo_aqui") == "si"]
if solos:
det = ["# Estos repositorios NO tienen ningún remoto fuera de esta máquina. Apagarla borra el",
"# código, y los clones de trabajo están en el mismo disco que también muere.",
"#",
"# Por cada uno: agregarle un remoto que viva en otro lado y empujar. El propio takana",
"# lo hace así (`scripts/espejo-setup.sh`: pushurl doble a gitea Y a un espejo privado).",
"#"]
for r in solos:
det.append(f"# {r['path']}")
det.append(f"# remoto actual (en esta máquina): {r['dentro'][:88]}")
paso("codigo", f"{len(solos)} repositorio(s) que sólo existen en la máquina que se va a borrar",
"\n".join(det),
verifica="revisión humana: cada uno tiene que tener un remoto FUERA antes de apagar nada",
verifica_tipo="humano",
nota="⚠ Es la dependencia circular de esta mudanza: `tawasuyu` produce 10 de los 13 "
"binarios que nadie más provee, y su único remoto es el gitea de esta misma "
"máquina. Sin esto, apagar el origen deja servicios que no se pueden reconstruir.")
# ── 0 ter. COHERENCIA ───────────────────────────────────────────────────────────────────────
incoh = incoherencias(censo)
if incoh: