Script de Replicación ZFS (zfsrep.sh)
Este script automatiza la copia de seguridad y replicación incremental de snapshots de ZFS desde el sistema local (solaris.local.com FreeBSD 14.4 ZFS) hacia un servidor remoto (tormenta.local.com FreeBSD 14.4 ZFS). Gestiona la creación de la snapshot diaria, el envío incremental a través de SSH y la purga automática de snapshots antiguas tanto en origen como en destino, respetando una retención de, (el valor de retention), 15 snapshots.
Descripción del funcionamiento
/home/carlos/cronlog.Contenido del script
#!/bin/sh
### BEGIN INFO
# PROVIDE:
# REQUIRE:
# KEYWORD:
### END INFO
### INICIO DEL GUION
# Estas variables se nombran primero porque están anidadas en otras variables.
snap_prefix=snap
retention=15
# Se necesitan rutas completas a estas utilidades al ejecutar el script desde 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"
today="$snap_prefix-`$date +%Y%m%d`"
snap_today="$src_0@$today"
# Patrón grep con sufijo numérico para evitar falsos positivos.
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`
# Detecta la snapshot más reciente del destino remoto para usarla como base.
# Todo el pipeline corre en tormenta vía SSH.
snap_base_name=`ssh $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}"
# snap_old_dst_0 lista las snapshots > retention en el destino remoto.
snap_old_dst_0=`ssh $host "$zfs list -t snapshot -o name \
| grep '${dst_0}@${snap_prefix}-[0-9]' \
| sort -r \
| sed 1,${retention}d \
| sort"`
log=/home/carlos/cronlog
echo >> $log
echo "zfsrep.sh" >> $log
$date >> $log
echo >> $log
# Busca la instantánea de hoy en origen y, si no la encuentras, créala.
if $zfs list -H -o name -t snapshot \
| $sort \
| $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
$zfs snapshot -r $snap_today >> $log 2>&1
fi
echo >> $log
# Comprueba si la instantánea de hoy ya existe en el destino remoto.
# Si existe, no hay nada que enviar; saltar directamente a la purga.
if ssh $host "$zfs list -H -o name -t snapshot \
| grep '${dst_0}@${today}$'" > /dev/null 2>&1
then
echo "La instantánea de hoy ya existe en destino remoto. Nada que enviar." >> $log
else
# Detecta la base del incremental y replica.
# Permite recuperarse de huecos de sincronización sin intervención manual.
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..." >> $log
$zfs send -R -i $snap_base $snap_today \
| ssh $host $zfs receive -vuF -x mountpoint $dst_0 >> $log 2>&1
echo >> $log
echo "Replicacion completada." >> $log
else
echo "No se encontró snapshot común entre origen y destino." >> $log
echo "Realizando envío completo (puede tardar)..." >> $log
$zfs send -R $snap_today \
| ssh $host $zfs receive -vuF -x mountpoint $dst_0 >> $log 2>&1
echo >> $log
echo "Envío completo finalizado." >> $log
fi
fi
echo >> $log
# Eliminar instantáneas locales más antiguas que el valor asignado a $retention.
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 -r >> $log 2>&1
else
echo "No se pudo encontrar ninguna instantánea local para destruir." >> $log
fi
echo >> $log
# Eliminar instantáneas remotas más antiguas que el valor asignado a $retention.
# Todo el pipeline, incluido zfs destroy, corre en tormenta vía SSH.
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 $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 -r" >> $log 2>&1
else
echo "No se pudo encontrar ninguna instantánea remota para destruir." >> $log
fi
echo "**********" >> $log
### FIN DEL SCRIPT
Estructura de zbackup (servidor)
carlos@tormenta:~ % zfs list -r zbackup NAME USED AVAIL REFER MOUNTPOINT zbackup 1008G 791G 92.5M none zbackup/dellhome 56.6G 791G 56.6G none zbackup/nfsv4 36.6G 791G 96K none zbackup/nfsv4/docs 7.80G 791G 7.80G none zbackup/nfsv4/secrets 28.8G 791G 28.8G none zbackup/recovery 755G 791G 104K none zbackup/recovery/solaris 282G 791G 282G none zbackup/recovery/tormenta 473G 791G 473G none zbackup/reserved 50G 841G 96K none zbackup/solaris 80.9G 791G 96K none zbackup/solaris/home 80.9G 791G 96K none zbackup/solaris/home/carlos 80.9G 791G 73.2G none zbackup/usr 147M 791G 104K none zbackup/usr/home 147M 791G 140M none
zfsrep.sh - Funcionamiento
Asigna rutas absolutas a los binarios (zfs, date, grep, etc.) para evitar problemas de $PATH al ejecutarse desde 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
Contexto
El script corre en solaris (cliente) y replica el dataset zroot/home/carlos hacia zbackup/solaris/home/carlos en tormenta (servidor), usando SSH como canal de transporte.
1 - Definición de variables
src_0="zroot/home/carlos" dst_0="zbackup/solaris/home/carlos" host="root@192.168.88.160" today="snap-20260901" snap_today="zroot/home/carlos@snap-fecha_de_hoy"
Se calculan los nombres de las snapshots y rutas antes de hacer nada. today se genera en este momento con $date, así que si el script corre a las 2:00 AM o a las 23:00 del mismo día, el nombre es idéntico - eso es lo que permite la comprobación de duplicado más adelante.
2 - Inventario de snapshots antiguas a purgar (local)
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`
Se calcula en este momento, antes de crear la snapshot de hoy. El pipeline:
Lista todas las snapshots del sistema Filtra las de zroot/home/carlos@snap- seguido de dígito Las ordena de más reciente a más antigua (sort -r) Elimina las primeras 15 líneas (sed 1,15d) - las 15 más recientes se conservan Lo que queda son las candidatas a destruir
Este cálculo es previo a la snapshot de hoy para que retention=15 sea exacto.
3 - Inventario remoto: base del incremental y purga
snap_base_name=`ssh $host "...pipeline en tormenta..."` snap_old_dst_0=`ssh $host "...pipeline en tormenta..."`
Se abren dos conexiones SSH a tormenta para:
snap_base_name: la snapshot más reciente que tiene zbackup/solaris/home/carlos. Será el punto de partida del incremental. Si el destino tiene snap-20260831, el incremental enviará solo los cambios entre ese día y hoy. snap_old_dst_0: lista de snapshots en destino que superan retention=15, candidatas a purga remota.
Todo el pipeline de grep/sort/sed corre dentro del SSH, en tormenta, para que el resultado que llega a solaris sea solo texto limpio.
4 - Snapshot de hoy en origen
if zfs list ... | grep "${snap_today}$"
then
"ya existe"
else
zfs snapshot -r $snap_today
fi
Comprueba si zroot/home/carlos@snap-20260901 ya existe. El flag -r crea la snapshot de forma recursiva - si hubiera datasets hijo bajo zroot/home/carlos, también se snapshotearían. Si ya existe (segunda ejecución del día), simplemente lo anota en el log y continúa.
5 - Comprobación de duplicado en destino
if ssh $host "zfs list ... | grep '${dst_0}@${today}$'"
then
"Nada que enviar"
else
# continuar con el send
fi
Pregunta a tormenta: ¿ya tienes zbackup/solaris/home/carlos@snap-20260901? Si la respuesta es sí, el script salta directamente a la purga - no hay nada que transferir. Esto evita el reenvío innecesario si cron lanza el script más de una vez al día.
6 - Replicación incremental (o completa)
Caso normal - base común encontrada:
zfs send -R -i $snap_base $snap_today \ | ssh $host zfs receive -vuF -x mountpoint $dst_0
zfs send genera un stream binario con los bloques que cambiaron entre snap_base y snap_today. Ese stream se pasa directamente por el pipe a SSH, que lo entrega a zfs receive en tormenta. No se escribe nada en disco en solaris - es un flujo directo en memoria.
Los flags:
-R: incluye snapshots hijas recursivamente -i: incremental entre dos snapshots -v: verbose en el log -u: no monta el dataset recibido -F: destruye en destino lo que no existe en el stream (mantiene coherencia) -x mountpoint: ignora la propiedad mountpoint del stream — el destino mantiene mountpoint=none
Caso de hueco - sin base común:
zfs send -R $snap_today | ssh $host zfs receive -vuF -x mountpoint $dst_0
Si no hay ninguna snapshot común entre origen y destino, se hace un envío completo. Más lento, pero garantiza que el destino queda sincronizado desde cero.
7 - Purga local
zfs list -t snapshot -o name
| grep "${src_0}@${snap_prefix}-[0-9]"
| sort -r | sed 1,15d | sort
| xargs -n 1 zfs destroy -r
Mismo pipeline que en el paso 2, pero esta vez ejecutando zfs destroy -r sobre cada snapshot antigua. El -r destruye también las snapshots hijas si las hubiera. Las 15 más recientes quedan intactas.
8 - Purga remota
ssh $host "zfs list ... | grep ... | sort -r | sed 1,15d | sort | xargs -n 1 zfs destroy -r"
Ídem pero en tormenta. Todo el pipeline - incluido el zfs destroy - corre dentro del SSH. Esto es crítico: si el destroy corriera en local, intentaría destruir zbackup/... en solaris, donde ese dataset no existe.
Entrada en cron para ejecución diaria a las 2:00 AM
crontab -e # Ejecución diaria de la copia de seguridad ZFS a las 2:00 AM 0 2 * * * /home/carlos/bin/zfsrep.sh Guardar y salir
Que sea ejcutable
chmod +x /home/carlos/bin/zfsrep.sh
Comprobar que el script funciona manualmente antes de programarlo
sh -x /home/carlos/bin/zfsrep.sh
Ejemplo de crontab completo (para root)
SHELL=/bin/sh PATH=/bin:/usr/bin:/sbin:/usr/sbin MAILTO=root # Ejecución diaria de la copia de seguridad ZFS a las 2:00 AM 0 2 * * * /home/carlos/bin/zfsrep.sh
Notas Finales
Robustez: La lógica de incremental/completo permite que el script se recupere automáticamente si se borra una instantánea intermedia.
Eficiencia: La mejora que evita reenvíos el mismo día ahorra ancho de banda y CPU.
Mantenimiento: La limpieza automática en origen y destino evita que el espacio se llene con instantáneas antiguas.
Portabilidad: Al usar rutas absolutas, el script funciona correctamente desde cron.
Con este sistema, tus datos están respaldados diariamente en un dispositivo remoto, con una política de retención clara y la capacidad de recuperación ante fallos de sincronización.