#!/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_ [OPTIONS]
#~ Options:
#~   -h, --help         Print this help.
#~   -n, --dry-run      Dry run.
#~   -v, --verbose      Verbose.
#~   -V, --version      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='2.0.2'

## Default settings
PATH=/bin:/usr/bin:/sbin:/usr/sbin
BASEDIR="/var/log/recap"
LOCKFILE="/var/lock/recaplog.lock"
LOG_COMPRESS=1
LOG_EXPIRY=15
LOGFILE="${BASEDIR}/recaplog.log"

# Usage Function
print_usage() {
  grep -E '^#~' "${0}" | sed -e 's/^#~\s\?//' -e "s/_tool_/$( basename ${0} )/"
}

# 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 empty_dir; do
      rmdir "${empty_dir}"
    done <<< "${empty_dirs}"
  fi
}

# Generates a list of names from a date with the format "YYYYMMDD"
declare -a reports=()
get_report_names_of_day() {
  local day="$1"
  log INFO "Finding reports from: ${day}"
  reports=(
    $( ls -1 "${BASEDIR}" 2>/dev/null |
         awk -F. \
         '
           /'${day}'-[0-9]{6}\.log$/ {sub("_'${day}'-[0-9]{6}","",$1); print $1}
         ' |
         sort -u
     )
  )
  log INFO "Reports found: [ ${reports[@]} ]"
}

# 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 found in the YESTERDAYDATE
  get_report_names_of_day "${YESTERDAYDATE}"
  local -a items=${reports[@]}
  if [[ -z ${items} ]]; then
    log ERROR "Unable to archive unexisting reports."
    return 0
  fi
  for item in ${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 occurred 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 occurred 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 occurred 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 occurred 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
}

# Set options
OPTIONS=hnvV
LONG_OPTIONS=help,dry-run,verbose,version

# Parse options and show usage if invalid options provided
! ALL_ARGS=$(getopt --options=${OPTIONS} --longoptions=${LONG_OPTIONS} --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
  print_usage
  exit 1
fi

# Set the positional parameters to the parsed options
eval set -- "${ALL_ARGS}"


# Avoid external variables to be defined
unset VERBOSE
unset DRYRUN

while [[ ${#} -gt 0 ]]; do
  case "${1}" in
    -n|--dry-run)
      DRYRUN=true
      VERBOSE=true
      shift
      ;;
    -v|--verbose)
      VERBOSE=true
      shift
      ;;
    -V|--version)
      echo "${_VERSION}"
      exit 0
      ;;
    -h|--help)
      print_usage
      exit 1
      ;;
    --)
      shift; break ;;
 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.
if [[ ! -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

# Acquire lock
recaploglock

# recap the logs
pack_old_logs
delete_old_logs

exit 0
