#!/bin/sh

# BAREOS® - Backup Archiving REcovery Open Sourced
#
# Copyright (C) 2018-2026 Bareos GmbH & Co. KG
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of version three of the GNU Affero General Public
# License as published by the Free Software Foundation and included
# in the file LICENSE.
#
# 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
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero 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.
#
# This Script gathers essential information needed for working
# efficiently on support cases, such as
# - installed Bareos packages
# - logfiles
# - bactraces
# - tracebacks
# - status information
# - information about pools, media, jobs
# - configuration with anonymized passwords
#
# A basic support of PAM protected console is available if
# you configure a pam credential file named ~/.bconsole_pam
# with your login and password.
#
# By default bconsole call will timeout with 300s of wait.
# You can shorten this timeout by exporting the following
# export BCONSOLE_TIMEOUT=30 to 30s for example
#
# By default resulting report is build in /var/tmp. If you
# prefer another location before running the scipt export the
# following variable export REPORT_DIR=/your/favorite/path 
#

SUPPORT_VERSION="20260713"
# Notice for Solaris awk need to be /usr/xpg4/bin/awk or gnu awk
MANDATORY_BINARIES='awk cp gzip mkdir sed tar tr stat xargs'

QUERYCATINFO=$(cat <<-SQLEND
SELECT ut.relname,
    pg_size_pretty(pg_total_relation_size(ut.relname::text)) AS total_sz,
    pg_size_pretty(pg_total_relation_size(ut.relname::text) - pg_table_size(ut.relname::text)) AS idx_sz,
    pgc.reltuples::bigint,
    CASE
      WHEN (pgc.reltuples < 0) THEN 0
      ELSE (pg_total_relation_size(ut.relname::text) / (pgc.reltuples+1))::int
    END AS bytes_per_row,
    ut.n_dead_tup,
    ut.last_vacuum,
    ut.last_autovacuum,
    ut.last_analyze,
    ut.last_autoanalyze
FROM
    pg_stat_user_tables AS ut,
    pg_class AS pgc
WHERE
    pgc.relname = ut.relname AND ut.relname != 'batch'
ORDER BY
    pg_total_relation_size(ut.relname::text) DESC;
SQLEND
)

QUERYIDXINFO=$(cat <<-SQLEND
SELECT
    t.schemaname,
    t.tablename,
    c.reltuples::bigint                            AS num_rows,
    pg_size_pretty(pg_relation_size(c.oid))        AS table_size,
    psai.indexrelname                              AS index_name,
    pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
    CASE WHEN i.indisunique THEN 'Y' ELSE 'N' END  AS "unique",
    psai.idx_scan                                  AS number_of_scans,
    psai.idx_tup_read                              AS tuples_read,
    psai.idx_tup_fetch                             AS tuples_fetched
FROM
    pg_tables t
    LEFT JOIN pg_class c ON t.tablename = c.relname
    LEFT JOIN pg_index i ON c.oid = i.indrelid
    LEFT JOIN pg_stat_all_indexes psai ON i.indexrelid = psai.indexrelid
WHERE
    t.schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY 1, 2;
SQLEND
)

QUERYPGSETTINGS="select name,setting,unit from pg_settings;"

os_version=$(uname -s)

check_for() {
  echo -ne "Checking for $*... " >&2
}

check_ok() {
  echo "ok" >&2
}

check_fail() {
  echo "not ok" >&2
  if [ "$res" ]; then
    echo "----------"
    echo "$res"
    echo "----------"
  fi
  exit 2
}

get_working_directory () {
    echo "show director verbose" | ${BCONSOLE} | awk -F'=' '/WorkingDirectory/ {gsub(" ","",$2);gsub("\"","",$2); print $2}'
}

enter_case_number () {
    TICKET_NUMBER=""
    while [ -z "${TICKET_NUMBER}" ]; do
        printf "Please enter the Support Ticket Number: "
        read -r TICKET_NUMBER
    done
    echo
}

version_info () {
  echo "Bareos support info tool version ${SUPPORT_VERSION}"
  echo
}

cpu_info () {
    if [ "${os_version}" = "Linux" ];then
        echo "Nb cpus: $(($(awk '/^processor/ {print $3}' /proc/cpuinfo | tail -n1) + 1))"
        awk '!$0{exit}1' /proc/cpuinfo
    elif [ "${os_version}" = "FreeBSD" ];then
        sysctl hw.model hw.machine hw.ncpu
    fi
}

memory_info () {
    command -v free > /dev/null 2>&1 && free -m
    command -v vmstat > /dev/null 2>&1 && vmstat && echo && vmstat -s
}

system_info () {
    echo "### System Info ###"
    command -v uname >/dev/null 2>&1 && uname -a
    command -v lsb_release >/dev/null 2>&1 && lsb_release -a
    if [ -r /etc/os-release ]; then
      . "/etc/os-release"
      echo "os-release: ${PRETTY_NAME} - ${VERSION_ID}"
    fi
    echo
    echo "### System Info - hostname ###"
    command -v hostnamectl > /dev/null 2>&1 && hostnamectl
    command -v hostname > /dev/null 2>&1 && hostname -f
    echo
    echo "### System Info - CPU & memory ###"
    cpu_info
    echo
    memory_info
    echo
    echo "### System Info - Disk usage ###"
    command -v df > /dev/null 2>&1 && df -h
    echo
    echo "### System Info - Mounted FS ###"
    command -v mount > /dev/null 2>&1 && mount
    echo
    echo "### System Info - block devices ###"
    command -v lsblk > /dev/null 2>&1 && lsblk
    echo
    echo "### System Info - Network ###"
    command -v ip > /dev/null 2>&1 && ip address
    command -v ip > /dev/null 2>&1 && ip stats show group link
    echo
    command -v ss > /dev/null 2>&1 && ss --tcp --processes --numeric --all | grep ":91"
    echo
    echo "### System Info - scsi info ###"
    command -v lsscsi > /dev/null 2>&1 && lsscsi -gv 2>&1
    if [ -d /dev/tape ]; then
        ls -lR /dev/tape
    fi
    echo
}

list_bareos_packages () {
    echo "### Installed Bareos/libpq Packages ###"
    command -v rpm >/dev/null 2>&1 && rpm -qa '*bareos*' '*libpq*'
    command -v dpkg-query >/dev/null 2>&1 && dpkg-query -l '*bareos*' '*libpq*'
    command -v pkg >/dev/null 2>&1 && pkg info '*bareos*' '*postgresql*'
    echo
}

report_daemon_status () {
    echo "### systemctl daemon status ###"
    command -v systemctl > /dev/null 2>&1 && systemctl status bareos-*.service
    # we don't try other service command as they either a symlink to systemctl
    # or have just too few interesting informations.
}

report_bareos_version () {
    echo "### Bareos Version ###"
    echo "version" | ${BCONSOLE}
    echo
}

report_subscriptions_status () {
    echo "### Bareos Subscriptions Status ###"
    echo "status subscriptions all" | ${BCONSOLE}
    echo
}

report_director_status () {
    echo "### Bareos Director Status ###"
    echo "status dir" | ${BCONSOLE}
    echo
}

report_configuration_status () {
    echo "### Bareos Director Configuration Status ###"
    echo "status configuration" | ${BCONSOLE}
    echo
}


report_storage_status () {
    echo "### Storage(s) Status ###"
    echo "#### List of Storages in Catalog DB ####"
    echo "list storages" | ${BCONSOLE}
    echo "#### Currently configured Storages ####"
    echo "show storage" | ${BCONSOLE} | sed 's/Password = .*/Password = "anonymized"/g'
    echo "show storage" | ${BCONSOLE} | awk -F"=" '/Name/ {gsub(" ","",$2);gsub("\"","",$2); print $2}' | \
    while read -r STORAGE_NAME; do
        echo "#### Status of Storage ${STORAGE_NAME} ####"
        echo "status storage=${STORAGE_NAME}" | ${BCONSOLE}
    done
    echo
}

report_scheduler_status () {
    echo "### Scheduler Status ###"
    echo "status scheduler days=31" | ${BCONSOLE}
    echo
}

report_pools () {
    echo "### Pools ###"
    echo "list pools" | ${BCONSOLE}
    echo "show pools" | ${BCONSOLE}
    echo
}

report_media () {
    echo "### Media ###"
    echo "list media" | ${BCONSOLE}
    echo
}

report_jobs () {
    echo "### Jobs Report ###"
    echo "list jobs days=1 limit=10" | ${BCONSOLE}
    for JOB_ID in $(echo "llist jobs days=1 limit=10" | ${BCONSOLE} | awk '/ jobid: / {gsub(",","",$2); print $2}'); do
        echo "#### JobId: ${JOB_ID} ####"
        echo "list joblog jobid=${JOB_ID}" | ${BCONSOLE}
        echo
    done
    echo
}

report_catalog_backup_jobs () {
    echo "### Catalog Backup Jobs Report ###"
    echo "list jobs job=BackupCatalog days=30 limit=10" | ${BCONSOLE}
    echo "llist jobs job=BackupCatalog days=30 limit=10" | ${BCONSOLE}
    for JOB_ID in $(echo "llist jobs job=BackupCatalog days=30 limit=10" | ${BCONSOLE} | awk '/ jobid: / {gsub(",","",$2); print $2}'); do
        echo "#### Catalog Backup JobId: ${JOB_ID} ####"
        echo "list joblog jobid=${JOB_ID}" | ${BCONSOLE}
        echo
    done
    echo
}

report_dir_hostnames () {
  xc_flag="$(if bareos-dir --version > /dev/null 2>&1;then echo '--xc';else echo '-xc';fi)"
  HOSTLIST=$(bareos-dir ${xc_flag} | awk -F '=' '/Address =/ {gsub(" ","",$2);gsub("\"","",$2); print $2}' | awk '!(seen[$0]++)' );
  if [ -x "$(which host)" ];then
    echo "#### Hostnames resolved by host command ####"
    for H in ${HOSTLIST}; do
      host "${H}"
    done
    echo
  fi

  if [ -x "$(which getent)" ];then
    echo "#### Hostnames resolved by getent hosts command ####"
    for H in ${HOSTLIST}; do
      getent hosts "${H}"
    done
    echo
  fi

  if [ -r /etc/hosts ]; then
    echo "#### Content of /etc/hosts ####"
    cat /etc/hosts
  elif [ -r /usr/local/etc/hosts ]; then
    echo "#### Content of /usr/local/etc/hosts ####"
    cat /usr/local/etc/hosts
  fi
  echo
}

list_working_dir () {
    echo "### Working Directory Contents ###"
    if [ -z "${WORKING_DIRECTORY}" ]; then
        echo "Could not get working directory"
    else
        echo "Working Directory: ${WORKING_DIRECTORY} $(readlink "${WORKING_DIRECTORY}")"
        ls -lah "${WORKING_DIRECTORY}/"
    fi
    echo
}

report_database_information () {
 #check if postgresql is in use
 DBDRIVER=$(echo "status dir" | bconsole | awk -F ':' '/^Daemon started/ {print $4}' | awk -F ',' '{print $1}')
 if [ "${DBDRIVER}" = "postgresql" ];then
    echo "=== Database (PostgreSQL) informations ==="
    printf "sqlquery\n%s\n" "${QUERYCATINFO}" | bconsole
    echo
    printf "sqlquery\n%s\n" "${QUERYIDXINFO}" | bconsole
    echo
    printf "sqlquery\n%s\n" "${QUERYPGSETTINGS}" | bconsole
    echo
 fi
 echo
}

report_list_jobtotals () {
    echo "### Bareos Jobs Totals ###"
    echo "list jobtotals" | ${BCONSOLE}
    echo
}

create_dir_report () {
    {
    report_bareos_version
    report_subscriptions_status
    report_dir_hostnames
    report_director_status
    report_configuration_status
    report_storage_status
    report_scheduler_status
    report_pools
    report_media
    report_jobs
    report_catalog_backup_jobs
    report_database_information
    report_list_jobtotals
    } >> "${REPORT_FILE}"
}

dump_long_media_list () {
    echo "llist media" | ${BCONSOLE} > "${OUT_DIR}/llist_media.txt"
}

list_media_on_disk () {
    if [ -d "${ETC_BAREOS}/bareos-sd.d/device" ]; then
        awk -F'=' '!(/^[[:space:]]*#/) && tolower($0) ~ /archive.*device/ {gsub(" ","",$2);gsub("\"","",$2); print $2}' "${ETC_BAREOS}"/bareos-sd.d/device/*.conf | awk '!(seen[$0]++) && !(/^\/dev/) && !(/^$/)' \
         | while read -r ARCHIVE_DEVICE; do
            {
            echo "${ARCHIVE_DEVICE}"
            ls -ld "${ARCHIVE_DEVICE}"
            find -H "${ARCHIVE_DEVICE}" -ls
            echo ""
            } >> "${OUT_DIR}/media_on_disk.txt"
        done
    fi
}

copy_config() {
local os_type="$(uname -s)"
# excluded files from configuration
cat <<EOF>"${OUT_DIR}/conf.excluded"
*.rndpwd*
*.rpm*
*.example
*.bak
*.pem
*.key
*.crt
*.pgpass
*.profile
*.ini
EOF
  if [ -z "${ETC_BAREOS}" ]; then
    return
  fi
  # trying to refactor in place as a variable will lead to errors.
  if [ "$os_type" = "Darwin" ] || [ "$os_type" = "FreeBSD" ]; then
    SEDI="sed -i \"\"" 
  else
    SEDI="sed --in-place"
  fi
  mkdir -p "${OUT_DIR}/conf"
  tar -cf - --exclude-from="${OUT_DIR}/conf.excluded" "${ETC_BAREOS}/" | tar -C "${OUT_DIR}/conf" -xf -
  find "${OUT_DIR}/conf" -type f -exec ${SEDI} \
    -e 's/Password[[:blank:]]*=.*/Password = "anonymized"/i' \
    -e 's/Key.*Encryption.*Key.*=.*/KeyEncryptionKey = "anonymized"/i' \
    -e 's/vcpass=[^\":]*/vcpass=anonymized/i' \
    "{}" \;
}

dump_director_config () {
    # only if installed
    if command -v bareos-dir >/dev/null 2>&1; then
        xc_flag="$(if bareos-dir --version > /dev/null 2>&1;then echo '--xc';else echo '-xc';fi)"
        # anonymize all password entries
        bareos-dir ${xc_flag} \
          | sed 's/Password = .*/Password = "anonymized"/g; s/KeyEncryptionKey = .*/KeyEncryptionKey = "anonymized"/g; s/vcpass=[^\":]*/vcpass=anonymized/g' \
          > "${OUT_DIR}/bareos-dir_config.txt"
    fi
}

dump_show_all_verbose () {
    echo "show all verbose" | ${BCONSOLE} \
      | sed 's/Password = .*/Password = "anonymized"/g; s/KeyEncryptionKey = .*/KeyEncryptionKey = "anonymized"/g; s/vcpass=[^\":]*/vcpass=anonymized/g' \
      > "${OUT_DIR}/show_all_verbose.txt"
}

dump_storagedaemon_config () {
    # only if installed
    if command -v bareos-sd >/dev/null 2>&1; then
        xc_flag="$(if bareos-sd --version > /dev/null 2>&1;then echo '--xc';else echo '-xc';fi)"
        # anonymize all password entries
        bareos-sd ${xc_flag} | sed 's/Password = .*/Password = "anonymized"/g' > "${OUT_DIR}/bareos-sd_config.txt"
    fi
}

dump_filedaemon_config () {
    # only if installed
    if command -v bareos-fd >/dev/null 2>&1; then
        xc_flag="$(if bareos-fd --version > /dev/null 2>&1;then echo '--xc';else echo '-xc';fi)"
        # anonymize all password entries
        bareos-fd ${xc_flag} | sed 's/Password = .*/Password = "anonymized"/g' > "${OUT_DIR}/bareos-fd_config.txt"
    fi
}

gather_journal () {
  local OUT_JNL_DIR="${OUT_DIR}/journal"
  mkdir -p "${OUT_JNL_DIR}"
  journalctl -b -k > "${OUT_JNL_DIR}/kernel_journal.log"
  for s in bareos-dir bareos-sd bareos-fd
  do
    journalctl -b -u "${s}" > "${OUT_JNL_DIR}/${s}_journal.log"
  done
  gzip "${OUT_JNL_DIR}"/*.log
}

gather_log_files () {
    OUT_LOG_DIR="${OUT_DIR}/log"
    mkdir -p "${OUT_LOG_DIR}"
    for LOG_FILE in $( echo "show messages" | ${BCONSOLE} | awk -F"=" 'tolower($0) ~ /append/ && !(seen[$2]++) {gsub(" ","",$2);gsub("\"","",$2); print $2}'); do
        TARGET_FILE="${OUT_LOG_DIR}/$(echo "${LOG_FILE}" | tr "/" "_").gz"
        gzip -c "${LOG_FILE}" > "${TARGET_FILE}"
        # gather any previously rotated log (bareos.log & bareos-audit.log)
        cp -a "${LOG_FILE}"-* "${OUT_LOG_DIR}/" >/dev/null 2>&1
        cp -a "${LOG_FILE}".* "${OUT_LOG_DIR}/" >/dev/null 2>&1
    done

    if [ -n "${LOG_FILE}" ]; then
        LOG_DIR=$(dirname "${LOG_FILE}")
    else
        if [ -d "/var/log/bareos" ]; then
            LOG_DIR="/var/log/bareos"
        fi
    fi
    # gather bareos_vadp_dumper logfiles, if any
    if [ -d "${LOG_DIR}" ]; then
        cp -a "${LOG_DIR}"/bareos_vadp_dumper.*.log "${OUT_LOG_DIR}/" >/dev/null 2>&1
        MTX_LOG_FILE="${LOG_DIR}/mtx.log"
        if [ -f "${MTX_LOG_FILE}" ]; then
            gzip -c "${MTX_LOG_FILE}" > "${OUT_LOG_DIR}/$(basename "${MTX_LOG_FILE}").gz"
        fi
    else
        echo ""
        echo "##### Warning - unable to find log directory #####"
        echo ""
    fi
}

gather_statefiles () {
    mkdir -p "${OUT_DIR}/states"
    cp -a "${WORKING_DIRECTORY}"/*.state "${OUT_DIR}/states/" >/dev/null 2>&1
    gzip "${OUT_DIR}"/states/*
}

gather_tracebacks () {
    mkdir -p "${OUT_DIR}/traces"
    cp -a "${WORKING_DIRECTORY}"/*.traceback "${OUT_DIR}/traces/" >/dev/null 2>&1
    cp -a "${WORKING_DIRECTORY}"/*.bactrace "${OUT_DIR}/traces/" >/dev/null 2>&1
    cp -a "${WORKING_DIRECTORY}"/*.trace "${OUT_DIR}/traces/" >/dev/null 2>&1
    gzip "${OUT_DIR}"/traces/*
}

check_report_size () {
# 25MB as attachment
MAX_REPORT_SIZE=24500000
# 3GB as upload
MAX_UPLOAD_SIZE=2900000000

RESULT_SIZE=0
if [ "${os_version}" = "Linux" ];then
    RESULT_SIZE=$(stat -c%s "${RESULT_FILE}")
elif [ "${os_version}" = "FreeBSD" ];then
    RESULT_SIZE=$(stat -f%k "${RESULT_FILE}")
fi

if [ "${RESULT_SIZE}" -gt "${MAX_REPORT_SIZE}" ]; then

  echo "
  The resulting report size seem over 25MB and
  can't be attached to support case ticket.
  "

  if [ "$RESULT_SIZE" -gt "$MAX_UPLOAD_SIZE" ]; then
    echo "
  The global size being over our maximum upload authorized size by file,
  we kindly ask you to split the file into piece before upload.
  the following command can help : split -b $((MAX_UPLOAD_SIZE/1000000))MB ${RESULT_FILE} ${RESULT_FILE}_part
  "
  fi

 echo "
 You are invited to upload the result to https://support.bareos.com/upload
 Once done please let a comment in the support ticket about new file(s) uploaded.
 "
 echo
fi

echo "Please attach ${RESULT_FILE} to Support Ticket ${TICKET_NUMBER}"

}

echo "Bareos Support Info Gathering Tool"
enter_case_number
echo "Gathering Support Info, please wait ..."

#check for mandatory binaries
for CMD in ${MANDATORY_BINARIES}; do
  check_for "${CMD} command"
  if res="$(command -v "${CMD}")"; then
    check_ok
  else
    echo "Please install missing ${CMD} "
    check_fail
  fi
  unset res
done

HAVE_BCONSOLE=false
check_for "bconsole command"
if BCONSOLE="$(command -v bconsole)"; then
  # check if we can connect to the director, which maybe pam protected.
  if [ -f ~/.bconsole_pam ];then
     BCONSOLE="${BCONSOLE} --pam-credentials-filename ~/.bconsole_pam"
  fi
  echo version | ${BCONSOLE} | grep "1000 OK:.*Version:" > /dev/null 2>&1
  ret=$?
  if [ ${ret} -eq 0 ]; then
    HAVE_BCONSOLE=true
    BCONSOLE_TIMEOUT="${BCONSOLE_TIMEOUT:-300}s"
    BCONSOLE="timeout --preserve-status ${BCONSOLE_TIMEOUT} ${BCONSOLE}"
    check_ok
  else
   echo ""
   echo "##### Warning - bconsole not working - PAM authentification without ~/.bconsole_pam credential file setup ? #####"
   echo ""
  fi
else
   echo ""
   echo "##### Warning - bconsole not found - report truncated #####"
   echo ""
fi

TMP_DIR=$(mktemp -d)
REPORT_DATE=$(date +%F_%H%M%S)
OUT_DIR="${TMP_DIR}/${TICKET_NUMBER}/${REPORT_DATE}"
mkdir -p "${OUT_DIR}"
REPORT_FILE="${OUT_DIR}/bareos_support_report.txt"
REPORT_DIR=${REPORT_DIR:-/var/tmp}

# check if normal location are symlinks
ETC_BAREOS=$(readlink /etc/bareos || readlink /usr/local/etc/bareos)
if [ -z "${ETC_BAREOS}" ]; then
  if [ -d "/etc/bareos" ]; then
    ETC_BAREOS="/etc/bareos"
  elif [ -d "/usr/local/etc/bareos" ]; then
    ETC_BAREOS="/usr/local/etc/bareos"
  fi
  if [ "${ETC_BAREOS}" ];then
    export ETC_BAREOS
  fi
fi
WORKING_DIRECTORY=$(get_working_directory)
if [ -z "${WORKING_DIRECTORY}" ]; then
    echo "Error getting Working Directory, bareos-dir not running?"
    if [ -d /var/lib/bareos ]; then
        echo "Guessed Working Directory /var/lib/bareos exists"
        WORKING_DIRECTORY="/var/lib/bareos"
    fi
fi

check_for "system information"
{
  version_info
  system_info
  list_bareos_packages
  report_daemon_status
} >> "${REPORT_FILE}"
check_ok

if [ ${HAVE_BCONSOLE} = "true" ]; then
  check_for "director information"
  create_dir_report
  dump_long_media_list
  dump_director_config
  dump_show_all_verbose
  check_ok
fi
check_for "Storage configuration"
dump_storagedaemon_config
check_ok
check_for "Filedaemon configuration"
dump_filedaemon_config
check_ok
check_for "journalctl command"
if command -v journalctl; then
  check_for "Bareos daemon journal"
  gather_journal
  check_ok
fi
check_for "Bareos logs"
gather_log_files
check_ok
check_for "Bareos state files"
gather_statefiles
check_ok
check_for "Bareos traceback"
gather_tracebacks
check_ok
check_for "volumes on local disk"
list_media_on_disk
check_ok
check_for "Bareos configuration"
list_working_dir >> "${REPORT_FILE}"
copy_config
check_ok

cd "${TMP_DIR}" || exit 1
RESULT_FILE="${REPORT_DIR}/bareos-support-info_${TICKET_NUMBER}_${REPORT_DATE}.tgz"
check_for "Assembling final tar"
tar cfz "${RESULT_FILE}" "${TICKET_NUMBER}"
check_ok
check_report_size

rm -rf "${TMP_DIR}"
