#!/bin/sh complain() { echo "ERROR: $*" logger -t "athena-auto-update" -p user.notice "$*" updstatus="failed" updmsg="$*" } warn() { updstatus="warning" updmsg="$*" echo "WARNING: $*" } save_success() { # We used to check for 'warning' here for non-fatal warnings. # There is no longer any such thing. updstatus="ok" updmsg="$*" } save_state() { rm -f $statfile echo "$updlast|$(date +"%s")|$updstatus|$updmsg" > $statfile } maybe_reboot() { if [ "$SKIP_REBOOT" = "y" ]; then return fi if [ -f /var/run/reboot-required ]; then # A package wants us to reboot the machine. Do so if no one is # logged in. Be paranoid about stale utmp entries. ttys=$(w -h -s | awk '{print $2}') for tty in $ttys; do pids=$(ps --no-heading -j -t "$tty" 2>/dev/null \ | awk '($1 == $3) {print $1}') if [ -n "$pids" ]; then return fi done # screen processes count as logins. if pgrep '^screen' > /dev/null; then return fi save_state reboot exit fi } SKIP_REBOOT="n" DEBUG="n" VERBOSE="n" while getopts "nvd" opt; do case "$opt" in d) DEBUG="y";; v) VERBOSE="y";; n) SKIP_REBOOT="y";; \?) echo "Usage: $0 [ -d ] [ -n ] [ -v ]" ;; esac done [ "$DEBUG" = "y" ] && VERBOSE="y" if [ 0 != "$(id -u)" ]; then echo "This script must be run as root." >&2 exit 1 fi # Don't run updates during a cluster login. # Unless forced if [ -e /var/run/athena-login ] && [ "$DEBUG" != "y" ]; then exit 0 fi # Avoid confusing the system by running two updates at once. pidfile=/var/run/athena-update.pid if [ -e $pidfile ]; then if ! kill -0 "$(cat $pidfile)" 2>/dev/null; then rm -f $pidfile fi fi (set -o noclobber; echo $$ > $pidfile) 2>/dev/null || exit trap 'rm -f $pidfile' EXIT statfile="/var/lib/athena-update-status" updstatus="unknown" updmsg="" updlast=$(date +"%s") # Get the last successful update if [ -f $statfile ]; then updlast=$(awk -F\| '{print $1;}' $statfile) updstatus=$(awk -F\| '{print $3;}' $statfile) fi # Make sure nothing expects input on stdin. exec &1 # Redirect further output to a log file. exec >>/var/log/athena-update 2>&1 # Write a log header now and a footer at exit. # Also write a target for cluster's /etc/nologin symlink. echo "-----" echo "** Beginning Athena auto-update at $(date)" cat > /var/run/athena-nologin << NOLOGIN This system is currently taking software updates. Please log in to a different system. NOLOGIN finish() { echo "** Ending Athena auto-update at $(date)" echo "-----" echo rm -f $pidfile rm -f /var/run/athena-nologin save_state exit } trap finish EXIT v() { [ "$VERBOSE" = "y" ] && echo "Running" "$@" >&3 echo "** Running:" "$@" "$@" } # Allow hesiod cluster info to specify the debathena apt release. # (Will do nothing if debathena-clusterinfo isn't installed.) [ -x /usr/sbin/save-cluster-info ] && v /usr/sbin/save-cluster-info cinfo=/var/run/athena-clusterinfo.sh slist=/etc/apt/sources.list.d/debathena.clusterinfo.list if [ -r "$cinfo" ] && ( [ ! -e "$slist" ] || [ -w "$slist" ] ); then echo "** Updating debathena.clusterinfo.list" [ -e "$slist" ] && rm "$slist" dsource="$(egrep -h '^deb(-src)? http://debathena\.mit\.edu/apt ' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null | sort -u)" if [ -n "$dsource" ]; then (. $cinfo echo "# This file is automatically updated by debathena-auto-update" echo "# based on your Hesiod cluster information. If you want to" echo "# make changes, do so in another file." echo case $APT_RELEASE in production) ;; proposed) echo "$dsource" | awk '$3 !~ /-/ {$3 = $3 "-proposed"; print}' ;; development) echo "$dsource" | awk '$3 !~ /-/ {$3 = $3 "-proposed"; print}' echo "$dsource" | awk '$3 !~ /-/ {$3 = $3 "-development"; print}' ;; esac ) > $slist else echo "Never mind, I can't figure out which sources.list line is Debathena's" fi else echo "** Skipping update of debathena.clusterinfo.list" fi # Tell apt not to expect user input during package installation. export DEBIAN_FRONTEND=noninteractive # Set conservative defaults in case file is missing UPDATE_FORCE_CONFFILE=old RUN_UPDATE_HOOK=no CLEANUP_OLD_KERNELS=no # Process defaults file [ -f /etc/default/debathena-auto-update ] && . /etc/default/debathena-auto-update # On cluster machines, force our desired settings # Ignore /etc/default/debathena-auto-update if dpkg-query --showformat '${Status}\n' -W "debathena-cluster" 2>/dev/null | grep -q ' installed$'; then CLEANUP_OLD_KERNELS=yes UPDATE_FORCE_CONFFILE=new RUN_UPDATE_HOOK=yes fi UPDATE_HOOK_URL="https://athena10.mit.edu/update-hook/debathena-update-hook.sh" UPDATE_HOOK_SUM="https://athena10.mit.edu/update-hook/debathena-update-hook-sha256sum" MITCA="/usr/share/debathena-auto-update/mitCA.crt" UPDATE_HOOK="/var/run/debathena-update-hook.sh" rm -f $UPDATE_HOOK if [ "$RUN_UPDATE_HOOK" = "yes" ] && \ curl -sf -o $UPDATE_HOOK --cacert $MITCA $UPDATE_HOOK_URL; then chmod 500 $UPDATE_HOOK SHA256SUM="$(curl -sf --cacert $MITCA $UPDATE_HOOK_SUM)" rv=$? if [ $rv != 0 ]; then complain "Failed to retrieve $UPDATE_HOOK_SUM (curl returned $rv)" exit fi LOCALSUM="$(sha256sum $UPDATE_HOOK | awk '{print $1}')" if [ "$SHA256SUM" != "$LOCALSUM" ]; then complain "bad update hook checksum ($SHA256SUM != $LOCALSUM)" exit fi if ! [ -f "/var/lib/athena-update-hooks/$SHA256SUM" ]; then if ! v $UPDATE_HOOK; then complain "update hook returned non-zero status" exit else touch "/var/lib/athena-update-hooks/$SHA256SUM" fi fi fi echo "Running apt-get install" if ! v apt-get --fix-broken --assume-yes install; then # Don't fail, because maybe dpkg --configure -a will save us echo "ERROR: apt-get install failed, but continuing anyway" fi # Configure any unconfigured packages (Trac #407) if ! v dpkg --configure -a; then complain "Failed to configure unconfigured packages." exit fi # A recently configured package may want a reboot save_success "Rebooted after dpkg --configure -a" maybe_reboot # Ensure that the mirrors aren't broken # Google's apt repos are ridiculously picky urls=$(cat /etc/apt/sources.list /etc/apt/sources.list.d/*.list | awk '!/^deb/ {next;} $2 ~ /^https/ {printf $2; if ($2 !~ /\/$/) printf "/"; printf "dists/"$3"/Release\n"}' | sort -u) failed="" for u in $urls; do curl -m 60 -sfL -o /dev/null $u if [ $? != 0 ]; then if [ -z "$failed" ]; then failed=$u else failed="$failed $u" fi fi done if [ -n "$failed" ]; then warn "Failed to contact mirror(s): $failed" exit fi # Update the apt cache. if ! v apt-get --assume-yes update; then complain "apt-get update failed" exit fi # If new licenses were installed since the last update, deal. licenses=/usr/share/debathena-license-config/debathena-license-selections if [ -f /var/lib/debathena-license-config/reconfigure_required ]; then rm -f /var/lib/debathena-license-config/reconfigure_required if [ -f $licenses ]; then for p in $(awk '{print $1}' $licenses); do if dpkg-query --showformat '${Status}\n' -W $p 2>/dev/null | grep -q ' installed$'; then if ! v dpkg-reconfigure -fnoninteractive $p; then # Don't fail here complain "Failed to dpkg-reconfigure $p" fi fi done else complain "Could not find $licenses" exit fi fi # Exit quietly (except for perhaps rebooting) if there are no upgrades # to take. pattern='^0 upgraded, 0 newly installed, 0 to remove' if v apt-get --simulate --assume-yes dist-upgrade | grep -q "$pattern"; then echo "Nothing to do!" save_success "No updates" maybe_reboot exit fi # Download packages first. echo "Downloading packages..." if ! v apt-get --quiet --assume-yes --download-only dist-upgrade; then complain "download failed" exit fi APTGET_OPTS= case $UPDATE_FORCE_CONFFILE in old) APTGET_OPTS="-o Dpkg::Options::=--force-confold" export UCF_FORCE_CONFFOLD=1 ;; new) APTGET_OPTS="-o Dpkg::Options::=--force-confnew" export UCF_FORCE_CONFFNEW=1 ;; *) complain "Invalid value for UPDATE_FORCE_CONFFILE" exit ;; esac # Perform the update. In some corner cases, apt-get might decide # that the best course of action is to remove the Debathena # metapackage, so be paranoid about that. result="$(apt-get --quiet --assume-yes --simulate dist-upgrade)" if [ $? -ne 0 ]; then complain "apt-get simulation failed!" elif echo "$result" | egrep -q '^Remv debathena-(cluster|workstation|login-graphical|login|standard|auto-update) '; then complain "metapackages would be removed by update:" $result else if v apt-get $APTGET_OPTS --quiet --assume-yes dist-upgrade; then # Successful update, reset $updlast updlast=$(date +"%s") save_success "Successful update" else complain "Simulation was successful, but update failed (shouldn't happen)" fi fi # Cleanup old kernels if [ "$CLEANUP_OLD_KERNELS" = "yes" ]; then echo "Cleaning up old kernels..." kernels=$(dpkg-query -W -f '${Package}:${Status}\n' linux-image-\*-generic | \ awk -F ':' '$2=="install ok installed" {print $1;}' | \ sed -e 's/^linux-image-//' | sort -V) numkernels=$(echo "$kernels" | wc -l) echo "Found $numkernels kernels: $kernels" if [ $numkernels -gt 2 ]; then toremove=$(echo "$kernels" | head -$(($numkernels-2))) kpkgs= for k in $toremove; do if [ "$(uname -r)" != "$k" ]; then kpkgs="$kpkgs linux-image-$k" fi done echo "Will remove these kernels: $kpkgs" echo "Simulating..." if v apt-get -y -s remove $kpkgs; then echo "Simulation ok, proceeding..." if ! v apt-get -y remove $kpkgs; then warn "Something went wrong trying to remove kernels!" fi else warn "Simulation failed!" fi fi fi # Finally, update the apt-file cache v apt-file update maybe_reboot exit