#!/bin/sh
#  virtlvm-backup: make (disk-image) backups of LVM-based virtual machines
#  Copyright (C) 2013 IOhannes m zmölnig / IEM

#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.

#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.

#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see {http://www.gnu.org/licenses/}.

## a gpg-key for encrypting the images
GPGKEY=
GPGEXTENSION=".gpg"

## which zip-algorithm to use
ZIP=gzip
ZIPEXTENSION=".gz"

## verbosity level
VERBOSE=0
DEBUG=
DEBUGlvl=0

## automatic mode
AUTOMATIC=0

## default timestamp (none) in filenames
TIMESTAMP=""
TIMESTAMPPREFIX=""
TIMESTAMPSUFFIX=""


BACKUPDIR=
VHOST=
VDISKSpre=

#BACKUPDIR=$1
BACKUPDIRVHOST=
#shift
#VHOST=$1
#shift
#VDISKSpre=$@
CMDLINE="$(readlink -f $0) $@"

error() {
  echo "$@" 1>&2
}
fatal() {
  error "$@"
  exit 1
}
debug () {
# DEBUG=    : no debugging, just run
# DEBUG=yes : print cmdline before running it
# DEBUG=fake: only print cmdline, don't run it
    case ${DEBUG} in
	fake)
	    error "DEBUG: $@"
	    ;;
	yes)
	    error "DEBUG: $@"
	    $@
	    ;;
	*)
	    $@
	    ;;
    esac
}
debugPipe () {
    case ${DEBUG} in
	fake)
	    error "DEBUG:| $@"
	    ;;
	yes)
	    error "DEBUG:| $@"
	    $@
	    ;;
	*)
	    $@
	    ;;
    esac
}
verbose() {
    if [ $VERBOSE -lt $1 ]; then
	shift
	error "$@"
    fi
}

output() {
## function implementation of redirection to file
## (can be 'debug'ged)
    cat > $1
}

usage() {
## -l: volume-group of snapshots
## -n ...: virtual host
## -o ...: backup-directory
## -e ...: gpg-key (to enable encryption)
## -z: (gzip) compression
## -j: (bzip) compression
## -J: (xz) compression
## -Z: compress
  error "$0 [-zh]  [-e <gpgkey>] [-A] -o <backupdir> -n <vname> [<vdisk1> ...]"
  error "	backup virtual machine named '<vname>' including it's disk(image)s <vdisk*>"
  error ""
  error "	-n <vname>	: name of the virtual machine to backup (MANDATORY)"
  error "	-o <backupdir>  : output directory (MANDATORY)"
  error "	-V              : add VHOST-name to output directory"
  error "	-A              : automatic mode (try guessing vhost configuration)"
  error "	-T              : add timestamp to output filenames"
  error "	-e <pgpkey>     : PGP-key to encrypt data with (e.g.: CF87837A)"
  error "	-v		: verbose printout (multiple times to raise verbosity)"
  error "	-q		: quiet printout (multiple times to lower verbosity)"
  error "	-d		: debug mode ('-d -d' will not create backups)"
  error "	-z		: gzip (if 'gzip' is installed)"
  error "	-Z		: zip/compress (if 'compress' is installed)"
  error "	-j		: bzip2 (if 'bzip2' is installed)"
  error "	-J		: xzip (if 'xz' is installed)"
  error ""
  error "	-h		: print this help"
  exit 1
}


showsettings() {
    error "VHOST      : ${VHOST}"
    error "BACKUPDIR  : ${BACKUPDIR}"
    error "GPGKEY     : ${GPGKEY}"
    error "ZIP        : ${ZIP} ('${ZIPEXTENSION}')"
    error "verbosity  : ${VERBOSE} (unused)"
    error
    error "$@"
    error
    usage
}

while getopts "n:o:l:e:qvzZjJAdTVh" opt; do
 case $opt in
  n)
    VHOST=$OPTARG
    ;;
  o)
    BACKUPDIR="${OPTARG}"
    ;;
  l)
    error "ignoring unused LVgroup option"
    ;;
  e)
    GPGKEY=$OPTARG
    ;;
  q)
    VERBOSE=$(( VERBOSE-1 ))
    ;;
  v)
    VERBOSE=$(( VERBOSE+1 ))
    ;;
  d)
    DEBUGlvl=$(( DEBUGlvl+1 ))
    ;;
  z)
    ZIP=gzip
    ZIPEXTENSION=".gz"
    ;;
  Z)
    ZIP=compress
    ZIPEXTENSION=".Z"
    ;;
  j)
    ZIP=bzip2
    ZIPEXTENSION=".bz2"
    ;;
  J)
    ZIP=xz
    ZIPEXTENSION=".xz"
    ;;
  A)
    AUTOMATIC=yes
    ;;
  V)
    BACKUPDIRVHOST=yes
    ;;
  T)
    TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
    ;;
  h|\?)
     usage
     ;;
  :)
     fatal "Option -${OPTARG} requires an argument"
     ;;
  *)
     error "fallback: $OPTARG"
 esac
done

shift $(( ${OPTIND} - 1 ))
VDISKSpre=$@

virsh_getdisks() {
    virsh dumpxml $1 2>/dev/null \
	| xpath -q -e "//domain/devices/disk/source" 2>/dev/null \
	| grep " dev=\"" \
	| sed -e 's|^.* dev="||' -e 's|".*$||'
}
lvm_filterdisks() {
    ## given a list of block-devices, it will return all devices that are part of an LVgroup
    local d
    for d in $@
    do
	lvs "${d}" >/dev/null 2>/dev/null && echo "$d"
    done
}


## make sure we have sane values
if [  "x${VHOST}" = "x"  ]; then showsettings "VM name missing"; fi

if [ "x${BACKUPDIRVHOST}" = "xyes" ]; then
 # backup to "/.../myvhost/20150808-1237.myvhost-dev_sda.img.gz"
 BACKUPDIR="${BACKUPDIR%/}/${VHOST}"
 TIMESTAMPPREFIX="${TIMESTAMP}."
else
 # backup to "/.../myvhost-dev_sda.img.20150808-1237.img.gz"
 TIMESTAMPSUFFIX=".${TIMESTAMP}"
fi
if [ "x${TIMESTAMP}" = "x" ]; then
 # if we don't have a timestamp, empty out the prefix/suffix...
 TIMESTAMPPREFIX=""
 TIMESTAMPSUFFIX=""
fi


mkdir -p "${BACKUPDIR}"
if [ ! -d "${BACKUPDIR}" ]; then showsettings "non-existant backupdir"; fi

DEBUG=""
if [ ${DEBUGlvl} -gt 0 ]; then DEBUG="yes"; fi
if [ ${DEBUGlvl} -gt 1 ]; then DEBUG="fake"; fi

## automatic mode:
#  try guessing possible disks to snapshot
if [ "xyes" = "x${AUTOMATIC}" ]; then
  if [ "x${VDISKSpre}" = "x" ]; then
      VDISKSpre=$(virsh_getdisks "${VHOST}")
  fi
fi

VDISKS=$(lvm_filterdisks ${VDISKSpre})

#########################################
# helper functions


#########################################
# get size of blockdevice (in bytes)
## (so we can create an appropriately sized snapshot)
get_disksize() {
  local DISK=$1
  if [ -e "${DISK}" ]; then
    /sbin/blockdev --getsize64 "${DISK}" 2>/dev/null || echo 0
  else
    echo 0
  fi
}

get_snapname() {
# $1: original disk (full path to device, e.g. /dev/virthosts/myserver)
# $2: snapname (e.g. 'SNAP-myserver-SNAP')
# returns: full path to snap-device, e.g. /dev/virthosts/SNAP-myserver-SNAP

## simplistic approach: remove file-name from $1 and prepend it to $2
    echo "${1%/*}/$2"
}

#########################################
# check whether a VHOST exists
check_vhost() {
  virsh domstate $1 >/dev/null 2>&1 || fatal "unable to query state of '$1'"
}

#########################################
# GUARD labels by prefixing/suffixing a string

# check whether the guard is present
check_guard() {
   local v=$1
   if [ "${v}" = "${v#SNAP-}" ]; then return 1; fi ## missing leading 'SNAP_'
   if [ "${v}" = "${v%-SNAP}" ]; then return 1; fi ## missing trailing 'SNAP_'
   return 0
}
# guard a label
make_guard() {
   echo "SNAP-$1-SNAP"
}
# remove guard from a label
un_guard() {
  local d=$1
  d=${d#SNAP-}
  d=${d%-SNAP}
  echo $d
}


#########################################
# create a backupfile (possibly zipped and/or encrypted)
do_clone() {
 local INFILE=$1
 local OUTFILE=$2

 local ZIPPER
 local ZEXT
 ZIPPER=$(which ${ZIP})
 if [ "x${ZIPPER}" = "x" ]; then
   error "compressor ${ZIP} not available"
   ZIPPER="cat"
   ZEXT=""
 else
   ZIPPER="$(which ${ZIP}) -c"
   ZEXT=${ZIPEXTENSION}
 fi

 local ENCRYPTOR
 local ENCEXT

 if [ "x${GPGKEY}" = "x" ]; then
   ENCRYPTOR="cat"
 else
  if [ "x$(which gpg)" = "x" ]; then
    error "gpg not available"
    GPGKEY=""
    ENCRYPTOR="cat"
  else
    ENCRYPTOR="$(which gpg) --encrypt --recipient ${GPGKEY} --output -"
    ENCEXT="${GPGEXTENSION}"
  fi
 fi

 if [ -e "${INFILE}" ]; then
   if [ "x${OUTFILE}" != "x" ]; then
       sync
       debug ${ZIPPER} "${INFILE}" | debugPipe ${ENCRYPTOR} | debugPipe output "${OUTFILE}${ZEXT}${ENCEXT}"
   fi
 fi
}

#########################################
# remove LV snapshots
do_snaprelease() {
 local d
 sync
 for d in $@
 do
  ## first check whether the device name has the GUARD-protection
  if check_guard "${d##*/}"; then
    ## check whether this is actually a block-device
    if [ -b "${d}" ]; then
     debug dmsetup remove "${d}"
     debug lvremove -f "${d}"
     sync
    else
     error "${d} does not exist..."
    fi
  else
    error "NOT removing unguarded '${d}'"
  fi
 done
 sync
}

collectsnap() {
  if [ -e "$2" ]; then
   echo "$2" > $1
  fi
}

#########################################
# create a snapshot while suspending/resuming a VM
do_snapshot() {
  local vhost
  local disks
  local dsize
  local suspended
  local d
  local sdisk
  local fullsdisk
  local snapfile
  snapfile=$(tempfile)

  suspended=""
  SNAPSHOTS=""

  vhost=$1
  shift
  disks=$@

echo "suspending VHOST: $vhost"
echo "cloning disks   : $disks"

  ## check whether this is a valid vhost
  check_vhost $vhost

  ## try to suspend the vhost
  ## if this fails, then it was not running and we don't need to resume it later...
  virsh suspend $vhost > /dev/null 2>&1 && suspended=true
  for d in $disks
  do
    dsize=$(get_disksize $d)
    if [ "$dsize" -gt 0 ]; then
      sdisk=$(make_guard ${vhost}-$(echo ${d#/} | sed -e 's|/|_|g'))
      fullsdisk=$(get_snapname ${d} ${sdisk})
      if [  -e "${fullsdisk}" ]; then
	  error "refusing to re-create already existing snaphot at ${fullsdisk}"
      else
	  debug lvcreate --snapshot --name "${sdisk}" --size ${dsize}B "${d}" 1>&2 && collectsnap "${snapfile}" "${fullsdisk}"
      fi
    else
      error "skipping empty/invalid disk '$d'"
    fi
  done

  if [ "x${suspended}" = "xtrue" ]; then
    virsh resume $vhost 1>&2
  fi
  SNAPSHOTS=$(cat ${snapfile})
  rm "${snapfile}"
}

#########################################
# dump the VM config and create backups of it's disks
do_backup() {
## dump the VHOST-config
    debug virsh dumpxml ${VHOST} | debugPipe output "${BACKUPDIR}/${TIMESTAMPPREFIX}${VHOST}${TIMESTAMPSUFFIX}.xml"

## make gzipped/encrypted snapshots of the disks
for d in ${SNAPSHOTS}; do
  do_clone "${d}" "${BACKUPDIR}/${TIMESTAMPPREFIX}$(un_guard ${d##*/})${TIMESTAMPSUFFIX}.img"
done
}

if [ -e "${BACKUPDIR}" ]; then
  if [ -d "${BACKUPDIR}" ]; then
    :
  else
    fatal "target '${BACKUPDIR}' already exists and is not a directory"
  fi
else
  mkdir -p "${BACKUPDIR}" || fatal "unable to create target '${BACKUPDIR}'"
fi

do_snapshot ${VHOST} ${VDISKS}
echo "${CMDLINE}" > "${BACKUPDIR}/${TIMESTAMPPREFIX}${VHOST}${TIMESTAMPSUFFIX}.sh"
do_backup
do_snaprelease ${SNAPSHOTS}
