#!/bin/bash
#
# prl_backup - utility for managing attached backups
#
# (c) 2014-2016. Parallels IP Holdings GmbH. All rights reserved.

KPARTX=kpartx
DMSETUP=dmsetup
LSBLK=lsblk
BLOCKDEV=blockdev
UDEVADM=udevadm
GETOPT=getopt
BASENAME=basename
MOUNT=mount
UMOUNT=umount

SERIAL_PREFIX="__bckp_"
OFFSET=1024
SCRIPTNAME=$("$BASENAME" $0)

msg() {
	echo "$@" >&2
}

die() {
	code=$1
	shift
	msg "$@"
	exit $code
}

usage() {
	msg "Usage: $SCRIPTNAME list [options]"
	msg "       $SCRIPTNAME enable /path/to/device"
	msg "       $SCRIPTNAME disable /path/to/device"
	msg "Options for the 'list' command"
	msg "       -e|--enabled  - Show only enabled attached backups"
	msg "       -d|--disabled - Show only disabled attached backups"
	msg "       -r|--raw      - Raw output, useful for scripting"
	exit 1
}

get_serial() {
	val="$($UDEVADM info --query=property -n $1 2>/dev/null | grep 'ID_SERIAL_SHORT=')"
	echo ${val##ID_SERIAL_SHORT=}
}

match_serial() {
	[[ "$1" =~ ^${SERIAL_PREFIX}.* ]] && return 1
	return 0
}

unmangle_serial() {
	echo ${1/"$SERIAL_PREFIX"/"backup"}
}

list() {
	local opt_raw=0
	local opt_enabled=0
	local opt_disabled=0
	local opts=$("$GETOPT" -o edr -l enabled,disabled,raw -n "$SCRIPTNAME" -- "$@")
	[ $? -ne 0 ] && usage
	eval set -- "$opts"
	while true
	do
		case $1 in
			-e|--enabled)
				opt_enabled=1
				shift
				;;
			-d|--disabled)
				opt_disabled=1
				shift
				;;
			-r|--raw)
				opt_raw=1
				shift
				;;
			--)
				shift; break
				;;
			*)
				usage
				;;
		esac
	done

	if [ $opt_enabled -eq 0 -a $opt_disabled -eq 0 ]; then
		opt_enabled=1
		opt_disabled=1
	fi

	local -A enabled disabled

	
	for dev in $("$LSBLK" --nodeps --noheadings --list --output NAME 2>/dev/null); do
		local serial=$(get_serial "$dev")
		match_serial "$serial"
		[ $? -ne 0 ] || continue
		serial=$(unmangle_serial "$serial")
		local node="/dev/mapper/$serial"
		local device="/dev/$dev"
		# check that dmsetup was properly executed
		if [ ! -e "$node" ]; then
			disabled[$device]=""
			continue
		fi
		enabled[$device]="$node"
	done
	
	if [ ${#enabled[@]} -eq 0 -a ${#disabled[@]} -eq 0 ]; then
		msg "Attached backups are not detected"
		exit 0
	fi
	
	local lvm=0
	local i text

	if [ ${#enabled[@]} -ne 0 -a $opt_enabled -eq 1 ]; then
		[ $opt_raw -eq 1 ] || echo -e "List of enabled attached backups:\n"
		i=1
		for dev in ${!enabled[@]}; do
			if [ $opt_raw -eq 1 ]; then
				text="$dev ${enabled[$dev]}"
				[ $opt_disabled -eq 1 ] && text="enabled $text"
				echo "$text"
			else
				echo "[$((i++))] $dev (${enabled[$dev]})"
				text="$($LSBLK --output NAME,TYPE,SIZE,FSTYPE,UUID,MOUNTPOINT ${enabled[$dev]} 2>/dev/null)"
				echo "$text" | grep -q LVM2_member
				[ $? -eq 0 ] && lvm=1
				echo -e "$text\n"
			fi
		done
	fi

	if [ ${#disabled[@]} -ne 0 -a $opt_disabled -eq 1 ]; then
		[ $opt_raw -eq 1 ] || echo -e "List of disabled attached backups:\n"
		i=1
		for dev in ${!disabled[@]}; do
			text="$dev"
			if [ $opt_raw -eq 1 ]; then
				[ $opt_enabled -eq 1 ] && text="disabled $text"
				echo "$text"
			else
				echo "[$((i++))] $text"
			fi
		done
		[ $opt_raw -eq 1 ] || echo ""
	fi

	if [ $opt_raw -eq 0 ]; then
		[ $lvm -eq 1 ] && msg "NOTE: before activating a LVM volume from the attached backup, consider changing its VG name using vgimportclone utility"
	fi
}

check_block_dev() {
	[ "$1" ] || die 1 "Device is not specified"
	[ -b "$1" ] || die 2 "$1 is not a block device"
}

fake_udev_rule() {
	$MOUNT --bind /dev/null "$1" || return 1
	$UDEVADM control --reload &>/dev/null
	return 0
}

unfake_udev_rule() {
	$UMOUNT "$1"
	if [ $? -ne 0 ]; then
		msg "Unable to $UMOUNT $1. Please try to unmount it manually"
		return
	fi
	$UDEVADM control --reload &>/dev/null
}

enable_one() {
	local dev="$1"
	
	check_block_dev "$dev"
	local serial=$(get_serial "$dev")
	match_serial "$serial"
	[ $? -ne 0 ] || die 3 "$dev is not an attached backup"
	serial=$(unmangle_serial "$serial")

	local name="$serial"
	local mname="/dev/mapper/$name"
	local rule="/lib/udev/rules.d/69-dm-lvm-metad.rules"
	
	"$DMSETUP" status $name &>/dev/null
	[ $? -ne 0 ] || die 4 "Attached backup $dev is already enabled"

	local size=$("$BLOCKDEV" --getsz "$dev" 2>/dev/null)
	[ $? -ne 0 -o -z "$size" ] && die 5 "Can not get the size of block device $dev"
	[ $size -le $(($OFFSET * 2)) ] && die 6 "Wrong disk size '$size' obtained from block device $dev"
		
	"$DMSETUP" create $name --table "0 $(($size - $OFFSET * 2)) linear $dev $OFFSET" &>/dev/null
	[ $? -eq 0 ] || die 7 "dmsetup failed to create a mapping for $dev"
	
	# Execution of /lib/udev/rules.d/69-dm-lvm-metad.rules will result in starting pvscan service 
	# via systemd (e.g. in Fedora 23). pvscan will scan available disks for physical volumes and 
	# detect duplicates since the backup LVM PV has the same UUID. After that system may start using backup
	# LVM instead of the original disks. For more information see #PSBM-42980.
	fake_udev_rule $rule && unfake=1

	"$KPARTX" -a -p p -s "$mname" &>/dev/null
	if [ $? -eq 0 ]; then
		$UDEVADM settle --timeout 10 &>/dev/null
	else
		msg "$KPARTX failed to parse the partition table on $mname"
	fi

	if [ $unfake -eq 1 ]; then
		 unfake_udev_rule $rule
	fi
}

disable_one() {
	local dev="$1"
	
	check_block_dev "$dev"
	local serial=$(get_serial "$dev")
	match_serial "$serial"
	[ $? -ne 0 ] || die 3 "$dev is not an attached backup"
	serial=$(unmangle_serial "$serial")

	local name="$serial"
	local mname="/dev/mapper/$name"

	"$DMSETUP" status "$name" &>/dev/null
	[ $? -eq 0 ] || die 4 "Attached backup $dev is not enabled"

	"$KPARTX" -d -p p -s "$mname" &>/dev/null
	[ $? -eq 0 ] || msg "Failed to disable partitions for attached backup $dev"

	"$DMSETUP" remove "$name" &>/dev/null
	[ $? -eq 0 ] || die 5 "Failed to remove mapping for attached backup $dev"
}

[ $# -ge 1 ] || usage

proceed=1
for exe in "$KPARTX" "$DMSETUP" "$LSBLK" "$BLOCKDEV" "$UDEVADM"; do
	path=$(which "$exe" 2>/dev/null)
	if [ $? -ne 0 -o -z "$path" ]; then
		msg "$exe utility is not found"
		proceed=0
	fi
done

[ $proceed -ne 1 ] && die 1 "Could not proceed because of unsatisfied dependencies"

case $1 in
	list)
		shift
		list "$@"
		;;
	enable)
		enable_one "$2"
		;;
	disable)
		disable_one "$2"
		;;
	*)
		usage
		;;
esac

exit 0
