Skip to content

Continuidad y copias: revisión siguiente, 19 octubre 2026

Evidencia viva de 22:10–22:17 CEST, posterior a la aplicación de mejoras. Se excluye el asunto de la credencial Synology por instrucción del usuario. Revisión acotada: estado existente, capacidad, políticas, últimas tareas y código; sin scrubs, backups nuevos, restauraciones de invitados ni notificaciones.

Estado de las copias nuevas

backup_originals.py --status en VM208 devuelve:

Grupo Estado Muestras restauradas
documents complete 3
proyectos complete 3
photos copying 0
pics pending 0

Proceso inicial python183860 vivo y rsync190064 ejecutándose; continuación timeout197548 esperando. No se relanza ni interrumpe. No hay estimación nueva fiable de finalización: el estado no publica bytes transferidos ni progreso. La protección de photos/pics no se declara completa por el mero inicio.

Fallo nuevo demostrado y corregido: drills PBS bajo cron

Los logs reales muestran FileNotFoundError el1oct en pmx50 y el5oct en pmx51. Ambos wrappers invocan el motor común sin fijar PATH. Con el PATH mínimo realista de cron /usr/bin:/bin, pvesh/hostname se localizan; pct, qm y qmrestore, instalados en /usr/sbin, no. Una llamada inocua a pct help bajo ese entorno reproduce FileNotFoundError antes de ejecutar la herramienta.

Se corrige scripts/simplification/pbs_restore_drill.py: cada subproceso usa PATH /usr/sbin:/usr/bin:/sbin:/bin y, si falta una herramienta, informa command_missing:NOMBRE. No se alteran los wrappers, aislamiento, objetivos, reserva de VMID, avisos ni calendarios. No era necesario cambiar los wrappers: el motor común es el propietario único de las invocaciones.

Antes del despliegue, los dos motores vivos coincidían con HEAD: <REDACTED>. Después ambos coinciden con la fuente corregida: <REDACTED>.

Validación: ocho tests pasan, incluidos búsqueda real de un ejecutable falso inofensivo con entorno cron y error identificable de herramienta ausente. En ambos hosts se comprobó sintaxis y se ejecutó ese stub temporal sin llamar al main ni crear un invitado. Lock de cluster ausente en la comprobación. El siguiente ensayo completo sigue pendiente; corregir esta causa no demuestra que las copias actuales arranquen o que otro fallo posterior no exista.

Rollback por host: original preservado en /root/rollbacks/pbs-drill-path-20261019/pbs_restore_drill.py. Reponer solo ese archivo si es necesario; no tocar calendarios ni datos PBS.

Estado PBS verificado

Elemento Estado observado
main 480GiB;301GiB usados;179GiB disponibles;63%
archive 1,5TiB;1,2TiB usados;230GiB disponibles;85%
main-prune daily2 / weekly1
archive-prune daily7 / weekly8 / monthly12
main-verify 17oct02:00–02:10:58,OK
archive-verify 18oct08:00–12:07:45,OK
main→archive sync 19oct06:00–06:29:44,OK
GC ambos / prune ambos 19oct,OK

No se ha podado ni cambiado ninguna retención. La política efectiva del servidor es más corta que las declaraciones históricas por job de PVE. Esta fotografía corresponde al19oct; el canónico de retención ya incorpora la selección de siete jobs y políticas PBS observadas el20oct a las13:15 CEST. Sus apartados restantes conservan explícitamente la referencia de agosto. Archive reside en loop sobre TerraMaster: amplía historia pero no es una copia independiente de ese NAS. Los errores de lectores auxiliares no invalidan por sí solos el sync: su tarea superior de hoy finalizó OK. Verify tampoco sustituye un ensayo de restauración.

Cuatro mejoras concretas siguientes

  1. P1 — Frescura por grupo de originales y progreso separado de éxito. El script solo guarda copying/complete, sin estado de error persistido ni heartbeat durante rsync; un proceso muerto puede dejar copying indefinido. La concurrencia ocupada devuelve0 sin haber creado una copia. RunWisp puede registrar fallo del proceso, pero su éxito no acredita todos los grupos. Exponer última finalización por grupo, proceso/heartbeat y error acotado al colector y alertas existentes; tolerar el primer ciclo largo sin falso aviso. Coste bajo; sin plataforma ni segundo calendario. No se altera el proceso vivo.

  2. P1 — Cerrar recuperación de aplicación tras completar originales. Las tres muestras por grupo son primeras entradas lexicográficas no vacías ≤16MiB: se repiten y no cubren archivos grandes. Una copia rsync de archivos vivos no constituye snapshot transaccional de Immich/Paperless. Hacer el próximo ensayo aislado con una exportación coherente de BD y sus originales, versiones de aplicación y manifiesto de cobertura; rotar muestras e incluir un archivo grande en la comprobación programada. Mantener producción intacta y no lanzar esto mientras termina la primera copia. Coste medio; no basta otra prueba de tres archivos para declarar recuperación de la aplicación completa.

  3. P2 — Capacidad y retención explícitas antes de purgar. Archive85% es un riesgo de crecimiento, no un fallo actual: GC/prune/syncOK. Medir tendencia diaria y espacio recuperable real antes de acortar historia. Las nuevas generaciones Synology no tienen poda automática; reserva64GiB detiene la copia antes de agotar el destino, pero no conserva indefinidamente la capacidad de crear generaciones nuevas. Integrar alertas de reserva/frescura y acordar una política con mínimo recuperable, validación previa y protección de la última generación buena. Actualizar la ficha canónica con las políticas efectivas. Coste bajo/medio; ninguna eliminación autorizada por esta auditoría.

  4. P2 — Segunda ubicación para el conjunto irremplazable. Persiste la falta de cobertura offsite acreditada. Priorizar documentos, fotos personales, secretos recuperables/configuración crítica y claves de restauración, aprovechando soporte externo existente si capacidad y custodia son adecuadas. NAS distinto en casa protege frente al fallo del NAS origen, pero no frente a pérdida de ambos equipos por el mismo incidente. El destino, cifrado y coste requieren elección concreta; no se contrata ni instala nada.

Fuentes: comandos de estado y manifests resumidos de VM208; logs/drill cron y hashes de ambos hosts; prune-job/verify-job/sync-job/task list y df de CT186; código mantenido scripts/original-backups/* y scripts/simplification/*. Ningún nombre de archivo personal, hash de contenido privado o secreto se publica.

Contrato de propiedad de discos del drill — revisión de fuente y convergencia posterior

pbs_restore_drill.py:isolate aceptaba cualquier volumen zfs-ha: aunque perteneciese a otro invitado. El control negativo local admitía vm-208-disk-0/subvol-217-disk-0 en un ensayo cuyo temporal era9901. No se observó ese caso en un restore real; la reserva/tag del guest no demostraba propiedad de sus discos.

La fuente ahora recibe el VMID temporal explícito y exige el volumen completo zfs-ha:vm-<temporal>-disk-N o zfs-ha:subvol-<temporal>-disk-N, incluido EFI/TPM. Rechaza prefijos, aliases file/volume en opciones, duplicados, formatos desconocidos y VM sin disco principal. Conserva opciones normales y excluye CDROM por propiedad exacta. No cambia selección, restauración, límites ni cleanup.

16 pruebas locales pasan, sin Proxmox/Docker/SSH: positivos9901/9902, discos ajenos de todas las clases, opciones y duplicados, CDROM y ausencia de disco; un qmrestore simulado con disco ajeno prueba que no se etiqueta, arranca ni destruye el guest y conserva su configuración para revisión manual. Esto verifica el nombre del volumen, no su contenido ni propiedad ZFS fuera de ese contrato; no elimina la ventana previa de restore antes de aislar. En la fase original sólo se preparó/revisó la fuente; no se ejecutó un drill ni un despliegue manual específico.

A las 07:36:50 CEST del 20 de octubre, lectura pasiva de ambos hosts: /usr/local/bin/pbs_restore_drill.py tenía SHA256 <REDACTED>, archivo regular, root:root y modo 0755. Ansible-pull había terminado con RC 0 en pmx50 (07:23:30–07:24:20) y pmx51 (07:24:26–07:25:25), con playbook normal sin filtros de tags. El role observability_collector copia este helper en ambos. Es consistente con convergencia automática: no demuestra qué pasada concreta lo copió ni que se ejecutara una restauración. Esta identidad corresponde a la guarda de volúmenes, anterior a la nueva revisión de cancelación.

Preflight de workspace Paperless — fuente preparada20oct, sin ejecución real

El ensayo nativo anterior usó un workspace privado; el helper general, sin embargo, aceptaba symlinks/directorios preexistentes y hacía chown antes de comprobar todos los destinos. La fuente scripts/original-backups/paperless-recovery-drill.py incorpora ahora un preflight completo antes de cualquier mkdir/chown/llamada al motor. Rechaza componentes .. explícitos antes de normalizar y exige ancestros reales sin symlinks, work0700 propiedad del caller e input privado con manifest regular sin symlink/hardlink. No lee contenido del manifiesto en ese preflight.

Los cuatro outputs data/media/consume/trash se comprueban juntos: directorios0700 propiedad del caller oUID1000; fresh admite ausentes o vacíos y rechaza cualquier salida poblada. Resume conserva los datos previos y exige SQLite privado regular, sin links, más checks de sidecars WAL/SHM/journal, index y resultados que serán sobrescritos. Los archivos de log conservan creación exclusiva: no se hace idempotente una repetición con el mismo log. Reanudar es una operación explícita, no un motivo para borrar artefactos de un intento previo. UID/GID1000 del contenedor y chown se conservan; el chown de directorios usa follow_symlinks=False.

Doce pruebas con fixtures temporales y motor/chown simulados cubren fresh/resume válidos, propietarios/permisos, raíz/ancestros/destinos/input/manifiesto/SQLite/sidecars/results enlazados, hardlinks, link/../work, logs existentes y un sibling tardío inseguro. Los rechazos prueban cero mkdir/chown/subprocess; pasan también las tres pruebas del verificador de exportación. Sin Docker, SSH, importaciones ni despliegue real de esta corrección.

Es un preflight por pathname, no una operación atómica ni un sandbox: un ancestro escribible por terceros o un proceso concurrente del mismoUID puede cambiar las rutas después de comprobarlas. Se debe conservar el workspace privado y un padre confiable durante el ensayo; la validación no elimina esa carrera ni inspecciona recursivamente todo el input/media. No se añadió cancelación ni se modificó el ciclo de vida del motor en esta tanda.

Cancelación y cleanup Paperless — siguiente corrección sólo fuente

El runner local del drill captura TERM/INT/HUP cooperativamente: registra la primera señal, comprueba el estado inmediatamente después del preflight antes de mkdir/chown/log y antes de cada acción del motor/después del handoff de Popen, durante communicate en intervalos de hasta 250 ms y tras esperar Redis. El scope exige un estado cooperativo explícito; se eliminó la rama asíncrona usada sólo por fixtures. No lanza excepciones asíncronas durante el handoff al finally. Los procesos CLI usan sesiones propias; el registro ocurre con las tres señales bloqueadas y el child restaura la máscara anterior antes de exec. TERM→KILL→reap se limita al grupo propio, conservando errores de reap.

Setup+import comparten un deadline monotónico de 900 s; creación del holder/Redis: 30 s, foreground sin Redis hasta 900 s, ping: 3 s, inspect: 5 s, stop: 15 s y rm: 10 s quedan además limitados por el presupuesto restante. Cleanup dispone de 90 s desde que empieza el primer fallo/cleanup: intenta ambos roles registrados antes del primer launch; sin presupuesto, registra desconocido y no lanza nuevos comandos. Es un límite del control userspace/CLI: no garantiza interrumpir syscalls del kernel en D-state ni fsync bloqueado.

Cada contenedor debe acreditar el label propio y un ID completo; stop/rm operan por ese ID, no por un nombre reutilizable. Estado Running se vuelve a comprobar. Una colisión ajena nunca se detiene/elimina; inspect fallido o daemon inaccesible no se interpretan como ausencia. Sólo import correcto y cleanup verificado permiten rm; fallos/cancelaciones conservan artefactos. Una creación que termine tarde después de fallar la CLI sigue siendo un límite: el intento de inspección no garantiza descubrir un contenedor que aún no exista en ese instante. SIGKILL, pérdida del host/daemon y estos estados desconocidos no permiten afirmar ausencia de huérfanos.

El resultado es privado (0600) y duradero mediante fsync de archivo/directorio; sólo contiene agregados/estados y clases de error, nunca stdout/stderr crudos ni tokens. La primera señal observada durante cleanup conserva salida no cero; las repetidas no lo interrumpen. La frontera terminal es el recibo fsynced después del cleanup y su checkpoint de cancelación (una primera señal observada durante ese fsync reescribe el recibo fallido). Una señal posterior a esa frontera no revierte operaciones ya terminadas; no se promete un recibo para preflight que aborta antes de cualquier acción del motor.

El template inspect contiene sólo ID, label de recuperación, Running/OOM/Image/límite de memoria y network. json (index ...) se conserva como contrato textual: no hay Go ni Docker usado para validación local. Sintaxis respaldada por Docker: json sobre un elemento y Go: argumentos agrupados entre paréntesis. Pasan 27 pruebas lifecycle, 12 de preflight y 3 de exportación sin motor real: incluyen foreground sin Redis correcto/fallido con cleanup singleton, cancelación observada tras preflight con cero mutaciones, HUP real en ambas transiciones cooperativas con handlers/máscaras restaurados, y árbol propio terminado conservando un grupo ajeno. No se presupone que el SO descarte HUP pendiente. No se ejecutó Docker, SSH ni una restauración real ni se desplegó esta corrección.

Ambos docker run llevan --pull never: las imágenes deben existir localmente por su ID exacto; si faltan, fallan sin descargar. Los tests comprueban ese argv en Paperless y Redis, sin nuevas llamadas image-inspect. Véase política pull de Docker.

Cierre de fuente: ciclo de vida PBS, 20 octubre 07:52 CEST

El motor compartido incorpora límites de ejecución y cancelación cooperativa, recibos privados durables y conservación de reserva ante resultado desconocido del restore. La destrucción vuelve a acreditar propiedad y configuración actual; la liberación sucede después de un recibo release_pending, con éxito terminal sólo tras liberar y persistir el resultado. La revisión independiente detectó y cerró una ventana de falso éxito ante una señal durante esa liberación.

Helper SHA256 <REDACTED>. Autor, coordinador y revisor independiente ejecutaron las 50 pruebas con éxito (34 de ciclo de vida y 16 previas ajustadas). CI incorpora cualquier archivo de esa suite como trigger; su resultado remoto se comprobará tras publicar. La guía operativa recoge la reconciliación de reservas retenidas y los límites de limpieza. No hubo restore, limpieza ni despliegue manual nuevo. La observación anterior de f7b4f654… en ambos hosts sigue siendo la última versión instalada acreditada; Ansible-pull puede propagar esta fuente después.

Seguimiento posterior: CI39377198784 SUCCESS en ambos Python3.10/3.12, con50 pruebas PBS en cada uno y el resto de suites de copias/Paperless y contratos Prometheus correctos. Publicación docs 39377198746 SUCCESS. A07:55:49, pmx50 tenía helper7e2c3fda tras pull natural07:55:00–07:55:48; pmx51 conservaba f7b4f654 y su último pull07:40:56–07:41:57 precedía la publicación. No se forzó convergencia ni se ejecutó el motor.

Convergencia cerrada a 07:58:26,971655 CEST: pmx51 ya tiene helper7e2c3fda, regular root:root0755, después de Ansible-pull natural07:55:59–07:57:15, success0/inactive/MainPID0. Junto a la observación anterior de pmx50, acredita el artefacto corregido en ambos hosts. No se ejecutó un drill ni se forzó el pull.

Intención configurada del publicador: candidato sólo en fuente

El histórico de dump_publication.enabled pertenece a la última invocación; no acredita si RunWisp tiene programado publicar. El candidato añade únicamente originals_dump_publication_configuration_known y, cuando vale1, originals_dump_publication_configured_enabled. Conserva sin renombrar ni combinar las métricas anteriores del último intento. No cambia alertas, histórico ni tareas. Los consumidores antiguos aún leen enabled histórico: alertas y dashboards no quedan reparados por estas dos métricas. Quedan pendientes preservar el último intento real de publicación ante copias válidas sin flag y adaptar consumidores, sin reconstruir el histórico ya sobrescrito ni combinar intención con histórico.

La lectura del TOML fijo de VM208 es privada, limitada a1MiB, sin enlaces y con identidad/permisos comprobados antes y después. Sólo reconoce el perfil nativo revisado de originals_backup y el comando tokenizado exacto, con o sin --publish-immich-dump; no ejecuta shell ni backend. Fragmentos TOML presentes, include ausente (default nativo no acreditado), vacío o distinto del perfil explícito runwisp.d/*.toml, colisión con servicio, campos disabled/enabled no soportados por RunWisp0.14, comandos ambiguos o errores producen desconocido, nunca un falso valor configurado0. No demuestra carga efectiva del daemon, elegibilidad completa, ejecución natural ni éxito de publicación.

Parser: stdlib tomllib, tomli o pip._vendor.tomli, siempre con código y padres root-owned, sin enlaces ni escritura de grupo/otros, acreditados antes de cada import de paquete. Se comprueban los entrypoints de esos módulos y sus directorios, no una auditoría transitiva de cada archivo importado: se confía en la instalación administrada por root; no protege frente a root comprometido ni promete detectar modificaciones de dependencias por root. VM208 Python3.10.12 tenía TOMLI1.0.3 vendorizado en el pip del sistema el20oct, probado sólo con TOML sintético en memoria. No se instala ninguna dependencia: si ese módulo desaparece o deja de ser confiable, la lectura pasa a desconocida y omite configured_enabled. La disponibilidad futura y las carreras posteriores con el mismo UID requieren observación; no se promete atomicidad entre configuración y reload. Importar el backend o pedir ayuda CLI no lee esta configuración ni carga el parser. Candidato aún sin instalar ni ejecutar contra métricas reales; pruebas locales sin jobs, Docker, SSH ni datos de copias; el cálculo de métricas se prueba con archivos privados y /proc sintético.

Histórico de publicación ante copias sin flag: candidato sólo en fuente

Una copia válida sin --publish-immich-dump no constituye un nuevo intento de publicación. El candidato conserva literalmente el diccionario previo dump_publication, incluidos errores, timestamps, hashes y estados pendientes, aunque cambie la generación de originales o falle esa copia. Sin diccionario previo se mantiene el estado inicial disabled; no se reconstruye el histórico sobrescrito anteriormente. Una solicitud explícita de publicación sigue creando un intento pending y conserva el último éxito como antes.

Las métricas históricas describen el último intento de publicación, no la intención programada ni la actividad de la invocación actual. Un pending anterior conservado puede requerir reconciliación y no demuestra proceso activo. La configuración se observa por las dos métricas aditivas separadas; no se mezclan con este histórico. No hace falta otro campo de invocación para preservar estos datos. No se cambian alertas, CLI, reloj del primer ciclo, configuración ni jobs. Pruebas locales con fuentes y destinos sintéticos; sin runtime, backup, métricas reales, lecturas NAS ni publicación de dumps.

Consumidores aplicados20oct12:22 CEST

Las alertas Failed/Stale ya usan configured_enabled; ConfigurationUnknown avisa tras10min con known!=1 o métrica ausente del exportador propietario. Required permanece íntegra. Fuente ff9a090/CI39406961586,17escenarios Promtool correctos; 91reglas instaladas y sanas en dos evaluaciones naturales. La copia fallida anterior sigue visible; no acredita publicación, restauración ni coherencia. Recibos y recuperación.