Auditoría acotada de consola de mantenimiento — 2026-10-19¶
Fuente revisada: ~/Dev/homelab-maint, HEAD 763ef72 al comenzar. No se
consultó ni mutó runtime CT212; no se ejecutaron tareas reales, SSH, apagados,
backups ni operaciones HA. Fixtures locales en
/tmp/homelab-maint-audit-20261019/reproduce.py, con estado/logs temporales y
herramientas simuladas. Esta revisión no certifica el inventario vivo.
1. Excepciones y aborto temprano bloqueaban indefinidamente la consola¶
_run_task sólo trataba timeout y cancelación durante consumo de stdout.
Errores de creación del proceso, apertura/escritura del log o persistencia
abandonaban RUNS y estado running; al ser exclusivas las tareas, cualquier
operación posterior recibía 409. Una cancelación antes de que arrancara la
corutina también dejaba el registro sin recoger.
Reproducción original: simulando OSError en create_subprocess_shell, la
corutina terminaba pero RUNS=['fixture'], tasks.fixture.status=running y
transition.status=running. No hubo proceso real en esta prueba.
Corrección LOCAL preparada por autorización del coordinador: wrapper con cierre
y liberación en finally, notificación de fin a SSE, fallo parcial persistido
cuando es posible y estado veraz en memoria si el disco falla. Conserva nivel
previo y último éxito ante excepciones. Creación protegida con shield: si se
cancela durante spawn, recoge y termina el proceso antes de liberar la exclusión.
Callback recoge cancelación anterior al arranque de la corutina. Se retiró la
espera artificial posterior al fin para no dejar ventana de aborto después de
confirmar éxito.
Abortar es idempotente: un segundo POST no vuelve a cancelar la recolección del proceso ni su terminación.
Validación: 17 pruebas locales pasan mediante
.venv/bin/python -m unittest discover -s tests -q; incluyen seis nuevas:
fallo spawn, fallo persistencia, fallo apertura log, aborto antes de empezar y
aborto durante spawn y doble aborto durante spawn. Se mantienen éxito, timeout silencioso/con salida, aborto,
niveles y preflight previo existentes. git diff --check limpio.
Pendiente revisión/commit/despliegue por coordinador; NO está aplicado a CT212.
Actualización del coordinador: revisado, suite final 17/17, publicado 60a85d5
y desplegado selectivamente. CT212 conserva L0, running=[], transición nula;
hash app.py <REDACTED>.
Los puntos 2 y 3 siguientes siguen pendientes.
2. Los niveles L2–L4 aún pueden ocultar fallos de comandos críticos¶
Casos reproducidos extrayendo cuerpos remotos originales de YAML y ejecutándolos localmente con herramientas simuladas que fallan:
enter-l2, primer paso:docker stop ... | tr ...; echodevuelve rc0 aunque Docker devuelva rc23. Afecta también aexit-l2condocker start.enter-l3, paso LXC: cadapct execdevuelve rc23, pero el bucle termina rc0 y sólo imprime 100/251/172/281/280 por suecho.exit-l3es equivalente.enter-l4, desmontaje VM208: fallo desudo -n umount -lseguido deecho umountedproduce rc0 y mensaje aparente de éxito. En fixture falló el stub sudo antes de llegar a umount; prueba exactamente que cualquier fallo de ese comando queda oculto por el echo final.
No basta con que _run_task propague rc: estos comandos le entregan rc0.
La verificación L3 cuenta procesos Docker sin comprobar un valor esperado;
no demuestra que las aplicaciones LXC se hayan parado. L2 verifica un solo
punto NFS y tampoco demuestra parada de todos los escritores. L4 comprueba NAS
sin ping, sin demostrar que los clientes hayan soltado montajes.
Propuesta: reconciliar primero inventario/propietario de cada recurso, después reemplazar cadenas permisivas por agregación explícita de errores y verificaciones que afirmen el estado requerido. Ausente/opcional debe declararse, no inferirse ignorando errores. No se reescribieron tareas ni inventario a ciegas.
3. Reinicio conserva transición interrumpida pero vuelve a admitir operaciones¶
El arranque hace STATE=load_state() y RUNS={} sin reconciliación. Si el proceso
cae o se reinicia durante una transición, el archivo conserva tareas/transición
running; la API no las considera al admitir una operación nueva. Además la UI
sólo destaca partial_or_failed, por lo que el estado running huérfano no tiene
el aviso de cambio parcial.
Fixture: persistir nivel L2 y transición hacia L3 en estado running, recargar
estado con RUNS={} y llamar api_run('nas-reboot', confirm=True) con runner
simulado devuelve {'started': 'nas-reboot'}. No se ejecutó ningún reinicio NAS.
Relevancia adicional: p51-maint-on avisa explícitamente de que CT212/consola
puede reiniciarse al migrarse. Persistencia de estado por sí sola no da garantía
de saber qué paso remoto terminó tras ese reinicio.
Propuesta: convertir tareas/transición running heredadas a interrumpidas al
arranque y exigir reconciliación explícita para mutaciones posteriores. Registrar
paso y resultado para orientar la recuperación, sin reanudar comandos remotos
ni elevar nivel automáticamente. No asumir que terminar SSH canceló la operación
remota. Este punto queda pendiente; no está cubierto por el wrapper nuevo.