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¶
-
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 dejarcopyingindefinido. 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. -
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.
-
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.
-
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.