#!/usr/bin/env bash
# shellcheck disable=SC1091

# Ad-hoc script for just patching the system.

# Environment variables:
# RKHUNTER - will be set to 0 if rkhunter exists and this variable is not set.
#            Setting it to 1 will skip using rkhunter.
# NEEDREST - will be set to 0 if needrestart exists and this variable is not set.
#            Setting it to 1 will skip using needrestart or any other helper
#            like dnf needs-restarting or zypper needs-rebooting / zypper ps.

# os_patching made by albatrossflavour et al., binary:
OSPBIN='/usr/local/bin/os_patching_fact_generation.sh'

. /etc/os-release || exit 1

# Debian act as if ID_LIKE wasn't necessary if ID == ID_LIKE. Great job, guys.
if [ "$ID" = "debian" ]; then
	ID_LIKE="debian"
fi


# 0. Internal helpers
myline() {
	[ -z "$1" ] && return 1
	[ -n "$COLUMNS" ] && MYCOLS="$COLUMNS"
	[ -z "$MYCOLS" ] && MYCOLS="$(/usr/bin/tput cols 2>/dev/null)"
	[ -z "$MYCOLS" ] && MYCOLS=16
	# determine how often $1 fits into $MYCOLS
	# bc rounds down, so we'll just mimick a ceiling function in bash
	denom="${#1}"
	(( iter=((MYCOLS+denom-1)/denom)-1 ))
	(( leftover=MYCOLS-(denom*iter) ))
	printf '\033[1m'
	c=0
	while [ "$c" -lt "$iter" ]; do
		printf '%b' "$1"
		c="$((c+1))"
	done
	printf '%b' "${1:0:leftover}"
	printf '\033[0m\n'
}

hline() {
	myline '─'
}
dline() {
	myline '┄'
}

header() {
	if [ -n "$1" ]; then
		hline
		printf '    \033[3m\033[1m%b\033[0m\n' "$1"
		hline
	fi
}

footer() {
	if [ -n "$1" ]; then
		dline
		printf '\033[3m\033[1m%b\033[0m\n' "$1"
	fi
}


# 1. Find out about auxiliary helpers like rkhunter
declare NRSBIN RKHBIN
[ -z "$RKHUNTER" ] && RKHUNTER=2
[ -z "$NEEDREST" ] && NEEDREST=2
if [ "$NEEDREST" -gt 1 ] ; then
	for bin in /usr/sbin/needrestart /usr/bin/needrestart; do
		if [ -x "$bin" ]; then
			NRSBIN="$bin"
			NEEDREST=0
			break
		fi
	done
fi
if [ "$RKHUNTER" -gt 1 ] ; then
	for bin in /usr/bin/rkhunter /usr/sbin/rkhunter; do
		if [ -x "$bin" ]; then
			RKHBIN="$bin"
			RKHUNTER=0
			break
		fi
	done
fi


# 2. Patching.
case "$ID_LIKE" in
	"debian")
		APTBIN='/usr/bin/apt'
		APTOPTS=(
			'-o' 'Apt::Cmd::Disable-Script-Warning=true'
			'-o' 'Dpkg::Progress-Fancy=False'
			'-o' 'Apt::Color=False'
			'-o' 'Dpkg::Use-Pty=False'
			'-o' 'Quiet::NoUpdate=True'
			'-o' 'APT::Get::AutomaticRemove=False'
			'-o' 'APT::Get::AutomaticRemove::Kernels=False'
			'-o' 'APT::Get::Assume-Yes=True'
		)
		if [ "$RKHUNTER" -eq 0 ]; then
			header 'Starting rkhunter check'
			"$RKHBIN" -c --sk || exit 120
		fi
		# 2.1. Package list refresh
		header 'Starting package list update'
		"$APTBIN" "${APTOPTS[@]}" update || exit 110
		ULIST="$("$APTBIN" "${APTOPTS[@]}" -q list --upgradable | grep -iP '^[0-9a-z_:\-+\.]+/.+' | sed 's/^\([^/]\+\).*/\1/')"
		# Only one update will be one update with or without line-break, and NO update will be also with or without line-break.
		# Solution: Always add a line-break, and grep away empty lines.
		UPDATENUM="$(printf '%b\n' "$ULIST" | grep -vcP '^$')"
		printf '\033[3m\033[1m%b update(s) found.\033[0m\n' "$UPDATENUM"
		# 2.2. Package update.
		# 2.2.1. No updates found?
		if [ "$UPDATENUM" -lt 1 ]; then
			printf '\033[3m\033[1m\033[2mSkipping updates.\033[0m\n'
		else
		# 2.2.2. Updates found?
			header 'Starting package updates'
			"$APTBIN" "${APTOPTS[@]}" full-upgrade || exit 112
			header 'Starting package auto-removal'
			"$APTBIN" "${APTOPTS[@]}" --purge autoremove || exit 113
			# 2.2.3. Package file index update
			if [ -x /usr/bin/apt-file ]; then
				printf 'Starting apt-file update'
				/usr/bin/apt-file "${APTOPTS[@]}" update || true
			fi
			if [ "$RKHUNTER" -eq 0 ]; then
				header 'Starting rkhunter update'
				"$RKHBIN" --propupd || exit 121
			fi
			# 2.2.4. Requirement for reboot
			if [ "$NEEDREST" -eq 0 ]; then
				header 'Starting needrestart investigation'
				"$NRSBIN" -b
				# Outdated comment (kind of), see $NEEDREST at the top of the file --
				# If we don't have needrestart, this will fail - which is OK, without
				# a means of controlling whether reboot is necessary we will reboot in any case.
				if ! "$NRSBIN" -p; then
					footer 'Outdated libraries or kernel found, rebooting'
					/usr/bin/systemctl reboot || reboot
				else
					if [ "$UPDATENUM" -gt 0 ]; then
						if [ -x "$OSPBIN" ]; then
							header 'Starting os_patching_fact_generation.sh'
							ospstart="$(/usr/bin/date '+%s')"
							"$OSPBIN"
							ospend="$(/usr/bin/date '+%s')"
							footer "...done ($((ospend - ospstart)) seconds)."
						fi
					fi
				fi
			elif [ "$NEEDREST" -gt 1 ]; then
				footer 'No needrestart found, rebooting'
				/usr/bin/systemctl reboot || reboot
			fi
		fi
	;;
	"suse"*)
		# Caution:
		# 1. Broken package dependenciers will not be solved
		# 2. Orphaned packages will be kept in-place
		
		# global zypper options
		ZGOPTS=(
			'-q'
			# this option also works with non-patching commands:
			'--non-interactive-include-reboot-patches'
			# --refresh is only an option to addrepo. Adding --no-refresh to
			# a refresh action, however, will ignore that flag and toss a warning.
			# Warnings are not nice, but we'll ignore that over here...
			'--no-refresh'
		)
		# update/upgrade zypper options
		ZUOPTS=(
			'-y' '--auto-agree-with-licenses' '--no-allow-downgrade'
			'--allow-name-change' '--allow-arch-change' '--allow-vendor-change'
			'--solver-focus' 'Update'
		)
		header 'Refreshing zypper "services"'
		mystart="$(/usr/bin/date '+%s')"
		# for now, refresh-services doesn't even display a warning regarding --no-refresh,
		# but we'll keep it in case that changes. Dang, zypper is a mess at times.
		/usr/bin/zypper "${ZGOPTS[@]}" refresh-services | grep -v -- '--no-refresh' 
		[ "${PIPESTATUS[0]}" -eq 0 ] && printf 'OK.\n' || exit 110
		myend="$(/usr/bin/date '+%s')"
		footer "...done ($((myend - mystart)) seconds)."

		header 'Refreshing repository cache'
		mystart="$(/usr/bin/date '+%s')"
		/usr/bin/zypper "${ZGOPTS[@]}" refresh | grep -v -- '--no-refresh'
		[ "${PIPESTATUS[0]}" -eq 0 ] && printf 'OK.\n' || exit 111
		myend="$(/usr/bin/date '+%s')"
		footer "...done ($((myend - mystart)) seconds)."

		# TODO: no amount of "-q" keeps zypper from delivering the verbose list of updates before
		# installing them. If only the zypper guys were modern in thinking script or automation approaches...
		header 'Running update'
		mystart="$(/usr/bin/date '+%s')"
		/usr/bin/zypper "${ZGOPTS[@]}" --no-refresh up "${ZUOPTS[@]}" && printf 'OK.\n' || exit 112
		myend="$(/usr/bin/date '+%s')"
		footer "...done ($((myend - mystart)) seconds)."

		header 'Running dist-upgrade'
		mystart="$(/usr/bin/date '+%s')"
		/usr/bin/zypper "${ZGOPTS[@]}" --no-refresh dup "${ZUOPTS[@]}" && printf 'OK.\n' || exit 113
		myend="$(/usr/bin/date '+%s')"
		footer "...done ($((myend - mystart)) seconds)."

		header 'Running "purge-kernels"'
		## yes, there is no general cleanup actions, there is only "-u" for "rm". But there _is_ purge-kernels.
		#mystart="$(/usr/bin/date '+%s')"
		#/usr/bin/zypper "${ZGOPTS[@]}" --no-refresh purge-kernels -y && printf 'OK.\n' || exit 114
		#myend="$(/usr/bin/date '+%s')"
		#footer "...done ($((myend - mystart)) seconds)."
		printf 'As long as SUSE do not puplate a "-y" flag for purge-kernels, this is among the things\n'
		printf 'that are infeasible. Continuing without.\n'

		# zypper: why deliver exit codes WHEN WE CAN TOSS EFFIN STRINGS AT THE CONSOLE ONLY
		# also... too many people rather tend to localise their systems....:
		export LANG=C # use "C" as safe haven, we DO NOT want this to fail
		# yes, in the very recent part they invented "needs-rebooting", but that only checks
		# core services and libraries... :( so we do both here. We want to be rather aggressive
		# on unattended updates in that we reboot if there are lingering "programs". Any of them.
		header 'Checking reboot requirement... '
		if ! /usr/bin/zypper -q needs-rebooting; then
			# TODO: zypper being locked by another "application" delivers RC7 - is this reserved
			#       exclusively for this case?
			footer 'Rebooting (zypper needs-rebooting)'
			/usr/bin/systemctl reboot
		elif [ "$(/usr/bin/zypper ps -sss 2>&1 | wc -l)" -gt 0 ]; then
			footer 'Rebooting (zypper ps)'
			/usr/bin/systemctl reboot
		else
			printf 'no reboot required.\n'
			if [ -x "$OSPBIN" ]; then
				header 'Starting os_patching_fact_generation.sh'
				ospstart="$(/usr/bin/date '+%s')"
				"$OSPBIN"
				ospend="$(/usr/bin/date '+%s')"
				footer "...done ($((ospend - ospstart)) seconds)."
			fi
		fi
	;;
	"rhel"*|"centos"*)
		# we do not use --skip-broken here - we keep our systems tidy, so any pollution may and should
		# cause an error :-)

		# On RHEL and derivatives, dnf populates separate caches for each user, requiring us to do something
		# like "list updates" as well as root (or inefficiently build the cache twice). :-}
		# This is scripted, so for now this script has to be called as root. We could consider using sudo below
		# in the future; however this would be a decision for the whole script as for now, we don't do escalations
		# anywhere.
		header 'Starting package list update'
		mystart="$(/usr/bin/date '+%s')"
		/usr/bin/dnf -d1 makecache && printf 'OK.\n' || exit 110
		myend="$(/usr/bin/date '+%s')"
		footer "...done ($((myend - mystart)) seconds)."
		ULIST="$(/usr/bin/dnf -d0 list --upgrades | grep -v '^Available Upgrades')"
		UPDATENUM="$(printf '%b\n' "$ULIST" | grep -vcP '^$')"
		printf '\033[3m\033[1m%b update(s) found.\033[0m\n' "$UPDATENUM"

		# No updates found?
		if [ "$UPDATENUM" -lt 1 ]; then
			printf '\033[3m\033[1m\033[2mSkipping updates.\033[0m\n'
		else
			header 'Starting package upgrade'
			mystart="$(/usr/bin/date '+%s')"
			/usr/bin/dnf --comment='os_patching_adhoc' -d1 --obsoletes --best -y upgrade &&\
				printf 'OK.\n' || exit 111
			myend="$(/usr/bin/date '+%s')"
			footer "...done ($((myend - mystart)) seconds)."

			header 'Checking reboot requirement'
			if ! /usr/bin/dnf -d1 needs-restarting -r; then
				footer 'Outdated libraries or kernel found, rebooting.'
				/usr/bin/systemctl reboot
			else
				if [ -x "$OSPBIN" ]; then
					header 'Starting os_patching_fact_generation.sh'
					ospstart="$(/usr/bin/date '+%s')"
					"$OSPBIN"
					ospend="$(/usr/bin/date '+%s')"
					footer "...done ($((ospend - ospstart)) seconds)."
				fi
			fi
		fi
	;;
esac
