Planner de mantenimiento offline — noche19–20 octubre 2026¶
Fuente nueva preparada en ~/Dev/homelab-maint, sin commit, push ni despliegue.
No se editaron app.py, tasks.yaml, estado, unidades, inventario desplegado ni
bloqueos de transición. No hubo lecturas remotas adicionales para este trabajo.
Todo resultado conserva offline=true y executable=false, incluso cuando
el borrador queda listo para revisión. L2–L4 siguen deshabilitados.
Entrega y contrato revisable¶
maintenance_plan.py: módulo purobuild_plan(snapshot, target)y CLI.tests/fixtures/maintenance_snapshot.json: fixture sintético; sus direcciones son de documentación y sus capacidades positivas no acreditan producción.tests/test_plan.py:28 pruebas propias; suite conjunta62 correctas.
El módulo no importa subprocess, SSH, sockets ni APIs de aplicación, y no tiene
funciones de ejecución o persistencia. La CLI únicamente lee un JSON local y
emite otro JSON. No es un nuevo colector: reutiliza los campos guests ya
recogidos por bin/maintenance-inventory, complementados con la extensión
grafo/snapshot descrita en el diseño previo. El inventario actual aún no
produce por sí solo el snapshot completo; faltan normalización y captura
revisadas. No se conecta este módulo al inventario remoto ni al panel.
cd ~/Dev/homelab-maint
.venv/bin/python maintenance_plan.py tests/fixtures/maintenance_snapshot.json --target L2
.venv/bin/python -B -m unittest discover -s tests -p test_plan.py -q
.venv/bin/python -B -m unittest discover -s tests -q
CLI:0 significa borrador listo para revisar;1 significa bloqueos operativos; 2 significa snapshot/formato no admitido. 0 nunca autoriza ejecución. La API Python rechaza esquema inválido con SnapshotError; cobertura/estado insuficientes producen blockers y un borrador no ejecutable, sin inventar éxito.
Datos y grafo¶
JSONv1 exige nivel origen exacto y cobertura explícita de owners, mounts, consumers, schedulers, exports, PBS, HA, selfheal y copies. Los guests conservan vmid/type/node/status; owners ambiguos, referencias ausentes o ciclos se rechazan. Cada recurso mantiene propietario y baseline filtrado.
El grafo relaciona exports→NFS→binds→servicios, fuente backups→loop HOST→mount ext4→bindCT→archive, y PBS→jobs. HA es consumidor de su export y selfheal se representa como barrera del NAS. Se distingue accesoRO/RW/unknown, propagando RO por binds y contemplando escritura por red declarada: un bindRO no borra un canal nativoRW. Desconocidos bloquean. La ausencia de mounts no acredita ausencia de I/O por red; el snapshot debe clasificarla explícitamente.
Se separan montajes configurados de montajes realmente presentes (mounted)
y loop configurado de loop observado (attached). Un campo ausente no significa
montado o desmontado. Identidades host/backing/mount/device discordantes,
exports persistentes diferentes del runtime, jobs/copia activos, operaciones
PBS pendientes o dueño no resuelto bloquean revisión favorable.
Campos de credenciales y propiedades no contempladas (por ejemplo env/command) son rechazados sin devolver su valor en errores. Sólo se admiten propiedades filtradas; nunca usar un Docker inspect completo como snapshot de entrada.
Plan y compensación exacta¶
dependencies y dependency_release_order explican el grafo completo.
stop_order contiene sólo recursos con parada/desmontaje/detach previstos,
excluyendo infraestructura protegida y aplicaciones dormidas sin acción propia.
actions son operaciones semánticas sin comandos shell, con before, after,
guarda de comparación y requisitos de revisión del adaptador. Cada acción tiene
compensación en orden inverso: restaurar exactamente before sólo si el estado
actual sigue siendo after; cualquier drift requiere revisión. checks declara
barreras de sólo lectura: mantenimiento PBS seguido de drain sin cancelaciones
y, tras detener un guest, ausencia de sus binds antes de umount host. El
counts0 del snapshot no sustituye esa comprobación posterior a cerrar admisión.
- L2 selecciona escritores activos/pausados; no inicia exited/created ni para lectoresRO sólo por su nombre. Preserva cliente y todas las opciones export, cambiando sólo política prevista. PBS usa mantenimiento nativo; un offline previo nunca se debilita a read-only. El proxy administrado por PBS queda bajo admisión de datastore, no se para antes de intentar su API.
- L3 requiere baselineL2 readonly y selección fría explícita. La compensación vuelve al estado ya enL2, por lo que no arranca escritores parados enL2.
- L4 añade canal HA permitido y recuperación soportada, baselineNASonline explícito, selfheal conocido, gating de jobs, política ProxmoxHA de huéspedes, liberación de binds, umount normal y loop desde host exacto. Apagado NAS es solicitud simbólica; retorno exige arranque confirmado independientemente, no una orden de corte/encendido Meross.
Cuando el plan detiene un guest para liberar binds, captura todos los runtimes contenidos declarados en su snapshot, también created/exited/paused y unidades inactivas, con restart policy/enablement. Exige runtime_complete y adaptador revisado de admisión al arrancar/restauración selectiva; si faltan, bloquea. La compensación no equivale a arrancar guest y asumir que Docker/systemd conservaron todo. Se deben suprimir activaciones automáticas y reconciliar los estados antes de abrir admisión. Un runtime protegido que caería con su guest también genera bloqueo.
Restaurar un Docker paused tras detenerlo no se declara operación atómica: el plan exige validación específica del adaptador, sin asumir start-paused. Los IDs de mount/PID/loop del snapshot son observacionales; la futura restauración debe validar identidad lógica y registrar nuevos IDs, no reutilizar un dispositivo por número. El módulo no implementa esos adaptadores ni un CAS remoto: sus guardas son el contrato pendiente.
Validación y límites tras el incidente real¶
28 tests del planner pasan: pureza/determinismo, RO/RW/networkunknown, propietarios, DAG/ciclos, compensación inversa, pausa/políticas/sleepers, L3baselineL2, PBSoffline previo, loop host/backing/device, copia/job activos, exportsdrift, HA/selfheal, baselineNASonline, montajes configurados/presentes y protección de runtimes al parar el guest. La CLI se ejecutó realmente contra fixtures locales y se comprueba que no crea ni cambia archivos del directorio temporal. Suite completa62 pasa y diffcheck limpio.
El reinicio real de pmx50 interrumpió esta preparación: se guardó la fuente y se pausó el trabajo; no se publicó el borrador incompleto. Tras autorización se terminó únicamente offline. El incidente demuestra la necesidad de mantener inventario, procesos legacy y recuperación como evidencias separadas: un estado running heredado, un snapshot parcial o un último heartbeat no autorizan reanudar ni repetir un comando remoto. El nuevo planner no adopta ni reanuda copias legacy, no corrige manifests actuales y no sustituye la reconciliación explícita de consola ni el procedimiento independiente de recuperación de backup.
Este trabajo habilita revisión y futuros ensayos aislados. No certifica la integridad del inventario vivo, ausencia de todos los clientes NAS, semántica de admisión PBS/HA ni restauración completa del runtime. Pendiente revisión del coordinador, colector de snapshot filtrado y adaptadores ensayados en aislamiento; no habilitar L2–L4 por la existencia de este módulo.