#!/bin/sh -e

##########################################################################
#   Script description:
#       Update all components of the system (packages, ports, base...)
#       
#   History:
#   Date        Name        Modification
#   2016-03-11  J Bacon     Begin
##########################################################################

usage()
{
    cat << EOM

Usage: $0 [--sync-pkg-cache user@host] [--binary | --binary+reboot | --defaults | --defaults+reboot | --yes]

user@host is used to rsync packages from another host that has been updated
more recently than this one.  This reduces load on the primary package servers
and may significantly speed up package upgrades.  The other host must have the
same architecture and OS version as this one.

EOM
    exit 1
}


##########################################################################
#   Function description:
#       Pause until user presses return
##########################################################################

pause()
{
    local junk
    
    printf "Press return to continue..."
    read junk
}


abuse_warning()
{
    cat << EOM
****************************************************************************

    Running auto-update-system non-interactively can overload the update
    servers if done in parallel.
    
    Run only one instance at a time.
    
    DO NOT ABUSE THIS FEATURE!!

****************************************************************************
EOM
    return 0
}


##########################################################################
#   Function description:
#       Record the last time of a full system update in hours since
#       the epoch
#       
#   History:
#   Date        Name        Modification
#   2021-06-13  J Bacon     Begin
##########################################################################

record_update_time()
{
    current_time=$(date '+%s')
    current_time=$((current_time / 3600))
    config_dir=$HOME/.config/auto-admin
    mkdir -p $config_dir
    printf "$current_time\n" > $config_dir/last-system-update
}


##########################################################################
#   Function description:
#       Remove unneeded dependencies
#
#   2018-01-13  Jason Bacon Begin
##########################################################################

freebsd_autoremove()
{
    if [ 0$autoremove != 0n ]; then
	printf "\nRemoving unneeded dependencies...\n"
	case $mode in
	interactive)
	    pkg autoremove
	    ;;
	*)
	    pkg autoremove -y
	    ;;
	esac
    fi
}

##########################################################################
#   Main
##########################################################################

: ${PORTSDIR:=/usr/ports}
export PORTSDIR

os_type=`auto-ostype`

if [ 0$1 = 0--help ]; then
    usage
fi

if ! auto-root-check $0; then
    # Prevent user from running a Trojan as root in the case their account
    # was compromised
    absolute="$(which $0)"
    # Don't count on -e being set at this point
    if ! auto-file-secure "$absolute"; then
	exit 1
    fi
    printf "Root "
    # exec quotes '$absolute --sync-pkg-cache', causing usage error
    # Assigning to cmd works around the problem
    cmd="$absolute $@"
    exec su -m root -c "$cmd"
fi

if [ 0$1 = 0--sync-pkg-cache ]; then
    if [ $# -ge 2 ]; then
	host=$2
	if echo $host | fgrep -q '@'; then
	    case $os_type in
	    FreeBSD)
		pkg_cache=/var/cache/pkg
		;;
	    Darwin|NetBSD)
		# FIXME: This depends on the bootstrap options
		pkg_cache=/var/db/pkgin/cache
		;;
	    RHEL)
		pkg_cache=/var/cache/yum
		;;
	    *)
		printf "$0: Not supported on $os_type.\n"
		exit 1
		;;
	    esac
	    printf "$host "
	    rsync -av ${host}:$pkg_cache/ $pkg_cache
	else
	    usage
	fi
	shift
	shift
    else
	usage
    fi
fi

mode=interactive    # Default
if [ $# = 1 ]; then
    if [ $1 = --binary ]; then
	mode=binary
    elif [ $1 = --binary+reboot ]; then
	mode=binary+reboot
    elif [ $1 = --defaults+reboot ]; then
	mode=defaults+reboot
    elif [ $1 = --defaults ]; then
	mode=defaults
    elif [ $1 = --yes ]; then
	mode=yes
    else
	usage
    fi
fi

case $os_type in
FreeBSD)
    : ${LOCALBASE:=/usr/local}
    if which svn > /dev/null; then
	svn=svn
    else
	svn=svnlite
    fi
    if [ ! -t 0 ]; then
	# Input redirected.  Being fed y/n responses.
	portsnap_flags=--interactive
	freebsd_update_flags=--not-running-from-cron
	abuse_warning
    fi
    
    # Don't group questions: Need to pause after each step to view output
    case $mode in
    binary|binary+reboot|defaults|defaults+reboot|yes)
	update_packages=y
	abuse_warning
	;;
    interactive)
	printf "Update installed packages? [y]/n "
	read update_packages
	;;
    esac
    
    if [ 0$update_packages != 0n ]; then
	critical_pkg_file=$LOCALBASE/etc/auto-admin/critical-packages
	if [ -e $critical_pkg_file ]; then
	    printf "Verifying availability of critical packages...\n"
	    printf "Use auto-mark-package-critical to update the list\n"
	    printf "or edit $critical_pkg_file.\n"
	    missing_packages=0
	    for pkg in $(cat $critical_pkg_file); do
		if pkg info -q $pkg; then
		    if pkg search "^${pkg}-[0-9].*" > /dev/null; then
			printf "$pkg is available.\n"
		    else
			printf "$pkg is installed and marked critical, but missing from the repository.\n"
			missing_packages=$(($missing_packages + 1))
		    fi
		fi
	    done
	    if [ $missing_packages -gt 0 ]; then
		cat << EOM

			       *** WARNING ***

Upgrading may remove the missing critical packages list above.
Check https://www.freebsd.org/support/bugreports/ for PRs related to the
packages, or try again later.  Most problems are fixed within a few days.

EOM
		if [ -t 1 ]; then
		    read -p "Continue with upgrades anyway? y/[n] " upgrade_anyway
		    if [ 0$upgrade_anyway != 0y ]; then
			exit
		    fi
		else
		    printf "stdout is not a terminal. Can't ask user what to do, so aborting.\n"
		    exit 1
		fi
	    fi
	fi
	case $mode in
	interactive)
	    printf "Autoremove unneeded packages? [y]/n "
	    read autoremove
	    ;;
	*)
	    autoremove=y
	    ;;
	esac
	
	freebsd_autoremove

	install_from_source=$LOCALBASE/etc/auto-admin/install-from-source
	if [ -e $install_from_source ]; then
	    printf "Cataloging install-from-source versions before upgrade...\n"
	    installed_versions_db=$HOME/.config/old-pkg-versions
	    mkdir -p $HOME/.config
	    rm -f $installed_versions_db
	    while read line; do
		port=$(echo $line | awk '{ print $1 }')
		pkgbase=$(auto-print-make-variable $port PKGBASE)
		installed_version=$(pkg query '%v' $pkgbase) || true
		printf "$pkgbase $installed_version\n" >> $installed_versions_db
	    done < $install_from_source
	fi
	
	auto_admin_old_version=$(pkg query '%v' auto-admin)
	printf "\n**** Updating installed packages...\n"
	pkg update
	pkg upgrade -y
	freebsd_autoremove
	pkg clean -y
	auto_admin_new_version=$(pkg query '%v' auto-admin)
	
	##########################################################################
	#   If auto-admin has been upgraded, restart auto-update-system
	#   to ensure use of the latest fixes
	##########################################################################
	
	if [ $auto_admin_new_version != $auto_admin_old_version ]; then
	    printf "Restarting with newer version of auto-update-system...\n"
	    auto-update-system "$@"
	fi
    fi
    
    case $mode in
    defaults|defaults+reboot|yes)
	update_ports=y
	portsnap_flags=--interactive
	;;
    binary|binary+reboot)
	update_ports=n
	;;
    interactive)
	printf "Update ports tree? [y]/n "
	read update_ports
	;;
    esac
    
    if [ 0$update_ports != 0n ]; then
	printf "\n**** Updating $PORTSDIR...\n"
	auto-check-ports-branch
	if [ -e $PORTSDIR/CHANGES ]; then
	    if [ -e $PORTSDIR/.svn ]; then
		$svn update $PORTSDIR
	    elif [ -e $PORTSDIR/.git ]; then
		(cd $PORTSDIR && git pull)
	    else
		printf "\nFetching latest ports...\n"
		portsnap fetch
		printf "Applying updates...\n"
		if [ -r $PORTSDIR/.portsnap.INDEX ]; then
		    portsnap $portsnap_flags update
		else
		    printf "$0: $PORTSDIR was not created by svn or portsnap.\n"
		    printf "Please remove $PORTSDIR and try again.\n"
		    exit
		fi
	    fi
	else
	    auto-ports-checkout
	fi
	if [ $(pkg -v) != $(auto-print-make-variable ports-mgmt/pkg PKGVERSION) ]; then
	    printf "Binary pkg is outdated.  Rebuilding from source...\n"
	    (cd $PORTSDIR/ports-mgmt/pkg && make -DBATCH clean stage \
		&& make -DBATCH deinstall reinstall)
	else
	    printf "Binary pkg is up-to-date.\n"
	fi
	if which wip-update; then
	    wip-update
	fi
	
	# Process ports marked by auto-mark-install-from-source
	printf "Checking for ports to be installed from source...\n"
	printf "Use auto-mark-install-from-source to update the list\n"
	printf "or edit $install_from_source.\n"
	set +e  # pkg query
	if [ -e $install_from_source ]; then
	    while read line; do
		port=$(echo $line | awk '{ print $1 }')
		pkgbase=$(auto-print-make-variable $port PKGBASE)
		installed_version=$(awk -v pkgbase=$pkgbase '$1 == pkgbase { print $2 }' $installed_versions_db)
		ports_version=$(auto-print-make-variable $port PKGVERSION)
		if [ ! -z $installed_version ]; then
		    if [ 0$ports_version != 0$installed_version ]; then
			reason=$(echo $line | awk '{ print $2 }')
			printf "Reinstalling $port from source, reason = $reason\n"
			printf "Installed: $installed_version\n"
			printf "Ports:     $ports_version\n"
			# This code is redundant with auto-install-packages,
			# but we don't use auto-install-packages here in case
			# auto-admin itself is marked for install from source
			# Remove other versions that might block install
			pkg remove -fy $(auto-print-make-variable $port PKGBASE) || true
			save_cwd=$(pwd)
			cd $PORTSDIR/$port
			make -DBATCH clean reinstall clean
			cd $save_cwd
		    else
			printf "$port is up-to-date.\n"
		    fi
		else
		    printf "$port is not installed.\n"
		fi
	    done < $install_from_source
	fi
	set -e
    fi
    
    plugin=$LOCALBASE/etc/auto-update-system-post-ports
    if [ -e $plugin ]; then
	owner=`stat -L $plugin | awk '{ print $5 }'`
	group_write=`stat -L $plugin | awk '{ print $3 }' | cut -c 6`
	world_write=`stat -L $plugin | awk '{ print $3 }' | cut -c 9`
	if [ $owner != root ]; then
	    printf "ERROR: $plugin is not owned by root!  You may have been hacked!\n"
	elif [ $group_write != - ]; then
	    printf "ERROR: $plugin is group writable!  You may have been hacked!\n"
	elif [ $world_write != - ]; then
	    printf "ERROR: $plugin is world writable!  You may have been hacked!\n"
	else
	    $plugin
	fi
    fi
    
    case $mode in
    binary|binary+reboot|defaults|defaults+reboot|yes)
	update_base=y
	freebsd_update_flags=--not-running-from-cron
	;;
    interactive)
	printf "Update base system? [y]/n "
	read update_base
	;;
    esac
    
    if [ 0$update_base != 0n ]; then
	printf "\n**** Updating base system...\n"
	if [ ! -e /boot.save ]; then
	    cp -Rp /boot /boot.save
	fi
	tmpfile=update-system.tmp
	freebsd-update $freebsd_update_flags fetch | tee $tmpfile
	
	if ! fgrep -q 'No updates' $tmpfile; then
	    freebsd-update $freebsd_update_flags install
	    if [ -e /boot/modules/ibcore.ko ]; then
		cat << EOM

Infiniband kernal modules detected.  Run auto-update-infiniband-modules, after
rebooting if a reboot is needed.

EOM
	    fi
	fi
	rm $tmpfile

	# FIXME: Temporary hack: Perms should be set by base
	# mandoc.db is automatically generated
	# Ensure it's readable on systems with restrictive root umask
	chmod 644 /usr/share/man/mandoc.db
    fi
    
    if [ 0$update_packages != 0n ] && [ 0$update_ports != 0n ] && \
       [ 0$update_base != 0n ]; then
	record_update_time
    fi
    ;;

RHEL)
    # Complete interrupted yum updates if the tool is installed and there
    # are any
    if ! which yum-complete-transaction; then
	yum install -y yum-utils
    fi
    case $mode in
    interactive)
	yum-complete-transaction || true
	yum update
	if [ $(auto-os-release) = RHEL7 ]; then
	    read -p 'Auto-remove unneeded dependencies? [y]/n ' autoremove
	    if [ 0$autoremove != 0n ]; then
		yum autoremove -y || true
	    fi
	fi
	;;
    binary|defaults)
	yum-complete-transaction || true
	yum update -y
	yum autoremove -y
	;;
    binary+reboot|defaults+reboot|yes)
	yes | yum-complete-transaction || true
	yes | yum update -y
	yum autoremove -y
	;;
    esac
    
    # auto-pkgsrc-prefix exits non-zero if no tree found
    PKGSRC=$(auto-pkgsrc-prefix) || true
    if [ -z "$PKGSRC" ]; then
	printf "$0: No active pkgsrc tree found.\n"
	exit 1
    fi
    if [ -e $PKGSRC/bin/pkgin ]; then
	auto-update-pkgsrc "$@"
	record_update_time
    fi
    
    if [ `auto-os-release` != RHEL6 ]; then
	needs-restarting -r || true
    fi
    printf "Services that need restarting:\n"
    needs-restarting
    ;;

NetBSD)
    auto-update-pkgsrc "$@"
    record_update_time
    
    printf "Update base system? [y]/n "
    read update_base
    if [ 0$update_base != 0n ]; then
	export CVS_RSH=ssh 
	if [ ! -e /usr/src ]; then
	    cd /usr
	    release=netbsd-`uname -r | cut -d . -f 1,2 | tr . - `
	    printf "Checking out $release src...\n"
	    pause
	    cvs -d anoncvs@anoncvs.NetBSD.org:/cvsroot co -r $release -P src
	    cd src
	else
	    cd /usr/src
	    cvs -q up -dP
	fi
	mkdir -p /usr/obj /usr/tools
	
	# Build tools
	# Added -U to silence warning
	./build.sh -O /usr/obj -T /usr/tools -U -u tools
	
	# Build kernel
	arch=$(uname -m)
	kern=AUTOUPDATE
	cp sys/arch/$arch/conf/GENERIC sys/arch/$arch/conf/$kern
	./build.sh -O ../obj -T ../tools -U kernel=$kern
	
	# Build userland
	./build.sh -O ../obj -T ../tools -U distribution
	
	# Install everything
	mv /netbsd /netbsd.old
	mv /usr/obj/sys/arch/$arch/compile/$kern/netbsd /

	cat << EOM

Rebooting...

After reboot, run the following to install the new userland:

cd /usr/src
./build.sh -O ../obj -T ../tools -U install=/
EOM
	pause
	shutdown -r now
    fi
    ;;

Darwin)
    # auto-pkgsrc-prefix exits non-zero if no tree found
    PKGSRC=$(auto-pkgsrc-prefix) || true
    if [ -z "$PKGSRC" ]; then
	printf "$0: No active pkgsrc tree found.\n"
	exit 1
    fi
    if [ -e $PKGSRC/bin/pkgin ]; then
	auto-update-pkgsrc "$@"
    fi
    
    # FIXME: Add softwareupdate logic for base updates
    case $mode in
    binary|binary+reboot|defaults|defaults+reboot|yes)
	update_base=y
	;;
    interactive)
	printf "Update base system? [y]/n "
	read update_base
	;;
    esac
    if [ 0$update_base != 0n ]; then
	printf "\n**** Updating base system...\n"
	softwareupdate --install --all
	record_update_time
    fi
    ;;

*)
    printf "$0: Not supported on $(auto-ostype).\n"
    exit 1
    ;;

esac

case $mode in
binary|defaults)
    reboot=n
    ;;
binary+reboot|defaults+reboot|yes)
    reboot=y
    ;;
interactive)
    printf "Reboot? y/[n] "
    read reboot
    ;;
esac
if [ 0$reboot = 0y ]; then
    printf "Rebooting...\n"
    sync    # Attempt to prevent Linux kernel update failures
    shutdown -r now
else
    cat << EOM

Be sure to reboot if you've updated the kernel, or restart any services
affected by package upgrades.

EOM
fi
