#!/bin/bash
#
# zfs-transfer-send
#
# Recibe tres parametros:
#
#   zfs-transfer-send filessystem snap_inicial snap_final
#
# Genera un stream ZFS incremental desde snap_inicial hasta snap_final
#
# o recibe un token para continuar una transferencia interrumpida previamente:
#
#   zfs-transfer-send token snap_final
#
# El stream esta comprimido con gzip.
# 
#

FECHA="20200319"
VERSION="0.0.2"

USA_MONDIR=0

######################################################
#                  Funciones utiles                  #
######################################################


MONDIR="/var/log/mon"
MONFILE=${MONDIR}/`basename $0`

termina_error() {
   # Registramos el suceso en MONDIR
   if [ $USA_MONDIR -gt 0 ]; then
      [ -d "$MONDIR" ] || mkdir -p "$MONDIR"
      if [ $? -gt 0 ]; then
         echo "*** Error al crear $MONDIR" >&2
      else
         echo ----- `date` ----- >> $MONFILE
         echo "*** error: $1" >> "$MONFILE"
         if [ -r $ERRFILE ]; then
            cat $ERRFILE >> "$MONFILE"
         fi
      fi
   fi


   # Informamos via stderr
   echo "[$(hostname)] *** error: $1" >&2
   if [ -r $ERRFILE ]; then
      cat $ERRFILE >&2
   fi

   rm -f $ERRFILE
   if [ "$2" ]; then
      exit $2
   else
      exit 1
   fi
}


despliega_uso() {
cat << EOF >&2

$(basename $0)

Genera un stream ZFS incremental desde snap_inicial hasta snap_final
o continua una transferencia usando un token de continuacion.

Uso:
 $(basename $0) [opciones] filesystem snap_inicial snap_final
 $(basename $0) [opciones] token snap_final

Opciones:

   -V             Muestra el numero de version
   -c <config>    Ruta al archivo de configuracion (Default: $CONFIGFILE) 

EOF
}


crea_temp() {
   # Archivo temporal
   local retfile=`mktemp ${TMPBASE}/$(basename $0).XXXXXX`
   if [ $? -gt 0 ]; then
      termina_error "Error al crear archivo temporal en $TMPBASE"
      exit 1
   fi
   
   echo $retfile
}

crea_temp_dir() {
   # Directorio temporal
   local retdir=`mktemp -d ${TMPBASE}/$(basename $0).XXXXXX`
   if [ $? -gt 0 ]; then
      termina_error "Error al crear directorio temporal en $TMPBASE"
      exit 1
   fi
   
   echo $retdir
}

#
# get_envdir $envdir
#
# For earch file f in the directory $envdir add a variable named f to the
# environment with the contents of file f as a value.
#

get_envdir() {
   local envdir=$1

   if [[ -d "$envdir" ]]; then
      for v in $('ls' -1 $envdir); do
         if [[ -s $v ]]; then
            [[ $v =~ ^\. || $v =~ ^= ]] || read -d"" -r "$v" < "${envdir}/${v}"
         else
            unset $v
         fi
      done
   fi
}

timestamp () {
   echo "[$(date --rfc-3339 seconds)]"
}

snap_exists () {
   local snap
   snap="$1"
   if ! $ZFS list "$snap" &> /dev/null; then
      return 1
   fi
   return 0
}


#######################################################
#                      Defaults                       #
#######################################################

# Archivo de configuracion
[ "$CONFIGFILE" ] || CONFIGFILE="/etc/zfs-transfer/$(basename $0).conf"

# Ruta para archivos temporales
[ "$TMPBASE" ] || TMPBASE="/tmp"

# Ruta a zfs
[[ "$ZFS" ]] || ZFS="/sbin/zfs"

# Comando de compresion
[[ "$COMPRESS_CMD" ]] || COMPRESS_CMD="/bin/gzip"

# Comando de buffer
[[ "$BUFFER_CMD" ]] || BUFFER_CMD="/usr/bin/mbuffer -q -m512M"

# Prefijo para los snapshots
[[ "$SNAP_PREFIX" ]] || SNAP_PREFIX="zft"

# No eliminar realmente los snapshots cuando nos lo pidan
[[ "$SNAP_DELETE_SIMULATE" ]] || SNAP_DELETE_SIMULATE=0


#######################################################
#              Procesamos las opciones                #
#######################################################

while getopts ":Vc:" opt; do
  case $opt in
    V)
      echo "$(basename $0) $VERSION, $FECHA"
      exit 0
      ;;
    c)
      CONFIGFILE="$OPTARG"
      ;;
    h)
      despliega_uso
      exit 0
      ;;
    \?)
      echo "$(basename $0): Opcion invalida: -$OPTARG" >&2
      despliega_uso
      exit 1
      ;;
    :)
      echo "$(basename $0): Opcion -$OPTARG requiere un argumento" >&2
      exit 1
      ;;
  esac
done

shift $(($OPTIND-1))

# Log de errores
ERRFILE=$( crea_temp ) || termina_error "Error al crear el archivo ERRFILE"

# Leemos el archivo de configuracion
if [ -f "$CONFIGFILE" ]; then
   source "$CONFIGFILE" 2>>$ERRFILE || termina_error "Leyendo el archivo de configuracion $CONFIGFILE"
fi

#####################################################
#             Argumentos posicionales               #
#####################################################

filesystem="$1"
snap_inicial="$2"
snap_final="$3"

#####################################################
#            Verificaciones de sanidad              #
#####################################################

if [[ -z "$filesystem" ]]; then
   despliega_uso
   termina_error "El parametro filesystem es requerido." 111
fi




#####################################################
#                 Bucle principal                   #
#####################################################

main () {

fs_snap_inicial="${filesystem}@${snap_inicial}"
fs_snap_final="${filesystem}@${snap_final}"

zfs_action="send";

##### Nos dieron un token? ######
if [[ $snap_inicial =~ ^token: ]]; then

   zfs_action="token";

   # Extraemos el token
   resume_token=$(echo "$snap_inicial" | sed 's/^token://g')

   # Y la continuamos!
   echo "[$(hostname)] Continuando transferencia incremental de $filesystem" >&2
   set -o pipefail
   $ZFS send -v -t "$resume_token" | $BUFFER_CMD | $COMPRESS_CMD
   zfs_retval=$?
   set +o pipefail

##### Nos solicitaron una transferencia inicial? ######
elif [[ $snap_inicial =~ ^init: ]]; then

   zfs_action="init";

   # Extraemos el nombre solicitado, si lo hay
   snap_final=$(echo "$snap_inicial" | sed 's/^init://g')

   # Si no nos dieron un snap final creamos uno en este momento
   if [[ -z "$snap_final" ]]; then
      snap_final="${SNAP_PREFIX}-$(date +%Y%m%d-%H%M)"
      fs_snap_final="${filesystem}@${snap_final}"
   fi

   # Verificar si existe antes de crearlo
   if ! snap_exists "$fs_snap_final"; then
      $ZFS snap "$fs_snap_final"
   fi

   # Y la mandamos!
   echo "[$(hostname)] Comenzando transferencia inicial de ${fs_snap_final}" >&2
   set -o pipefail
   $ZFS send -v -D "$fs_snap_final" | $BUFFER_CMD | $COMPRESS_CMD
   zfs_retval=$?
   set +o pipefail

##### Nos pidieron hacer limpieza de snapshots? ######
elif [[ $snap_inicial =~ ^clean: ]]; then 

   zfs_action="clean";

   # Si solo queremos simulacion de la destruccion de snaps
   # ponemos la bandera -n para el '$ZFS destroy' mas abajo
   dest_nstr=""
   if [[ $SNAP_DELETE_SIMULATE -gt 0 ]]; then
      dest_nstr="-n"
   fi

   # Extraemos el snapshot que conservaremos
   snap_objetivo=$(echo "$snap_inicial" | sed 's/^clean://g')
   fs_snap_objetivo="${filesystem}@${snap_objetivo}"

   if ! snap_exists "$fs_snap_objetivo"; then
      termina_error "Solicitaste hacer limpieza pero no existe el snapshot remanente solicitado: '$snap_objetivo'."
   fi

   # snap_inicial YA esta en el respaldo remoto asi que
   # eliminamos todos los snapshots anteriores a ese.
   #
   # Solo queremos eliminar los que han sido creados por zfs-transfer,
   # estos son aquellos snapshots cuyo nombre comienza con "$SNAP_PREFIX-"
   echo "[$(hostname)] Eliminando los snapshots '$SNAP_PREFIX' anteriores a '$snap_objetivo'" >&2

   fs_snap_prefix="${filesystem}@${SNAP_PREFIX}-"

   $ZFS list -H -t snap "$filesystem" | cut -f1 | egrep "^${fs_snap_prefix}" | while read s; do
      [[ $DEBUG -gt 0 ]] && echo "[$(hostname)] s: $s" >&2
      if [[ $s =~ ^${fs_snap_objetivo}$ ]]; then
         break
      else
         $ZFS destroy $dest_nstr -v "$s" |& while read l; do echo "[$(hostname)] $l" >&2; done
      fi
   done

else
   ##### Tenemos un snap inicial #####

   if [[ "$snap_inicial" ]]; then

      # Si NOS DIERON un snap inicial verifiquemos si esta presente en el pool
      if ! snap_exists "$fs_snap_inicial"; then
         termina_error "El snapshot $fs_snap_inicial no existe en este host."
      fi

   else 
      # Si NO tenemos snap_inicial, el snap inicial sera el mas reciente SNAP_PREFIX-xxxxxxx
      fs_snap_inicial=$($ZFS list -H -t snap "$filesystem" | egrep "^${filesystem}@${SNAP_PREFIX}-" | tail -n1 | awk '{print $1}')
      if [[ -z "$fs_snap_inicial" ]]; then
         termina_error "No pude determinar el snap inicial."
      fi
   fi

   # Si no nos dieron un snap final creamos uno en este momento
   if [[ -z "$snap_final" ]]; then
      snap_final="${SNAP_PREFIX}-$(date +%Y%m%d-%H%M)"
      fs_snap_final="${filesystem}@${snap_final}"
   fi

   # Primero, el snapshot de destino
   # Verificar si existe antes de crearlo nos permite usar
   # zfs-transfer-send para generar transferencias entre cualesquiera
   # nombres de snapshots que querramos.
   if ! snap_exists "$fs_snap_final"; then
      $ZFS snap "$fs_snap_final"
   fi

   # Y lo enviamos!
   echo "[$(hostname)] Iniciando envio incremental de ${fs_snap_inicial} a ${fs_snap_final}" >&2
   set -o pipefail
   $ZFS send -v -D -i "$fs_snap_inicial" "$fs_snap_final" | $BUFFER_CMD | $COMPRESS_CMD
   zfs_retval=$?
   set +o pipefail

fi

if [[ $zfs_retval -eq 0 ]]; then
   if [[ $zfs_action != "clean" ]]; then
      [[ $zfs_action == "token" ]] && fs_snap_final="$filesystem"
      echo "[$(hostname)] Envio incremental a ${fs_snap_final} generado. Aun falta la confirmacion de recepcion. Espera." >&2
   fi
else
   [[ $zfs_action == "token" ]] && fs_snap_final="$filesystem"
   echo "[$(hostname)] El envio incremental a ${fs_snap_final} HA FALLADO." >&2
fi
}

main
rm -f $ERRFILE
exit $?

