censar: leer la config del servidor web — cinco sitios sirven un root que NO EXISTE
La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Eso lo dice el
FICHERO DE CONFIGURACIÓN, y leerlo no toca al origen: ni una petición, ni riesgo de fail2ban.
Para leerlo se agregó un LECTOR de Caddy al centro (`formatos/caddy.py` ya tenía el escritor), y el
censo lo usa en vez de tener su propio parser a medias — los de nginx y apache ya existían, y dos
parsers del mismo formato es cómo se separan sin que nadie lo note. Cuarto par de la familia web.
Sobre el Caddyfile real de gioser, cinco sitios apuntan a un `root` que no existe — y son justo los
que devolvían los 502 que en su día hicieron que el censo SE BANEARA A SÍ MISMO al sondearlos:
aura.gioser.net → /var/www/aura_frontend · sigma → /var/www/sigma/frontend
summa → /var/www/summa/frontend · kosmofono → … · dev.summa → …
Es evidencia MÁS FUERTE que el DNS: no hay nada que servir, devuelve 502 resuelva donde resuelva. Va
como recomendación `muere` con la ruta y el fichero donde está el bloque.
**Y sirve para lo contrario, que es donde el aviso hacía daño.** Un directorio que la config SÍ
referencia no es huérfano: el plan marcaba `/var/www/git-tawasuyu` como «nadie lo recuerda» estando
servido, y ese aviso aplicado tira `git.tawasuyu.net`.
Dos bugs propios, los dos encontrados contra el fichero real y no sobre un ejemplo mío:
· El `root` de ese sitio vive DENTRO de un `handle`, y yo saltaba los bloques anidados enteros por no
saber modelarlos. Que el pivote no sepa MODELAR algo no es razón para no VERLO: el bloque sigue
marcado SIN-TRADUCIR pero sus `root`/`reverse_proxy` se leen. Con eso aparecieron dos raíces
ausentes más, también anidadas.
· Detectar la cabecera de sitio con una lista negra de directivas dejaba pasar `log { output file … {`
y el snippet `(acceso) {`: DOS dominios inventados que el censo habría puesto a decidir. La regla
que aguanta es positiva — todos los tokens de la cabecera tienen que PARECER una dirección.
Sin regresión: `nginx → caddy` y `apache → caddy` siguen dando `Valid configuration` con el caddy del
corpus.
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,48 @@ 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 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
|
||||
**fichero de configuración**, y leerlo no toca al origen — ni una petición, ni riesgo de fail2ban.
|
||||
|
||||
Para leerlo se agregó un **lector de Caddy** al centro (`formatos/caddy.py`, que ya tenía el
|
||||
escritor), y el censo lo usa en vez de tener su propio parser a medias: los lectores de nginx y
|
||||
apache ya existían, y dos parsers del mismo formato es cómo se separan sin que nadie lo note.
|
||||
|
||||
El resultado inmediato, sobre el Caddyfile real de gioser — **cinco sitios apuntan a un `root` que no
|
||||
existe**, y son justo los que devolvían los 502 que en su día hicieron que el censo *se baneara a sí
|
||||
mismo* al sondearlos por HTTP:
|
||||
|
||||
```
|
||||
aura.gioser.net → /var/www/aura_frontend NO EXISTE
|
||||
sigma.gioser.net → /var/www/sigma/frontend NO EXISTE
|
||||
summa.gioser.net → /var/www/summa/frontend NO EXISTE
|
||||
kosmofono.gioser.net → /var/www/kosmofono/frontend NO EXISTE
|
||||
dev.summa.gioser.net → /var/www/summa-dev/frontend NO EXISTE
|
||||
```
|
||||
|
||||
Es evidencia **más fuerte que el DNS**: no hay nada que servir, devuelve 502 resuelva donde resuelva.
|
||||
|
||||
#### Y sirve para lo contrario, que es donde el aviso hacía daño
|
||||
|
||||
Un directorio que la config **sí** referencia no es un huérfano, diga lo que diga su nombre. Sin
|
||||
esto, el plan marcaba `/var/www/git-tawasuyu` como «nadie lo recuerda» estando servido — y ese aviso,
|
||||
aplicado, **tira `git.tawasuyu.net`**.
|
||||
|
||||
La primera versión del lector seguía fallando en eso, y el motivo es instructivo: el `root` de ese
|
||||
sitio vive **dentro de un `handle`**, y yo saltaba los bloques anidados enteros por no saber
|
||||
modelarlos. Que el pivote no sepa **modelar** algo no es razón para no **verlo**: ahora el bloque
|
||||
sigue marcado SIN-TRADUCIR, pero sus `root` y `reverse_proxy` se leen. Con eso aparecieron dos raíces
|
||||
ausentes más, que también estaban anidadas.
|
||||
|
||||
#### Falsos sitios: una lista negra no alcanzaba
|
||||
|
||||
Detectar la cabecera de un sitio descartando directivas conocidas (`handle {`, `route {`, `@bot {`)
|
||||
dejaba pasar `log { output file … {` y el snippet `(acceso) {` — **dos dominios inventados** que el
|
||||
censo habría puesto a decidir. La regla que aguanta es positiva: *todos* los tokens de la cabecera
|
||||
tienen que **parecer una dirección**.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -288,6 +288,75 @@ def declarados(t):
|
||||
return fuentes
|
||||
|
||||
|
||||
# Config del servidor web → qué sitio apunta a qué directorio. Se lee con los LECTORES del centro de
|
||||
# traducción (`formatos/`), no con un parser propio: ya existen para caddy, nginx y apache, y tener
|
||||
# dos parsers del mismo formato es cómo se separan sin que nadie lo note.
|
||||
_CONFS = [("/etc/caddy/Caddyfile", "caddy"),
|
||||
("/etc/nginx/nginx.conf", "nginx"),
|
||||
("/etc/nginx/sites-enabled/*", "nginx"),
|
||||
("/etc/apache2/sites-enabled/*", "apache"),
|
||||
("/etc/httpd/conf.d/*", "apache")]
|
||||
|
||||
|
||||
def sitios_web(t):
|
||||
"""Los sitios declarados en la config, con su raíz y si esa raíz EXISTE.
|
||||
|
||||
── POR QUÉ ESTO NO LO PODÍA DECIR EL DNS ───────────────────────────────────────────────────
|
||||
La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Un bloque
|
||||
cuyo `root` apunta a un directorio que NO existe es un fósil que devuelve 502 — y eso se lee del
|
||||
fichero: **sin red, sin pedirle una página al servidor, sin arriesgar su fail2ban**. Medido en
|
||||
gioser: 3 sitios (`aura`, `sigma`, `kosmofono`) apuntan a raíces borradas, lo que explica los
|
||||
502 que en su día hicieron que el censo se baneara a sí mismo al sondearlos por HTTP.
|
||||
|
||||
Y sirve para lo contrario: un directorio que la config SÍ referencia no es un huérfano. Sin esto
|
||||
el censo marcó `/var/www/git-tawasuyu` como «nadie lo recuerda» cuando es un sitio vivo."""
|
||||
import formatos
|
||||
lectores, _ = formatos.cargar()
|
||||
res = []
|
||||
for patron, fmt in _CONFS:
|
||||
if fmt not in lectores:
|
||||
continue
|
||||
ficheros = [patron]
|
||||
if "*" in patron:
|
||||
out, ok = t.run(f"ls -1 {patron} 2>/dev/null")
|
||||
ficheros = out.split() if ok and out.strip() else []
|
||||
for f in ficheros:
|
||||
texto, ok = t.run(f"cat {f} 2>/dev/null")
|
||||
if not ok or not texto.strip() or texto.startswith("(sonda falló"):
|
||||
continue
|
||||
try:
|
||||
cfg = lectores[fmt][0](texto, f)
|
||||
except Exception as e:
|
||||
# Un parser que revienta NO puede tumbar el censo entero, pero tampoco callarse:
|
||||
# sin esto uno cree que el servidor no declara sitios, que es lo contrario del dato.
|
||||
res.append({"config": f, "formato": fmt, "nombres": "", "raiz": "",
|
||||
"raiz_existe": "error", "detalle": f"no se pudo leer: {e}"[:120]})
|
||||
continue
|
||||
for s in cfg.sitios:
|
||||
# El `root` del sitio Y los de sus rutas anidadas: un `handle` con su propio `root`
|
||||
# referencia disco igual, y no verlo es lo que hacía pasar por huérfano a un sitio vivo.
|
||||
raices = [(s.raiz or "").rstrip("/")] + [
|
||||
a.valor.rstrip("/") for r in s.rutas for a in r.acciones if a.tipo == "archivos"]
|
||||
for raiz in dict.fromkeys(x for x in raices if x) or [""]:
|
||||
res.append({"config": f, "formato": fmt,
|
||||
"nombres": " ".join(s.nombres), "raiz": raiz,
|
||||
"raiz_existe": "", "detalle": ""})
|
||||
# Un solo comando para todas las raíces: una sonda por directorio sobre SSH es lentísima.
|
||||
raices = sorted({r["raiz"] for r in res if r["raiz"]})
|
||||
if raices:
|
||||
guion = "; ".join(f'[ -d {shlex.quote(d)} ] && echo "SI {d}" || echo "NO {d}"' for d in raices)
|
||||
out, _ = t.run(guion)
|
||||
estado = {}
|
||||
for l in out.splitlines():
|
||||
p = l.split(None, 1)
|
||||
if len(p) == 2:
|
||||
estado[p[1]] = "si" if p[0] == "SI" else "no"
|
||||
for r in res:
|
||||
if r["raiz"]:
|
||||
r["raiz_existe"] = estado.get(r["raiz"], "?")
|
||||
return res
|
||||
|
||||
|
||||
def dominios(t):
|
||||
"""Los dominios que la máquina DICE servir, contrastados con DNS y con un GET real.
|
||||
|
||||
@@ -578,6 +647,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("sitios", []):
|
||||
L.append("[[sitio]]")
|
||||
for k, v in x.items():
|
||||
L.append(f"{k} = {toml_escape(v)}")
|
||||
L.append("")
|
||||
for x in c.get("contenido", []):
|
||||
L.append("[[contenido]]")
|
||||
for k, v in x.items():
|
||||
@@ -700,6 +774,7 @@ def main():
|
||||
pts = puertos(t)
|
||||
nombres = dominios(t)
|
||||
cont = contenido_web(t)
|
||||
sitios = sitios_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:
|
||||
@@ -721,7 +796,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, "contenido": cont, "montajes": montajes(t),
|
||||
"dominios": doms, "datos": dat, "contenido": cont, "sitios": sitios, "montajes": montajes(t),
|
||||
"fuera_de_git": fuera_de_git(t),
|
||||
}
|
||||
resumen(censo)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Plugin de formato: ESCRITOR de Caddy (SDD 29)."""
|
||||
import os, sys
|
||||
"""Plugin de formato: Caddy — ESCRITOR y LECTOR (SDD 29)."""
|
||||
import os, re, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from formatos import escritor # noqa: E402
|
||||
from formatos import escritor, lector # noqa: E402
|
||||
|
||||
|
||||
def _direcciones(s):
|
||||
@@ -72,3 +72,131 @@ def escribir(cfg):
|
||||
if e["motivo"]:
|
||||
L.append(f"# └─ {e['motivo'][:96]}")
|
||||
return "\n".join(L) + "\n"
|
||||
|
||||
|
||||
# ── LECTOR ──────────────────────────────────────────────────────────────────────────────────────
|
||||
# Caddy se lee además de escribirse por dos motivos: mucha gente se muda DESDE Caddy, y —el que
|
||||
# empujó a escribirlo— el CENSO necesita saber a qué directorio apunta cada sitio. Un bloque cuyo
|
||||
# `root` no existe es un fósil que sirve 502, y eso se detecta leyendo el fichero: sin red, sin
|
||||
# pedirle una página al servidor, sin arriesgar su fail2ban.
|
||||
|
||||
_DIRECTIVAS_SITIO = {"root", "reverse_proxy", "file_server", "redir", "respond", "tls", "try_files",
|
||||
"handle", "handle_path", "encode", "header", "log", "php_fastcgi", "rewrite",
|
||||
"route", "import", "basicauth", "abort", "@"}
|
||||
|
||||
|
||||
# Una dirección de sitio de Caddy: `ejemplo.com`, `:8080`, `http://x.com:81`, `*.ejemplo.com`.
|
||||
_DIR = re.compile(r'^(?:(?:https?|h2c)://)?(?:\*\.)?[A-Za-z0-9_.-]+(?::\d+)?/?$|^:\d+$')
|
||||
|
||||
|
||||
def _es_cabecera_de_sitio(ln):
|
||||
"""`ejemplo.com, www.ejemplo.com {` — una cabecera de sitio, no una directiva con bloque.
|
||||
|
||||
⚠ La distinción no es cosmética, y una lista negra de directivas NO alcanza: probado contra el
|
||||
Caddyfile real, `log { output file /var/log/…` y el snippet `(acceso) {` pasaban por sitios, o
|
||||
sea DOS dominios inventados que el censo habría puesto a decidir. La regla que sí aguanta es
|
||||
positiva: **todos** los tokens de la cabecera tienen que PARECER una dirección."""
|
||||
if not ln.endswith("{") or ln.startswith(("}", "@", "#", "(")):
|
||||
return False
|
||||
cab = ln[:-1].strip()
|
||||
if not cab:
|
||||
return False
|
||||
piezas = [x for x in cab.replace(",", " ").split() if x]
|
||||
if not piezas or piezas[0].lstrip("@") in _DIRECTIVAS_SITIO:
|
||||
return False
|
||||
return all(_DIR.match(x) for x in piezas)
|
||||
|
||||
|
||||
@lector("caddy", familia="web")
|
||||
def leer(texto, nombre=""):
|
||||
from modelo import Config, Sitio, Ruta, Accion
|
||||
cfg = Config(formato_origen="caddy")
|
||||
acta = cfg.acta
|
||||
sitio, prof, anidado = None, 0, ""
|
||||
for n, cruda in enumerate(texto.splitlines(), 1):
|
||||
ln = cruda.strip()
|
||||
if not ln or ln.startswith("#"):
|
||||
continue
|
||||
if sitio is None:
|
||||
if _es_cabecera_de_sitio(ln):
|
||||
sitio, prof = Sitio(origen=n), 1
|
||||
for d in ln[:-1].replace(",", " ").split():
|
||||
d = d.strip()
|
||||
esquema = d.split("://")[0] if "://" in d else ""
|
||||
host = d.split("://")[-1]
|
||||
host, _, puerto = host.partition(":")
|
||||
if puerto:
|
||||
sitio.puertos.append(puerto)
|
||||
elif esquema:
|
||||
sitio.puertos.append("80" if esquema == "http" else "443")
|
||||
if host:
|
||||
sitio.nombres.append(host)
|
||||
acta.anota(n, "traducido", ln)
|
||||
elif ln.endswith("{"):
|
||||
prof += 1
|
||||
elif ln == "}":
|
||||
prof = max(0, prof - 1)
|
||||
else:
|
||||
# Opciones globales, `import`, snippets: no son un sitio y no se pierden en silencio.
|
||||
acta.anota(n, "no-aplica", ln, "fuera de todo bloque de sitio")
|
||||
continue
|
||||
|
||||
if ln.endswith("{"):
|
||||
prof += 1
|
||||
anidado = ln[:-1].strip()
|
||||
acta.anota(n, "SIN-TRADUCIR", ln,
|
||||
"bloque anidado (`handle`, `route`, matcher…): el pivote no lo modela. De su "
|
||||
"interior sólo se leen `root` y `reverse_proxy`")
|
||||
continue
|
||||
if ln == "}":
|
||||
prof -= 1
|
||||
if prof == 0:
|
||||
cfg.sitios.append(sitio)
|
||||
sitio = None
|
||||
continue
|
||||
if prof > 1:
|
||||
# ⚠ El bloque no se traduce, pero lo que APUNTA A DISCO sí se lee. Medido: el `root` de
|
||||
# `git.tawasuyu.net` vive dentro de un `handle`, así que saltar el bloque entero hacía
|
||||
# que el censo diera `/var/www/git-tawasuyu` por huérfano estando servido — y ese aviso,
|
||||
# aplicado, tira el sitio. Que el pivote no sepa MODELAR algo no es razón para no VERLO.
|
||||
pa = ln.split()
|
||||
if pa[0] == "root" and len(pa) >= 2:
|
||||
r = Ruta(patron=anidado, clase="prefijo", origen=n)
|
||||
r.acciones.append(Accion("archivos", pa[-1]))
|
||||
sitio.rutas.append(r)
|
||||
elif pa[0] == "reverse_proxy" and len(pa) >= 2:
|
||||
r = Ruta(patron=anidado, clase="prefijo", origen=n)
|
||||
r.acciones.append(Accion("proxy", pa[-1]))
|
||||
sitio.rutas.append(r)
|
||||
continue
|
||||
|
||||
p = ln.split()
|
||||
d = p[0]
|
||||
if d == "root" and len(p) >= 2:
|
||||
sitio.raiz = p[-1]
|
||||
acta.anota(n, "traducido", ln)
|
||||
elif d == "reverse_proxy" and len(p) >= 2:
|
||||
sitio.acciones.append(Accion("proxy", p[-1]))
|
||||
acta.anota(n, "traducido", ln)
|
||||
elif d == "file_server":
|
||||
sitio.acciones.append(Accion("archivos", sitio.raiz))
|
||||
acta.anota(n, "traducido", ln)
|
||||
elif d == "redir" and len(p) >= 2:
|
||||
sitio.acciones.append(Accion("redir", " ".join(p[1:])))
|
||||
acta.anota(n, "traducido", ln)
|
||||
elif d == "tls" and len(p) >= 3:
|
||||
sitio.tls = {"cert": p[1], "key": p[2]}
|
||||
acta.anota(n, "traducido", ln)
|
||||
elif d == "try_files":
|
||||
sitio.acciones.append(Accion("intentar", " ".join(p[1:])))
|
||||
acta.anota(n, "traducido", ln)
|
||||
elif d in ("encode", "log", "header", "tls"):
|
||||
acta.anota(n, "no-aplica", ln, f"`{d}` es afinado del servidor, no de lo que sirve")
|
||||
else:
|
||||
acta.anota(n, "SIN-TRADUCIR", ln, f"directiva `{d}` no reconocida")
|
||||
|
||||
if sitio is not None:
|
||||
acta.anota(sitio.origen, "SIN-TRADUCIR", f"sitio {', '.join(sitio.nombres)}",
|
||||
"el bloque no se cierra: falta una llave. Se descarta, porque un sitio a medias "
|
||||
"traducido es peor que uno ausente")
|
||||
return cfg
|
||||
|
||||
@@ -306,7 +306,7 @@ def origen_binario(e, repo=None):
|
||||
return ("desconocido", motivo or "no se pudo leer su binario: hace falta averiguarlo a mano")
|
||||
|
||||
|
||||
def recomendar(tipo, e, datos_que_mueren=()):
|
||||
def recomendar(tipo, e, datos_que_mueren=(), sitios=()):
|
||||
"""Devuelve `(decision, motivo)`. **Recomendación, nunca decisión**: se imprime con su razón para
|
||||
que aceptarla sea barato y equivocarse sea caro de no notar.
|
||||
|
||||
@@ -334,6 +334,15 @@ def recomendar(tipo, e, datos_que_mueren=()):
|
||||
return "", ""
|
||||
|
||||
if tipo == "dominio":
|
||||
# ⚠ Evidencia más fuerte que el DNS, y sin tocar la red: si el bloque de este dominio en la
|
||||
# config del servidor apunta a un directorio que NO EXISTE, no hay nada que servir — devuelve
|
||||
# 502 resuelva donde resuelva. Medido en gioser: `aura`, `sigma` y `kosmofono` apuntan a
|
||||
# raíces borradas, y son justo los que en su día devolvían los 502 que hicieron que el censo
|
||||
# se baneara a sí mismo al sondearlos por HTTP. Se lee del fichero; no hace falta preguntar.
|
||||
for st in sitios:
|
||||
if e.get("domain") in (st.get("nombres") or "").split() and st.get("raiz_existe") == "no":
|
||||
return "muere", (f"su bloque en {st['config']} apunta a `{st['raiz']}`, que NO "
|
||||
f"EXISTE: no hay nada que servir, devuelve 502 resuelva donde resuelva")
|
||||
if c == "vivo":
|
||||
return "muda", "resuelve a esta máquina y contesta"
|
||||
if c == "ya-mudado":
|
||||
@@ -363,7 +372,7 @@ def recomendar(tipo, e, datos_que_mueren=()):
|
||||
|
||||
|
||||
def sugerir(tipo, e, datos_que_mueren=()):
|
||||
return recomendar(tipo, e, datos_que_mueren)[0]
|
||||
return recomendar(tipo, e, datos_que_mueren, sitios)[0]
|
||||
|
||||
|
||||
def _etiqueta(tipo, nombre, e):
|
||||
@@ -411,7 +420,7 @@ def revisar(censo, interactivo=True):
|
||||
# ── 2. lo demás, con recomendación y motivo ─────────────────────────────────────────────────
|
||||
grupos = {"muda": [], "muere": [], "": []}
|
||||
for t, n, e in resto:
|
||||
d, motivo = recomendar(t, e, mueren)
|
||||
d, motivo = recomendar(t, e, mueren, censo.get("sitio", []))
|
||||
e["_rec"], e["_motivo"] = d, motivo
|
||||
grupos[d].append((t, n, e))
|
||||
|
||||
@@ -666,12 +675,26 @@ def pasos(censo, target, key):
|
||||
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.")
|
||||
# ⚠ Un directorio que la CONFIG referencia no es un huérfano, diga lo que diga su
|
||||
# nombre. Sin esto el censo marcó `/var/www/git-tawasuyu` como «nadie lo recuerda»
|
||||
# estando servido en la línea 174 del Caddyfile — y ese aviso, aplicado, tira el sitio.
|
||||
servidos = {}
|
||||
for st in censo.get("sitio", []):
|
||||
if st.get("raiz"):
|
||||
servidos.setdefault(st["raiz"].rstrip("/"), []).append(st.get("nombres", ""))
|
||||
for c in contenido:
|
||||
ruta = c["path"].rstrip("/")
|
||||
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}")
|
||||
if ruta in servidos:
|
||||
marca = ("⚠ LO SIRVE la config para: " +
|
||||
", ".join(" ".join(servidos[ruta]).split()[:3]) +
|
||||
" — no es huérfano; moverlo tira el sitio")
|
||||
elif quien:
|
||||
marca = ("podría ser de: " + ", ".join(quien[:4]) +
|
||||
(f" (+{len(quien)-4})" if len(quien) > 4 else ""))
|
||||
else:
|
||||
marca = "no corresponde a NINGÚN dominio que muera ni lo sirve la config: miralo"
|
||||
det.append(f"# {ruta: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"
|
||||
|
||||
Reference in New Issue
Block a user