#!/bin/sh
set -e

# /usr/share/bilibop/physical_volumes_filter
# Get|set the 'filter' (or 'global_filter') and 'obtain_device_list_from_udev'
# variables in lvm.conf(5).

### BEGIN ###

PROG="${0##*/}"
SOPTS="a:bDd:f:ghilnor:sux:"
LOPTS="accept:,blank,default,delimiter:,exclude:,file:,global,help,init,list-pv,noglobal,overwrite,reject:,show,udev"

# Check if 'lvm' is installed; if not, exit unconditionally.
have_lvm="1"
for dir in /sbin /usr/sbin /bin /usr/bin; do
	if [ -x "${dir}/lvm" ]; then
		have_lvm="$?"
		break
	fi
done

if [ "${have_lvm}" = "1" ]; then
	echo "${PROG}: lvm command not found." >&2
	exit 11
fi
unset have_lvm

# Initialize other variables and load bilibop functions
LVM_CONF="${LVM_SYSTEM_DIR:=/etc/lvm}/lvm.conf"

blank="false"       # --blank
default="false"     # --default
init="false"        # --init
listpv="false"      # --list-pv
overwrite="false"   # --overwrite
show="false"        # --show
udev="false"        # --udev
accept_all="false"  # --accept all
filter="false"      # --accept, --reject, --exclude
global="false"      # --global
noglobal="false"    # --noglobal
B="|"; E="${B}"     # --delimiter

exit_code=""        # for -o with unwritable file
reject_path=""      # not empty if /dev/disk/by-path is already rejected
reject_block=""     # not empty if /dev/block is already rejected
FILTER_STRING=""    # final result of all -a, -r or -x options
ALREADY_DONE=""     # list of already treated devices

. /usr/lib/bilibop/pvfilter.sh

# short_usage() ============================================================={{{
# Print short help in case of error.
short_usage() {
	cat <<EOF
Usage:
    ${PROG} -h|--help
    ${PROG} [OPTIONS [ARGUMENTS]]
EOF
}
# ===========================================================================}}}
# usage() ==================================================================={{{
# Display options summary when asked with -h or --help option.
usage() {
	cat <<EOF
${PROG}: get/set the filter of Physical Volumes in LVM2 configuration file.
See also lvm.conf(5) for some details.

Usage:
  ${PROG} [--file FILE]
  ${PROG} --help
  ${PROG} --init [--file FILE] [--global|--noglobal]
  ${PROG} --default [--show] [--file FILE]
  ${PROG} --list-pv [--show] [--udev]
  ${PROG} [--blank] [--file FILE] [--noglobal] [--overwrite] [--show] [--udev] [--delimiter ?] [--accept ARG] [--reject ARG] [--exclude ARG]

Options:
  -a ARG, --accept=ARG
  -r ARG, --reject=ARG
        Accept or reject device(s). Each -a or -r option can be invoked
        several times, and the filter will be build in the same order
        than the commandline options. The resulting patterns will be
        prepended to the existing rules. ARG can be:
        all         all Physical Volumes
        insidev     only PV on internal disks
        bilibop     only PV on the same disk than the root filesystem
        other       only PV on other disks
        <DEVICE>    a specific block device
        verbatim=*  an arbitrary string

  -b, --blank
        Start with a blank 'filter' before to apply --accept and
        --reject options.

  -D, --default
        Reset filter to its default value (i.e. accept all).

  -d ?, --delimiter ?
        Set the delimiter to use at the beginning and end of a 'accept'
        or 'reject' pattern. Can be invoked several times, one before
        each -a or -r option. Valid delimiters can be:
        "!", "#", "%", "+", ",", ".", ":", ";", "=", "@", "|", "{}",
        "[]" and "()". Most of them must be protected to not be
        interpreted by the shell.

  -f FILE, --file FILE
        Use FILE as an alternative LVM configuration file.

  -g, --global
        Set the 'global_filter' instead of 'filter'. If this variable is
        not supported (i.e. lvm2 < 2.02.98), this option is silently
        ignored. Note that this variable is not set by default.
        If 'global_filter' is supported and if it is set (even with an
        empty array), then --global is implicit. See also --noglobal.

  -h, --help
        Print this message on stdout and exit.

  -i, --init
        Initialize lvm.conf if one of the file itself, its 'devices'
        section, or the 'filter' or 'obtain_device_list_from_udev'
        variables are missing. Filter is then set to its default value
        (i.e. [ "a|.*|" ] for 'filter', [ ] for 'global_filter').

  -l, --list-pv
        List block devices with LVM2_member fstype and exit.

  -n, --noglobal
        Do not modify 'global_filter', even if it is already set. This
        overrides the implicit behaviour of the --global option, and
        forces the command to be applied to the 'filter' variable. See
        also --global.

  -o, --overwrite
        Overwrite the configuration file (lvm.conf).

  -s, --show
        Show the filter rules in use or built with other options. Used
        with --list-pv, show all symlinks found in /dev for each listed
        PV.

  -u, --udev
        Set the 'obtain_device_list_from_udev' variable to 1 and obtain
        device list from udev. When used with --list-pv, override the
        -s option and show only the symlinks managed by udev.

  -x ARG, --exclude ARG
        Same as --reject, except that all symlinks to devices to reject
        are rejected. ARG can be:
        insidev     only PV on internal disks
        bilibop     only PV on the same disk than the root filesystem
        other       only PV on other disks
        <DEVICE>    a specific block device
EOF
}
# ===========================================================================}}}

### Parse options ##############################################################
if ARGS="$(getopt -o ${SOPTS} --long ${LOPTS} -n ${PROG} -- "${@}")"; then
	eval set -- "${ARGS}"
else
	short_usage >&2
	exit 1
fi
################################################################################

# Parse options ============================================================={{{
while true; do
	case "${1}" in
		-h|--help)
			usage
			exit 0
			;;
		-i|--init)
			init="true"
			shift
			;;
		-g|--global)
			global="true"
			noglobal="false"
			shift
			;;
		-n|--noglobal)
			noglobal="true"
			global="false"
			shift
			;;
		-f|--file)
			LVM_CONF="${2}"
			shift 2
			;;
		-s|--show)
			show="true"
			shift
			;;
		-u|--udev)
			udev="true"
			shift
			;;
		-b|--blank)
			blank="true"
			shift
			;;
		-o|--overwrite)
			overwrite="true"
			shift
			;;
		-l|--list-pv)
			listpv="true"
			shift
			;;
		-D|--default)
			default="true"
			shift
			;;
		-d|--delimiter)
			# This option will be parsed again later
			shift 2
			;;
		-a|--accept)
			# This option will be parsed again later, but we have to know in
			# advance if the filter will contain something like "a|.*|". In
			# that case, the list of devices to reject must contain all the
			# symlinks to the rejected devices.
			if [ "${2}" = "all" ]; then
				accept_all="true"
			fi
			shift 2
			;;
		-r|--reject)
			# This option will be parsed again later
			shift 2
			;;
		-x|--exclude)
			# This option will be parsed again later
			shift 2
			;;
		--)
			shift
			break
			;;
		*)
			unknown_argument "${1}" >&2
			short_usage >&2
			exit 1
			;;
	esac
done

# Options will be parsed again:
eval set -- "${ARGS}"

# Fix the behaviour of the --global/--noglobal options: if 'global_filter' is
# supported and is set, the command will be applied to it, unless --noglobal
# is invoked; if 'global_filter' is supported but not set, the command will
# be applied to 'filter', unless --global is invoked (with --init); and if
# 'global_filter' is not supported, --global/--noglobal are silently ignored.
if _pvfilter_has_global; then
	global_filter_is_supported="true"
	if grep -qs '^[[:blank:]]*global_filter[[:blank:]]*=[[:blank:]]*\[.*\]' ${LVM_CONF}; then
		[ "${noglobal}" = "true" ] || global="true"
	fi
else
	global_filter_is_supported="false"
	global="false"
fi
# ===========================================================================}}}

#################################################################
# At first, treat options that override, bypass or reset others #
#################################################################

# 0. If no option is invoked, or only -f FILE or --file FILE, just
#    display the actual settings (if possible) and exit:
if [ "${1}" = "--" ] ||
	[ "${3}" = "--" -a "${1}" = "-f" ] ||
	[ "${3}" = "--" -a "${1}" = "--file" ]; then
	not_found=0
	grep -qs '^[[:blank:]]*devices[[:blank:]]{' ${LVM_CONF} || not_found="$?"
	case "${not_found}" in
		0)
			grep '^[[:blank:]]*obtain_device_list_from_udev[[:blank:]]*=' ${LVM_CONF} ||
			not_found=$((not_found+$?))

			if [ "${global_filter_is_supported}" = "true" ]; then
				grep '^[[:blank:]]*\(global_\)\?filter[[:blank:]]*=' ${LVM_CONF} ||
				not_found=$((not_found+$?))
			else
				grep '^[[:blank:]]*filter[[:blank:]]*=' ${LVM_CONF} ||
				not_found=$((not_found+$?))
			fi

			[ "${not_found}" != "0" ] &&
				echo "${PROG}: a needed variable is not defined in ${LVM_CONF}" >&2 &&
				echo "Use '--init' option to create it." >&2
			exit ${not_found}
			;;
		1)
			echo "${PROG}: 'devices' section is missing in ${LVM_CONF}." >&2
			echo "Use '--init' option to create it." >&2
			exit 10
			;;
		2)
			echo "${PROG}: ${LVM_CONF} does not exist." >&2
			echo "Use '--init' option to create it." >&2
			exit 10
			;;
	esac
	exit $?
fi

# 1. -l, --list-pv
#    List LVM2 members - even those rejected by the filter - and exit.
if [ "${listpv}" = "true" ]; then
	_pvfilter_list_pv
	exit $?
fi

# 2. -i, --init
#    Check lvm.conf consistency (relatively to its 'devices' section and/or
#    'obtain_device_list_from_udev' and 'filter' variables); add missing
#    file, section and/or variables if asked by --init, and exit. If --init
#    is not invoked but something is missing, exit too.
if [ "${init}" = "true" ]; then
	if [ -f "${LVM_CONF}" ]; then
		_pvfilter_init_device_filters || exit $?
	else
		_pvfilter_init_lvm_configfile || exit $?
	fi
	exit 0
else
	_pvfilter_init_lvm_configfile || exit $?
	_pvfilter_init_device_filters || exit $?
fi

# 3. -d, --default
#    Reset -b, -u, -o, -a and -r options.
if [ "${default}" = "true" ]; then
	echo "${PROG}: ${LVM_CONF} reset to default filter (accept all)." >&2
	udev="true"
	blank="true"
	overwrite="true"
	ARGS="--accept all --"
	eval set -- "${ARGS}"
fi

# 4. Some options cannot be invoked by unprivileged users:
if [ ! -r "${LVM_CONF}" ]; then
	echo "${PROG}: ${LVM_CONF} is not readable." >&2
	exit 10

elif [ ! -w "${LVM_CONF}" ]; then
	if [ "${overwrite}" = "true" ]; then
		overwrite="false"
		show="true"
		exit_code="11"
		exec 1>&2
		echo "${PROG}: you don't have write permissions on ${LVM_CONF}"
		echo "THIS IS A DRY RUN:"
	fi
fi

# Build the strings to filter ==============================================={{{
# For that, we parse options and arguments again:
while true; do
	case "${1}" in
		--)
			shift
			break
			;;
		-d|--delimiter)
			_pvfilter_delimiter "${2}"
			shift 2
			;;
		-a|--accept)
			case "${2}" in
				all)
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }\"a${B}.*${E}\""
					filter="true"
					blank="true"
					break
					;;
				verbatim=*)
					verbatim="${2#verbatim=}"
					[ -n "${verbatim}" ] &&
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }\"a${B}${verbatim}${E}\""
					;;
				bilibop|insidev|other|/dev/*)
					_pvfilter_list_filter_devices "${2}"
					[ -n "${diskid}" ] &&
					diskid="$(_pvfilter_accept_string "" ${diskid})" &&
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${diskid:+\"${diskid}\"}"
					[ -n "${dmname}" ] &&
					dmname="$(_pvfilter_accept_string /dev/mapper ${dmname})" &&
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${dmname:+\"${dmname}\"}"
					[ -n "${lvname}" ] &&
					lvname="$(_pvfilter_accept_string "" ${lvname})" &&
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${lvname:+\"${lvname}\"}"
					;;
				*)
					unknown_argument "${2}" >&2
					short_usage >&2
					exit 1
					;;
			esac
			filter="true"
			shift 2
			;;
		-r|--reject)
			case "${2}" in
				all)
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }\"r${B}.*${E}\""
					filter="true"
					blank="true"
					break
					;;
				verbatim=*)
					verbatim="${2#verbatim=}"
					[ -n "${verbatim}" ] &&
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }\"r${B}${verbatim}${E}\""
					;;
				bilibop|insidev|other|/dev/*)
					if [ "${accept_all}" = "true" ]; then
						_pvfilter_list_exclude_devices "${2}"
						[ -n "${devlist}" ] &&
						devlist="$(_pvfilter_reject_string "" ${devlist} -f)" &&
						FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${devlist:+\"${devlist}\"}"
						[ -n "${dirlist}" ] &&
						dirlist="$(_pvfilter_reject_string /dev ${dirlist} -d)" &&
						FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${dirlist:+\"${dirlist}\"}"
					else
						_pvfilter_list_filter_devices "${2}"
						[ -n "${diskid}" ] &&
						diskid="$(_pvfilter_reject_string "" ${diskid})" &&
						FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${diskid:+\"${diskid}\"}"
						[ -n "${dmname}" ] &&
						dmname="$(_pvfilter_reject_string /dev/mapper ${dmname})" &&
						FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${dmname:+\"${dmname}\"}"
						[ -n "${lvname}" ] &&
						lvname="$(_pvfilter_accept_string "" ${lvname})" &&
						FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${lvname:+\"${lvname}\"}"
					fi
					;;
				*)
					unknown_argument "${2}" >&2
					short_usage >&2
					exit 1
					;;
			esac
			filter="true"
			shift 2
			;;
		-x|--exclude)
			case "${2}" in
				bilibop|insidev|other|/dev/*)
					_pvfilter_list_exclude_devices "${2}"
					[ -n "${devlist}" ] &&
					devlist="$(_pvfilter_reject_string "" ${devlist} -f)" &&
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${devlist:+\"${devlist}\"}"
					[ -n "${dirlist}" ] &&
					dirlist="$(_pvfilter_reject_string /dev ${dirlist} -d)" &&
					FILTER_STRING="${FILTER_STRING:+${FILTER_STRING}, }${dirlist:+\"${dirlist}\"}"
					;;
				*)
					unknown_argument "${2}" >&2
					short_usage >&2
					exit 1
					;;
			esac
			filter="true"
			shift 2
			;;
		*)
			shift
			;;
	esac
done
# ===========================================================================}}}

# Run now!
LVM_TEMP="$(mktemp /tmp/lvm.conf.XXXXXXX)"
trap "rm -f ${LVM_TEMP}" 0 2 3 6 9 15

[ "${udev}" = "true" ] && u=1 || u=0
sed "s,^\(\s*obtain_device_list_from_udev\s*=\s*\).*,\1${u}," ${LVM_CONF} >${LVM_TEMP}

if [ "${global}" = "true" -a "${blank}" = "true" ]; then
	sed -i "s?^\(\s*global_filter\s*=\s*\[\).*]?\1${FILTER_STRING:+ ${FILTER_STRING}} ]?" ${LVM_TEMP}
elif [ "${global}" = "true" ]; then
	sed -i "s?^\(\s*global_filter\s*=\s*\[\)?\1${FILTER_STRING:+ ${FILTER_STRING}, }?" ${LVM_TEMP}
elif [ "${blank}" = "true" ]; then
	sed -i "s?^\(\s*filter\s*=\s*\[\).*]?\1${FILTER_STRING:+ ${FILTER_STRING}} ]?" ${LVM_TEMP}
else
	sed -i "s?^\(\s*filter\s*=\s*\[\)?\1${FILTER_STRING:+ ${FILTER_STRING}, }?" ${LVM_TEMP}
fi

# Overwrite the file:
if [ "${overwrite}" = "true" ]; then
	if ! diff -q ${LVM_CONF} ${LVM_TEMP} >/dev/null; then
		cp ${LVM_CONF} ${LVM_CONF}.bak
		cat ${LVM_TEMP} >${LVM_CONF}
	fi

	if [ "${show}" = "true" ]; then
		if [ "${global_filter_is_supported}" = "true" ]; then
			grep '^[[:blank:]]*\(\(global_\)\?filter\|obtain_device_list_from_udev\)[[:blank:]]*=' ${LVM_CONF}
		else
			grep '^[[:blank:]]*\(filter\|obtain_device_list_from_udev\)[[:blank:]]*=' ${LVM_CONF}
		fi
	fi

# Show only (--show is implicit when --overwrite is not invoked):
elif [ "${global_filter_is_supported}" = "true" ]; then
	grep '^[[:blank:]]*\(\(global_\)\?filter\|obtain_device_list_from_udev\)[[:blank:]]*=' ${LVM_TEMP}
else
	grep '^[[:blank:]]*\(filter\|obtain_device_list_from_udev\)[[:blank:]]*=' ${LVM_TEMP}
fi

exit ${exit_code}

### END ###
# vim: ts=4 sts=4 sw=4
