Skip to content

Diseño funcional L2–L4: estado previo y restauración selectiva — 19 octubre 2026

Lecturas acotadas aproximadamente23:52–23:58CEST. Propuesta: no código, transiciones, cambios de horarios, montajes, flags, servicios ni escrituras en NAS/HA. No se hicieron llamadas Home Assistant por shell, backups, restores, Sablier dynamic ni cortes durante copias. Los niveles siguen deshabilitados. Este documento no certifica su funcionamiento.

1. Fuentes y grafo actual comprobado

Reutilizar ~/Dev/homelab-maint/bin/maintenance-inventory, no crear otra plataforma. Extenderlo con relaciones montajes→binds→consumidores y propiedades filtradas; no incorporar env completos, tokens o datos de aplicación al informe.

La API cluster sigue mostrando28 invitados,26LXC y2VM. Todos los propietarios se resolvieron antes de consultar LXC. Se consultaron configuraciones de los26 LXC extrayendo sólo mpN; no hay otros bind mounts declarados en ellas aparte de los resumidos debajo. Esto no descarta montajes manuales dentro de invitados, acceso de red desde aplicaciones o clientes ajenos al inventario.

Recurso Relación NAS observada Consecuencia
VM208 en proxmox Media→/mnt/nfs_media, NFSv3 TCP RW Escritores y lectores de Docker, jobs y copia de originales
Ambos nodos PVE Media→/mnt/nas/media, RO; backups→/mnt/pve/terramaster, RW El plan antiguo omitía el montaje Media RO; ambos se necesitan para L4
CT218 en proxmox2 binds PVE music, podcasts y audiobooks, todosRO Navidrome/ABS lectores; no pararlos por llamarlos escritores L2
CT220 en proxmox2 binds PVE movies/tvshows/music/manga/twitch/youtube, todosRO Jellyfin Docker lector; liberar bind refs antes de desmontaje host
CT251 en proxmox /mnt/pve/terramaster/offline-kit→/mnt/nas/offline-kit, RW Tiene potencial escritor nativo aunque Kiwix Docker seaRO
CT186 en proxmox /mnt/pbs-archive→/archive; datastore main local650GiB Archive depende del loop montado por el HOST, no de un loop administrable dentro del CT
CT281 en proxmox /srv/wp-pulse es subvol ZFS local No tratarlo como cliente NAS sólo por pertenecer al plan antiguo
HA VM171 NAS observa conexión NFS desde.171 Cliente real que requiere canal permitido de Home Assistant para L4

NAS ve conexiones NFS TCP activas desde.208,.50,.51 y.171. La ausencia de otros peers en una foto no descarta clientes inactivos, UDP/SMB o accesos nativos NAS. No se despertó Bazzite ni se asumió que estuviera apagado o sin montajes.

Escritores y lectores Docker

VM208 tiene 18 contenedores activos con bind NAS RW:

  • Descarga/ingestión: plundrio-scanner, onepacerr, plundrio-onepacerr, plundrio, slskd, soularr, metube y ambos twitch-rec.
  • Biblioteca: sonarr, radarr, lidarr, bazarr y mylar3.
  • Documentos: paperless y trip-planner.
  • Fotos y vídeo: immich-server y nopor-ingest.

Son candidatos a quiesce L2, no una autorización de parada ni prueba de que cada uno esté escribiendo ahora. El plan viejo omitía Mylar3 y onepacerr. Otros contenedores controlan escritores indirectamente; requieren considerar el grafo API/control, aunque no tengan bind NAS propio. Un montajeRW da capacidad, no prueba actividad; unoRO no demuestra que la aplicación carezca de otro canal.

Lectores activos observados: tunarr en208, Navidrome/ABS en218 y Jellyfin en220. Las copias antiguas Jellyfin/Navidrome/ABS de208 están exited; Stash y Pinchflat también estaban exited. No se arrancarán al salir de mantenimiento.

CT251 tiene kiwix running con mountRO y kiwix-library-init exited con mountRW. Units nativas reales: rag-api.service y zim-http.service, no los nombres antiguos homelab-rag-api. Falta inspeccionar el uso nativo de offline-kit antes de clasificar CT251 completo como lector o escritor. CT217 no declaró bind NAS ni mount Docker /mnt en la inspección; eso no acredita ausencia de clientes por red y no justifica apagar monitorización en un plan frío.

PBS: loop y horarios reales

En host proxmox, /dev/loop0 está asociado exactamente a /mnt/pve/terramaster/pbs-archive.img, montado ext4 en /mnt/pbs-archive. CT186 recibe un bind en /archive. Dentro del CT, losetup muestra /dev/loop0 (lost): no usar ese namespace como propietario del detach. El antiguo plan buscaba /backups/pbs-archive.img, ruta distinta de la real. No se ejecutó umount ni losetup mutativo.

Objeto PBS Horario declarado Afectado por NAS
sync main-to-archive 06:00 Sí, escribe archive
archive-verify sun08:00 Sí, lee archive
archive-prune daily Sí, modifica archive
archive GC daily Sí, modifica archive
main-verify sat02:00 Main local; conservar salvo parada completa PBS
main-prune y main GC daily Main local; conservar salvo parada completa PBS

Por tanto, retirar sólo el horario sync no hace archive inactivo. API instalada admite maintenance-mode read-only/offline y endpoint active-operations. La consulta actual de archive devuelve read0/write0; no había sync activo. No es una reserva ni garantía futura: el mecanismo correcto debe impedir admisión nueva y después esperar el drain.

Propuesta: usar modo nativo de mantenimiento de archive, conservando su estado previo exacto, en lugar de vaciar horarios arbitrariamente. Para L2, evaluar read-only con admisión bloqueada y drain de escrituras; para L4, offline y drain de lecturas/escrituras. Confirmar la semántica de la versión y todos sus consumidores antes de habilitarlo. El API anuncia digest para escrituras condicionales; hay que implementar su obtención y compare-and-set, no asumirlo. La documentación oficial describe esa admisión y finalización de operaciones previas.

Si liberar el bind exige detener CT186 en el primer plan conservador, declarar que main también queda temporalmente indisponible y pausar exactamente los jobs PVE afectados. No prometer main disponible mientras se para el proxy o CT. Un futuro detach exclusivo de archive necesita ensayo aislado de namespaces.

Automatismos que pueden volver a escribir

RunWisp activo en208 y ambos nodos. Runtime208 tiene19 jobs; destacan:

Job Cron declarado
homelab-ctl watch / update cada5min /04:00
putio_cleanup cada2min
pg_dumps 01:30
asmr_converter 03:30
trip_mail_ingest cada minuto
trip_full_backup 04:41
series_discovery 10:15
originals_backup 11:10
qwen_image_backup 21:43
downloads_cleanup / audiomuse 05:15 /05:17

El resto también debe constar en snapshot, sin imprimir comandos/env. pmx50 tiene docker-fleet-update-nudge09:00; pmx51 no tenía jobs declarados en ese TOML. No se encontró timezone explícito en el bloque daemon de esos tres TOML: guardar horarios y configuración/entorno horario originales, sin reinterpretarlos.

PVE tiene siete jobs backup habilitados a01:00,02:00(dos),03:00,04:30,05:00 y 05:30, todos sobre storage pbs; cubren los28 invitados. Son independientes de RunWisp. Parar un daemon no implica parar sus hijos ya iniciados: verificar jobs activos y la copia con su lock+PID+cmdline saneados, sin usar sólo un last.json potencialmente obsoleto. Mientras la copia de originales esté viva, la admisión de cualquier fase que afecte sus fuentes debe denegarse.

2. NAS: barreras concretas

/etc/exports y exportfs muestran seis exports RW (Media, backups, media, syncthing, public, Matematico), todos para cliente192.168.0.XXX/24. El plan viejo usaba *: y opciones parciales: eso podría ampliar alcance o perder atributos. Cualquier cambio futuro debe conservar cliente, ruta, opciones y fuente de verdad exactos, cambiando sólo la propiedad prevista. exportfs -ra no es una restauración del estado previo si alguien modificó el fichero entretanto.

L2 debe definirse como NFS objetivo sin escritores conocidos/admisión nueva; no equivale a hacer todo el volumen NAS de sólo lectura ni a detener SMB, syncthing u otros escritores internos. La propiedad de /etc/exports frente a TOS/automatismos y el uso de los otros exports aún no están acreditados. Una política limitada a Media reduce alcance, pero cambia el contrato antiguo y debe quedar explícita en el plan; no anunciar un L2 global a partir de Media.

nas-volume-selfheal.timer corre cada minuto. Su script puede emitir sysrq-b, tiene guardas anticorrupción/rate-limit y reconoce /etc/nas-selfheal-disabled. El flag estaba ausente. El plan L4 debe registrar estado/identidad previos del flag y del timer y coordinar esa barrera; al volver, retirar sólo el flag creado por esa transición si sigue siendo el mismo. Nunca borrar un flag ajeno ni activar un timer que estaba desactivado. No se ejecutó el script, cambió el flag ni llamó scrub como prueba.

Home Assistant sigue siendo barrera real: existe un cliente NFS observado, pero faltan herramientas permitidas para verificar/desmontar/recuperar mounts. No se sustituye el canal exigido por SSH, curl Supervisor ni shell del add-on. No se afirma que apagar NAS mantenga la casa funcional con ese cliente activo.

3. Modelo de estado y recuperación exacta

Extender el diario existente de consola; no un daemon nuevo. Cada transición necesita snapshot inmutable root0600 y action journal bajo su propio ID. Esquema conceptual, no código ejecutable:

{
  "transition_id": "identidad única",
  "origin_level": "L1",
  "target_level": "L2",
  "inventory_fingerprint": "hash de propiedades filtradas",
  "resources": {
    "docker": [{"owner": "VM/CT", "id": "ID exacto", "state": "running|paused|exited|created", "restart_policy": "valor previo"}],
    "units": [{"owner": "host/guest", "name": "unidad exacta", "active": "valor previo", "enabled": "valor previo"}],
    "pbs": {"archive_maintenance_mode": "ausente o valor exacto", "jobs": "ID, horario/disable exactos y digest"},
    "exports": [{"path": "export", "client": "cliente exacto", "options": "opciones exactas"}],
    "mounts": [{"owner": "namespace", "id": "mount ID", "source": "origen exacto", "target": "destino", "options": "valor previo"}],
    "loops": [{"owner": "HOST", "device": "device exacto", "backing_file": "ruta exacta", "read_only": false}],
    "flags": [{"owner": "NAS", "path": "flag conocido", "existed": false}]
  },
  "actions": [{"id": "acción", "resource_id": "identidad", "before": "valor previo", "expected_after": "valor nuestro", "status": "intent|applied|verified|uncertain", "compensation": "reponer sólo before"}]
}

Persistir intención antes de cada mutación remota y resultado después. Un SSH255, timeout, reinicio de consola o owner cambiado deja uncertain, bloquea mutaciones y exige revisión ya implementada; no vuelve a ejecutar el remoto. Resolver propietario antes de cada acción, no sólo al construir el plan.

Mount IDs, PIDs y números loop son identidades de la observación actual, no valores que puedan exigirse idénticos tras recrear un montaje. La restauración debe acreditar identidad lógica (host, backing file/identidad, filesystem, origen, destino y opciones) y registrar los nuevos IDs; nunca reutilizar un loop por coincidir solamente su número. La fase de desmontaje sí comprueba que el ID actual es el observado antes de actuar, para no tocar un recurso ajeno.

Compensaciones: sólo acciones verificadas como propias y sólo si el estado actual sigue siendo expected_after. Drift implica revisión, nunca sobrescribir un cambio ajeno. Restaurar ausencia frente a valor vacío y paused frente a running de forma diferenciada. No compose up indiscriminado, docker unpause global, horarios06:00 inventados ni arranque de aplicaciones dormidas. Mantener snapshots de cada nivel: salir de L3 restaura el baseline ya en L2, dejando parados los escritores que pertenecen a L2.

4. Secuencia funcional propuesta

Admisión común: recovery libre, nivel origen exacto, owner único, fuentes y runtime concordantes, copia/jobs críticos inactivos, inventario suficientemente reciente y sin consumidores desconocidos. Comprobar suficiente espacio local para snapshot/logs, nunca usar NFS como almacén del diario de recuperación.

Entrada L2: snapshot→impedir reactivación (scheduler, Sablier y controles que puedan iniciar escritores)→esperar jobs activos sin cancelarlos→parar sólo escritores clasificados y sus productores necesarios→guardas archive/drain→ aplicar export/lectura dentro del alcance acreditado→verificación no mutativa. No sustituir verificación por touch de un fichero común. El estado exportfs permite comprobar política de servidor sin escribir; el grafo confirma qué escritores permanecen y qué cobertura falta. No elevar nivel si es desconocida.

Salida L2 aL1: validar snapshot/drift→recuperar exactamente opciones y modo archive previos→verificar→arrancar sólo identidades que L2 detuvo, conservando estado paused si venían pausadas deL1→restaurar sólo scheduling/admisión que L2 alteró. Nada que L1 paró se vuelve a habilitar todavía.

Entrada L3: desdeL2, detener aplicaciones/lectores seleccionados con planes por owner y por servicio real; conservar casa, DNS, proxy, identidad, monitorización y consola. No enviar el mismo conjunto de units a todos los CT. Especificar qué significa «frío»: no todos los28 invitados apagados. Bazzite no se corta con Meross; su quiesce necesita contrato propio y canal permitido.

Salida L3 aL2: compensar sólo accionesL3, manteniendo readonly y escritores L2 detenidos. No recrear stacks ni sustituir imágenes. Si cambió ID o política, replanificar tras revisión, no asumir que el nombre equivale al recurso previo.

Entrada L4: exige además canal HA, todos los clientes/internos conocidos y selfheal coordinado. Archive offline/drain; liberar binds de CT186/218/220/251 según plan y política Proxmox HA propia (distinta de Home Assistant); no parar un invitado gestionado sin tratar su desired state o HA podría reactivarlo. Desmontar consumidores antes de host, luego loop exacto desde host propietario, luego backups y Media en ambos PVE. Desactivar storage sólo si procede y registrando estado previo global/node. Umount normal y verificación por mountinfo/loop/active-operations; cualquier busy o identidad dudosa aborta. Sólo con todas las barreras verificadas se puede solicitar apagado NAS. Ni ping fallido ni puerto cerrado bastan para certificar apagado físico.

Salida L4 aL3: NAS arrancado por canal acordado→identidad/exports esperados→ montajes host precisos con flags previos→loop y filesystem exactos→binds/guests necesarios→PBS y lectura/drain→restaurar sólo accionesL4. No habilitar writers L2, jobs o usuariosL3 antes de su transición correspondiente. Restaurar selfheal y storage mediante compare-and-set contra los valores que puso esta transición. No intentar rutas alternativas de loop/mount hasta que una «parezca funcionar».

5. Qué puede implementarse ahora y qué sigue bloqueado

Implementable y verificable sin ninguna parada en la siguiente ronda:

  1. Extensión filtrada de maintenance-inventory: owners/mounts/binds/DockerRW, schedules, fuentes, snapshots y fingerprints, con desconocidos explícitos.
  2. Planner puro que genere DAG entrada/compensación y estime cortes por recurso; nunca confundir salidaL3 con regreso aL0.
  3. Snapshot/journal de propiedades tipadas y diferencias, precondiciones CAS, revisión de drift y pantalla de plan. No almacenar secretos.
  4. Fixtures offline para running/paused/exited, schedules ausentes, owner migrado, loop equivocado, bind persistente, writer desconocido, busy mount, copia activa, HAsin herramientas, flags ajenos y reinicio parcial.
  5. Adaptadores sin mutaciones o simulados: errores rc/SSH, recepción de resultado dudoso, restore selectivo y preservación de último nivel confirmado.

Barreras reales para activar transiciones:

  • Copias actuales: esperar cierre comprobado; no cortar para ensayar.
  • HA cliente NFS sin herramientas permitidas: impide acreditarL4.
  • Uso nativoCT251, escritores internos NAS y clientes ajenos: cobertura aún incompleta; sockets y Docker inspect no bastan.
  • Fuente/propiedad de exports y barrera selfheal deben integrarse, no asumir persistencia de un cambio sólo en exportfs.
  • Loop y bind lifecycle deben probarse en fixture/entorno aislado; no usar el archive real como prueba de umount.
  • Admisión Sablier/automatismos y desired state Proxmox HA han de formar parte del plan; parar procesos sin cerrar esas entradas no establece quiesce.

Estas barreras no impiden implementar el grafo y planner offline. Sí impiden presentarlos como transición certificada. Mantener los bloqueos de consola hasta disponer de contratos, pruebas aisladas y evidencia completa del alcance.