#!/bin/bash
#
#   Copyright (C) 2017 Rackspace, Inc.
#
#   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, write to the Free Software Foundation, Inc.,
#   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#
#~ Usage: _tool_ [-hnv]
#~ Options:
#~   -h             Print this help.
#~   -n             Dry run.
#~   -v             Verbose.
#~   -V             Print version and exit.
#~
#~ recaplog will not produce output if no tty is found, this is useful when
#~ running from cron.
#~

## Version
declare -r _VERSION='1.3.0'

## Default settings
PATH=/bin:/usr/bin:/sbin:/usr/sbin
BASEDIR="/var/log/recap"
LOCKFILE="/var/lock/recaplog.lock"
LOG_COMPRESS=1
LOG_EXPIRY=15
BACKUP_ITEMS="fdisk mysql netstat ps pstree resources"
LOGFILE="${BASEDIR}/recaplog.log"


# Function to generate timestamps
ts() {
  TS_FLAGS='--rfc-3339=seconds'
  date "${TS_FLAGS}"
}

# Function to log messages
log() {
  # does not work in a while-loop as spawns a new shell
  local msg_type=$1
  shift
  local log_entry="$*"
  ## This avoids sending any output to stdout when executed through cron
  ## is helpful to avoid emails submitted, instead the logs contain the
  ## possible ERRORS
  if ! tty -s; then
    echo "$( ts ) [${msg_type}] ${log_entry}" 2>&1 >> "${LOGFILE}"
    return 0
  fi
  if [[ "${VERBOSE}" ]]; then
    echo "$( ts ) [${msg_type}] ${log_entry}" 2>&1 | tee -a "${LOGFILE}"
    return 0
  fi
  if [[ "${msg_type}" =~ "ERROR" ||
        "${msg_type}" =~ "WARNING" ]]; then
    echo "$( ts ) [${msg_type}] ${log_entry}" 2>&1 | tee -a "${LOGFILE}"
  else
    echo "$( ts ) [${msg_type}] ${log_entry}" 2>&1 >> "${LOGFILE}"
  fi
}

# Function to delete log files older than LOG_EXPIRY days
delete_old_logs() {
  local matched_files=''
  local empty_dirs=''
  local count_files=0
  local count_dirs=0
  if [[ "${LOG_EXPIRY}" -eq 0 ]]; then
    log INFO "Skipping old log file expiration..."
    return 0
  fi
  # Proceed with deletion of old logs
  log INFO "Deleting log files older than ${LOG_EXPIRY} days..."
  # Matching files and empty directories if any.
  # Using now posix-extended regex flavor to ease the matching of logs, gzips
  # or tarballs
  matched_files=$(
    find "${BASEDIR}" -maxdepth 2 -regextype posix-extended \
      -type f -mtime +${LOG_EXPIRY} -regex \
      "^${BASEDIR}/(.*/)?[a-z]+_([a-z]+_)?[0-9]{8}(-[0-9]{6})?\.log(\.tar)?(\.gz)?$"
  )
  if [[ -n "${matched_files}" ]]; then
    count_files=$( wc -l <<<"${matched_files}" )
  fi
  # If dry run, log and return
  if [[ "${DRYRUN}" ]]; then
    log INFO "Would delete: ${count_files} log files."
    return 0
  else
    log INFO "Deleting: ${count_files} log files."
  fi
  # Deleting old logs, compressed/tarballs and empty dirs if any
  if [[ "${count_files}" -gt 0 ]]; then
    while read matched_file; do
      rm -f "${matched_file}"
    done <<< "${matched_files}"
  fi
  # Deleting empty dirs generated by recaplog when no compressing logs.
  empty_dirs=$( find "${BASEDIR}" -maxdepth 1 -regextype posix-extended \
                  -regex "^${BASEDIR}/[a-z]+_[a-z]+_[0-9]{8}$" -empty \
                  -type d )
  if [[ -n "${empty_dirs}" ]]; then
    count_dirs=$( wc -l <<<"${empty_dirs}" )
  fi
  log INFO "Deleting: ${count_dirs} empty directories."
  if [[ "${count_dirs}" -gt 0 ]]; then
    while read emtpy_dir; do
      rmdir "${emtpy_dir}"
    done <<< "${empty_dirs}"
  fi
}

# Function to compress/move daily log files
# Previously the logs were concatenated and could be compressed, that is a
# problem for parsing them at a later time as reported in:
# https://github.com/rackerlabs/recap/pull/28
# The new behaviour is to move the log files to a directory then opt for
# compression, if compressed then it will delete the original logs.
pack_old_logs() {
  local -a errors
  local YESTERDAYDATE=$( date -d "now -1 day" "+%Y%m%d" )
  if [[ "${LOG_COMPRESS}" -eq 1 ]]; then
    log INFO "Compressing old log files"
  else
    log INFO "Not compressing old log files"
  fi
  # Backing up only items defined in the config
  for item in ${BACKUP_ITEMS}; do
    YESTERDAY_ITEM_DIR="${BASEDIR}/${item}_daily_${YESTERDAYDATE}"
    err=''
    log INFO "Packing ${item}..."
    # Finding logs for item from yesterday date. 
    output=$( ls -1 "${BASEDIR}/${item}_${YESTERDAYDATE}"-*.log 2>&1 )
    if [[ $? -ne 0 ]]; then
      output=$( tr -d '\n' <<< "${output}" )
      log ERROR "An error ocurred while reading logs: '${output}', skipping..."
      continue
    fi
    log_count=$( wc -l <<< "${output}" )
    # Moving logs to yesterday item's directory
    if [[ "${DRYRUN}" ]]; then
      log INFO "Would move ${log_count} logs to: ${YESTERDAY_ITEM_DIR}"
    else
      log INFO "Moving ${log_count} logs to: ${YESTERDAY_ITEM_DIR}"
      err=$(
        {
          mkdir -p "${YESTERDAY_ITEM_DIR}";
          mv "${BASEDIR}/${item}_${YESTERDAYDATE}"-*.log \
             "${YESTERDAY_ITEM_DIR}";
        } 2>&1
      )
      err=$( tr -d '\n' <<< "${err}" )
      if [[ ! -z "${err}" ]]; then
        log ERROR "An error ocurred while attempting to move logs: '${err}', "\
                  "skipping..."
        continue
      fi
    fi
    # Compressing logs
    # Move to the next loop if compress is not enabled
    if [[ "${LOG_COMPRESS}" -ne 1 ]]; then
      continue
    fi
    OUTPUTFILE="${YESTERDAY_ITEM_DIR}.log.tar.gz"
    if [[ "${DRYRUN}" ]]; then
      log INFO "Would compress ${log_count} logs into: ${OUTPUTFILE}"
      continue
    fi
    # Compressing logs when enabled
    log INFO "Compressing ${log_count} logs into: ${OUTPUTFILE}"
    err=$(
      {
        tar czf "${OUTPUTFILE}" \
                "${YESTERDAY_ITEM_DIR}"/*.log 2>/dev/null &&
        touch -t ${YESTERDAYDATE}0000 "${OUTPUTFILE}";
      } 2>&1
    )
    err=$(tr -d '\n' <<< "${err}" )
    if [[ ! -z "${err}" ]]; then
      log ERROR "An error ocurred while attempting to compress logs: '${err}'"\
                ", skipping..."
      continue
    fi
    log INFO "Deleting ${log_count} logs."
    rm_err=$( { rm -Rf "${YESTERDAY_ITEM_DIR}"; } 2>&1 >/dev/null )
    rm_err=$( tr -d '\n' <<< "${rm_err}" )
    if [[ ! -z "${rm_err}" ]]; then
      log ERROR "An error ocurred while deleting logs '${rm_err}', skipping..."
    fi
  done
}

# Cleanup function to remove lock
cleanup() {
  log INFO "$( basename $0 ) ($$): Caught signal - deleting ${LOCKFILE}"
  rm -f "${LOCKFILE}"
  log INFO "${banner_end}"
}

# Create a Lock so that recaplog does not try to run over itself.
recaploglock() {
  (set -C; echo "$$" > "${LOCKFILE}") 2>/dev/null
  if [[ $? -ne 0 ]]; then
    log ERROR "$( basename $0 ) ($$): Lock File exists - exiting"
    exit 1
  else
    trap 'exit 2' 1 2 15 17 23
    trap 'cleanup' EXIT
    log INFO "$( basename $0 ) ($$): Created lock file: ${LOCKFILE}"
  fi
}


# Avoid external variables to be defined
unset VERBOSE
unset DRYRUN

while getopts hnvV flag; do
  case ${flag} in
    n)
      DRYRUN=1
      VERBOSE=1
      ;;
    v)
      VERBOSE=1
      ;;
    V)
      echo "${_VERSION}"
      exit 0
      ;;
    h|*)
      grep -E '^#~' $0 | sed -e 's/^#~\s\?//' \
                             -e "s/_tool_/$( basename $0 )/"
      exit 1
      ;;
 esac
done

# Verify running as root
if [[ "$( id -u )" != "0" ]]; then
  echo "This script must be run as root." >&2
  exit 1
fi

# Define the headers for the run
if [[ "${DRYRUN}" ]]; then
  banner_start="--- Starting $( basename $0 )[$$] (dry-run) ---"
  banner_end="--- Ending $( basename $0 )[$$] (dry-run) ---"
else
  banner_start="--- Starting $( basename $0 )[$$] ---"
  banner_end="--- Ending $( basename $0 )[$$] ---"
fi

# Start logging
log INFO "${banner_start}"

# Check for the configuration file.
# The following creates awareness about the old config location in
# /etc/recap, taking precendence the new config location /etc/recap.conf.
if [[ -r /etc/recap &&
      -r /etc/recap.conf ]]; then
  log WARNING "Configuration files exist at old(/etc/recap) and"\
              "new(/etc/recap.conf) locations. The file from the new"\
              "location will be used."
  log WARNING "Please move any missing configuration to /etc/recap.conf."
  source /etc/recap.conf
elif [[ -r /etc/recap &&
        ! -r /etc/recap.conf ]]; then
  log WARNING "Configuration file exists at old(/etc/recap) location."\
              "The file will be read."\
              "Please move your configuration file to /etc/recap.conf."\
              "Next version of recap will not read /etc/recap"
  source /etc/recap
elif [[ ! -r /etc/recap.conf ]]; then
  log WARNING "No configuration file found. Expecting /etc/recap.conf."
  log WARNING "Proceeding with defaults."
else
  source /etc/recap.conf
fi

# Aquire lock
recaploglock

# recap the logs
pack_old_logs
delete_old_logs

exit 0
