#!/bin/bash
#
# A convenience script for ReadyKernel.
#
# Copyright (c) 2016-2019 Virtuozzo International GmbH
#

if [[ $EUID -ne 0 ]]; then
	echo "The command requires root privileges"
	exit 1
fi

COMMAND=$0
INSTALLDIR=/var/lib/kpatch
PATCHBASENAME=readykernel-patch
KERNELVER=$(uname -r)
PATCHPACKAGE=$PATCHBASENAME-$KERNELVER

BASEURL="${RK_API_BASEURL:-https://readykernel.com}"
APIURL="${BASEURL%/}/api/v1"

CRONDIR=/etc/cron.d

source /etc/os-release
DISTRO=$ID

# utils
KPATCHBIN=/usr/sbin/kpatch-with-retries
if [[ -x /var/lib/rk-rhel/sbin/vzsubscribe ]]; then
	VZSUBSCRIBEBIN=/var/lib/rk-rhel/sbin/vzsubscribe
elif [[ -x /var/lib/rk-rhel/bin/vzsubscribe ]]; then
	VZSUBSCRIBEBIN=/var/lib/rk-rhel/bin/vzsubscribe
elif [[ -x /usr/libexec/vzsubscribe ]]; then
	VZSUBSCRIBEBIN=/usr/libexec/vzsubscribe
elif [[ -x /usr/sbin/vzsubscribe ]]; then
	VZSUBSCRIBEBIN=/usr/sbin/vzsubscribe
else
	VZSUBSCRIBEBIN=/usr/bin/vzsubscribe
fi

YUMBIN=/usr/bin/yum
APTBIN=/usr/bin/apt-get

if [ -e $YUMBIN ] ; then
	SKIP_YUM_PLUGINS=""
	for plug in openvz virtuozzo hci-release; do
		if [ -e /etc/yum/pluginconf.d/${plug}.conf ] ; then
			SKIP_YUM_PLUGINS="$SKIP_YUM_PLUGINS --disableplugin=${plug}"
		fi
	done

	INSTALL_CMD="$YUMBIN ${SKIP_YUM_PLUGINS} install -y -q"
	REMOVE_CMD="$YUMBIN ${SKIP_YUM_PLUGINS} remove -y"
	MODE="yum"
elif [ -e $APTBIN ] ; then
	export DEBIAN_FRONTEND=noninteractive
	INSTALL_CMD="$APTBIN install -y"
	REMOVE_CMD="$APTBIN remove -y"
	MODE="apt"
fi

# Depending on the kernel version and configuration, the information about
# the loaded RK patches can be located in different directories in sysfs.
if [[ -e /sys/kernel/kpatch/patches ]] ; then
	# kpatch < 0.4
	SYSFS="/sys/kernel/kpatch/patches"
elif [[ -e /sys/kernel/kpatch ]] ; then
	# kpatch >= 0.4
	SYSFS="/sys/kernel/kpatch"
else
	# assuming Livepatch is used
	SYSFS="/sys/kernel/livepatch"
fi

REPOQUERYBIN=/usr/bin/repoquery
LOGDIR=/var/log/readykernel
LOGFILE=$LOGDIR/readykernel.log
LOGLASTFILE=$LOGDIR/last.log

ERRORS=()
LOGS=()

export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

has_native_kpatch () {
	# CentOS and RHEL may come with their own 'kpatch' package preinstalled.
	if [[ $DISTRO = rhel ]] || [[ $DISTRO = centos ]]; then
		rpm -q kpatch > /dev/null 2>&1 && return 0
	fi

	# no "native" kpatch
	return 1
}

__find_module () {
	MODULE="$1"
	[[ -f "$MODULE" ]] && return

	MODULE=$INSTALLDIR/$(uname -r)/"$1"
	[[ -f "$MODULE" ]] && return

	return 1
}

mod_name () {
	MODNAME="$(basename $1)"
	MODNAME="${MODNAME%.ko}"
	MODNAME="${MODNAME//-/_}"
}

find_module () {
	arg="$1"
	if [[ "$arg" =~ \.ko ]]; then
		__find_module "$arg" || return 1
		mod_name "$MODULE"
		return
	else
		for i in $INSTALLDIR/$(uname -r)/*; do
			mod_name "$i"
			if [[ $MODNAME == $arg ]]; then
				MODULE="$i"
				return
			fi
		done
	fi

	return 1
}

installed_package_name() {
	local PKG_NAME
	if [[ $MODE == "yum" ]]; then
		PKG_NAME=$(rpm -q --whatprovides "$PATCHPACKAGE" --qf "%{NAME}\n" 2>/dev/null | head -n 1; exit ${PIPESTATUS[0]})
	else
		PKG_NAME=$(apt list --installed "$PATCHPACKAGE" 2>/dev/null | grep "$PATCHPACKAGE" | cut -f1 -d/ | head -n 1; exit ${PIPESTATUS[1]})
	fi

	[[ $? != 0 ]] && return 1

	echo "$PKG_NAME"
}

mod_file()
{
	local PKG_NAME MOD_FILE
	PKG_NAME=$(installed_package_name)

	[[ $? != 0 ]] && return 1

	if [[ $MODE == "yum" ]]; then
		MOD_FILE=$(rpm -ql "$PKG_NAME" 2>/dev/null | grep -m 1 \.ko; exit ${PIPESTATUS[0]})
	else
		MOD_FILE=$(dpkg -L "$PKG_NAME" 2>/dev/null | grep -m 1 \.ko; exit ${PIPESTATUS[0]})
	fi
	[[ $? = 0 ]] && [[ -n "$MOD_FILE" ]] && echo $MOD_FILE
}

check_license() {
	[[ "$($VZSUBSCRIBEBIN info 2>/dev/null)" =~ "system successfully registered" ]]
}

usage_cmd() {
	printf '   %-20s\n      %s\n' "$1" "$2"
}

print_inactive_usage() {
	usage_cmd "initialize [--remove-patches-before-install]" "Initialize ReadyKernel interactively"
	usage_cmd "licinfo" "Show information about the installed license"
	usage_cmd "licload [--accept-eula] <KEY>" "Install the license key <KEY>"
	usage_cmd "licunload" "Remove the license"
	echo
	usage_cmd "report" "Create a report for the technical support team"
	usage_cmd "help" "Show help on command usage"
}

print_usage() {
	echo "Usage: $COMMAND ACTION [OPTIONS]"
	echo "Supported actions are:"
	usage_cmd "info" "Show the current ReadyKernel status"
	usage_cmd "check-update" "Check for a newer ReadyKernel patch for the current kernel"
	usage_cmd "update" "Install the latest ReadyKernel patches"
	usage_cmd "autoupdate [enable|disable] [<HOUR>]" "Enable or disable automatic installation of the latest ReadyKernel patches. If enabled, the service will check for updates daily at the specified hour set in 24-hour format, server time."
	echo
	usage_cmd "load" "Load the latest installed ReadyKernel patch"
	usage_cmd "autoload [enable|disable]" "Enable or disable automatic loading of the latest installed ReadyKernel patches"
	usage_cmd "load-replace" "Unload all the kernel patches (ReadyKernel and other), then load the latest installed ReadyKernel patch"
	usage_cmd "patch-info" "Show information about the loaded ReadyKernel patch"
	usage_cmd "unload" "Unload the ReadyKernel patch"
	echo
	print_inactive_usage
}

print_no_license() {
	print_error "The system is not yet registered. Use '$COMMAND licload <KEY>' to register." >&2
}

usage() {
	print_usage >&2
}

print_newest_patch_name() {
	local PATCHDIR
	PATCHDIR="$SYSFS"
	[[ -d $PATCHDIR ]] || return

	for mod in $PATCHDIR/*; do echo $(basename "$mod"); done \
		| grep kpatch_cumulative | sort -V -r | head -n 1
}

kpatch_list() {
	[[ -x $KPATCHBIN ]] || echo "No patch has been loaded" && return 0
	$KPATCHBIN list
}

kpatch_load() {
	local PATCH
	PATCH=$(print_newest_patch_name)
	if [[ -n "$PATCH" ]]; then
		echo "The patch $PATCH is already loaded"
		return 1
	fi

	PATCH=$(mod_file)
	if [[ -z "$PATCH" ]]; then
		print_error "No patch has been found" >&2
		return 1
	fi

	if [[ -e /snap/bin/canonical-livepatch ]]; then
		print_error "Canonical-livepatch detected, will not load the patch" >&2
		return 1
	fi
	check_executable $KPATCHBIN || return 1
	$KPATCHBIN load "$PATCH"
}

kpatch_unload() {
	local PATCH
	PATCH=$(print_newest_patch_name)
	if [[ -z "$PATCH" ]]; then
		echo "No patch has been loaded"
		return
	fi

	check_executable $KPATCHBIN || return 1
	$KPATCHBIN unload "$PATCH"
}

load_replace() {
	local PATCH
	PATCH=$(mod_file)
	if [[ -z "$PATCH" ]]; then
		print_error "No patch has been found" >&2
		return 1
	fi

	check_executable $KPATCHBIN || return 1
	$KPATCHBIN replace "$PATCH"
}

kpatch_info() {
	local PATCH
	PATCH=$(print_newest_patch_name)
	if [[ -z "$PATCH" ]]; then
		echo No patch has been loaded
		return
	fi

	check_executable $KPATCHBIN || return 1
	$KPATCHBIN info "$PATCH"
}

vzsubscribe_register() {
	"$VZSUBSCRIBEBIN" register "$@"
}

vzsubscribe_unregister() {
	$VZSUBSCRIBEBIN unregister
}

vzsubscribe_info() {
	local l rv=1
	while read l; do
		[[ ! $l =~ "system successfully registered" ]] || rv=0
		printf '%s\n' "$l"
	done < <($VZSUBSCRIBEBIN info)
	(( rv == 0 )) || print_no_license
	return 0  # as per VZP-924
}

clean_repo_yum() {
	[[ -x $REPOQUERYBIN ]] || return

	local PKG_NAME REPO_NAME

	PKG_NAME=$(installed_package_name)
	[[ $? != 0 ]] && return 1

	REPO_NAME=$($REPOQUERYBIN --qf "%{repoid}" "$PKG_NAME" 2>/dev/null)

	[[ $? = 0 ]] && [[ -n "$REPO_NAME" ]] && $YUMBIN clean all --disablerepo="*" \
		--enablerepo="$REPO_NAME" >/dev/null 2>&1
}

clean_repo_apt() {
#	apt-get update -o Dir::Etc::sourcelist="/etc/apt/sources.list.d/readykernel.list" >/dev/null 2>&1
#	apt-get update -o Dir::Etc::sourcelist="/etc/apt/sources.list.d/subscription.list" >/dev/null 2>&1
	apt-get update >/dev/null 2>&1
}

install_package() {
	local retval
	local pkg

	pkg=$1
	[[ -z $(check_provides $pkg) ]] && print_error "No package $pkg available" >&2 && return 1
	log_command $INSTALL_CMD $pkg
	retval=$?
	[[ $retval != 0 ]] && print_error "Failed to install the new package (package manager error)" >&2
	return $retval
}

update_patches() {
	check_license || { print_no_license; return 1; }
	clean_repo_${MODE}

	local RESULT=0
	local CURRENTKERNELVER=$(uname -r)
	local RET=0
	unset SKIP_KERNEL_VER_CHECK
	for KERNELVER in `ls -d /lib/modules/*/ | xargs -n1 basename`; do
		[[ -d "/lib/modules/$KERNELVER/kernel" ]] || continue

		echo "Checking for patch updates for kernel $KERNELVER..."
		if [[ -z "$(check_provides $PATCHBASENAME-$KERNELVER)" ]]; then
			echo "No ReadyKernel patches are available for the kernel $KERNELVER."
			[[ $KERNELVER = $CURRENTKERNELVER ]] && RESULT=1
			echo
			continue
		fi

		install_package $PATCHBASENAME-$KERNELVER
		if [[ $? != 0 ]]; then
			RESULT=1
		fi
		echo
	done
	return $RESULT
}

auto_update()
{
	if [[ -z "$1" ]]; then
		[[ -f $CRONDIR/readykernel ]] && echo "Automatic updating is enabled" && return
		echo "Automatic updating is disabled" && return
	fi

	case "$1" in
	"enable"|"--enable")
		local HOUR

		if [[ -z "$2" ]]; then
			HOUR=12
		elif [[ "$2" =~ ^[0-9]+$ ]] && [ "$2" -ge 0 -a "$2" -le 23 ]; then
			HOUR=$(($2/1))
		else
			print_error "Wrong hour value. Specify an hour value from 0 to 23" >&2 && return 1
		fi

		local MINUTE=$(( $RANDOM % 16 ))
		local CRONTIME="$MINUTE $HOUR * * * root"
		if echo "$CRONTIME $(realpath $COMMAND) update; /bin/cp -f $LOGLASTFILE $LOGDIR/autoupdate.log" > $CRONDIR/readykernel; then
			printf "Automatic updating has been scheduled for %02d:%02d daily (server time)" $HOUR $MINUTE; echo
		else
			print_error "failed to enable automatic updating" >&2
			return 1
		fi
		;;

	"disable"|"--disable")
		if rm -f $CRONDIR/readykernel; then
			echo "Automatic updating has been disabled"
		else
			print_error "Failed to disable automatic updating" >&2
			return 1
		fi
		;;

	*)
		echo "Unknown command: autoupdate $1"
		usage
		return 1
		;;
	esac
}

auto_load() {
	[[ -z "$1" ]] && print_error "Too few arguments" >&2 && usage && return 1

	local MOD_FILE
	MOD_FILE=$(mod_file)

	[[ -z "$MOD_FILE" ]] && echo No patches have been installed && return
	mod_name "$MOD_FILE"

	case "$1" in
	"enable"|"--enable")
		$KPATCHBIN install "$MOD_FILE"
		return $?
		;;

	"disable"|"--disable")
		$KPATCHBIN uninstall "$MODNAME"
		return $?
		;;

	*)
		echo "Unknown command: autoload $1"
		usage
		return 1
		;;
	esac
}

# Usage: check_provides <patch-package-or-provide_name>
check_provides() {
	[[ -z "$1" ]] && return 1

	if [[ $MODE == "yum" ]]; then
		log_command $YUMBIN provides -q $1
	else
		log_command apt-cache search $1
	fi
}

check_update() {
	check_license || { print_no_license; return 1; }
	
	clean_repo_${MODE}
	local PKG_NAME AVAILABLE UPDATES
	PKG_NAME=$(installed_package_name)

	if [[ $? != 0 ]]; then
		# No patch has been installed yet. Nothing to update.
		if [[ $MODE == "yum" ]]; then
			AVAILABLE=$(check_provides $PATCHPACKAGE | \
			grep $PATCHBASENAME | tail -n 2 | head -n 1 | cut -f1 -d ":"; exit ${PIPESTATUS[0]})
		else
			AVAILABLE=$(check_provides $PATCHPACKAGE | cut -f1 -d\  ; exit ${PIPESTATUS[0]})
		fi

		[[ $? != 0 ]] && print_error "Failed to find the package ($MODE error)" >&2 && return 1

		[[ -n $AVAILABLE ]] && echo A new patch is available: && echo "$AVAILABLE"
		return
	fi

	if [[ $MODE == "yum" ]]; then
		UPDATES=$($YUMBIN check-update -q $PKG_NAME)
	else
		UPDATES=$(apt list --upgradable $PKG_NAME 2>/dev/null | grep $PKG_NAME ; exit ${PIPESTATUS[0]})
	fi

	[[ $? = 1 ]] && print_error "Failed to check updates ($MODE error)" >&2 && return 1

	if [[ -z "$UPDATES" ]]; then
		echo No updates are available
	else
		echo A new version is available:
		echo "$UPDATES"
	fi
}

create_report() {
	check_executable /usr/bin/zip || return 1

	local TMPREPORT PATCH ZIPFILE
	TMPREPORT=$(mktemp -d)
	[[ $? != 0 ]] && echo Error: Unable to create tmp directory >&2 && return 1

	(cd $TMPREPORT; $VZSUBSCRIBEBIN diagnostics >/dev/null)
	print_info >$TMPREPORT/readykernel_info.txt 2>&1 
	$KPATCHBIN list >$TMPREPORT/kpatch_list.txt 2>&1 
	cp /var/log/kpatch.log $TMPREPORT/

	cp -r /var/log/readykernel $TMPREPORT >/dev/null 2>&1

	PATCH=$(print_newest_patch_name)
	[[ -n "$PATCH" ]] && $KPATCHBIN info "$PATCH" >$TMPDIR/kpatch_info.txt 2>&1

	ZIPFILE=$(realpath readykernel-report-$(date +%Y-%m-%d-%H-%M-%S))
	(cd $TMPREPORT; /usr/bin/zip -r $ZIPFILE  *)

	echo The report file $ZIPFILE.zip has been created
	rm -rf "$TMPREPORT"
}

initialize_interactive() {
	local LIC_KEY
	local AUTOUPDATE=X
	local REMOVE_KPATCH

	if has_native_kpatch; then
		REMOVE_KPATCH=
		[[ "t$1" == "t--remove-patches-before-install" ]] && REMOVE_KPATCH=y
		echo
		echo "Another live patching system detected: 'kpatch' package is installed. It may be unsafe to use both that system and ReadyKernel at the same time."
		while [[ -t 0 ]] && ! [[ "t$REMOVE_KPATCH" =~ ^t(y|Y|n|N)$ ]]; do
			echo
			read -n 1 -p $' > Do you want to remove that package so that ReadyKernel updates could be installed? (Y/N) ' REMOVE_KPATCH
		done
		echo

		if [[ $REMOVE_KPATCH =~ ^(y|Y)$ ]]; then
			echo "Trying to remove 'kpatch'."
			$REMOVE_CMD kpatch || return 1
		else
			echo "ReadyKernel updates cannot be used safely if other live patching tools for the kernel are present in the system. If you would like to use ReadyKernel, please remove other live patching tools first."
			return 1
		fi
	fi

	if ! check_license; then
		[[ -t 0 ]] && read -t 60 -n 50 -p $' > Enter license key: ' LIC_KEY

		echo
		if [[ -n "$LIC_KEY" ]]; then
			vzsubscribe_register "$LIC_KEY"
			[[ $? != 0 ]] && echo "Initialization has been cancelled" && return 1
		else
			echo "You cannot continue without a license key."
			return 1
		fi
	else
		echo
		echo " > The system is already registered."
	fi

	while [[ -t 0 ]] && [[ -n $AUTOUPDATE ]] && ! [[ $AUTOUPDATE =~ ^(y|Y|n|N)$ ]]; do
		echo
		AUTOUPDATE=y
		read -t 60 -n 1 -p $' > Do you want to enable automatic updating? (Y/n) ' AUTOUPDATE
	done

	[[ -n $AUTOUPDATE ]] && echo

	if [[ -z $AUTOUPDATE ]] || [[ $AUTOUPDATE =~ ^(y|Y)$ ]]; then
		auto_update "enable"
	else
		auto_update "disable"
	fi

	echo
	echo " > Patch installation has been initiated."
	echo
	update_patches && echo && print_info

	if [[ -e /snap/bin/canonical-livepatch ]]; then
		print_error "Canonical-livepatch detected; it is highly recommended to remove it, it is dangerous to have ReadyKernel and LivePatch together" >&2
	fi

}

unset MODULE

[[ "$#" -lt 1 ]] && usage && exit 1

check_executable() {
	[[ -x $1 ]] && return
	echo "error: $1 is not found" >&2
	return 1
}

check_executable $VZSUBSCRIBEBIN || exit 1
if [[ $MODE == "yum" ]] ; then
    check_executable $YUMBIN || exit 1
else
    check_executable $APTBIN || exit 1
fi

print_info() {
	PATCH=$(print_newest_patch_name)
	if [[ -z "$PATCH" ]]; then
		NR_PATCHES=$(ls -ld $SYSFS/kpatch* 2>/dev/null | wc -l)
		echo "Loaded patches: $NR_PATCHES."
		return 1
	fi

	VERREL_RAW=$(echo $PATCH | sed 's/^kpatch_cumulative_//;s/_/\./g')
	PATCHVER=$(echo $VERREL_RAW | sed 's/\.r.*$//')
	VERREL=$(echo $VERREL_RAW | sed 's/\.r/-/')

	if [[ $MODE == "yum" ]]; then
		PKG_NAME=$(rpm -q --whatprovides $PATCHPACKAGE --qf "%{NAME}-%{VERSION}-%{RELEASE}\n" 2>/dev/null | head -n 1; exit ${PIPESTATUS[0]})
	else
		PKG_NAME=$(installed_package_name)-${VERREL}
	fi

	[[ $? = 0 ]] && echo "Patch name: $PKG_NAME"

	if find_module "$PATCH"; then
		echo "Patch module: $PATCH"
		echo -n "File: "
		modinfo -F "filename" "$MODULE" || die "failed to get info for module $PATCH"
	else
		echo "Patch module $PATCH is loaded but not installed."
	fi

	echo "Version: $PATCHVER"

	echo
	INFO_FILE=/usr/share/readykernel-patch-$(uname -r)/info-$VERREL.txt
	if [[ -f "$INFO_FILE" ]]; then
		cat "$INFO_FILE"
	else
		INFO_FILE=/usr/share/kpatch-patch-$(uname -r)/info-$VERREL.txt
		[[ -f "$INFO_FILE" ]] && cat "$INFO_FILE"
	fi
}

check_argc() {
	[[ "$1" -le "$2" ]] && return 0
	print_error "Too many arguments" >&2 && usage && return 1
}

TEMPLOG=$(mktemp)
[[ $? != 0 ]] && TEMPLOG=

print_date() {
	date "+%Y-%m-%d %H:%M:%S"
}

print_error() {
	ERRORS+=("$1")
	echo "Error: $1"
}

log_command() {
	local COMMAND RESULT
	[[ -z "$TEMPLOG" ]] && $@ 2>/dev/null && return $?

	COMMAND=$@
	$@ 2>$TEMPLOG
	RESULT=$?

	[[ $RESULT = 0 ]] && return $RESULT
	LOGS+=( "call: $COMMAND" "return: $RESULT" "stderr:" )
	IFS=$'\n'
	LOGS+=($(cat $TEMPLOG))
	unset IFS
	return $RESULT
}

COMMANDLOG=$@
LOGS=("date: `print_date`" "cmd: $COMMANDLOG")

SUBCOMMAND="$1"
shift
case "${SUBCOMMAND}" in
"info"|"--info")
	check_argc $# 0 || exit 1

	print_info
	;;

"list"|"-l"|"--list")
	check_argc $# 0 || exit 1

	kpatch_list
	;;

"load"|"--load")
	check_argc $# 0 || exit 1

	kpatch_load
	;;

"load-replace"|"--load-replace")
	check_argc $# 0 || exit 1

	load_replace
	;;

"unload"|"--unload")
	check_argc $# 0 || exit 1

	kpatch_unload
	;;

"patch-info"|"--patch-info")
	check_argc $# 0 || exit 1

	kpatch_info
	;;

"check-update"|"--check-update")
	check_argc $# 0 || exit 1

	check_update
	;;

"licinfo"|"--licinfo")
	check_argc $# 0 || exit 1

	vzsubscribe_info
	;;

"licload"|"--licload")
	check_argc $# 2 || exit 1

	vzsubscribe_register "$@"
	;;

"licunload"|"--licunload")
	check_argc $# 0 || exit 1

	vzsubscribe_unregister
	;;

"report"|"--report")
	check_argc $# 0 || exit 1

	create_report
	;;

"update"|"--update")
	check_argc $# 0 || exit 1

	# Just in case someone skips 'readykernel init'.
	if has_native_kpatch; then
		print_error "Another live patching system detected: 'kpatch' package is installed. It may be unsafe to use both that system and ReadyKernel at the same time." >&2
		exit 1
	fi

	update_patches
	exit $?
	;;

"autoupdate"|"--autoupdate")
	check_argc $# 2 || exit 1

	auto_update "$@"
	;;

"autoload"|"--autoload")
	check_argc $# 1 || exit 1

	auto_load "$@"
	;;

"init"|"initialize"|"--initialize")
	check_argc $# 1 || exit 1

	initialize_interactive "$@"
	;;

"help"|"-h"|"--help")
	check_argc $# 0 || exit 1

	print_usage
	;;

*)
	echo "Unknown command: \"$1\""
	usage
	exit 1
esac

RESULT=$?
[[ -n "$TEMPLOG" ]] && rm $TEMPLOG

LOGS+=( "exit: $RESULT" "errors:" )
printf "%s\n" "${LOGS[@]}" > $LOGLASTFILE
[[ ${#ERRORS[@]} != 0 ]] && printf "%s\n" "${ERRORS[@]}" >> $LOGLASTFILE
cat $LOGLASTFILE >> $LOGFILE

exit $RESULT
