Replicación PVE: colección completa y fallo visible — 20 de octubre de 2026¶
La fase inicial fue sólo fuente; el despliegue posterior se registra al final. La reproducción local usa un ejecutable pvesr falso, directorio privado y el bloque original del colector con únicamente PATH y salida sustituidos: pvesr termina con código 1, pero el pipeline sin pipefail devuelve 0 y publica un archivo sin filas de trabajos. Esto acredita un fallo de observabilidad; no una avería actual de replicación.
Contrato y fuente oficial¶
El formateador oficial de pvesr status imprime ocho columnas. State puede contener un error con espacios; se divide como máximo siete veces. La API de estado es GET. La instalación oficial sitúa el CLI en /usr/bin: Makefile y defines.mk.
El nuevo helper ansible/roles/observability_collector/files/pvesr-metrics.py no consulta PVE ni escribe por defecto: muestra un plan. Sólo --collect, como root, consulta /usr/bin/pvesr status. Limita la captura durante la lectura a 1 MiB y 15 segundos; valida cabecera, todas las filas, campos, fechas y JobID únicos antes de renderizar. Un timeout, código no cero, salida vacía o formato inválido nunca publica filas parciales ni ceros inventados por trabajo. Una cabecera válida sin trabajos sí es una colección válida.
Se mantienen pvesr_fail_count{jobid,vmid,target} y pvesr_state{jobid,vmid,target}. Sólo OK produce estado 1; SYNCING sigue produciendo 0, igual que antes. Este último valor aislado no prueba una replicación rota. Los mensajes de error no aparecen en métricas ni stderr; stderr utiliza una descripción genérica.
Se añaden dos gauges sin etiquetas:
pvesr_collection_success: 1 sólo tras obtener y validar toda la respuesta; 0 ante fallo de colección.pvesr_collection_timestamp_seconds: instante del intento, no última sincronización, backup ni última colección correcta. Permite detectar publicación ausente o antigua junto con la señal de éxito.
La publicación abre cada componente del directorio con O_DIRECTORY|O_NOFOLLOW; crea el temporal 0600, sustituye el destino y limpia mediante operaciones relativas al descriptor retenido. El archivo final queda 0644, con fsync previo y del directorio posterior. Se reabre la cadena y compara dispositivo/inodo del directorio antes y después del reemplazo para detectar cambios de identidad. Rechaza ancestros enlazados o escribibles por grupo/otros y destinos enlazados/no regulares/con múltiples enlaces. En producción el ejecutor, el directorio hoja node-exporter y el destino siguen siendo root. La única excepción de propietario es el path exacto /var/lib/prometheus: puede pertenecer al UID de pwd.getpwnam("prometheus"), cuenta confirmada en ambos nodos el 20 de octubre a las 14:19:16 CEST (UID 116, grupo prometheus GID 124, modo 0755). Esta excepción no se extiende a otros paths, al UID literal 116 ni al directorio hoja. Un fallo previo al reemplazo conserva el archivo anterior y limpia el temporal; si falla el fsync posterior al reemplazo, el resultado es incierto respecto a durabilidad y se devuelve error, sin afirmar rollback. Se confía en root y en el servicio propietario de ese único padre; el anclaje por descriptor evita seguir un directorio sustituido y la revalidación detecta cambios observados, pero no acredita ausencia de cualquier renombre concurrente que ocurra y se revierta entre las comprobaciones. No se cambian propietarios ni permisos de los padres.
Integración y pruebas¶
El rol instala el helper antes del wrapper. El wrapper invoca el helper dentro de una condición: registra su fallo para devolver código final 1 y continúa los segmentos originales de NAS originals o capacidad PBS. No se modifica su contenido ni el cron existente de cinco minutos. Si el helper falta, el wrapper también falla sin impedir esos segmentos. No hay nuevo daemon, cron ni jobs de replicación.
Prueba reproducible:
python3 -m unittest discover -s ansible/roles/observability_collector/tests -p test_pvesr_metrics.py -q
bash -n ansible/roles/observability_collector/files/pvesr-collect.sh
Resultado local: 25 pruebas PASS en Python 3.12 y 3.14; sintaxis Bash y git diff --check correctos. Cubren CLI falso con respuesta parcial y RC1, formato inválido, cero trabajos válido, errores con espacios, SYNCING, timeout, límite de salida, limpieza de grupo de procesos y cancelaciones reales TERM/INT/HUP, publicación fallida, symlinks, CLI plan/help/argumentos inválidos y abreviaturas sin acciones, política de propietario nombrado sólo para el padre exacto, rechazo de otros UID/path/modos/symlinks, deriva por renombre del directorio con limpieza anclada, colisión del nombre temporal preservando bytes/inodo ajenos y cerrando descriptores, orden de instalación y continuidad de ambos segmentos mediante dobles locales. No se ejecutan SSH, pct, PVE ni datos de copias. Los tests no consultan PVE. La ejecución Linux nativa de estas pruebas y la validación del candidato instalado quedan pendientes de la revisión de root.
La revisión detectó dos ventanas de restauración de handlers ante instalación parcial y señal pendiente al primer desbloqueo. Se capturan los handlers antes de instalarlos y toda restauración queda protegida. Durante spawn/registro un handler cooperativo guarda la primera señal sin lanzar; el hijo queda asignado antes de propagarla, sin preexec_fn ni máscara bloqueada durante fork/exec. Sólo se enmascaran las transiciones entre handlers. La limpieza TERM/KILL/reap del grupo propio también se aplica tras un estado válido: un hijo que cerrase stdout no queda vivo. Las señales repetidas se difieren durante limpieza. Las siete pruebas añadidas conservan las catorce originales e incluyen ambos fallos de instalación/restauración, handoff TERM/INT/HUP real, máscara del hijo, rechazo antes de spawn fuera del hilo principal o sin capacidad POSIX, fallo de spawn y limpieza tras éxito manteniendo otro grupo sintético intacto. El helper exige hilo principal y soporte POSIX de señales antes de crear el hijo; no garantiza cancelación de tareas bloqueadas indefinidamente en el kernel, ni protección frente SIGKILL o pérdida de energía. Las alertas de éxito/frescura deben revisarse junto al despliegue; este cambio no modifica la regla existente de errores por trabajo ni convierte la ausencia de trabajos en salud de replicación acreditada. Rollback fuente: revertir los archivos de esta propuesta en conjunto; no ejecutar el rol completo para aplicar o retirar este cambio acotado.
La revisión de la creación exclusiva detectó un fallo adicional: registrar el nombre temporal antes de O_EXCL permitía limpiar un archivo ajeno si ese nombre ya existía. Ahora la propiedad del temporal se registra sólo después de obtener el descriptor creado; una colisión aborta sin borrar ni sobrescribir el archivo preexistente ni las métricas anteriores. La regresión usa un temporal sintético real y comprueba bytes, dispositivo/inodo y cierre de descriptores.
Instalación y observación natural20oct14:41 CEST¶
Fuente publicada en main; CI Linux de copias 39423177307, Ansible y documentación correctas. El workflow Docker falló al importar el nuevo test antes de instalar Jinja2. Se reprodujo en un venv limpio y se adelantó únicamente ese paso; la nueva CI39423911108 terminó correctamente sobre main8e0a5ad. No se omitieron tests.
Instalador acotado4280e3c6 ejecutado una vez por host:
- pmx14:39:42: helper308f39d4 root:root0644 instalado antes del wrapperb733a08f
root:root0755. Compilación nativa, plan sin colección y sintaxis Bash correctos.
Cron y métricas idénticos durante la instalación. Backup raíz0700
/root/rollback-pvesr-collector-20261020T123942Z-bydhcaja, wrapper.before0600 con la versión6251e916 y recibo instalado sin revisión manual pendiente. - pmx2: abortó en baseline, sin reemplazar archivos ni crear backup. La lectura independiente14:41:20,6 demostró que wrapperb733a08f y helper308f39d4 ya estaban presentes, con mtime14:38:31 y14:38:30 respectivamente, anteriores al intento. Root:root, modos0755/0644, un enlace y compilación nativa correctos; cron e identidades de directorios coinciden con la referencia. Se adopta el estado comprobado, sin reintentar ni atribuir su instalación a un mecanismo concreto sin evidencia de su ejecución.
Posverificación independiente14:41:20–21: pmx coincide con su recibo completo.
Los archivos pvesr.prom de ambos nodos contienen collection_success1 y timestamp
1792500000, intento natural14:40:00 CEST. pmx tiene0trabajos; pmx2,14trabajos con
FailCount0 y StateOK. Es una observación de esa respuesta, no garantía de RPO,
restauración ni ausencia de errores posteriores. No se invocó --collect ni
pvesr status durante instalación o posverificación; el cron existente produjo
la muestra. Evidencias privadas: pmx
<REDACTED>;
diagnóstico pmx2
<REDACTED>.
La regla PVEReplicationCollectorUnhealthy se añadió como un único grupo al
archivo activo, conservando exactos los40049bytes anteriores. Resultado40979B,
SHA256 <REDACTED>,
misma identidad de bind/inodo1029537/1000:1000/0664. Promtool nativo valida92reglas;
una única recarga local POST devuelve200, sin reinicio. Backup0700
/root/rollback-pvesr-collector-alerts-20261020-7_q_xqgt, previo alerts.before.yml
0600, recibos preparados/escritos/finales conservados.
Dos evaluaciones naturales14:40:53 y14:41:08:92/92reglas con healthOK y nueva
alerta inactive, sin error. Ambos targets exactos publican éxito1 y timestamp del
intento14:40, con edades58–77s. No se forzó evaluación, scrape, colección ni job.
Evidencia privada de posverificación Prometheus SHA256
<REDACTED>.
Las alertas de copias y capacidad mantienen sus estados reales; no se declaran
resueltas por esta nueva regla.
Recuperación manual del colector: comprobar primero identidades/hashes actuales y ausencia de intervención posterior; restaurar wrapper6251e916 antes de retirar el helper nuevo. En pmx usar wrapper.before del backup indicado, root:root0755, reemplazo atómico en su mismo directorio y fsync. En pmx2 no existe backup de este instalador: obtener el wrapper6251e916 de la fuente histórica verificada antes de preparar cualquier restauración; no suponer que se guardó un archivo remoto. Retirar helper308f39d4 sólo después del wrapper antiguo y comprobando su identidad exacta y que no existía en el preflight. No tocar métricas, cron ni permisos de directorios. Reconciliar también la fuente publicada para impedir reaparición por el mecanismo existente; no ejecutar el rol completo.
Recuperación manual de alertas: bajo el lock del archivo, comprobar recibo y archivo db55da8b sin escrituras posteriores; restaurar sólo alerts.before.yml in-place para conservar el bind/inodo. Validar91reglas con promtool nativo antes y después y recargar una vez; inspeccionar ante cualquier incertidumbre, sin reintentos o rollback automáticos. No restaurar métricas, reglas ajenas, estado ni configuración de aplicaciones.