Páginas

lunes, 21 de septiembre de 2026

FreeBSD 14.4 Script Envío Remoto Versión Definitiva

# 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  none
FreeBSD es genial!.

No hay comentarios:

Publicar un comentario