# cat ~/bin/zfsresp.sh
###########################
#!/bin/sh
### BEGIN INFO
# PROVIDE:
# REQUIRE:
# KEYWORD:
# VERSION: 1.3.0
# FECHA: 2026-09-21
### END INFO
### INICIO DEL GUION
# Detectar fallos en cualquier punto de la tubería (zfs send | ssh).
set -o pipefail
snap_prefix=snap
retention=30 # margen amplio por si el cliente pasa días apagado
# Rutas absolutas para cron.
date=/bin/date
cut=/usr/bin/cut
grep=/usr/bin/grep
sed=/usr/bin/sed
sort=/usr/bin/sort
xargs=/usr/bin/xargs
zfs=/sbin/zfs
src_0="zroot/home/carlos"
dst_0="zbackup/solaris/home/carlos"
host="root@192.168.88.160"
# Keepalive SSH: evita "Timeout, server ... not responding".
SSH_OPTS="-o ConnectTimeout=10 -o ServerAliveInterval=30 \
-o ServerAliveCountMax=10 -o TCPKeepAlive=yes \
-o BatchMode=yes"
today="$snap_prefix-`$date +%Y%m%d`"
snap_today="$src_0@$today"
log=/home/carlos/cronlog
echo >> $log
echo "zfsrep.sh (ejecucion manual)" >> $log
$date >> $log
echo >> $log
# Comprobar conectividad SSH antes de nada.
if ! ssh $SSH_OPTS $host true 2>/dev/null; then
echo "ERROR: no hay conexion SSH con $host. Abortando." >> $log
echo "**********" >> $log
exit 1
fi
# Limpia el estado del destino: desmonta y aborta receives colgados.
# Elimina el error "dataset is busy".
clean_dst() {
ssh $SSH_OPTS $host "zfs unmount '$dst_0' 2>/dev/null; \
zfs receive -A '$dst_0' 2>/dev/null; \
true"
}
# --- Snapshots locales antiguas (para purga) ---
snap_old=`$zfs list -t snapshot -o name \
| $grep "${src_0}@${snap_prefix}-[0-9]" \
| $sort -r \
| $sed 1,${retention}d \
| $sort \
| $xargs -n 1`
# --- Snapshot base en destino remoto ---
snap_base_name=`ssh $SSH_OPTS $host "zfs list -t snapshot -o name \
| grep '${dst_0}@${snap_prefix}-[0-9]' \
| sort -r \
| sed 1q \
| cut -d@ -f2"`
snap_base="${src_0}@$snap_base_name"
# --- Snapshots remotas antiguas (para purga) ---
snap_old_dst_0=`ssh $SSH_OPTS $host "zfs list -t snapshot -o name \
| grep '${dst_0}@${snap_prefix}-[0-9]' \
| sort -r \
| sed 1,${retention}d \
| sort"`
# --- Snapshot de hoy en origen ---
if $zfs list -H -o name -t snapshot | $grep "${snap_today}$" > /dev/null
then
echo "La instantánea de hoy '$snap_today' ya existe." >> $log
else
echo "Tomando la instantánea de hoy: $snap_today" >> $log
# Sin -r: no hay datasets hijos.
if ! $zfs snapshot "$snap_today" >> $log 2>&1; then
echo "ERROR: no se pudo crear la instantánea $snap_today." >> $log
echo "**********" >> $log
exit 1
fi
fi
echo >> $log
# --- Comparar la PUNTA de origen y destino ---
# No basta con "existe hoy en destino": puede existir la de hoy pero
# faltar intermedias si hubo un apagado prolongado. Comparamos extremos.
snap_last_src=`$zfs list -t snapshot -o name \
| $grep "${src_0}@${snap_prefix}-[0-9]" \
| $sort -r \
| sed 1q`
snap_last_dst=`ssh $SSH_OPTS $host "zfs list -t snapshot -o name \
| grep '${dst_0}@${snap_prefix}-[0-9]' \
| sort -r \
| sed 1q"`
src_suffix=`echo "$snap_last_src" | $cut -d@ -f2`
dst_suffix=`echo "$snap_last_dst" | $cut -d@ -f2`
if [ -n "$src_suffix" ] && [ "$src_suffix" = "$dst_suffix" ]; then
echo "Origen y destino ya sincronizados en @$src_suffix. Nada que enviar." >> $log
else
echo "Destino desfasado (@${dst_suffix:-vacio} vs @${src_suffix:-vacio}). Replicando..." >> $log
if [ -n "$snap_base_name" ] && $zfs list -H -o name -t snapshot \
| $grep "${snap_base}$" > /dev/null
then
echo "Usando '$snap_base' como base del incremental (-I)..." >> $log
clean_dst
# Sin -R (no hay hijos). -I envía TODOS los snapshots intermedios
# entre base y hoy, rellenando huecos de días apagado.
if $zfs send -I "$snap_base" "$snap_today" \
| ssh $SSH_OPTS $host "zfs receive -vuF -x mountpoint '$dst_0'"
then
echo "Replicacion completada." >> $log
else
echo "ERROR: fallo la replicacion incremental." >> $log
echo "**********" >> $log
exit 1
fi
else
echo "No se encontró snapshot común entre origen y destino." >> $log
echo "Realizando envío completo (puede tardar)..." >> $log
clean_dst
# Sin -R.
if $zfs send "$snap_today" \
| ssh $SSH_OPTS $host "zfs receive -vuF -x mountpoint '$dst_0'"
then
echo "Envío completo finalizado." >> $log
else
echo "ERROR: fallo el envio completo." >> $log
echo "**********" >> $log
exit 1
fi
fi
fi
echo >> $log
# --- Purga local ---
echo "Intentando destruir instantáneas antiguas $src_0..." >> $log
if [ -n "$snap_old" ]
then
echo "Destruyendo las siguientes instantáneas antiguas:" >> $log
echo "$snap_old" >> $log
$zfs list -t snapshot -o name \
| $grep "${src_0}@${snap_prefix}-[0-9]" \
| $sort -r \
| $sed 1,${retention}d \
| $sort \
| $xargs -n 1 $zfs destroy >> $log 2>&1
else
echo "No se pudo encontrar ninguna instantánea local para destruir." >> $log
fi
echo >> $log
# --- Purga remota ---
echo "Intentando destruir instantáneas antiguas $dst_0..." >> $log
if [ -n "$snap_old_dst_0" ]
then
echo "Destruyendo las siguientes instantáneas antiguas en $host:" >> $log
echo "$snap_old_dst_0" >> $log
ssh $SSH_OPTS $host "zfs list -t snapshot -o name \
| grep '${dst_0}@${snap_prefix}-[0-9]' \
| sort -r \
| sed 1,${retention}d \
| sort \
| xargs -n 1 zfs destroy" >> $log 2>&1
else
echo "No se pudo encontrar ninguna instantánea remota para destruir." >> $log
fi
echo "**********" >> $log
############ FINAL DEL SCRIPT ############
Lanzar el script
cd /home/carlos/bin ./zfsrep.sh
receiving incremental stream of zroot/home/carlos@snap-20260921 into zbackup/solaris/home/carlos@snap-20260921 received 186M stream in 101.31 seconds (1.84M/sec)
Verificar que todo llegó bien
En tormenta, comprobar que están todas las snapshots intermedias:
ssh root@192.168.88.160 "zfs list -t snapshot -o name -r zbackup/solaris/home/carlos" NAME ... zbackup/solaris/home/carlos@snap-20260811 zbackup/solaris/home/carlos@snap-20260812 zbackup/solaris/home/carlos@snap-20260813 zbackup/solaris/home/carlos@snap-20260814 zbackup/solaris/home/carlos@snap-20260815 zbackup/solaris/home/carlos@snap-20260916 zbackup/solaris/home/carlos@snap-20260918 zbackup/solaris/home/carlos@snap-20260919 zbackup/solaris/home/carlos@snap-20260920 zbackup/solaris/home/carlos@snap-20260921
Si se lanza el script zfsrep.sh seguido. Debe decir:
Origen y destino ya sincronizados en @snap-20260920. Nada que enviar.
Si dice eso, el script es idempotente
Revisar archivo /home/carlos/cronlog
cat ~/cronlog
********** zfsrep.sh (ejecucion manual) Mon Sep 21 07:16:13 CEST 2026 Tomando la instantánea de hoy: zroot/home/carlos@snap-20260921 Destino desfasado (@snap-20260920 vs @snap-20260921). Replicando... Usando 'zroot/home/carlos@snap-20260920' como base del incremental (-I)... Replicacion completada. Intentando destruir instantáneas antiguas zroot/home/carlos... No se pudo encontrar ninguna instantánea local para destruir. Intentando destruir instantáneas antiguas zbackup/solaris/home/carlos... No se pudo encontrar ninguna instantánea remota para destruir. **********
Comprobar que no queda un receive pendiente
ssh root@192.168.88.160 "zfs get receive_resume_token zbackup/solaris/home/carlos" NAME PROPERTY VALUE SOURCE zbackup/solaris/home/carlos receive_resume_token - -
Debe devolver - (sin receive pendiente).
Resumen del estado
Aspecto Estado Conexión SSH con tormenta - Correcta Detección de desfase - Correcta Replicación incremental con -I - Completada Relleno de huecos por apagados - Funciona dataset is busy - Resuelto por clean_dst() Timeout SSH - Resuelto por SSH_OPTS Mensaje "Replicacion completada" - Ahora es fiable Idempotencia (2ª ejecución) - Funciona
La comparación de puntas funciona (detecta que ya está al día).
El script es idempotente: ejecutarlo varias veces no rompe nada ni duplica envíos.
No hay receives colgados ni estados "busy" residuales.
"Replicacion completada" ahora es un mensaje fiable, no un falso positivo.
Estado final del script
Escenario Comportamiento Cliente apagado varios días -I rellena los snapshots intermedios Destino ya al día - Salta sin enviar nada Destino con receive colgado - clean_dst() lo aborta antes de recibir SSH lento o pausado - ServerAliveInterval evita el timeout Fallo en zfs send o ssh - pipefail + if lo detectan y abortan con error Sin hijos en el dataset -R eliminado; operaciones simples Retención - 30 snapshots, purga automática en ambos lados
Recomendaciones
Si algún vez el cliente pasa más de 30 días apagado, la base común desaparecerá y el script hará un envío completo aunque tardará más. Si se quiere evitar, debe aumentarse el valor de retention, o crear una snapshot "ancla" con zfs hold que nunca se borre.
Guardar una copia del script corregido en un sitio seguro (por ejemplo, dentro del propio pool ZFS que se replica, o en git).
Revisar el log cada cierto tiempo:
tail -50 /home/carlos/cronlog
Si algún día salta "ERROR: fallo la replicacion...", sabrás que es real y no un falso "completada".
Se puede considerar automatizarlo con cron si se quiere. El script ya tiene rutas absolutas y BatchMode=yes, así que está preparado. Solo se necesita una clave SSH sin passphrase entre carlos@solaris y root@tormenta (la tiene, porque funciona manualmente).
Servidor tormenta - zpools
carlos@tormenta:~ % zpool list NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT zbackup 1.81T 958G 898G - - 0% 51% 1.00x ONLINE - zroot 448G 206G 242G - - 8% 45% 1.00x ONLINE -
carlos@tormenta:~ % zfs list -r zbackup NAME USED AVAIL REFER MOUNTPOINT zbackup 1008G 790G 92.5M none zbackup/dellhome 56.6G 790G 56.6G none zbackup/nfsv4 36.6G 790G 96K none zbackup/nfsv4/docs 7.80G 790G 7.80G none zbackup/nfsv4/secrets 28.8G 790G 28.8G none zbackup/recovery 755G 790G 104K none zbackup/recovery/solaris 282G 790G 282G none zbackup/recovery/tormenta 473G 790G 473G none zbackup/reserved 50G 840G 96K none zbackup/solaris 81.4G 790G 96K none zbackup/solaris/home 81.4G 790G 96K none zbackup/solaris/home/carlos 81.4G 790G 74.3G none zbackup/usr 147M 790G 104K none zbackup/usr/home 147M 790G 140M noneFreeBSD es genial!.
No hay comentarios:
Publicar un comentario