#!/bin/bash
VERSION="3.6 [17 Nov 2021]"
THIS="$(basename "$0")"
COLUMNS=$(stty size 2>/dev/null||echo 80); COLUMNS=${COLUMNS##* }
while getopts ":dfhlw" optname; do
	case "$optname" in
		"d")	DEBUG="y";;
		"f")	FORCE="y";;
		"h")  HELP="y";;
		"l")	CHANGELOG="y";;
		"w")  COLUMNS=30000;; #suppress line-breaking
		"?")  echo "Unknown option $OPTARG"; exit 1;;
		":")  echo "No argument value for option $OPTARG"; exit 1;;
		*)   # Should not occur
         echo "Unknown error while processing options"; exit 1;;
    esac
done
shift $((OPTIND-1))
echo -e "\n$THIS v$VERSION by Dominic (try -h for help)\n${THIS//?/=}\n"
if [[ -n $HELP ]]; then
	HELP="y"
	echo -e "GNU/Linux script for a machine where LVM (Logical Volume \
Management) is used. It removes/deletes an LVM snapshot, and can be called \
by another process or used when a snapshot has been unintentionally left over \
by another process. \
It tries various escalating steps to remove the snapshot; in rare cases the \
removal might not be completed until after a reboot and \
then a rerun of $THIS.

For safety reasons, $THIS will refuse to remove the specified object \
if it is not an LVM snapshot.

It also removes the mountpoint for the snapshot, if one exists.

You can find the name of your snapshot with the command 'sudo lvs'.

Tested under LVM 2.02.176(2), 2.02.133(2), 2.02.98(2), 2.02.95(2), 2.02.66(2).

Options     :
-f - don't ask before proceeding - use with care!
-h - show help and exit

Example     :
    sudo ./$THIS homebackup

Exit Codes  : 0 indicates success or no action was required (e.g. no such snapshot exists)
              1 indicates a problem occurred

Dependencies: awk, basename, bash, fold, grep, lvm, stty, umount

License: Copyright © 2021 Dominic Raferd. Licensed under the Apache License, \
Version 2.0 (the \"License\"); you may not use this file except in compliance \
with the License. You may obtain a copy of the License at \
https://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable \
law or agreed to in writing, software distributed under the License is \
distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY \
KIND, either express or implied. See the License for the specific language \
governing permissions and limitations under the License.
"|fold -sw $COLUMNS
fi
if [[ -n $CHANGELOG ]]; then
	[[ -n $HELP ]] && echo "Changelog:"
	echo "\
3.6 [17 Nov 2021] enable changelog, revise for better compliance with shellcheck, remove support for older LVM versions
3.5 [04 Mar 2021] a couple of bugfixes
3.4 [04 Mar 2019] updated version testing information in help
3.3 [22 Jul 2016] updated code to identify snapshots, add debug mode, add changelog
3.2 [02 Sep 2015] previous version"|fold -sw $COLUMNS
fi
[[ -n $HELP$CHANGELOG ]] && exit 0
[ "$(id -u)" != "0" ] && echo -e "Error: $THIS must be run as root\n">&2 && exit 1
SNAPSHOTNAME=$1
if [ "$(echo "$SNAPSHOTNAME"|awk -F/ '{print NF}')" != "1" ]; then
	echo "Invalid name '$SNAPSHOTNAME': should not contain any slashes. Aborting...">&2
	exit 1
fi

# find the LV devicepath and check it is a unique snapshot

# return LVMVERSION as 3 numeric values e.g. 2 2 133
LVMVER=( $(pvs --version|sed -n '/LVM version/p'|awk -F"[.()]" '{print (substr($1,length($1))+0),($2+0),($3+0)}') )
[[ -n $DEBUG ]] && echo -e "Debug mode\nLVMVER: '${LVMVER[*]}'"
if [[ ${LVMVER[0]} -gt 2 ]] || [[ ${LVMVER[0]} -eq 2 && ${LVMVER[1]} -gt 2 ]] || [[ ${LVMVER[0]} -eq 2 && ${LVMVER[1]} -eq 2 && ${LVMVER[2]} -ge 95 ]]; then
	# this approach [1] is more elegant but does not work for LVM 2.02.66(2) (2010-05-20)
	# because lv_path is not supported in lvs - it does work for 2.02.95(2) (2012-03-06)
	DATA=( $(lvs -o lv_attr,lv_path -S "lv_name=\"$SNAPSHOTNAME\"&&lv_attr=~'^s.*'" --noheadings) )
	[[ -n $DEBUG ]] && echo -e "devicepath discovery mode 1\nDATA: '${DATA[*]}'"
	[[ ${DATA[0]} =~ [Ss][-A-Za-z]{9} ]] || { echo "Unable to find '$SNAPSHOTNAME' as a snapshot logical volume, no action required"; exit 0; }
	[[ ${#DATA[@]} -ne 2 ]] && { echo "Found more than one '$SNAPSHOTNAME' as snapshot logical volumes, aborting...">&2; exit 1; }
	DEVICEPATH="${DATA[1]}"
else
	echo "Sorry, $THIS does not work with this old version of LVM (${LVMVER[*]})..." >&2; exit 1
	## this approach [2] works for earlier versions of LVM
	## but because it uses lvdisplay is more likely to break in later versions of LVM
	#DEVICEPATH=($(lvdisplay 2>/dev/null|grep -E "^  LV (Name|Path).*/$SNAPSHOTNAME$"|awk '{print $3}'))
	#[[ -n $DEBUG ]] && echo -e "devicepath discovery mode 2\nDEVICEPATH: '${DEVICEPATH[*]}'"
	#[[ -n ${DEVICEPATH[1]} ]] && { echo "Found more than one '$SNAPSHOTNAME' as snapshot logical volumes, aborting...">&2; exit 1; }
	#[[ -z ${DEVICEPATH[*]} ]] && { echo "'$SNAPSHOTNAME' does not exist as a logical volume, no action required"; exit 0; }
	#ISSNAPSHOT="$(lvs 2>/dev/null|grep "^  $SNAPSHOTNAME "|awk '{print substr($3,1,1)}')"
	#[[ $ISSNAPSHOT != "s" && $ISSNAPSHOT != "S" ]] && { echo -e "'$SNAPSHOTNAME' found as LV '${DEVICEPATH[*]}', but it is not a snapshot\nPlease check with 'sudo lvs'\nAborting...">&2; exit 1; }
fi

# LV path should be something like /dev/myvg/mylv
[ "$(echo "$DEVICEPATH"|awk -F/ '{print NF}')" != "4" ] && { echo "An unknown error occurred, aborting..." >&2; exit 1; }
echo "Identified '$SNAPSHOTNAME' as LVM snapshot logical volume '$DEVICEPATH'"

if [ -z "$FORCE" ]; then
	read -rp "About to delete '$SNAPSHOTNAME', are you sure (y/-)? " -t 20
	[[ "$REPLY" != "y" && "$REPLY" != "Y" ]] && { echo "Aborting...">&2; exit 1; }
fi
lvremove -f "$DEVICEPATH" 2>/dev/null && { echo "Successfully removed '$SNAPSHOTNAME' using lvremove -f"; [[ -d /mnt/$SNAPSHOTNAME ]] && rm -rf "/mnt/$SNAPSHOTNAME"; exit 0; }
echo -e "Unable to do lvremove -f at first attempt, will try to umount"
umount "$DEVICEPATH" || umount "$DEVICEPATH" -l
ERR=$?
if [ $ERR -eq 0 ]; then
	echo "Successfully umounted '$SNAPSHOTNAME'"
else
	echo "Umount failed with error $ERR"
fi
if grep -qF "/$SNAPSHOTNAME " /etc/mtab; then
	echo -e "Unknown error: unable to umount $DEVICEPATH - $SNAPSHOTNAME is listed in mtab\nAborting..." >&2
	exit 1
fi
[[ -d /mnt/$SNAPSHOTNAME ]] && rm -rf "/mnt/$SNAPSHOTNAME"
echo -en "lvremove -f [2nd attempt]: "
lvremove -f "$DEVICEPATH" 2>/dev/null && echo -e "All seems OK" && exit 0
VGNAME="$(echo "$DEVICEPATH"|awk -F/ '{print $3}')"
echo -en "FAIL\nTrying with dmsetup\ndmsetup remove -f ${VGNAME//-/--}-$SNAPSHOTNAME: "
dmsetup remove -f "${VGNAME//-/--}-$SNAPSHOTNAME" && echo -e "OK\nAll seems OK" && exit 0
echo -en "FAIL\nlvchange -an with lvremove -f: "
lvchange -an "$DEVICEPATH" && lvremove -f "$DEVICEPATH" 2>/dev/null && echo -e "All seems OK" && exit 0
echo "FAIL"
echo "Unable to remove '$SNAPSHOTNAME', please reboot then try again.">&2
exit 1
