Skip to content

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 puro build_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.