#!/bin/sh

# Set up apt-auto on a Debian host.

# Copyright (C) 2009, 2010 Bob Proulx
#
# 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 3 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/>.

# Written by Bob Proulx <bob@proulx.com>

VERSION=0

test -x /usr/bin/dpkg || exit 1
test -x /usr/bin/apt-get || exit 1
test -r /etc/debian_version || exit 1

progname=$(basename $0)

print_help()
{
    # Create a help message suitable for help2man to format into a man
    # page directly from the online --help and --version message.
cat <<'EOF'
Usage: apt-auto [options]

Set up apt-auto on a Debian host.

Options:
     --help     output help message
     --quiet    output less information
     --version  output version information

Examples:

The most common use is to run the script.

  $ apt-auto-setup

But it is also possible to pull this from the web and run it on the
fly.  Since wget is now part of a minimum debootstrap'd image no
additional dependencies need to be installed.

  $ wget -q --no-cache -O - http://www.proulx.com/~bob/debian/apt-auto | sh

Report bugs to Bob Proulx <bob@proulx.com>
EOF
}

print_version()
{
    cat <<EOF
$progname $VERSION
Written by Bob Proulx.

Copyright (C) 2010 Bob Proulx
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
}

SHORTOPTS="qv"
LONGOPTS="help,quiet,verbose,version"

if $(getopt -T >/dev/null 2>&1) ; [ $? = 4 ] ; then # New longopts getopt.
    OPTS=$(getopt -o $SHORTOPTS --long $LONGOPTS -n "$progname" -- "$@")
else # Old classic getopt.
    # Special handling for --help and --version on old getopt.
    case $1 in --help) print_help ; exit 0 ;; esac
    case $1 in --version) print_version ; exit 0 ;; esac
    OPTS=$(getopt $SHORTOPTS "$@")
fi

if [ $? -ne 0 ]; then
    echo "'$progname --help' for more information" 1>&2
    exit 1
fi

eval set -- "$OPTS"

quiet=false
verbose=false
while [ $# -gt 0 ]; do
    : debug: $1
    case $1 in
        --help)
            print_help
            exit 0
            ;;
        --quiet|-q)
            quiet=true
            shift
            ;;
        --verbose|-v)
            verbose=true
            shift
            ;;
        --version)
            print_version
            exit 0
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Internal Error: option processing error: $1" 1>&2
            exit 1
            ;;
    esac
done

if ! (apt-get --version) >/dev/null 2>&1; then
  : No apt-get available so not an apt machine, exiting.
  exit 1
fi

if ! (dpkg --version) >/dev/null 2>&1; then
  : No dpkg available so not an dpkg machine, exiting.
  exit 1
fi

if [ ! -f /etc/apt/sources.list ]; then
  echo "Error: No /etc/apt/sources.list" 1>&2
  echo "Not running on a Debian GNU/Linux machine?" 1>&2
  exit 1
fi

if [ ! -w / ]; then
  echo "Error: Must be root to run this script" 1>&2
  exit 1
fi

case $(</etc/debian_version) in
    3.0*) debver="woody" ;;
    3.1*) debver="sarge" ;;
    4.0*) debver="etch" ;;
    5.0*) debver="lenny" ;;
    *)
	echo "Unknown /etc/debian_version found."
	exit 1
	;;
esac

case $debver in
  lenny) : okay, working with Lenny, FIXME experimental ;;
  *)
    echo "Error: This script only handles Debian Lenny machines." 1>&2
    exit 1
    ;;
esac

tdir=""
trap 'rm -rf $tdir' EXIT
tdir=$(mktemp -d /tmp/apt-auto-setup.XXXXXXXX) || exit 1

# Utility Routines

mypurgepkg()
{
    for pkgname in "$@"; do
	if [ -d /usr/share/doc/$pkgname ]; then
	    dpkg --purge $pkgname
	fi
    done
}

myinstallpkg()
{
    for pkgname in "$@"; do
	if [ ! -d /usr/share/doc/$pkgname ]; then
	    apt-get install -y $pkgname
	fi
    done
}

myinstallbin()
{
    src=$1
    dst=$2
    mkdir -p "$(dirname "$dst")"
    test -f "$dst" || touch "$dst"
    if ! cmp -s "$src" "$dst"; then
	cp "$src" "$dst"
    fi
    case $(ls -ldog "$dst" | awk '{print$1}') in
	-rwxr-xr-x) : okay permissions "$dst" ;;
	*) chmod u=rwx,go=rx "$dst" ;;
    esac
    $quiet || echo "Info: Installed $dst"
}

# Quiet down the installer.
export DEBIAN_FRONTEND=noninteractive
export DEBCONF_ADMIN_EMAIL=""
test -t 0 && exec </dev/null

cat >$tdir/detect-if-reboot-needed <<\EOFDETECTIFREBOOT
#!/bin/sh

# A simple brute force script to detect if a reboot is needed to
# install the latest installed kernel.
#
# Copyright 2008, 2009, 2010 Bob Proulx <bob@proulx.com>
#
# 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/>.

verbose=true

# Walk through the logs from newest to oldest.  This can be fooled by
# root foolishly or maliciously touching timestamps on files, which is
# unlikely.  Besides as new updates appear the newest file will always
# contain the newest data and that problem will automatically correct
# itself.  Most importantly it allows us to efficiently process the
# least number of files and to stop as soon as we find the date of the
# last kernel installation.
# Be sure to handle both types of log rotation styles.
#   /var/log/dpkg.log
#   /var/log/dpkg.log-20100201
#   /var/log/dpkg.log-20100101.gz
#   /var/log/dpkg.log.1
#   /var/log/dpkg.log.2.gz
# Example data line format.
#   2008-02-11 10:59:19 status installed linux-image-2.6.18-6-686 2.6.18.dfsg.1-18etch1
for f in $(ls -t1 /var/log/dpkg.log*); do
  case $f in
    *.gz) cat=zcat ;;
    *) cat=cat ;;
  esac
  install_line="$($cat $f | awk '/status installed linux-image-/{print$1,$2}' | sort -r | head -n1)"
  if [ -n "$install_line" ]; then
    installed_at=$(date -d "$install_line" +%s)
    if [ -n "$installed_at" ]; then
      break
    fi
  fi
done
if [ -z "$installed_at" ]; then
  echo "Error: Unable to determine when kernel was installed." 1>&2
  exit 1
fi

# 'LC_ALL=C who -b' format:
#          system boot  2007-10-08 16:00
# 'LC_ALL=en_US.UTF-8 who -b' format:
#          system boot  2008-08-23 10:35
booted_at=$(date -d "$(LC_ALL=en_US.UTF-8 who -b | awk '/system boot/{print $3,$4}')" +%s)
if [ -z "$booted_at" ]; then
  echo "Error: Unable to determine when system was last booted." 1>&2
  exit 1
fi

# One small problem is that install time is logged to the second but
# system boot time is only logged to the minute.  It is possible to
# install and reboot within the minute.  Need to round down the
# install time to the previous minute too.

install_seconds=$(( $installed_at % 60 ))
installed_at=$(( $installed_at - $install_seconds ))

if [ $(($installed_at - $booted_at)) -gt 0 ]; then
  if $verbose; then
    echo installed=$installed_at $(date -R -d @$installed_at)
    echo booted...=$booted_at $(date -R -d @$booted_at)
    echo "Reboot is needed to boot newly installed kernel."
    echo uptime since kernel install $(($installed_at - $booted_at))
  fi
  exit 1
fi

exit 0
EOFDETECTIFREBOOT
src="$tdir/detect-if-reboot-needed"
dst="/usr/local/bin/detect-if-reboot-needed"
myinstallbin "$src" "$dst"

cat >$tdir/sources.list <<EOF
deb http://ftp.us.debian.org/debian/ $debver main contrib non-free
deb-src http://ftp.us.debian.org/debian/ $debver main contrib non-free

deb http://volatile.debian.org/debian-volatile $debver/volatile main contrib non-free
deb-src http://volatile.debian.org/debian-volatile $debver/volatile main contrib non-free

deb http://security.debian.org/ $debver/updates main contrib non-free
deb-src http://security.debian.org/ $debver/updates main contrib non-free
EOF
mkdir -p /usr/local/etc/apt-auto
test -f /usr/local/etc/apt-auto/sources.list \
    || touch /usr/local/etc/apt-auto/sources.list
if ! cmp -s $tdir/sources.list /usr/local/etc/apt-auto/sources.list; then
    cp $tdir/sources.list /usr/local/etc/apt-auto/sources.list
fi

cat >$tdir/apt-auto <<\EOFAPTAUTO
#!/bin/sh
#
# apt-auto -- Run APT automatically to upgrade a system.
#

#
# Copyright (C) 2007, 2008, 2009, 2010 by Bob Proulx
#
# 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 3 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/>.
#

# Written by Bob Proulx <bob@proulx.com>.

PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

progname=$(basename $0)

print_help()
{
    # This script is designed to use help2man to build hteman page
    # directly from the script itself.
    #   help2man $0
    # Check the man page formatting after making changes here.
    cat <<'EOF'
Usage: apt-auto [options]

Run APT automatically to upgrade a system.

This script is designed to be run from cron to run apt automatically
to upgrade a system specifically for security upgrades
non-interactively.

Options:
  -d, --download-only   only download the files
  -i, --interactive     run apt interactively
  -c, --clean           run apt --clean to remove cached deb files
  -n, --print-only      run apt -s to dry-run
  -q, --quiet           print less output
  -s, --simulate        alias for -n,--print-only

Examples:

The most common use is to run the script simply with no options.

  $ apt-auto

Next most often used is with --download-only.

  $ apt-auto --download-only

This is usually done by cron in /etc/cron.daily/apt-auto

  #!/bin/bash
  test -t 0 || sleep $(( $RANDOM % 3600 ))
  exec apt-auto
Or:
  #!/bin/bash
  test -t 0 || sleep $(( $RANDOM % 3600 ))
  exec apt-auto --download-only

EOF
}

SHORTOPTS="dicnqs"
LONGOPTS="download-only,interactive,clean,print-only,quiet,simulate"

if $(getopt -T >/dev/null 2>&1) ; [ $? = 4 ] ; then # New longopts getopt.
    OPTS=$(getopt -o $SHORTOPTS --long $LONGOPTS -n "$progname" -- "$@")
else # Old classic getopt.
    # Special handling for --help and --version on old getopt.
    case $1 in --help) print_help ; exit 0 ;; esac
    case $1 in --version) print_version ; exit 0 ;; esac
    OPTS=$(getopt $SHORTOPTS "$@")
fi

if [ $? -ne 0 ]; then
    echo "'$progname --help' for more information" 1>&2
    exit 1
fi

eval set -- "$OPTS"

clean=false
downloadonly=false
interactive=false
quietflag=false
simulate=false
while [ $# -gt 0 ]; do
    case $1 in
	-d|--download-only)
	    downloadonly=true
	    aptopts="-d"
	    shift
	    ;;
	-i|--interactive)
	    interactive=true
	    aptopts=""
	    shift
	    ;;
	-c|--clean)
	    clean=true
	    shift
	    ;;
	-q|--quiet)
	    quietflag=true
	    shift
	    ;;
	-n|-s|print-only|simulate)
	    simulate=false
	    shift
	    ;;
	--)
	    shift ; break
	    ;;
	*)
	    echo "Internal Error: invalid arguments: $1" 1>&2
	    exit 1
	    ;;
    esac
done

if [ $# -gt 0 ]; then
    echo "Error: too many arguments" 1>&2
    exit 1
fi

if [ ! -w / ]; then
    echo "Error: Superuser access is required." 1>&2
    exit 1
fi

aptopts=""

if $downloadonly; then
    aptopts="$aptopts -d"
fi

if ! $interactive; then
    aptopts="$aptopts -y"
fi

if $simulate; then
    aptopts="$aptopts -s"
fi

umask 02

###############################################################################

ADDR=root
PREFIX=/usr/local
CACHE_DIR=$PREFIX/var/cache/apt-auto
CNF_DIR=$PREFIX/etc/apt-auto

CONFIG=$CNF_DIR/apt.conf

if [ ! -f $CNF_DIR/sources.list ]; then
    echo "Error: Must configure $CNF_DIR/sources.list" 1>&2
    exit 1
fi

if [ ! -f "$CONFIG" ]; then
    # If config does not exist then create a default one on the fly.
    trap 'rm -f $TMPFILE' EXIT
    TMPFILE=$(mktemp) || exit 1
    cat >$TMPFILE <<EOF
Acquire::http::No-Cache "true";
APT::Quiet "true";
APT::Get::Show-Upgraded "true";
APT::Get::Remove "false";
APT::Get::List-Cleanup "false";
Dir::Etc::SourceList "$CNF_DIR/sources.list";
Dir::State::Lists "$CACHE_DIR/lists";
Dir::Cache "$CACHE_DIR/cache";
DPkg::Pre-Install-Pkgs {"/usr/sbin/dpkg-preconfigure --apt";};
DPkg::Options {"--force-confmiss";"--force-confold"};
EOF
    CONFIG=$TMPFILE
fi

logging=false
if [ ! -t 0 ]; then
    test -f $CACHE_DIR/log && savelog -q $CACHE_DIR/log
    mkdir -p $CACHE_DIR || exit 1
    exec >>$CACHE_DIR/log 2>&1
    quiet=--quiet
    logging=true
fi

$quietflag && reallyquiet=--quiet=2 || reallyquiet=""

$logging && echo APT-AUTO BEGIN: $(date -R)

cleanup()
{
    $logging && echo APT-AUTO END: $(date -R)

    # The apt-get log line looks like this:
    #   0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
    if [ -f $CACHE_DIR/log ]; then
        if ! grep -q "^0 upgraded" $CACHE_DIR/log; then
	    mailx -s "apt-auto completed on $(hostname)" $ADDR < $CACHE_DIR/log
        fi
    fi
    rm -f $TMPFILE
}

trap 'cleanup' EXIT

if [ ! -f $CONFIG ]; then
  echo "Error: missing $CONFIG file" 1>&2
  exit 1
fi

mkdir -p $CACHE_DIR/lists/partial || exit 1
mkdir -p $CACHE_DIR/cache/archives/partial || exit 1

if $clean; then
  if ! apt-get --config-file $CONFIG clean; then
    echo "Error: apt-get clean exited with errors." 1>&2
    exit 1
  fi
  exit 0
fi

export DEBIAN_FRONTEND=noninteractive
export DEBCONF_ADMIN_EMAIL=""

$quietflag || echo apt-get --config-file $CONFIG update
if ! apt-get --config-file $CONFIG $reallyquiet update; then
  echo "Error: apt-get update exited with errors." 1>&2
  exit 1
fi

echo apt-get --config-file $CONFIG $quiet -u $aptopts dist-upgrade
if ! apt-get --config-file $CONFIG $quiet -u $aptopts dist-upgrade; then
  echo "Error: apt-get dist-upgrade exited with errors." 1>&2
  exit 1
fi

if ! apt-get --config-file $CONFIG --quiet=2 autoclean; then
  echo "Error: apt-get autoclean exited with errors." 1>&2
  exit 1
fi

# Since noatime is such a popular fstab mount option avoid using atime.
find "$CACHE_DIR/cache/archives" -maxdepth 2 -name '*.deb' -ctime +3 \
  -exec rm -f {} +

if ! detect-if-reboot-needed; then
  echo "Reboot is needed to install new kernel." 1>&2
fi

exit 0
EOFAPTAUTO
src="$tdir/apt-auto"
dst="/usr/local/bin/apt-auto"
myinstallbin "$src" "$dst"

cat >$tdir/apt-auto <<\EOF
#!/bin/bash
test -t 0 || sleep $(( $RANDOM % 3600 ))
exec apt-auto
EOF
src="$tdir/apt-auto"
dst="/etc/cron.daily/apt-auto"
myinstallbin "$src" "$dst"

exit 0
