#!/bin/sh

help() {
   echo "$(basename $0) [opciones] <disco.qcw>
   Limpia el disco virtual de información innecesaria para disminuir su tamaño.

Opciones:

  -h         Muestra este mensaje de ayuda.
  -k         La imagen mantiene la base del disco original.
  -l NOMBRE  Nombre del sistema para logs. Por defecto, 'log'. Si se deja
             en blanco (-l ''), se sobreentiende que no es independiente.
  -r NOMBRE  Nombre del sistema raíz. Por defecto, 'raiz'.
  -y         Elimina el disco original.

"
}


#
# Tratamiento de errores
#
# $1: Códido de salida. Si es 0, sólo muestra el mensaje de error.
# $*: Mensaje de error
error() {
   local EXITCODE=$1
   shift

   if [ $EXITCODE -eq 0 ]; then
      echo "¡ATENCIÓN! "$* >&2
   else
      echo "ERROR. "$* >&2
      exit $EXITCODE
   fi
}


soy_root() {
   [ $(id -u) -eq 0 ]
}


#
# Monta el disco en el dispositivo nbd
# $1: Dispositivo NBD
# $2: Disco qcw
#
mount_nbd() {

   #
   # Carga el módulo nbd
   #
   # $1: max_parts. Por defecto, 7
   load_nbd_module() {
      grep -q '^nbd\>' /proc/modules && return 0
      modprobe nbd max_parts="${1:-7}"
   }

   load_nbd_module
   qemu-nbd -c "$1" "$2" || exit 1
   sleep $SEC
   parts_in_kernel "$1" || partx -a "$1"
   sleep $SEC
   parts_in_kernel "$1" || error 1 "La imagen no parece tener particiones."
}


#
# Obtiene los RAIDs del disco virtual
#
# $1: Dispositivo NBD
# stdout: Líneas de la forma "dev1:dev2"
#  donde dev1 es la partición física que contiene el RAID
#  y dev2 el dispositivo sobre el que se ha montado automáticamente.
#  Por ejemplo, /dev/nbd0p3:/dev/md/0
#
get_raid() {
   mdadm --query "$1"p* | \
      awk -F: -v OFS=":" '$2 ~ /device/ {match($2, "/dev/md[/0-9]+"); print $1, substr($2, RSTART, RLENGTH)}'
}


#
# Selecciona un dispositivo de raid desocupado.
#
select_md() {
   local i=0
   while [ -b "/dev/md$i" ]; do
      i=$((i+1))
   done
   echo "/dev/md$i"
}


#
# Comprueba si el dispositivo tiene definidas particiones
# $1: El dispositivo
#
has_parts() {
   partx -l "$1" > /dev/null 2>&1
}


#
# Comprueba si el dispositivo tiene sus particiones cargadas en el núcleo
# $1: El dispositivo
#
parts_in_kernel() {
   set -- "$1"p*
   [ -b "$1" ]
}


#
# Comprueba si el dispositivo se encuentra entre los listados
# $1: El dispositivo
# $*: Lista de dispositivos
#
filter_part() {
   local dev=$1
   shift
   expr "$*" : ".*$dev\>" > /dev/null
}


#
# Genera el nombre de la nueva imagen
#
get_new_name() {
   echo "${1%.*}-light.${1##*.}"
}


#
# Desmonta por completo la imagen
#
umount_nbd() {
   local raid

   [ -n "$VGS" ] && vgchange -an $VGS
   sleep "$SEC"
   [ -n "$RAIDS" ] && mdadm --stop $RAIDS
   sleep "$SEC"
   qemu-nbd -d "$NBD"
}


#
# Determina cuál es el dispositivo de la partición
# $1: Nombre del volumen lógico o dispositivo
#
get_device() {
   local part="$1"

   set -- $LVS

   [ ! -b "$part" ] && part=$(expr "$*" : ".*\(/dev/mapper/[0-9a-zA-Z]\+-$part\)\b")
   [ -n "$part" ] && echo "$part"
}


#
# Limpia la partición raiz
#
clean_root() {
   local part="$1"

   mount "$part" "$MOUNTDIR"
   rm -rf "$MOUNTDIR"/root/.bash_history "$MOUNTDIR"/root/.viminfo "$MOUNTDIR"/root/.ssh
   rm -f "$MOUNTDIR"/var/lib/apt/lists/*dists*
   find "$MOUNTDIR" -name "*.pyc" -delete
   touch "$MOUNTDIR"/root/.REFERENCIA
   umount "$MOUNTDIR"
}


#
# Limpia los registros
#
clean_log() {
   local varlog part="${1:-$2}"

   [ -z "$1" ] && varlog="/var/log"

   mount "$part" "$MOUNTDIR"
   find "$MOUNTDIR$varlog" -type f | while read -r F; do true > "$F"; done
   umount "$MOUNTDIR"
}


#
# Limpia las particiones
#
clean_parts() {
   local part type

   #
   # Rellena con ceros el espacio libre.
   #
   # $1: partición.
   put_zeros() {
      local part="$1"

      printf "Limpiando $part: "
      type=$(lsblk -lno FSTYPE "$part")
      case "$type" in
         ext[234])
            echo
            zerofree -v "$part"
            ;;
         swap) 
            cat /dev/zero > "$part" 2>/dev/null
            mkswap -L SWAP -U "$(lsblk -nlo uuid "$part")" "$part"
            echo "SWAP rellena con ceros"
            ;;
         "") echo "no parece tener sistema de particiones";;
         ntfs|vfat*) mount "$part" "$MOUNTDIR"
            cat /dev/zero > "$MOUNTDIR/zero" 2>/dev/null
            rm -f "$MOUNTDIR/zero"
            umount "$MOUNTDIR"
            echo "$type relleno con ceros"
            ;;
         *) echo "no se hace nada con el tipo '$type'"
      esac
   }

   for part in "$@"; do
      filter_part "$part" $EXCEPTIONS && continue
      put_zeros "$part"
   done
}


#
# Crea una version comprimida del disco
#
convert_disk() {
   nice qemu-img convert -p -c -f qcow2 "$IMG" -O qcow2 "$1" $BASE
   chown $(stat -c "%U" "$IMG") "$1"
   chmod -w "$1"
}


#
## Programa principal
#

{ ## Tratamiento de parámetros
   RAIZ="raiz"     # Nombre de la partición raíz. 
   LOG="log"       # Nombre de la partición para /var/log
   NBD="/dev/nbd0"
   MOUNTDIR="/mnt" # Punto de montaje
   IMG=            # Nombre de la imagen.
   BASE=           # La imagen resultante no mantiene la base de la original.
   SEC=.5
   DELETE=

   while [ $# -ne 0 ]; do
      case $1 in
         -h|--help)
             help
             exit 0
             ;; 
         -r) RAIZ=$2
             shift
             ;;
         -l) LOG=$2
             shift
             ;;
         -k) BASE=1;;
         -y) DELETE=1;;
         -*) error 0 "$1: Opción desconocida."
             help
             exit 2
             ;;
          *) [ -z "$IMG" ] || error 2 "Demasiados argumentos: sólo puede haber un nombre de imagen."
             IMG=$1
      esac
      shift
   done

   [ -n "$IMG" ] || error 2 "¿Qué imagen desea limpiar?"
   [ -f "$IMG" ] || error 1 "$IMG: La imagen no existe"

   if [ -n "$BASE" ]; then
      BASE=$(qemu-img info "$IMG" | grep -E -m1 "^backing")
      if [ -n "$BASE" ]; then
         BASE=$(echo $BASE | grep -Po '(?<=\s)[^ \t\)]+(?=\S*$)')
         [ -f "$BASE" ] || error 1 "La imagen de base $BASE no existe" >&2
         BASE="-o backing_file="$BASE
      fi
   fi
}

soy_root || error 1 Debe ejecutarme como administrador

trap 'umount_nbd' EXIT TERM INT

mount_nbd "$NBD" "$IMG"

#
# Se detectan los dispositivos de RAID asociados al disco virtual
#
RAIDS=
PARTS="$NBD"p*
EXCEPTIONS=
for part in $(get_raid "$NBD"); do
   mddevice=$(realpath "${part#*:}")
   if [ "$mddevice" != "$part" ]; then
      part="${part%:*}"
   else  # El dispotivo de RAID no se montó automáticamente
      mddevice=$(select_md)
      mdadm --assemble "$mddevice" "$part"
   fi
   EXCEPTIONS="$EXCEPTIONS $part"

   # Si hay definida una tabla de particiones, apuntamos las particiones del raid.
   has_parts "$mddevice" && PARTS="$PARTS ${mddevice}p*"
   RAIDS="$RAIDS $mddevice"
done


#
# Se detectan los grupos de volúmenes asociados al disco virtual
#
VGS=
LVS=
for vg in $(vgs -o name,pv_name --noheadings --separator :); do
   part=${vg#*:}
   filter_part "$part" $PARTS $RAIDS || continue
   vg=${vg%:*}
   vgchange -ay "$vg"
   VGS="$VGS $vg"
   LVS="$LVS /dev/mapper/$vg-*"
   # Si el grupo está definido en una parte,
   # esa parte ya no contendrá un sistema de ficheros
   filter_part "$part" $PARTS && EXCEPTIONS="$EXCEPTIONS $part"
done


raiz=$(get_device "$RAIZ") || error 1 "La raíz '$RAIZ' no existe"
if [ -n "$LOG" ]; then
   log=$(get_device "$LOG") || error 1 "El dispositivo de logs '$LOG' no existe"
fi


clean_root "$raiz"
clean_log "$log" "$raiz"
clean_parts $PARTS $LVS

umount_nbd

convert_disk "$(get_new_name "$IMG")"
[ -n "$DELETE" ] && rm -f "$IMG"
