#!/bin/bash

# Check kernel live patching status
# Libor Pechacek <lpechacek@suse.com>

unset VERBOSE

function klp_in_progress() {
    for p in /sys/kernel/livepatch/*; do
	    [ 0$(cat "$p/transition" 2>/dev/null) -ne 0 ] && return 0
    done
    return 1
}

function klp_dump_blocking_threads() {
    if [[ $EUID -ne 0 ]]; then
       echo "Warning: running as non-root user, display will be limited" >&2
    fi

    unset PIDS

    TRANSITIONING_PATCH="$(grep -ls '^1$' /sys/kernel/livepatch/*/transition | head -n1)"

    if [ -n "$TRANSITIONING_PATCH" ]; then
	TRANSITION_DIRECTION=$(cat "${TRANSITIONING_PATCH/%\/transition/\/enabled}")

	for DIR in /proc/[0-9]*/task/[0-9]*; do
	    PATCH_STATE=$(cat $DIR/patch_state 2>/dev/null)
	    if [ -n "$PATCH_STATE" ] && [ "$PATCH_STATE" -ge 0 \
		-a "$PATCH_STATE" -ne "$TRANSITION_DIRECTION" ]; then
		PID=${DIR#/proc/}
		PID=${PID%/task/*}
		TID=${DIR#*/task/}
		if [ -n "$VERBOSE" ]; then
		    COMM="$(cat $DIR/cmdline 2>/dev/null | tr \\0 \ )"
		    # fallback to the command name, for example for kernel threads
		    [ -z "$COMM" ] && COMM="[$(cat $DIR/comm 2>/dev/null | tr \\0 \ )]"
		    if [ ${VERBOSE:-0} -gt 1 ]; then
			STACK=$(cat $DIR/stack 2>/dev/null | sed 's/^/  /')
		    fi
		    # don't write out anything in case the process has exited
		    if [ -e "$DIR" ]; then
			    echo "$PID $TID $COMM"
			    [ ${VERBOSE:-0} -gt 1 ] && echo "$STACK"
		    fi
		else
		    echo $PID $TID
		fi
		PIDS="$PIDS $PID"
	    fi
	done
    fi

    if [ -z "$PIDS" -a -n "$VERBOSE" ]; then
	echo "no threads with klp_in_progress set"
    fi
}

function klp_status() {
    if klp_in_progress ; then
	echo "in_progress"
    else
	echo "ready"
    fi
}

function klp_check() {
    if klp_in_progress ; then
	echo "Following processes have not finished a previous kernel live patching yet:"
	VERBOSE=2 klp_dump_blocking_threads
	return 1
    fi
}

function klp_patches() {
    unset PATCHES_FOUND
    for d in /sys/kernel/livepatch/*; do
	[ ! -d "$d" ] && continue
	PATCH_NAME=${d#/sys/kernel/livepatch/}
	PATCH_MOD=${PATCH_NAME}
	echo "${PATCH_MOD}"
	if [ -n "$VERBOSE" ]; then
	    klp_detailed_patch_info "${PATCH_MOD}" | sed 's/^/    /'
	    echo
	fi
	PATCHES_FOUND=1
    done
    if [ -z "$PATCHES_FOUND" -a -n "$VERBOSE" ]; then
	echo "no patch"
    fi
}

function klp_patch_rpm_name() {
    # srcversion is the link between loaded kernel module and its RPM
    SRCVERSION=$(cat "/sys/module/$1/srcversion")

    # exit when the module cannot be tracked down
    MODPATH=$(/usr/sbin/modinfo -n "$1" 2>/dev/null) || exit
    MODSRCVERSION=$(/usr/sbin/modinfo -F srcversion "$1")

    if [ "$SRCVERSION" != "$MODSRCVERSION" ]; then
	echo "Warning: patch module srcversion does not match the on-disk checksum:" \
	     "$1 ($SRCVERSION/$MODSRCVERSION)" >&2
        exit 1
    fi

    echo $(rpm -qf "${MODPATH}" 2>/dev/null)
}

function klp_info_from_rpm() {

    RPMNAME=$(klp_patch_rpm_name "$1")
    [ -n "$RPMNAME" ] || exit

    REFS=($(rpm -q --changelog "${RPMNAME}" | \
	sed 's/^[[:space:]]*KLP:[[:space:]]*\(.*\)/\1/;t b;d;:b s/[[:space:]]/\n/g' | \
	sort -ru))
    declare -a CVES
    declare -a BUGS_FATES
    for REF in "${REFS[@]}"; do
        if [ ${REF:0:3} = 'CVE' ]; then
             CVES+=($REF)
        else
             BUGS_FATES+=($REF)
        fi
    done

    declare -p RPMNAME
    declare -p CVES
    declare -p BUGS_FATES
}

function klp_detailed_patch_info() {
    REFCNT=$(cat "/sys/module/$1/refcnt")
    ACTIVE=$([[ "$REFCNT" -eq 0 ]]; echo $?)

    echo "active: ${ACTIVE}"

    # collect info if we have it; first try the "cache" (bsc#1191344)
    SRCVERSION=$(cat "/sys/module/$1/srcversion")
    CACHE_FILE="/var/cache/livepatch/$1-$SRCVERSION"
    if [ -e "$CACHE_FILE" ]; then
        . "$CACHE_FILE"
    else
        KLP_INFO=$(klp_info_from_rpm $1)
        echo "$KLP_INFO" > "$CACHE_FILE"
        eval "$KLP_INFO"
    fi

    [ -n "$RPMNAME" ] || exit
    echo "RPM: ${RPMNAME}"
    echo -n "CVE: "
    if [ ${#CVES[*]} -gt 0 ]; then
	echo ${CVES[*]}
    else
        echo -n "(none"
        [ ${#BUGS_FATES[*]} -eq 0 ] && echo -n " - this is an initial kernel live patch"
        echo ")"
    fi
    echo -n "bug fixes and enhancements: "
    if [ ${#BUGS_FATES[*]} -gt 0 ]; then
	echo ${BUGS_FATES[*]}
    else
	echo "(none)"
    fi

    if [ ${VERBOSE:-0} -gt 1 ]; then
	SHORT_RPMNAME=$(rpm -q --qf "%{name}" "$RPMNAME" 2>/dev/null)

	echo -n "Update status: "
	if zypper -qn --no-refresh up -D "$SHORT_RPMNAME" 2>/dev/null | fgrep -q "package to upgrade"; then
	    echo "newer version is available"
	else
	    echo "up to date"
	fi

	EXP_DATE=$(grep "^$SHORT_RPMNAME," /usr/share/lifecycle/data/sle-module-live-patching.lifecycle 2>/dev/null \
	    | cut -d, -f3)

	echo -n "Patches issued until: "
	if [ -n "$EXP_DATE" ]; then
	    echo "$EXP_DATE"
	else
	    echo "to be announced"
	fi
    fi
}

function klp_downgrade()
{
    VERBOSE_ORIG="$VERBOSE"
    unset VERBOSE

    for patch in $(klp_patches); do
        RPM_FULL_NAME=$(klp_patch_rpm_name "$patch")
        if [ -z "$RPM_FULL_NAME" ]; then
            echo "Warning: cannot determine RPM package for $patch" >&2
            continue
        fi

        RPM_INFO=$(rpm -q --qf '%{name};%{version}' "$RPM_FULL_NAME")
        RPM_VERSION=${RPM_INFO#*;}
        RPM_NAME=${RPM_INFO%;*}
        if [ "$RPM_VERSION" -le 1 ]; then
            echo "$RPM_FULL_NAME is the initial kernel live patch and cannot be downgraded."
            continue
        fi

        ZYPPER_COMMAND="zypper -n in --oldpackage $RPM_NAME = $(($RPM_VERSION-1))"
        echo "KLP tool will replace the current kernel live patch with its previous version."
        echo "The command for downgrade is: $ZYPPER_COMMAND"
        if [ -z "$NON_INTERACTIVE" ]; then
            read -p "Continue? (y/N) " -n 1 -r
            echo
        else
            REPLY=Y
        fi
        if [[ $REPLY =~ ^[Yy]$ ]]; then
            eval $ZYPPER_COMMAND
        fi
    done

    VERBOSE="$VERBOSE_ORIG"
}

USAGE="Usage: $0 [-h][-v] COMMAND
Query kernel live patching status.

Commands:
	status:    display the overall status of kernel live patching
	patches:   display the list of loaded patches
	blocking:  list execution threads that are preventing kernel
                   live patching from finishing
        downgrade: revert the current live patch by installing
                   the previous one

Options:
	-h	   print this help
	-n	   non-interactive mode
	-v	   more detailed output

Report bugs at https://bugzilla.suse.com/"
PKGVERSION="@@VERSION@@"

while getopts hnv-: opt
do
    case $opt$OPTARG in
    -help|h)
	exec echo "$USAGE" ;;
    -non-interactive|n)
	NON_INTERACTIVE=1 ;;
    -version)
	exec echo "klp $PKGVERSION" ;;
    -verbose|v) VERBOSE=$((${VERBOSE:-0} + 1)) ;;
    *)
	echo "$0: try '$0 --help'" >&2; exit 1 ;;
    esac
done

shift `expr $OPTIND - 1`

if [ $# -lt 1 ]; then
    echo -e "Error: no command provided\n" >&2
    echo "$USAGE"
    exit 1
fi

case $1 in
    blocking) klp_dump_blocking_threads ;;
    status) klp_status ;;
    check) klp_check ;;
    store_patch_info)
        SRCVERSION=$(cat "/sys/module/$2/srcversion")
        klp_info_from_rpm $2 > "/var/cache/livepatch/$2-$SRCVERSION" ;;
    patches) klp_patches ;;
    downgrade) klp_downgrade ;;
    *) echo "Error: unknown command \`$1'"; exit 1 ;;
esac

# vim: ai sw=4 et sts=4 ft=sh
