#! /bin/sh
#
# Copyright © 2012, 2014, 2015, 2017 Richard Kettlewell.
#
# 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/>.
set -e

SNAPS=/var/lib/rsbackup/snapshots
DIVISOR=5

# By default LVM whines about any FDs it doesn't know about
export LVM_SUPPRESS_FD_WARNINGS=1

x() {
  fatal_errors=true
  case "$1" in
  +e )
    shift
    fatal_errors=false
    ;;
  esac

  echo "HOOK: EXEC:" "$@" >&2
  if $fatal_errors; then
    "$@"
  else
    set +e
    "$@"
    status=$?
    set -e
  fi
}

while [ $# -gt 0 ]; do
  case "$1" in
  --help | -h )
    cat <<EOF
Usage:
  rsbackup-snapshot-hook [OPTIONS]

Options:
  --snaps, -s PATH       Snapshot directory path (${SNAPS})
  --divisor, -d DIVISOR  How much smaller snapshot can be (${DIVISOR})
  --help, -h             Display usage message
  --version, -V          Display version string

This script is intended to be run as an rsbackup pre/post-volume-hook,
not interactively.
EOF
    exit 0
    ;;
  -s | --snaps )
    shift
    SNAPS="$1"
    shift
    ;;
  -d | --divisor )
    shift
    DIVISOR="$1"
    shift
    ;;
  -V | --version )
    echo "rsbackup-snapshot-hook 10.0"
    exit 0
    ;;
  * )
    echo "ERROR: unknown option '$1'" >&2
    exit 1
    ;;
  esac
done

# The path where the snapshot will be mounted
snap=$SNAPS/$RSBACKUP_VOLUME

# How to execute commands on the host
case $RSBACKUP_SSH_TARGET in
localhost )
  remote=""
  ;;
* )
  remote="ssh $RSBACKUP_SSH_TARGET"
  ;;
esac

# Only use snapshots if configured to do so
if ${RSBACKUP_ACT:-false} && $remote test -e $snap; then
  # Identify the device name
  devname=$($remote df ${RSBACKUP_VOLUME_PATH}|awk '/^\// { print $1}')
  # Canonicalize the device name to something LVM commands will recognise
  # (in case df gave us something like /dev/dm-0).
  # (dmsetup will barf if $devname isn't an LV)
  dmname=$($remote dmsetup info --noheadings -c -o name "${devname}")
  if [ -z "${dmname}" ]; then
    echo >&2 "ERROR: no info from dmsetup for device ${devname}"
    exit 1
  fi
  # Assume the name returned by dmsetup corresponds to a thing in /dev/mapper
  dev=/dev/mapper/"${dmname}"
  # Pull out components.
  # (whitespace separators are good enough: not permitted in LV/VG names,
  # and if an empty component is followed by a non-empty one, something's
  # gone badly wrong)
  dmsplit=$($remote dmsetup splitname --noheadings --separator ' ' "${dmname}" LVM)
  read -r vg lv lvlayer <<EOF
${dmsplit}
EOF
  if [ -z "${vg}" ] || [ -z "${lv}" ]; then
    echo >&2 "ERROR: failed to parse ${dmname} as LVM name (dmsetup splitname gave \"$dmsplit\")"
    exit 1
  elif [ ! -z "${lvlayer}" ]; then
    # All three components nonempty means we've ended up with an LVM
    # internal layer, which is probably a mistake, which we shouldn't
    # compound by continuing.
    # (An LVM internal layer / sub-LV almost certainly shouldn't have been
    # mounted; LVM invents them when it needs to compose multiple devmapper
    # functionalities, for instance when creating a snapshot, or in some
    # RAID situations -- see e.g. lvconvert(8) and lvmraid(7).)
    echo >&2 "ERROR: cowardly refusing to work with sub-LV (${dmname} has non-empty layer component \"$lvlayer\")"
    exit 1
  fi
  snaplv="${lv}.snap"
  # Predict the snapshot device path. (Use the LVM-created alias rather
  # than /dev/mapper/ so we don't have to contend with hyphen-stuffing.)
  snapdev="/dev/${vg}/${snaplv}"
  case ${RSBACKUP_HOOK} in
  pre-volume-hook )
   # Tidy up any leftovers
   if $remote [ -e $snapdev ]; then
     x $remote umount $snap >&2 || true
     x $remote lvremove --force $snapdev >&2 || true
   fi
   # Find out the size of the source volume
   lvsz=$($remote lvdisplay $dev | awk '/Current LE/ { print $3 }')
   lvname=$($remote lvdisplay $dev | awk '/LV Path/ { print $3 }')
   if [ "$lvname" = "" ]; then
     lvname=$($remote lvdisplay $dev | awk '/LV Name/ { print $3 }')
   fi
   snaplvsz=$(($lvsz / $DIVISOR))
   # Create and mount the snapshot
   x $remote lvcreate --extents $snaplvsz --name $snaplv --snapshot $lvname >&2
   # Snapshots may need fscking before mounting
   # fsck status is a bitmap; 1 means that errors were corrected.
   # All the other nonzero bits are fatal.
   x +e $remote fsck -a $snapdev >&2
   case $status in
   0 | 1 )
     ;;
   * )
     x $remote lvremove --force $snapdev >&2
     exit $status
     ;;
   esac
   x $remote mount -o ro $snapdev $snap >&2
   # Backup from the snapshot, not the master
   echo $snap
   ;;
  post-volume-hook )
   # Tidy up
   x $remote umount $snap >&2
   x $remote lvremove --force $snapdev >&2
   ;;
  esac
fi
