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:
- Extensión filtrada de maintenance-inventory: owners/mounts/binds/DockerRW, schedules, fuentes, snapshots y fingerprints, con desconocidos explícitos.
- Planner puro que genere DAG entrada/compensación y estime cortes por recurso; nunca confundir salidaL3 con regreso aL0.
- Snapshot/journal de propiedades tipadas y diferencias, precondiciones CAS, revisión de drift y pantalla de plan. No almacenar secretos.
- 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.
- 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.