#!/bin/bash
#
# zfs-transfer-receive
#
# Invoca a zfs-transfer-send a traves de un shell remoto
# y recibe el flujo ZFS generado.
#
# Si la transferencia se interrumpio anteriormente y existe
# un receive_resume_token en el sistema de archivos de destino
# entonces envia el token a zfs-transfer-send para continuarla.
#
#

set -o pipefail

FECHA="20200319"
VERSION="0.0.2"

[[ "$DEBUG" ]] || DEBUG=0
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

Invoca a zfs-transfer-send a traves de ssh.

Solicita el envio del filesystem 'origen' recibiendolo con el mismo
nombre o como 'destino' si es especificado.

Uso: $(basename $0) [opciones] usuario@host origen [destino]

Opciones:

   -s <nombre>    El nombre del snapshot inicial del origen.
   -S <nombre>    El nombre del snapshot final del origen.
   -a             Usar automaticamente como snapshot inicial el snapshot mas
                  reciente en el DESTINO cuyo nombre comience por el prefijo
                  usado para los snapshots ($SNAP_PREFIX).
   -d             Solicitar al servidor ORIGEN que elimine los snapshots mas
                  antiguos que el snapshot inicial y cuyo nombre comience con
                  el prefijo solicitado.
   -D             Eliminar los snapshots antiguos en el servidor DESTINO.
   -z <0|1>       Si el destino no existe, solicitar una transferencia inicial.
   -i <ruta>      Ruta a la llave SSH de acceso al sistema origen.
   -p <prefijo>   Indica el prefijo usado para los snapshots
                  (Default: $SNAP_PREFIX).
   -V             Muestra el numero de version
   -c <config>    Ruta al archivo de configuracion (Default: $CONFIGFILE) 

Ejemplos:
   zfs-transfer-send zfs@servidor pool/respaldo
   zfs-transfer-send zfs@servidor pool/respaldo tank/cliente/backups
   zfs-transfer-send -a -d zfs@servidor pool/respaldo tank/cliente/backups

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                       #
#######################################################

# Direccion del administrador del sistema
[ "$ADMIN" ] || ADMIN="monitor@asic-linux.com.mx"

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

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

# Comando de compresion
[[ "$UNCOMPRESS_CMD" ]] || UNCOMPRESS_CMD="/bin/gzip -d"

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

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

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

# Calcular automaticamente el snap inicial?
[[ "$AUTO_SNAP_INICIAL" ]] || AUTO_SNAP_INICIAL=0

# Eliminar en el ORIGEN los snapshots anteriores al inicial?
[[ "$DELETE_OLD_SNAPS" ]] || DELETE_OLD_SNAPS=0

# Solicitar una transferencia inicial si no existe el fs destino?
[[ "$REQUEST_INIT" ]] || REQUEST_INIT=0

# Eliminar en el DESTINO los snapshots anteriores al inicial?
[[ "$DELETE_OLD_SNAPS_LOCAL" ]] || DELETE_OLD_SNAPS_LOCAL=0

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




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

while getopts ":Vc:i:s:S:p:adDhz:" opt; do
  case $opt in
    i)
      SSH_ID="$OPTARG"
      ;;
    s)
      snap_inicial="$OPTARG"
      ;;
    S)
      snap_final="$OPTARG"
      ;;
    p)
      SNAP_PREFIX="$OPTARG"
      ;;
    a)
      AUTO_SNAP_INICIAL=1
      ;;
    d)
      DELETE_OLD_SNAPS=1
      ;;
    D)
      DELETE_OLD_SNAPS_LOCAL=1
      ;;
    z)
      REQUEST_INIT="$OPTARG"
      ;;
    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               #
#####################################################

sshaccess="$1"
fsorig="$2"
fsdest="$3"

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

# ZFS es ejecutable?
[[ -x "$ZFS" ]]  || termina_error "$ZFS no es ejecutable."

# El id existe?
if [[ "$SSH_ID" ]]; then
   [[ -r "$SSH_ID" ]] || termina_error "La llave de autenticacion ($SSH_ID) no es legible."
fi

if [[ -z "$sshaccess" || -z "$fsorig" ]]; then
   despliega_uso
   termina_error "Los argumentos 'usuario@host' y 'origen' son requeridos." 111
fi

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

main () {

 # Tenemos una llave SSH?
 ssh_id_str=""
 if [[ $SSH_ID ]]; then
    ssh_id_str="-i ""$SSH_ID"
 fi

 # Si no tenemos un fs destino, sera el fs origen
 if [[ -z "$fsdest" ]]; then
    fsdest="$fsorig"
 fi

 # Si tenemos un snap_final pero no un snap_inicial, asumimos -a
 if [[ "$snap_final" && -z "$snap_inicial" ]]; then
    echo "[$(hostname)] ADVERTENCIA: Un snapshot final fue especificado pero no uno inicial. Opcion -a activada implicitamente." >&2
    AUTO_SNAP_INICIAL=1
 fi

 # Existe el fs destino?
 if $ZFS list "$fsdest" &> /dev/null; then
    # Si existe el destino. Tenemos un token?
    token=$($ZFS get -H receive_resume_token "$fsdest" | awk '{print $3}')
    # Calculamos el snap inicial
    if [[ $AUTO_SNAP_INICIAL -gt 0 ]]; then
       snap_inicial=$($ZFS list -H -t snap "$fsdest" | egrep "^${fsdest}@${SNAP_PREFIX}-" | tail -n1 | awk '{print $1}' | cut -f2 -d@)
       if [[ -z "$snap_inicial" ]]; then
          termina_error "No pude determinar el snap inicial. Existen snapshots con el prefijo '${SNAP_PREFIX}' en este host?"
       fi
    fi
#
#    if [[ $token == "-" ]]; then
#       # Si existe y no tenemos un token entonces calculamos el snap inicial
#       if [[ $AUTO_SNAP_INICIAL -gt 0 ]]; then
#          snap_inicial=$($ZFS list -H -t snap "$fsdest" | egrep "^${fsdest}@${SNAP_PREFIX}-" | tail -n1 | awk '{print $1}' | cut -f2 -d@)
#          if [[ -z "$snap_inicial" ]]; then
#             termina_error "No pude determinar el snap inicial. Existen snapshots con el prefijo '${SNAP_PREFIX}' en este host?"
#          fi
#       fi
#    fi

 else
    # No existe. Nos dieron permiso para crearlo?
    if [[ $REQUEST_INIT -eq 0 ]]; then
       # Si no nos pidieron inicializar con -i, reportamos que no existe.
       termina_error "El destino '$fsdest' no existe en este host. Tal vez quieras usar la opcion '-z 1'."
    fi
 fi

 # Cualquiera de las opciones de limpieza requiere un snap_inicial
 if [[ $DELETE_OLD_SNAPS -gt 0 || $DELETE_OLD_SNAPS_LOCAL -gt 0 ]]; then
    if [[ -z "$snap_inicial" ]]; then
       termina_error "Solicitaste eliminar los snapshots antiguos pero no tengo un snapshot inicial. Abortando."
    fi
 fi

 ############ Limpieza remota ############

 # Si nos pidieron hacer limpieza remota, es lo primero que hacemos.
 if [[ $DELETE_OLD_SNAPS -gt 0 ]]; then
    echo "[$(hostname)] Solicitando a $(echo $sshaccess | cut -f2 -d@) la eliminacion de los snapshots previos a '${fsorig}@${snap_inicial}'" >&2
    ssh $ssh_id_str "$sshaccess" "$fsorig clean:${snap_inicial}"
 fi

 ############ Limpieza local ############

 # Si solo queremos simulacion de la destruccion de snaps
 # ponemos la bandera -n para el '$ZFS destroy' mas abajo
 if [[ $DELETE_OLD_SNAPS_LOCAL -gt 0 ]]; then
    dest_nstr=""
    if [[ $SNAP_DELETE_SIMULATE_LOCAL -gt 0 ]]; then
       dest_nstr="-n"
    fi
    fs_snap_objetivo="${fsdest}@${snap_inicial}"
    if ! snap_exists "$fs_snap_objetivo"; then
       termina_error "Solicitaste hacer limpieza pero no existe el snapshot remanente solicitado: '$fs_snap_objetivo'."
    fi
    fs_snap_prefix="${fsdest}@${SNAP_PREFIX}-"
    echo "[$(hostname)] Iniciando limpieza local de snapshots anteriores a ${fs_snap_objetivo}"
    $ZFS list -H -t snap "$fsdest" | cut -f1 | egrep "^${fs_snap_prefix}" | while read s; do
       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
 fi

 ############ Ahora, la transferencia ############

 set -o pipefail

 # Existe el destino?
 if $ZFS list "$fsdest" &> /dev/null; then

    if [[ $token == "-" ]]; then
       # No tenemos token, venga el incremental
       echo "[$(hostname)] Iniciando recepcion incremental de '$fsorig' desde $sshaccess en '$fsdest'" >&2
       ssh $ssh_id_str "$sshaccess" "$fsorig $snap_inicial $snap_final" | $BUFFER_CMD | $UNCOMPRESS_CMD | zfs receive -s -F "$fsdest"
       zfs_retval=$?
    else
       # Si tenemos token, venga la continuacion
       echo "[$(hostname)] Continuando el envio de '$fsorig' desde $sshaccess hacia '$fsdest'" >&2
       ssh $ssh_id_str "$sshaccess" "$fsorig token:${token}" | $BUFFER_CMD  | $UNCOMPRESS_CMD | zfs receive -s -F "$fsdest"
       zfs_retval=$?
    fi

 else
    # No existe el destino. Si llegamos hasta aqui y no existe quiere decir que tenemos la opcion -z1. Solicitemoslo al origen.
    echo "[$(hostname)] Creando dataset '$fsdest'" >&2
    $ZFS create "$fsdest" || termina_error "creando dataset '$fsdest'. Existe el dataset padre '$(dirname "$fsdest")'?"
    ssh $ssh_id_str "$sshaccess" "$fsorig init:" | $BUFFER_CMD | $UNCOMPRESS_CMD | zfs receive -s -F "$fsdest"
    zfs_retval=$?
 fi
 
 set +o pipefail


 if [[ $zfs_retval -eq 0 ]]; then
    echo "[$(hostname)] '$fsdest' fue recibido exitosamente." >&2
 else
    echo "[$(hostname)] La recepcion de '$fsdest' HA FALLADO." >&2
 fi

}

main
rm -f $ERRFILE
exit $zfs_retval

