#!/bin/bash
#
# auter is a yum-cron type package which implements automatic updates on an
# individual server with features such as predownloading packages and reboots.
#
#
# Copyright 2016 Rackspace, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use
# this file except in compliance with the License.  You may obtain a copy of the
# License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations under the License.
#


declare -r -x AUTERVERSION="1.0.0"
declare -r -x SCRIPTDIR="/etc/auter"
declare -r -x DATADIR="/var/lib/auter"
declare -r -x LOCKFILE="${DATADIR}/enabled"
declare -r -x PIDFILE="/var/run/auter/auter.pid"

# Set default options - these can be overridden in the config file or with a command line argument
declare -x -l AUTOREBOOT="no"
declare -x -i REBOOTCALL=0
declare -x -a PACKAGEMANAGEROPTIONS
declare -x -l PREDOWNLOADUPDATES="yes"
declare -x -l ONLYINSTALLFROMPREP="no"
declare -x CONFIGFILE="/etc/auter/auter.conf"
declare -x DOWNLOADDIR="/var/cache/auter"
declare -x -i MAXDELAY=3600
declare -x CONFIGSET="default"
declare -x ROTATE="5"
declare -x PREPREPSCRIPTDIR="${SCRIPTDIR}/pre-prep.d"
declare -x POSTPREPSCRIPTDIR="${SCRIPTDIR}/post-prep.d"
declare -x PREAPPLYSCRIPTDIR="${SCRIPTDIR}/pre-apply.d"
declare -x POSTAPPLYSCRIPTDIR="${SCRIPTDIR}/post-apply.d"
declare -x PREREBOOTSCRIPTDIR="${SCRIPTDIR}/pre-reboot.d"
declare -x POSTREBOOTSCRIPTDIR="${SCRIPTDIR}/post-reboot.d"
declare -x -i SKIPALLSCRIPTS=0
declare -x -a SKIPPHASESCRIPTS
declare -x -a SKIPSCRIPTNAMES

# Adding extra super-user PATHS if required
for p in /usr/local/sbin /usr/sbin /sbin; do
  [[ ! ${PATH} =~ (^|:)${p}(:|$) ]] && declare -x PATH="${p}:${PATH}"
done

function default_signal_handling() {
  trap 'rm -f "${PIDFILE}"' SIGINT SIGTERM
}

# The man page is generated in part from the print_help() text by running:
#   help2man --include=auter.help2man --no-info ./auter > auter.man
function print_help() {
  echo "
Usage: auter [--enable|--disable|--status] [--prep] [--apply] [--reboot] [--postreboot] [--config=<configfile>] [OPTIONS]

Automatic Update Transaction Execution by Rackspace. A wrapper around cron and yum/dnf/apt to manage system updates with the ability to configure automatic reboots and custom scripts.

Actions:
  --enable      Enable auter
  --disable     Disable auter. Also deletes unused pidfile if it exists
  --status      Show whether enabled or disabled
  --prep        Pre-download updates before applying
  --apply       Apply updates, and reboot if AUTOREBOOT=yes
  --reboot      Reboot system including pre/post reboot scripts
  --postreboot  Run post reboot script

Options:
  --config=FILE Specify the full path to an auter config file. Defaults to /etc/auter/auter.conf
  --stdout      Always log to STDOUT, regardless of not having a tty
  --maxdelay    Override MAXDELAY from the command line
  --skip-all-scripts
                Skip the executions of all custom scripts (Default in /etc/auter/*.d/)

  --skip-scripts-by-phase=PHASE
                Skip the execution of the custom scripts for the specified phase. You can specify myltiple phases.

                Valid Phases: pre-prep, post-prep, pre-apply, post-apply, pre-reboot, post-reboot.

                Example: --skip-scripts-by-phase=\"pre-prep,post-apply,pre-reboot\"

  --skip-scripts-by-name=SCRIPTNAME
                Skip specific scripts by name. You can specify myltiple phases.

                Example: --skip-scripts-by-name=\"10-configsnap-pre, 20-startApp.sh\"

  --no-wall	If possible, suppress shutdown wall messages in the reboot phase
  -h, --help    Show this help text
  -v, --version Show the version
"
}

function logit() {
  # If running on a tty, or the --stdout option is provided, print to screen
  ( tty -s || [[ $STDOUT ]] ) && echo "$1"
  logger -p info -t auter "$1"
}

function read_config() {
  if [[ -f "${CONFIGFILE}" ]]; then
    source "${CONFIGFILE}"
  elif [[ "${CUSTOMCONFIG}" ]]; then
    logit "ERROR: Custom config file ${CONFIGFILE} does not exist"
    quit 5
  else
    logit "WARNING: Using default config values."
  fi

  IFS=' ' read -r -a PACKAGEMANAGEROPTIONS <<< "${PACKAGEMANAGEROPTIONS[*]}"
}

function rotate_file {
  declare OUTPUT_FILE="$1"
  declare -a REMOVE_FILES

  #Rotate old file
  for i in $(seq $((ROTATE-1)) -1 1); do
    [[ -e "${OUTPUT_FILE}.$i" ]] && mv -f "${OUTPUT_FILE}.$i" "${OUTPUT_FILE}.$((i+1))"
  done

  # Move base files to basefile.1
  [[ -e "${OUTPUT_FILE}" ]] && mv "${OUTPUT_FILE}" "${OUTPUT_FILE}.1"

  readarray -t REMOVE_FILES <<< "$(find "$DATADIR" -type f -name "$(basename "$OUTPUT_FILE").*" | awk -F'.' -v s="$ROTATE" '($NF+1)>s')"
  # Finally check for extra file from the rotation, and remove if it exists
  [[ -n "${REMOVE_FILES[*]}" ]] && rm -f "${REMOVE_FILES[@]}"
}

function run_script {
  SCRIPT="$1"
  PHASE="$2"
  if [[ "${SKIPALLSCRIPTS}" -eq 0 ]]; then
    SKIP=0
    # Check if the phase scripts have been skipped with --skip-scripts-by-phase
    if [[ -n "${SKIPPHASESCRIPTS[*]}" ]]; then
      for SKIPPHASESCRIPT in "${SKIPPHASESCRIPTS[@]}"; do
        [[ "${SKIPPHASESCRIPT,,}" == *"${PHASE,,}"* ]] && SKIP=1
      done
    fi

    # Check if the script has been excluded with --skip-scripts-by-name
    if [[ -n "${SKIPSCRIPTNAMES[*]}" ]]; then
      for SKIPSCRIPTNAME in "${SKIPSCRIPTNAMES[@]}"; do
        [[ ${SCRIPT,,} == *"${SKIPSCRIPTNAME,,}"* ]] && SKIP=1
      done
    fi

    if [[ "${SKIP}" -eq 0 ]]; then
      if [[ -x "${SCRIPT}" ]] && [[ -f "${SCRIPT}" ]]; then
        logit "INFO: Running ${PHASE} script ${SCRIPT}"
        ${SCRIPT}
        local RC=$?
        if [[ "${RC}" -ne 0 ]]; then
          logit "ERROR: ${PHASE} script ${SCRIPT} exited with non-zero exit code ${RC}. Aborting auter run."
          quit 8
        fi
      elif [[ -f "${SCRIPT}" ]]; then
        logit "ERROR: ${PHASE} script ${SCRIPT} exists but the execute bit is not set. Skipping."
      fi
    else
      logit "INFO: Skipping script ${SCRIPT}"
    fi
  else
    logit "INFO: The --skip-all-scripts flag was used. NOT executing ${SCRIPT} as part of the ${PHASE} phase"
  fi
}

# Check whether yum, or dnf is available
function check_package_manager() {
  if [[ -x /usr/bin/dnf ]]; then
    echo dnf
  elif [[ -x /usr/bin/yum ]]; then
    echo yum
  elif [[ -x /usr/bin/apt-get ]]; then
    echo apt-get
  else
    logit "ERROR: Cannot find yum, dnf or apt-get"
    exit 7
  fi
}

function reboot_server() {
  for SCRIPT in "${PREREBOOTSCRIPTDIR}"/*; do
    run_script "${SCRIPT}" "Pre-Reboot"
  done

  if [[ -d "${POSTREBOOTSCRIPTDIR}" ]]; then
    logit "INFO: Creating post-reboot hook /etc/cron.d/auter-postreboot-${CONFIGSET}"
    echo -e "@reboot root /usr/bin/auter --postreboot --config ${CONFIGFILE}" > "/etc/cron.d/auter-postreboot-${CONFIGSET}"
    chown root.root "/etc/cron.d/auter-postreboot-${CONFIGSET}"
    chmod 0644 "/etc/cron.d/auter-postreboot-${CONFIGSET}"
  fi

  logit "INFO: Rebooting server"
  if [[ ${NOWALLMSG} -eq 1 ]] && shutdown --help | grep -q "no-wall"; then
    /sbin/shutdown --no-wall -r +2 "auter: System reboot to apply updates" &>/dev/null &
  else
    /sbin/shutdown -r +2 "auter: System reboot to apply updates" &>/dev/null &
  fi
}

function post_reboot() {
  logit "INFO: Removed post-reboot hook: /etc/cron.d/auter-postreboot-${CONFIGSET}"
  rm -f "/etc/cron.d/auter-postreboot-${CONFIGSET}"
  tty -s || sleep 300

  for SCRIPT in "${POSTREBOOTSCRIPTDIR}"/*; do
    run_script "${SCRIPT}" "Post-Reboot"
  done
}

function print_status() {
  if [[ -f "${LOCKFILE}" ]] && [[ -f "${PIDFILE}" ]]; then
    if CURRENTPIDSTATUS="$(kill -0 "$(cat ${PIDFILE})" 2>&1 )"; then
      echo "auter is currently enabled and running"
    elif [[ "${CURRENTPIDSTATUS}" == *"No such process"* ]]; then
      echo "auter is currently enabled and pid file exists but process is dead"
    elif [[ "${CURRENTPIDSTATUS}" == *"Operation not permitted"* ]]; then
      echo "auter is enabled but permission denied on ${PIDFILE}. Run 'auter --status' as root"
    fi
  elif [[ -f "${LOCKFILE}" ]] && [[ ! -f "${PIDFILE}" ]]; then
    echo "auter is currently enabled and not running"
  else
    echo "auter is currently disabled"
  fi
}

# Needed to cleanup our PID file. The only argument is the exit code to use.
function quit() {
  [[ -f "$PIDFILE" ]] && rm -f "$PIDFILE"
  exit "$1"
}

function log_last_run() {
    logit "INFO: Auter successfully ran at $(date -Iseconds)"
}


# Main

# Make sure we trap signals and clean up the PID before exiting
default_signal_handling

ARGS="$*"
if ! OPTS=$(getopt -n "$0" -o h,v --long prep,apply,enable,disable,reboot,postreboot,version,help,stdout,no-wall,status,config:,skip-all-scripts,skip-scripts-by-phase:,skip-scripts-by-name:,maxdelay: -- "$@"); then
  echo "See '$0 --help' for valid options."
  quit 1
fi

eval set -- "$OPTS"
unset OPTS

while true ; do
  case "$1" in
    -h|--help) print_help ; exit 0;;
    -v|--version) echo "auter ${AUTERVERSION}" ; exit 0;;
    --stdout) STDOUT=1 ; shift;;
    --no-wall) NOWALLMSG=1 ; shift;;
    --maxdelay) __MAXDELAY="$2" ; shift 2;;
    --config) CONFIGSET="" ; CONFIGFILE="$2" ; CUSTOMCONFIG=1 ; shift 2;;
    --prep) PREP=1 ; shift;;
    --apply) APPLY=1 ; shift;;
    --reboot) REBOOTCALL=1 ; shift;;
    --postreboot) POSTREBOOT=1 ; shift;;
    --enable) ENABLE=1 ; shift;;
    --disable) DISABLE=1 ; shift;;
    --skip-all-scripts) SKIPALLSCRIPTS=1 ; shift;;
    --skip-scripts-by-phase) IFS=',' read -r -a SKIPPHASESCRIPTS <<< "$2" ; shift 2;;
    --skip-scripts-by-name) IFS=',' read -r -a SKIPSCRIPTNAMES <<< "$2" ; shift 2;;
    --status) print_status ; quit 0;;
    --) shift ; break;;
  esac
done

[[ ! "$ARGS" =~ --(prep|apply|reboot|postreboot|enable|disable)  ]] && print_help && quit 1

readonly PACKAGE_MANAGER=$(check_package_manager)

# Do this after option processing so --help and --status still work.
if [[ "$(whoami)" != "root" ]]; then
  echo "Script must be run as root"
  exit 5
fi

if [[ ! -d "${DATADIR}" ]]; then
  logit "FATAL ERROR: auter DATADIR ${DATADIR} does not exist."
  exit 5
fi

if [[ "${ENABLE}" ]] ; then
  touch "${LOCKFILE}"
  echo "DO NOT DELETE THIS FILE. This file is automatically generated by auter. To disable auter, run auter --disable instead." > "${LOCKFILE}"
  logit "INFO: auter enabled"
  exit 0
fi

if [[ "${DISABLE}" ]] ; then
  rm -f "${LOCKFILE}"
  if [[ -f "${PIDFILE}" ]] && ! kill -0 "$(cat ${PIDFILE})" &>/dev/null; then
      rm -f "${PIDFILE}"
      logit "INFO: auter disabled and cleared pid file"
  else
    logit "INFO: auter disabled"
  fi
  exit 0
fi

if [[ ! -f "${LOCKFILE}" ]]; then
  logit "WARNING: auter disabled. Please run auter --enable to enable automatic updates."
  exit 4
fi

# PID file checking to make sure multiple copies of auter don't run at once.
PIDDIR=$(dirname "${PIDFILE}")
if [[ ! -d "${PIDDIR}" ]]; then
  install -m 755 -o root -g root -d "${PIDDIR}"
fi

# Note: ALL script exits after this block must use the quit() function instead so the PIDfile is cleaned up.
if [[ -f "${PIDFILE}" ]]; then
  logit "ERROR: auter is already running or ${PIDFILE} exists."
  exit 6
else
  echo "$$" > "${PIDFILE}"
fi

read_config

# CONFIGSET needs to be set if we're using a custom configuration file.
if [[ -z "${CONFIGSET}" ]]; then
  logit "ERROR: You must specify the CONFIGSET variable in custom config file ${CONFIGFILE} to avoid naming collisions"
  quit 5
fi

if [[ "${ONLYINSTALLFROMPREP}" == "yes" ]]; then
  if [[ ! -d "${DOWNLOADDIR}/${CONFIGSET}" ]]; then
    install -m 755 -o root -g root -d "${DOWNLOADDIR}/${CONFIGSET}"
  elif [[ $(stat -c %G%U%a "${DOWNLOADDIR}") != rootroot[0-9][0-9][0145] ]]; then
    logit "ERROR: ${DOWNLOADDIR}/${CONFIGSET} does not have the correct permissions."
    quit 3
  fi
fi

# Validate the SKIPPHASESCRIPTS values
if [[ -n "${SKIPPHASESCRIPTS[*]}" ]]; then
  for SKIPPHASESCRIPT in "${SKIPPHASESCRIPTS[@]}"; do
    if [[ "${SKIPPHASESCRIPT}" =~ ^(pre|post)-(prep|apply|reboot)$ ]]; then
       logit "INFO: The --skip-scripts-by-phase argument was used. Skipping ${SKIPPHASESCRIPT} scripts"
    else
       logit "ERROR: The --skip-scripts-by-phase argument was used with an invalid option: '${SKIPPHASESCRIPT}'. Exiting"
       quit 1
    fi
  done
fi

logit "INFO: Running with: $0 ${ARGS}"

# If --maxdelay is set on the command line, override the config file.
if [[ -n "${__MAXDELAY}" ]]
then
   MAXDELAY="${__MAXDELAY}"
   logit "INFO: Overriding MAXDELAY from command line: --maxdelay=${__MAXDELAY}"
else
  tty -s && MAXDELAY=1 && logit "INFO: Running in an interactive shell, disabling all random sleeps"
fi
[[ "${MAXDELAY}" -lt 1 ]] && MAXDELAY=1

# There is an explicit quit here to avoid auter automatically running any
# other unexpected functions.
[[ "${POSTREBOOT}" ]] && post_reboot && quit 0

# Source the module for the specific package manager.
. /usr/lib/auter/auter.module

# The following 3 functions are provided by the previously sourced /usr/lib/auter/auter.module
# Run the package manager specific check for locks
[[ "${PREP}" ]] && prepare_updates
[[ "${APPLY}" ]] && apply_updates

[[ $REBOOTCALL -eq 1 ]] && reboot_server

quit 0
