#!/bin/bash
# Collect a day's XiVO logs into a tarball; from a xivo node, also gather every remote node over SSH.

set -u

readonly progname="${0##*/}"

readonly SYSTEM_LOGS="/var/log/syslog"

readonly XIVO_LOGS="/var/log/asterisk/full
/var/log/nginx/xivo.access.log
/var/log/xivo-agentd.log
/var/log/xivo-ctid/xivo-ctid.log
/var/log/xivo-web-interface/xivo.log"

readonly XDS_LOGS="/var/log/asterisk/full"

readonly CC_LOGS="/var/log/xivocc/xuc/xuc.log
/var/log/xivocc/recording-server/recording-server.log"

readonly CC_OPTIONAL_LOGS="/var/log/xivocc/xuc/xuc_ami.log"

readonly SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new)

readonly XDS_SSH_KEY="/root/.ssh/rsync_xds"

DATE=""
TICKET=""
DEST=""
COLLECT_DIR=""
STREAM=0
T0=0
T1=0
MISSING=()

usage() {
    cat <<EOF
Usage: ${progname} <YYYYMMDD> [ticket number]

Collect the XiVO logs of the given day into a directory and a tarball.

  YYYYMMDD       day to collect logs for (e.g. 20260610)
  ticket number  optional; logs go to /var/local/issue<ticket number>/<YYYYMMDD>
                 when omitted a /var/local/xivo-collect-logs-<YYYYMMDD>.XXXX
                 directory is used (printed at the end)

On a xivo or ucaddon node the media servers and the xivocc are discovered from
the local database and their logs are collected over SSH (as root). Each node's
logs land under its own <node>/ sub-directory, all packed into one tarball.
EOF
}

msg() {
    echo "$@" >&2
}

die() {
    echo "${progname}: $*" >&2
    exit 1
}

note_missing() {
    MISSING+=("$1")
}

copy_file() {
    local f="$1" rel err
    rel="${f#/var/log/}"
    err=$(cd /var/log && cp -a --parents "${rel}" "${COLLECT_DIR}/" 2>&1)
    if [ -e "${COLLECT_DIR}/${rel}" ]; then
        msg "  + ${f}"
    else
        note_missing "${f} (copy failed: ${err})"
    fi
}

collect_rotated() {
    local base="$1"
    local optional="${2:-}"
    local candidates=() prev_mtime=0 selected=0

    shopt -s nullglob
    candidates=( "${base}" "${base}".* )
    shopt -u nullglob

    if [ "${#candidates[@]}" -eq 0 ]; then
        [ -n "${optional}" ] || note_missing "${base} (not found)"
        return
    fi

    while IFS=$'\t' read -r mtime path; do
        if [ "${mtime}" -gt "${T0}" ] && [ "${prev_mtime}" -lt "${T1}" ]; then
            copy_file "${path}"
            selected=1
        fi
        prev_mtime="${mtime}"
    done < <(stat -c '%Y	%n' "${candidates[@]}" 2>/dev/null | sort -n)

    if [ "${selected}" -eq 0 ] && [ -z "${optional}" ]; then
        note_missing "${base} (no rotated file covering ${DATE})"
    fi
}

collect_log_list() {
    local log
    while IFS= read -r log; do
        [ -n "${log}" ] && collect_rotated "${log}"
    done <<< "$1"
}

collect_atop() {
    local matches=()
    shopt -s nullglob
    matches=( /var/log/atop/*"${DATE}"* )
    shopt -u nullglob

    if [ "${#matches[@]}" -eq 0 ]; then
        note_missing "/var/log/atop/atop_${DATE} (not found)"
        return
    fi
    local f
    for f in "${matches[@]}"; do
        copy_file "${f}"
    done
}

detect_role() {
    local has_xivo=0 has_cc=0 has_mds=0
    [ -d /etc/docker/xivo ] && has_xivo=1
    [ -d /etc/docker/compose ] && has_cc=1
    [ -d /etc/docker/mds ] && has_mds=1

    if [ "${has_mds}" -eq 1 ]; then
        echo "xds"
    elif [ "${has_xivo}" -eq 1 ] && [ "${has_cc}" -eq 1 ]; then
        echo "ucaddon"
    elif [ "${has_xivo}" -eq 1 ]; then
        echo "xivo"
    elif [ "${has_cc}" -eq 1 ]; then
        echo "cc"
    else
        echo "unknown"
    fi
}

collect_for_role() {
    local role="$1"
    case "${role}" in
        xivo)
            collect_log_list "${XIVO_LOGS}"
            collect_atop
            ;;
        xds)
            collect_log_list "${XDS_LOGS}"
            collect_atop
            ;;
        cc)
            collect_log_list "${CC_LOGS}"
            collect_rotated "${CC_OPTIONAL_LOGS}" optional
            ;;
        ucaddon)
            collect_log_list "${XIVO_LOGS}"
            collect_atop
            collect_log_list "${CC_LOGS}"
            collect_rotated "${CC_OPTIONAL_LOGS}" optional
            ;;
    esac
}

discover_mds() {
    psql -U asterisk -Atc \
        "select voip_ip from mediaserver where name != 'default' order by name" 2>/dev/null
}

discover_xivocc() {
    psql -U asterisk -Atc \
        "select host from accesswebservice where name = 'xivows'" 2>/dev/null | head -n1
}

collect_remote() {
    local kind="$1" host="$2"
    local extra_opts=("${@:3}")
    local dir="${DEST}/${kind}-${host}"
    local tarball="${dir}.tar.gz"
    local errfile="${dir}.err"

    msg "Collecting ${kind} logs from ${host} ..."
    mkdir -p "${dir}"

    if ssh "${SSH_OPTS[@]}" "${extra_opts[@]}" "root@${host}" \
            xivo-collect-logs "${DATE}" --stream > "${tarball}" 2>"${errfile}"; then
        if tar -xzf "${tarball}" -C "${dir}" 2>/dev/null; then
            rm -f "${tarball}" "${errfile}"
        else
            note_missing "${kind} ${host}: could not extract collected logs"
            rm -f "${tarball}" "${errfile}"
            rmdir "${dir}" 2>/dev/null
        fi
    else
        local reason
        reason=$(tail -n 1 "${errfile}" 2>/dev/null)
        [ -n "${reason}" ] || reason="ssh connection failed"
        note_missing "${kind} ${host}: ${reason}"
        rm -f "${tarball}" "${errfile}"
        rmdir "${dir}" 2>/dev/null
    fi
}

collect_remote_mds() {
    local ip found=0
    while IFS= read -r ip; do
        [ -n "${ip}" ] || continue
        collect_remote mds "${ip}" -i "${XDS_SSH_KEY}"
        found=1
    done < <(discover_mds)
    [ "${found}" -eq 1 ] || msg "No media server found in the database."
}

collect_remote_xivocc() {
    local host
    host=$(discover_xivocc)
    if [ -z "${host}" ]; then
        note_missing "xivocc (no 'xivows' host in accesswebservice)"
        return
    fi
    collect_remote xivocc "${host}"
}

parse_date() {
    [[ "${DATE}" =~ ^[0-9]{8}$ ]] || die "invalid date '${DATE}', expected YYYYMMDD"
    local y="${DATE:0:4}" m="${DATE:4:2}" d="${DATE:6:2}"
    T0=$(date -d "${y}-${m}-${d}" +%s 2>/dev/null) \
        || die "invalid date '${DATE}'"
    T1=$((T0 + 86400))
}

prepare_dest() {
    if [ -n "${TICKET}" ]; then
        DEST="/var/local/issue${TICKET}/${DATE}"
        mkdir -p "${DEST}" || die "cannot create ${DEST}"
    else
        DEST=$(mktemp -d "/var/local/xivo-collect-logs-${DATE}.XXXXXX") \
            || die "cannot create a temporary directory"
    fi
}

report_missing() {
    if [ "${#MISSING[@]}" -gt 0 ]; then
        msg ""
        msg "Could not collect the following (check manually / via scp if on another host):"
        local item
        for item in "${MISSING[@]}"; do
            msg "  - ${item}"
        done
    fi
}

parse_args() {
    local args=()
    while [ "$#" -gt 0 ]; do
        case "$1" in
            -h|--help) usage; exit 0 ;;
            --stream)  STREAM=1; shift ;;
            --)        shift; while [ "$#" -gt 0 ]; do args+=("$1"); shift; done ;;
            -*)        usage >&2; die "unknown option '$1'" ;;
            *)         args+=("$1"); shift ;;
        esac
    done

    if [ "${#args[@]}" -lt 1 ] || [ "${#args[@]}" -gt 2 ]; then
        usage >&2
        exit 1
    fi
    DATE="${args[0]}"
    TICKET="${args[1]:-}"
}

collect_local() {
    local role="$1"
    collect_log_list "${SYSTEM_LOGS}"
    collect_for_role "${role}"
}

run_stream() {
    local role
    role=$(detect_role)
    [ "${role}" = "unknown" ] \
        && die "cannot detect deployment role (no /etc/docker/{xivo,compose,mds})"

    DEST=$(mktemp -d "/tmp/xivo-collect-logs-${DATE}.XXXXXX") \
        || die "cannot create a temporary directory"
    COLLECT_DIR="${DEST}"

    msg "Collecting day ${DATE} (role ${role}) ..."
    collect_local "${role}"

    tar -czf - -C "${DEST}" . 2>/dev/null
    local status=$?
    rm -rf "${DEST}"

    report_missing
    return "${status}"
}

run_normal() {
    local role
    role=$(detect_role)
    [ "${role}" = "unknown" ] \
        && die "cannot detect deployment role (no /etc/docker/{xivo,compose,mds})"

    prepare_dest

    msg "Detected role : ${role}"
    msg "Collecting day: ${DATE}"
    msg "Destination   : ${DEST}"
    msg ""

    case "${role}" in
        xivo|ucaddon)
            COLLECT_DIR="${DEST}/xivo"
            mkdir -p "${COLLECT_DIR}"
            collect_local "${role}"
            collect_remote_mds
            [ "${role}" = "ucaddon" ] || collect_remote_xivocc
            ;;
        *)
            COLLECT_DIR="${DEST}"
            collect_local "${role}"
            ;;
    esac

    local archive="${DEST}.tar.gz"
    if tar -czf "${archive}" -C "${DEST}" . 2>/dev/null; then
        msg ""
        msg "Archive       : ${archive}"
    else
        archive=""
    fi

    msg ""
    msg "Logs collected in: ${DEST}"
    [ -n "${archive}" ] && msg "Tarball ready    : ${archive}"

    report_missing
}

main() {
    parse_args "$@"

    [ "$(id -u)" -eq 0 ] || die "must be run as root to read /var/log"

    parse_date

    if [ "${STREAM}" -eq 1 ]; then
        run_stream
    else
        run_normal
    fi
}

main "$@"
