#!/bin/sh

#
# Configure LVM so that only the PVs belonging to our OpenStack
# infrastructure VGs are visible to host LVM.
#
# Supported VG naming conventions:
#
#   Compute:
#       cl1com2vg0
#
#   Volume:
#       cl1vol1vg0
#
# Everything else is rejected by LVM's filter/global_filter.
#
# IMPORTANT:
#   This script derives the allowed PVs from the VGs that are currently
#   visible BEFORE installing the restrictive filter.
#
#   If an unexpected VG is found, the script aborts rather than risking
#   making a legitimate VG inaccessible.
#
#   Kernel device names like /dev/sda or /dev/nvme0n1 are not stable
#   across reboots: the kernel assigns them depending on the order in
#   which devices are discovered. Whitelisting them could therefore
#   match the wrong disk after a reboot. Instead, each PV is allowed
#   under two stable aliases:
#
#     - the most stable /dev/disk/by-id/ link matching the device
#       (wwn-*, nvme-eui.*, dm-uuid-mpath-*, ...),
#     - /dev/disk/by-id/lvm-pv-uuid-<PV_UUID>, which udev always
#       creates for a PV.
#
#   LVM applies the filter to every alias found while scanning /dev,
#   and a device is visible if ANY of its aliases is accepted, so
#   these patterns are enough to keep the PVs visible, whichever
#   kernel names they get after a reboot.
#

set -eu

LVMCONF=/etc/lvm/lvm.conf

# Remove any restrictive scan directive.
# LVM should perform normal device discovery; filter/global_filter
# provide the actual allow/deny policy.
# Also, OCI was previously setting up: scan = [ "/dev/disk/by-path/" ]
# which has to be removed.
sed -i -E '/^[[:space:]]*scan[[:space:]]*=/d' "$LVMCONF"

###############################################################################
# Configuration
###############################################################################

#
# Valid infrastructure VG names.
#
# Examples:
#
#   cl1com3vg0
#   cl2com45vg1
#   cl3vol1vg0
#   cl4vol25vg0
#
HOST_VG_RE='^[[:alnum:]]+(com|vol|ctrl|msg|sql|sqlmsg)[0-9]+vg[0-9]+$'

###############################################################################
# Sanity checks
###############################################################################

if [ "$(id -u)" != "0" ]; then
	echo "ERROR: this script must be run as root." >&2
	exit 1
fi

if [ ! -f "$LVMCONF" ]; then
	echo "ERROR: $LVMCONF does not exist." >&2
	exit 1
fi

if ! command -v vgs >/dev/null 2>&1; then
	echo "ERROR: vgs command not found." >&2
	exit 1
fi

if ! command -v pvs >/dev/null 2>&1; then
	echo "ERROR: pvs command not found." >&2
	exit 1
fi


###############################################################################
# Find all currently visible VGs
###############################################################################

echo "Checking currently visible VGs..."
ALL_VGS=$(vgs --noheadings --readonly --options vg_name 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' || true)
if [ -z "${ALL_VGS}" ]; then
	echo "ERROR: no VGs are currently visible." >&2
	echo "Refusing to install an LVM filter." >&2
	exit 1
fi


###############################################################################
# Verify that every currently visible VG is one of ours
###############################################################################

HOST_VGS=""

for VG in $ALL_VGS; do
	if ! echo "$VG" | grep -Eq "$HOST_VG_RE"; then
		echo "ERROR: unexpected VG found: $VG" >&2
		echo "" >&2
		echo "Expected VG names to match:" >&2
		echo "  $HOST_VG_RE" >&2
		echo "" >&2
		echo "Refusing to modify $LVMCONF." >&2
		exit 1
	fi
	HOST_VGS="${HOST_VGS}${HOST_VGS:+
}${VG}"
done


# Escape a device path for use inside an LVM regex
escape_lvm_regex() {
	printf '%s' "$1" |
		sed 's/[.[\*^$()+?{|\\]/\\&/g'
}


# Find the most stable alias of a device under /dev/disk/by-id/.
#
# Kernel names (eg: /dev/sda) are not stable across reboots, so the
# filter must whitelist the device under one of its persistent names.
#
# Usage: find_stable_alias <canonical_device_path> [<by_id_dir>]
#
# Prints the best alias found, or nothing if there is none (eg: a
# virtio disk without serial). The caller falls back to the kernel
# name in that case.
#
# Note that lvm-pv-uuid-* links are skipped here: the caller adds the
# corresponding rule itself, from the PV UUID reported by pvs.
find_stable_alias() {
	DEV_PATH="$1"
	BY_ID_DIR="${2:-/dev/disk/by-id}"
	CANDIDATES=""

	# Collect all by-id links resolving to the same device.
	# (If BY_ID_DIR doesn't exist, the glob stays literal and the
	# -e test below skips it, so this is safe.)
	for LINK in "$BY_ID_DIR"/*; do
		[ -e "$LINK" ] || continue
		case "$LINK" in
		*/lvm-pv-uuid-*)	continue ;;
		esac
		if [ "$(readlink -f "$LINK")" = "$DEV_PATH" ]; then
			CANDIDATES="${CANDIDATES}${LINK}
"
		fi
	done

	# Return the first candidate matching the most stable prefix,
	# in order of preference.
	for PREFIX in 'dm-uuid-mpath-' 'wwn-' 'nvme-eui.' 'nvme-nguid.' 'scsi-36' 'scsi-3' 'scsi-' 'ata-' 'md-uuid-' 'nvme-' 'virtio-' 'usb-' 'dm-name-'; do
		for LINK in $CANDIDATES; do
			case "$LINK" in
			*"$PREFIX"*)
				printf '%s' "$LINK"
				return 0
				;;
			esac
		done
	done

	return 0
}


# Build the list of allowed PVs
FILTER_RULES=""

for VG in $HOST_VGS; do
	echo "Finding PVs for VG: $VG"
	PV_LIST=$(pvs --noheadings --readonly --select "vg_name = $VG" --options pv_name 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' || true)

	if [ -z "$PV_LIST" ]; then
		echo "ERROR: VG '$VG' has no visible PVs." >&2
		echo "Refusing to modify $LVMCONF." >&2
		exit 1
	fi

	for PV in $PV_LIST; do
		# Resolve /dev/mapper/foo, symlinks, etc. to the underlying
		# canonical device.
		PV_REAL=$(readlink -f "$PV")
		if [ -z "$PV_REAL" ] || [ ! -e "$PV_REAL" ]; then
			echo "ERROR: cannot resolve PV '$PV'." >&2
			echo "Refusing to modify $LVMCONF." >&2
			exit 1
		fi

		# Prefer a stable alias of the device over its kernel name.
		PV_ALIAS=$(find_stable_alias "$PV_REAL")
		if [ -n "$PV_ALIAS" ]; then
			echo "  allowed PV: $PV -> $PV_ALIAS"
		else
			# No stable alias found (eg: virtio disk without
			# serial): keep the device name, like before.
			echo "  allowed PV: $PV -> $PV_REAL"
			PV_ALIAS="$PV_REAL"
		fi

		ESCAPED=$(escape_lvm_regex "$PV_ALIAS")

		if [ -n "$FILTER_RULES" ]; then
			FILTER_RULES="${FILTER_RULES}, "
		fi
		FILTER_RULES="${FILTER_RULES}\"a|^${ESCAPED}$|\""

		# Also allow the PV under its udev-generated lvm-pv-uuid
		# link, built from the UUID reported by pvs so that the
		# rule doesn't depend on the link existing right now.
		PV_UUID=$(pvs --noheadings --readonly --options pv_uuid "$PV" 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' || true)
		if [ -n "$PV_UUID" ]; then
			ESCAPED_UUID=$(escape_lvm_regex "$PV_UUID")
			FILTER_RULES="${FILTER_RULES}, \"a|^/dev/disk/by-id/lvm-pv-uuid-${ESCAPED_UUID}$|\""
		fi
	done
done

# This shouldn't be there, as it may happen a compute
# has no VG, for example.
#if [ -z "$FILTER_RULES" ]; then
#	echo "ERROR: no allowed PVs were found." >&2
#	echo "Refusing to modify $LVMCONF." >&2
#	exit 1
#fi

###############################################################################
# Construct the final filter
###############################################################################
# Explicitly allow our infrastructure PVs.
# Then reject EVERYTHING else.
# The final r|.*| is extremely important.
if [ -z "$FILTER_RULES" ]; then
	FILTER_VALUE="[ \"r|.*|\" ]"
else
	FILTER_VALUE="[ ${FILTER_RULES}, \"r|.*|\" ]"
fi

echo
echo "LVM filter to install:"
echo
echo "  $FILTER_VALUE"
echo


###############################################################################
# Replace an existing directive in-place
###############################################################################

replace_directive() {
	directive="$1"
	value="$2"

	sed -i -E "s#^([[:space:]]*)${directive}[[:space:]]*=.*\$#\1${directive} = ${value}#" "$LVMCONF"
}


###############################################################################
# Add a missing directive to the devices section
###############################################################################

add_directive(){
	directive="$1"
	value="$2"

	# Prefer the normal "devices {" line.
	if grep -Eq '^[[:space:]]*devices[[:space:]]*\{' "$LVMCONF"; then
		sed -i "/^[[:space:]]*devices[[:space:]]*{/a\\
    ${directive} = ${value}" "$LVMCONF"
		return
	fi

	echo "ERROR: cannot find 'devices {' in $LVMCONF." >&2
	exit 1
}


# Update filter
if grep -Eq '^[[:space:]]*filter[[:space:]]*=' "$LVMCONF"; then
	echo "Updating existing filter directive..."
	replace_directive "filter" "$FILTER_VALUE"
else
	echo "Adding filter directive..."
	add_directive "filter" "$FILTER_VALUE"
fi

# Update global_filter
if grep -Eq '^[[:space:]]*global_filter[[:space:]]*=' "$LVMCONF"; then
	echo "Updating existing global_filter directive..."
	replace_directive "global_filter" "$FILTER_VALUE"
else
	echo "Adding global_filter directive..."
	add_directive "global_filter" "$FILTER_VALUE"
fi


# Display resulting configuration

echo
echo "Installed configuration:"
echo

if command -v lvmconfig >/dev/null 2>&1; then
	echo "filter:"
	lvmconfig --type current devices/filter || true

	echo
	echo "global_filter:"
	lvmconfig --type current devices/global_filter || true
else
	grep -E '^[[:space:]]*(filter|global_filter)[[:space:]]*=' "$LVMCONF" || true
fi


# Verify that LVM still sees our infrastructure VGs
echo
echo "Visible VGs after configuration change:"
echo

vgs --readonly

echo
echo "Done."
echo
echo "NOTE:"
echo "Existing device-mapper mappings are NOT removed by an LVM filter."
echo "If customer vg_* mappings already exist in 'dmsetup ls', they may"
echo "remain until the corresponding device-mapper devices are explicitly"
echo "removed by the component that created them."
