1. Replicación de datos ZFS FreeBSD
2. Script Copia de Seguridad de un Conjunto de Datos ZFS Envío Remoto Versión 1.0.0
3. Script Replicación Incremental ZFS Cliente Servidor - Envío Remoto Versión 1.1.0
# cat ~/bin/zfsresp.sh
###########################
#!/bin/sh
### BEGIN INFO
# PROVIDE:
# REQUIRE:
# KEYWORD:
# NOMBRE: Replicación Incremental ZFS cliente - servidor
# DESC: Replica zroot/home/carlos (solaris) -> zbackup/solaris/home/carlos (tormenta)
# VERSION: 1.1.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 "$NOMBRE v$VERSION" >> $log
echo "(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
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
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
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!.







