#!/bin/bash
#
# Copyright (c) 2017-2021 Virtuozzo International GmbH, All rights reserved.

MOUNTDIR=`dirname $0`
OFFLINE=false
[ $# -ge 1 ] && [ $1 = '--offline' ] && OFFLINE=true

# Determine if this is a systemd-based OS
IS_SYSTEMD=0
if which systemctl >/dev/null 2>&1; then
	INIT_LINK=$(readlink /sbin/init)
	if [ -n "$INIT_LINK" -a "${INIT_LINK##*/}" == "systemd" ]; then
		IS_SYSTEMD=1
	fi
fi

echo "Installing guest tools, version: $(cat $MOUNTDIR/VERSION)"

# Install tools installer for udev
install -m 0755 $MOUNTDIR/install-tools /usr/bin/

# Install the udev trigger
cat $MOUNTDIR/90-guest_iso.rules > /etc/udev/rules.d/90-guest_iso.rules
/sbin/udevadm control --reload

# Install rpms
ARCH=`uname -m`

PKGMNGR=""
FALLBACK_PKGMNGR=""
ERROR_CODE=0

# Ignore signals to be able to update guest tools even if guest service is restarted
# (this can break 'prlctl enter' connection but update process should not die in this case)
trap 'true' 1 15

function get_pkg_arch() {
	local format=$1

	if [ "x$ARCH" != "xx86_64" ]; then
		pkg_arch="i?86"
	else
		if [ $format = "rpm" ]; then
			pkg_arch="x86_64"
		else
			pkg_arch="amd64"
		fi
	fi

	echo $pkg_arch
}

function get_os_id()
{
	local id="unknown"
	local src="/etc/os-release"

	if [ -e "$src" ]; then
		id=`. $src 2>/dev/null && echo $ID`
	fi

	echo $id
}

function get_os_version()
{
	local ver="0"
	local src="/etc/os-release"

	if [ -e "$src" ]; then
		ver=`. $src 2>/dev/null && echo $VERSION_ID | sed 's/\.//g'`
	fi

	echo $ver
}

function get_rhel_release()
{
	local rel="0"
	local src="/etc/system-release"

	if [ -e "$src" ]; then
		rel=`sed 's/.*release //' $src 2>/dev/null | cut -f1,2 -d. | cut -f1 -d\  | sed 's/\.//'`
	fi

	# Check if we actually get a number and return 0 if not
	if [ "$rel" -eq "$rel" ] 2>/dev/null; then
		echo $rel
	else
		echo 0
	fi
}

# Try to run the installation command. If dpkg database lock detected, try to repeat
# until either success, or some other error, or attempts limit is reached.
# Though it's called for all package managers, the detect only works for apt/dpkg
# (rpm has its own lock-wait system).
function try_install_pkg_impl()
{
	local cmdline="$*"
	local MAX_ATTEMPTS=100
	local ATTEMPT=0
	local WAIT_TO_RETRY=3   # Number of seconds to wait before next attempt
	local LOCKED
	local EXITCODE

	echo "Running: $*"
	while true; do
		LOCKED=0
		# This is a "proxy" for a command output. It reads from the supplied file descriptor (which provides
		# redirected stdout+stderr from the command), prints it back to stdout and checks whether it contains
		# the message about the DB lock.
		while read ln; do
			if [ "${ln:0:9}" == "EXITCODE:" ]; then
				EXITCODE=${ln:9}
			else
				echo "$ln"
			fi
			if echo "$ln" | grep -Eq "dpkg status database is locked by another process|E: Could not get lock /var/lib/dpkg/lock"; then
				LOCKED=1
			fi
		done < <($cmdline 2>&1; echo "EXITCODE:$?")  # Run command and supply its stdout into while's stdin via a file descriptor
		# EXITCODE is used to fetch the command exit code and emulate it when returning from the function.

		if [ "$LOCKED" == "1" ]; then
			ATTEMPT=$((ATTEMPT+1))
			if (( $ATTEMPT >= $MAX_ATTEMPTS )); then
				echo
				echo "DB is locked. Maximum number of attempts ($MAX_ATTEMPTS) is reached, aborting."
				echo
				return 2
			else
				echo
				echo "DB is locked, sleep then repeat..."
				echo
				sleep $WAIT_TO_RETRY
			fi
		else
			break
		fi
	done
	return $EXITCODE
}

# Check whether input RPM file is already installed
function rpm_check()
{
	local FMT="%{EPOCH}:%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n"
	local pkg=$1
	# Fetch name-version-etc from the RPM file
	local pkgspec=$(rpm -qp --qf="$FMT" $pkg)
	# Fetch name from the RPM file, request for installed packages with this name
	# and search whether our particular version is installed
	rpm -q --qf="$FMT" $(rpm -qp --qf="%{NAME}" $pkg) 2>&1 | grep -q "$pkgspec"
}

# Check whether input DEB file is already installed
function deb_check()
{
	local FMT='${Package}_${Version}_${Architecture}'
	local pkg=$1
	# Fetch name-version-etc from the DEB file
	local pkgspec=$(dpkg-deb -W --showformat="$FMT" $pkg)
	# Fetch name from the DEB file, request for installed packages with this name
	# and search whether our particular version is installed
	dpkg-query -W -f "$FMT:\${db:Status-Status}\\n" $(dpkg-deb -W --showformat='${Package}' $pkg) | grep -q "$pkgspec:installed"
}

function try_install_pkg()
{
	local pkgs=$1
	local new_pkgs=""
	local CHECKER="${format}_check"  # Dynamic function name, like rpm_check
	local ERR

	# Exclude all packages that are installed, because trying to install them again is an error
	for pkg in $pkgs; do
		if $CHECKER $pkg; then
			echo "Package $pkg is already installed, skipping."
		else
			new_pkgs="$new_pkgs $pkg"
		fi
	done
	if [ -z "$new_pkgs" ]; then
		echo "All requested packages are already installed."
		return 0
	fi

	if $OFFLINE; then
		try_install_pkg_impl $FALLBACK_PKGMNGR $new_pkgs
	else
		try_install_pkg_impl $PKGMNGR $new_pkgs || try_install_pkg_impl $FALLBACK_PKGMNGR $new_pkgs
	fi
	ERR=$?
	if [ $ERROR_CODE -eq 0 ]; then
		ERROR_CODE=$ERR
	fi
	return $ERR
}

function install_packages()
{
	os_id=$(get_os_id)
	os_version=$(get_os_version)

	if [ "$os_id" = "altlinux" ]; then
		PKGMNGR="apt-get -y install"
		FALLBACK_PKGMNGR="rpm -Uvh"
		# TODO: Uncomment when dkms is fixed!
		#local kver=$(uname -r)
		#kernel_package=$(rpm -qf --qf='%{NAME}=%{VERSION}-%{RELEASE}' /lib/modules/$kver)
		#devel_package=$(echo $kernel_package | sed "s/kernel-image-/kernel-headers-modules-/")
		#$PKGMNGR $devel_package || echo "Failed to install header files for the running kernel"
		if [ "$IS_SYSTEMD" == "1" ]; then
			suffix="suse"   # suse packages happenned to be the most compatible
		else
			suffix="el6"    # el6 have no systemd, and are built compatible with Alt Linux
		fi
		dkms_suffix=""  # dkms packages cannot be installed (rpm goes into deadlock)
		format="rpm"
	elif [ "$os_id" = "fedora" ]; then
		PKGMNGR="dnf -y install"
		FALLBACK_PKGMNGR="rpm -Uvh"
		$PKGMNGR kernel-devel-`uname -r` || echo "Failed to install header files for the running kernel"
		suffix="el7"
		format="rpm"
		if [ "$os_version" -eq "23" ]; then
			dkms_suffix="fc23"
			balloon_suffix=$dkms_suffix
		fi
		cp $MOUNTDIR/repofiles/centos8.repo /etc/yum.repos.d/
	elif [ -f /etc/redhat-release ]; then
		PKGMNGR="yum -y install"
		FALLBACK_PKGMNGR="rpm -Uvh"
		os_release=$(get_rhel_release)
		if [ "$os_version" -ne "7" -a "$os_version" -ne "70" -o "$os_release" -lt "73" ]; then
			$PKGMNGR kernel-devel-`uname -r` || echo "Failed to install header files for the running kernel"
		fi
		format="rpm"
		if [ "$os_version" -eq "6" ]; then
			dkms_suffix="el6"
			balloon_suffix=$dkms_suffix
			suffix="el6"
			cp $MOUNTDIR/repofiles/centos6.repo /etc/yum.repos.d/
		elif [ "$os_version" -eq "7" -o "$os_version" -eq "70" ]; then
			# We need vz balloon & dkms only for RH between 7.0 and 7.2
			if [ "$os_release" -lt "73" ]; then
				dkms_suffix="el7"
				balloon_suffix=$dkms_suffix
			fi
			suffix="el7"
			cp $MOUNTDIR/repofiles/centos7.repo /etc/yum.repos.d/
		# Can't check os_release here because for centos 6.10, release is equal to 610
		elif [ "$os_version" -gt "80" -o "$os_version" -eq "8" ]; then
			suffix="el7"
			cp $MOUNTDIR/repofiles/centos8.repo /etc/yum.repos.d/
		else
			echo "WARNING: Failed to detect system version on the basis of /etc/os-release"
			if [ "$IS_SYSTEMD" == "1" ]; then
				suffix="el7"
			else
				suffix="el6"
			fi
			dkms_suffix=$suffix
			balloon_suffix=$suffix
		fi
	elif [ "$os_id" = "debian" -o "$os_id" = "ubuntu" -o -f /etc/debian_version ]; then
		apt-get update
		DEBIAN_FRONTEND=noninteractive try_install_pkg_impl apt-get -y install gdebi-core
		PKGMNGR="gdebi -n"
		FALLBACK_PKGMNGR="dpkg -i"
		FALLBACK_HEADERS=$(dpkg-query -W -f='${binary:Package}\n' linux-image-* | head -n 1 | /bin/sed 's/linux-image-//')
		DEBIAN_FRONTEND=noninteractive try_install_pkg_impl apt-get -y install linux-headers-`uname -r` || DEBIAN_FRONTEND=noninteractive try_install_pkg_impl apt-get -y install linux-headers-${FALLBACK_HEADERS} || echo "Failed to install header files for the running kernel"
		format="deb"
		suffix=""
		vz_guest_udev_suffix="ubuntu16"
		if [ "$os_version" -eq "7" ]; then
			dkms_suffix="debian7"
			balloon_suffix=$dkms_suffix
			vz_guest_udev_suffix="debian7"
		elif [ "$os_version" -eq "8" ]; then
			dkms_suffix="debian8"
			balloon_suffix=$dkms_suffix
			vz_guest_udev_suffix="debian7"
		elif [ "$os_version" -eq "1404" ]; then
			dkms_suffix="ubuntu14"
			balloon_suffix=$dkms_suffix
			# Modern ubuntu-14 switched to kernels from ubuntu-16
			uname -r | grep -q ^4 && balloon_suffix="ubuntu16"
			vz_guest_udev_suffix="debian7"
		elif [ "$os_version" -eq "1504" ]; then
			dkms_suffix="ubuntu1504"
			balloon_suffix=$dkms_suffix
			vz_guest_udev_suffix="debian7"
			cp $MOUNTDIR/repofiles/ubuntu14.list /etc/apt/sources.list.d/vz-guest-tools.list
		elif [ "$os_version" -eq "1510" ]; then
			dkms_suffix="ubuntu1510"
			balloon_suffix=$dkms_suffix
			vz_guest_udev_suffix="debian7"
			cp $MOUNTDIR/repofiles/ubuntu14.list /etc/apt/sources.list.d/vz-guest-tools.list
		elif [ "$os_version" -eq "1604" -o "$os_version" -eq "1610" ]; then
			dkms_suffix="ubuntu16"
			balloon_suffix=$dkms_suffix
		fi

		# Install apt sources
		if [ "$os_version" -ge "7" -a "$os_version" -le "10" ]; then
			cp $MOUNTDIR/repofiles/debian${os_version}.list /etc/apt/sources.list.d/vz-guest-tools.list
		elif [ "$os_version" -ge "11" -a "$os_version" -le "1000" ]; then
			# Just use debian 10 for newer debians
			cp $MOUNTDIR/repofiles/debian10.list /etc/apt/sources.list.d/vz-guest-tools.list
		elif [ "$os_version" -ge "1400" -a "$os_version" -le "1600" ]; then
			cp $MOUNTDIR/repofiles/ubuntu14.list /etc/apt/sources.list.d/vz-guest-tools.list
		elif [ "$os_version" -ge "1604" -a "$os_version" -le "1800" ]; then
			cp $MOUNTDIR/repofiles/ubuntu16.list /etc/apt/sources.list.d/vz-guest-tools.list
		elif [ "$os_version" -ge "1804" -a "$os_version" -le "2000" ]; then
			cp $MOUNTDIR/repofiles/ubuntu18.list /etc/apt/sources.list.d/vz-guest-tools.list
		elif [ "$os_version" -ge "2004" ]; then
			cp $MOUNTDIR/repofiles/ubuntu20.list /etc/apt/sources.list.d/vz-guest-tools.list
		fi

	elif [ "$os_id" = "opensuse" -o "$os_id" = "sles" -o -f /etc/SuSE-release ]; then
		PKGMNGR="zypper --non-interactive install"
		FALLBACK_PKGMNGR="rpm -Uvh"
		format="rpm"
		suffix="suse"
		cp $MOUNTDIR/repofiles/suse.repo /etc/zypp/repos.d/
		if [ "$os_version" -eq "421" -o "$os_version" -eq "422" -o "$os_version" -eq "423" ]; then
			local kver=$(uname -r)
			kernel_package=$(rpm -qf --qf='%{NAME}:%{VERSION}-%{RELEASE}' /lib/modules/$kver)
			devel_package=$(echo $kernel_package | sed "s/:/-devel-/")
			$PKGMNGR $devel_package || echo "Failed to install header files for the running kernel"
			dkms_suffix="suse42.1"
			balloon_suffix=$dkms_suffix
		fi
	fi

	if [ x"$format" = "x" ]; then
		echo "Unsupported distribution! Nothing to install!" && return
	fi

	# Install dkms, if possible
	# Always drop the blacklist - handled by initscript for now
	rm -f /etc/modprobe.d/vzvirtio-backlist.conf > /dev/null 2>&1
	if [ x"$balloon_suffix" != "x" ]; then
		if [ $format != "rpm" ]; then
			# For Deb using per-package installation to make sure that dkms is installed before balloon
			for pkg in $MOUNTDIR/dkms/dkms*${dkms_suffix}* \
					$MOUNTDIR/dkms/vzvirtio*${balloon_suffix}*; do
				try_install_pkg "${pkg}"
			done
		else
			try_install_pkg "$MOUNTDIR/dkms/dkms-2*${dkms_suffix}* $MOUNTDIR/dkms/dkms-vzvirtio*${balloon_suffix}*"
		fi
	fi

	# Install other packages
	arch=$(get_pkg_arch $format)
	echo "WARNING: Installing/updating guest tools service, connection to VM can be lost"
	if [ $format = "rpm" ]; then
		if [ "$os_id" = "altlinux" -a "$IS_SYSTEMD" == "1" ]; then
			# Hack for Alt Linux 7: systemd ignores services from /usr/lib/systemd/system
			if [ ! -d /usr/lib/systemd/system ]; then
				SYSTEMD_HACK1=1
				ln -s /lib/systemd/system /usr/lib/systemd/system
			fi
			# Workaround for wrong path in post-install script
			if [ ! -f /usr/bin/systemctl ]; then
				SYSTEMD_HACK2=1
				ln -s /bin/systemctl /usr/bin/systemctl
			fi
		fi
		# Make sure to get rid of the standard qemu-guest-agent if it's present
		if rpm -q qemu-guest-agent >/dev/null 2>&1; then
			try_install_pkg_impl rpm -e qemu-guest-agent
		fi
		# Remove non-packaged prl_backup if it's present
		if ! rpm -q vz-guest-prl_backup >/dev/null 2>&1; then
			rm -f /usr/bin/prl_backup 2>/dev/null
		fi
		try_install_pkg "$MOUNTDIR/guest_pkgs/qemu-*.$suffix.$arch.rpm"
		if [ "$SYSTEMD_HACK1" == "1" ]; then
			rm -f /usr/lib/systemd/system
		fi
		if [ "$SYSTEMD_HACK2" == "1" ]; then
			rm -f /usr/bin/systemctl
		fi
		try_install_pkg "$MOUNTDIR/guest_pkgs/prl_nettool*.$suffix.$arch.rpm"
		if [ $suffix = "suse" ]; then
			try_install_pkg "$MOUNTDIR/guest_pkgs/vz-guest-udev*suse.noarch.rpm"
			try_install_pkg "$MOUNTDIR/guest_pkgs/vz-guest-prl_backup*suse.noarch.rpm"
		else
			try_install_pkg "$MOUNTDIR/guest_pkgs/vz-guest-udev*el.noarch.rpm"
			try_install_pkg "$MOUNTDIR/guest_pkgs/vz-guest-prl_backup*el.noarch.rpm"
		fi
	else
		# Make sure to get rid of the standard qemu-guest-agent if it's present
		if [ "$(dpkg-query -W -f '${db:Status-Status}' qemu-guest-agent 2>/dev/null)" = 'installed' ]; then
			try_install_pkg_impl dpkg -r qemu-guest-agent
		fi
		# Remove non-packaged prl_backup if it's present
		if [ "$(dpkg-query -W -f '${db:Status-Status}' vz-guest-prl-backup 2>/dev/null)" != 'installed' ]; then
			rm -f /usr/bin/prl_backup 2>/dev/null
		fi
		for pkg in $MOUNTDIR/guest_pkgs/qemu*$arch.deb \
				$MOUNTDIR/guest_pkgs/prl-nettool*$arch.deb \
				$MOUNTDIR/guest_pkgs/vz-guest-prl-backup*_all.deb \
				$MOUNTDIR/guest_pkgs/vz-guest-udev*${vz_guest_udev_suffix}_all.deb ; do
			try_install_pkg "${pkg}"
		done
		# Workaround for plymouth preventing system to reboot, PSBM-105810
		chmod u+s /sbin/unix_chkpwd || :
	fi
}

function install_version() {
	mkdir -p /usr/share/qemu-ga
	install -m 644 $MOUNTDIR/VERSION /usr/share/qemu-ga/
	ERR=$?
	if [ $ERR -ne 0 ]; then
		echo "Failed to install the version file"
		if [ $ERROR_CODE -eq 0 ]; then
			ERROR_CODE=$ERR
		fi
	else
		echo "Successfully installed the version file"
	fi
}

# Install statically linked version of fstrim
# and enable its periodic launching in cron
function install_static_trim() {
	local fstrim_path="/usr/local/bin/fstrim-static"

	mkdir -p `dirname $fstrim_path`
	gunzip $MOUNTDIR/fstrim*static.gz -c > $fstrim_path
	chmod 0755 $fstrim_path
	cat >/etc/cron.weekly/fstrim <<EOF
#!/bin/sh
# trim all mounted file systems which support it
$fstrim_path --all
EOF
	chmod a+x /etc/cron.weekly/fstrim
}

function enable_trim_systemd() {
	for action in enable start; do
		systemctl $action fstrim.timer
	done
}

function enable_trim() {
	os_id=$(get_os_id)
	os_version=$(get_os_version)

	if [ "$os_id" = "altlinux" ]; then
		if [ "${os_version:0:1}" == "p" ]; then
			# Cut off the first letter
			os_version=${os_version:1}
		fi
		if [ "${os_version:0:1}" == "7" ]; then
			install_static_trim
		elif [ "${os_version:0:1}" == "8" ]; then
			if [ "$IS_SYSTEMD" == "1" ]; then
				enable_trim_systemd
			else
				install_static_trim
			fi
		else
			echo "WARNING: Failed to detect system version on the basis of /etc/os-release"
			install_static_trim
		fi
	elif [ "$os_id" = "fedora" ]; then
		enable_trim_systemd
	elif [ -f /etc/redhat-release ]; then
		if [ "$os_version" -eq "6" ]; then
			install_static_trim
		elif [ "$os_version" -ge "7" ]; then
			enable_trim_systemd
		else
			echo "WARNING: Failed to detect system version on the basis of /etc/os-release"
			install_static_trim
		fi
	elif [ "$os_id" = "debian" -o "$os_id" = "ubuntu" -o -f /etc/debian_version ]; then
		if [ "$os_version" -eq "7" ]; then
			install_static_trim
		elif [ "$os_version" -eq "8" -o "$os_version" -eq "9" ]; then
			for mod in service timer; do
				cp /usr/share/doc/util-linux/examples/fstrim.$mod /etc/systemd/system
			done
			enable_trim_systemd
		elif [ "$os_version" -eq "10" ]; then
			enable_trim_systemd
		elif [ "$os_version" -ge "1604" ]; then
			echo "Default fstrim settings should be ok for us"
		else
			install_static_trim
		fi
	elif [ "$os_id" = "opensuse" ]; then
		if [ "$os_version" -ge "421" ]; then
			enable_trim_systemd
		else
			install_static_trim
		fi
	elif [ "$os_id" = "sles" ]; then
		if [ "$os_version" -ge "12" ]; then
			enable_trim_systemd
		else
			install_static_trim
		fi
	fi
}

# Enable trim only if the tools are installed for the first time
[ -e /usr/share/qemu-ga/VERSION ] || enable_trim

# Shut down and then start QEMU GA manually in case it's not upgraded, or fails to restart properly
if [ "$IS_SYSTEMD" == "1" ]; then
	systemctl stop qemu-guest-agent
else
	/etc/init.d/qemu-guest-agent stop
fi

install_version
install_packages

if [ "$IS_SYSTEMD" == "1" ]; then
	systemctl daemon-reload
	systemctl start qemu-guest-agent
else
	if ! /etc/init.d/qemu-guest-agent status >/dev/null 2>&1; then
		/etc/init.d/qemu-guest-agent start
	fi
fi

if [ $ERROR_CODE -eq 0 ]; then
	echo "Done!"
else
	echo "There were errors during installation."
fi
exit $ERROR_CODE
