#!/bin/bash
#
# Copyright (C) 2020-2024 Thorsten Kukuk <kukuk@thkukuk.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

export LANG=C.UTF-8

# Default variables
PKG_MANAGER="zypper"
UPDATE_CMD="auto"
REBOOT_CMD="auto"
REBOOT_METHOD="reboot"
IGNORE_SERVICES=""
RESTART_SERVICES="yes"
IGNORE_SERVICES_FROM_RESTART="dbus dbus-broker virtlockd"
SERVICES_TRIGGERING_REBOOT=""
SERVICES_TRIGGERING_SOFT_REBOOT="dbus dbus-broker"
RETVAL=0
LOG_TAG="os-update"

# Additional variables per package manager
ZYPPER_NONINTERACTIVE="-y --auto-agree-with-product-licenses"
ZYPPER_NOREFRESH="--no-refresh"


log_error()
{
	echo "$@" >&2
	local LOG_ARGS="--priority user.err"
	if [ -n "$LOG_TAG" ]; then
		LOG_ARGS="$LOG_ARGS -t $LOG_TAG"
	fi
	logger ${LOG_ARGS} "$@"
}

log_info()
{
	echo "$@"
	local LOG_ARGS=""
	if [ -n "$LOG_TAG" ]; then
		LOG_ARGS="$LOG_ARGS -t $LOG_TAG"
	fi
	logger ${LOG_ARGS} "$@"
}

usage() {
	echo "Syntax: os-update"
	echo ""
	echo "Update the system with automatic reboot if required."
	echo ""
	echo "Options:"
	echo "--help, -h                 Display this help and exit"
	echo "--version                  Display version and exit"
}

print_version() {
	echo "os-update 1.21"
}

check_and_reboot() {
	if [ -x /usr/bin/needs-restarting ]; then
		needs-restarting -r
		if [ $? -eq 1 ] ; then
			reboottrigger="yes"
			if [ -f "/run/reboot-needed" ] && [ -s "/run/reboot-needed" ]; then
				case $(cat /run/reboot-needed) in
					"soft-reboot")
						log_info "Update suggests reboot with soft-reboot"
						REBOOT_METHOD="soft-reboot"
					;;
					*)
						log_info "Update suggests reboot with $(cat /run/reboot-needed), using hard reboot"
						REBOOT_METHOD="reboot"
					;;
				esac
			else
				log_info "Update suggests reboot"
				REBOOT_METHOD="reboot"
			fi
		fi
	else
		release=$(uname -r)
		flavor=${release##*-}
		version=${release%-*}
		for rpm in $(rpm -q "kernel-${flavor}") ; do
			installed=${rpm#kernel-"${flavor}"-}
			if [[ ${installed} > ${version}.1.$(uname -m) ]] ; then
				log_info "New kernel detected, trigger reboot"
				reboottrigger="yes"
				REBOOT_METHOD="reboot"
			fi
		done
	fi
	if [ "$reboottrigger" = "yes" ]; then
	    REBOOTMGRCTL_CMD=$(command -v rebootmgrctl)
		case "$REBOOT_CMD" in
			"auto")
				if [ -n "$REBOOTMGRCTL_CMD" ]; then
					if "$REBOOTMGRCTL_CMD" is-active -q; then
						log_info "Trigger reboot with rebootmgrctl $REBOOT_METHOD"
						"$REBOOTMGRCTL_CMD" $REBOOT_METHOD
					else
						log_info "Trigger reboot with systemctl $REBOOT_METHOD"
						systemctl $REBOOT_METHOD
					fi
				else
					log_info "Trigger reboot with systemctl $REBOOT_METHOD"
					systemctl $REBOOT_METHOD
				fi
			;;
			"rebootmgr")
				if [ -z "$REBOOTMGRCTL_CMD" ]; then
					log_error "ERROR: rebootmgrctl not installed"
					exit 1
				fi
				if ! "$REBOOTMGRCTL_CMD" is-active -q; then
					log_error "ERROR: rebootmgrd not running"
					exit 1
				fi
				log_info "Trigger reboot with rebootmgrctl $REBOOT_METHOD"
				"$REBOOTMGRCTL_CMD" $REBOOT_METHOD
			;;
			"reboot")
				log_info "Trigger reboot with systemctl $REBOOT_METHOD"
				systemctl $REBOOT_METHOD
			;;
			"no"|"none")
				log_info "A $REBOOT_METHOD is required"
			;;
		esac
	fi
}

eval_zypper_retval() {
	RETVAL=$1

	if [ "$RETVAL" -eq 0 ] || [ "$RETVAL" -eq 102 ] || [ "$RETVAL" -eq 103 ]; then
		log_info "Update was successful"
	else
		log_error "ERROR: update failed, exit code was ${RETVAL}"
		if [ "$RETVAL" -ne 106 ]; then
			exit 1
		fi
	fi
}

zypper_refresh() {
	if ! zypper refresh; then
		log_error "ERROR: refresh failed"
		exit 1
	fi
}

zypper_security() {
	zypper_refresh
	zypper ${ZYPPER_NOREFRESH} patch ${ZYPPER_NONINTERACTIVE} --category security
	eval_zypper_retval $?
}

zypper_up() {
	zypper_refresh
	zypper ${ZYPPER_NOREFRESH} up ${ZYPPER_NONINTERACTIVE}
	eval_zypper_retval $?
}

zypper_dup() {
	zypper_refresh
	zypper ${ZYPPER_NOREFRESH} dup ${ZYPPER_NONINTERACTIVE}
	eval_zypper_retval $?
}

zypper_restart_services() {
	if [[ $(lsof -w -a -d DEL -p 1 -Ff | grep -c "^fDEL") -gt 0 ]]; then
		log_info "Reexecing systemd"
		systemctl daemon-reexec
	fi
	reboottrigger="no"
	for service in $(zypper ps --print %s); do
	        do_ignore=0
		for ignore in $IGNORE_SERVICES; do
			if [ "$ignore" = "$service" ]; then
				log_info "Ignoring $service as it is excluded in IGNORE_SERVICES"
				do_ignore=1
				break
			fi
		done
		if [ $do_ignore -eq 1 ]; then
		    continue
		fi
		for rebootservice in $SERVICES_TRIGGERING_SOFT_REBOOT; do
			if [ "$rebootservice" = "$service" ]; then
				log_info "$service triggers a soft-reboot as it is included in SERVICES_TRIGGERING_SOFT_REBOOT"
				reboottrigger="yes"
				REBOOT_METHOD="soft-reboot"
				break
			fi
		done
		for rebootservice in $SERVICES_TRIGGERING_REBOOT; do
			if [ "$rebootservice" = "$service" ]; then
				log_info "$service triggers a reboot as it is included in SERVICES_TRIGGERING_REBOOT"
				reboottrigger="yes"
				REBOOT_METHOD="reboot"
				break
			fi
		done
		for ignore in $IGNORE_SERVICES_FROM_RESTART; do
			if [ "$ignore" = "$service" ]; then
				log_info "Not restarting $service as it is excluded in IGNORE_SERVICES_FROM_RESTART"
				break
			fi
		done
		if [ "$ignore" != "$service" ] && [ "$service" != "os-update" ]; then
			log_info "Restarting $service"
			systemctl try-restart "$service".service
		fi
	done
}

run_zypper() {
	case "$UPDATE_CMD" in
		auto)
			. /etc/os-release
			case "$NAME" in
				"SLES"|"openSUSE Leap")
					zypper_up
				;;
				"openSUSE Tumbleweed"|"openSUSE Tumbleweed-Slowroll")
					zypper_dup
				;;
				*)
					log_error "ERROR: your OS is not supported in 'auto' mode"
				;;
			esac
		;;
		dup)
			zypper_dup
		;;
		up)
			zypper_up
		;;
		security)
			zypper_security
		;;
		*)
			log_error "ERROR: UPDATE_CMD ($UPDATE_CMD) unknown"
			exit 1
		;;
	esac
}

#
# Main
#

while true; do
	if [ $# -eq 0 ]; then
		break
	fi

	case "$1" in
		-h|--help)
			usage
			exit 0
		;;
		--version)
			print_version
			exit 0
		;;
		*)
			usage
			exit 1
		;;
	esac
done

if [ $# -ne 0 ]; then
	usage
	exit 1
fi

# Read configuration variables

# Overwrite with vendor specific values if exist:
# New vendor config
if [ -e /usr/share/os-update/os-update.conf ]; then
  . /usr/share/os-update/os-update.conf
fi
# Outdated config directory
if [ -e /usr/etc/os-update.conf ]; then
	. /usr/etc/os-update.conf
fi
# Overwrite with admin specific values if exist:
if [ -e /etc/os-update.conf ]; then
	. /etc/os-update.conf
fi

run_${PKG_MANAGER}

case "$RESTART_SERVICES" in
	[yY]*)
		zypper_restart_services
	;;
esac

check_and_reboot

exit $RETVAL
