#!/usr/bin/bash
# Copyright EnterpriseDB Corporation, 2014-2024. All Rights Reserved.

# used as a key in the recovery.conf file header
EDB_FM="EDB Failover Manager"

# version name
EFM=efm-4.9

# Java files
EFM_CONFIG=/etc/sysconfig/$EFM
RUN_JAVA=/usr/edb/$EFM/bin/runJavaApplication.sh
LIB=/usr/edb/$EFM/lib/EFM-4.9.jar
CLASS=com.enterprisedb.efm.main.ConfigCommand

# declare these to avoid static analysis warnings due to use as outvars from getProps
DATA_DIR=

usage() {
    echo $"Usage: $0 promotestandby             <cluster name|props file location>"
    echo $"       $0 writerecoveryconf          <cluster name|props file location>"
    echo $"       $0 writecustomrecoveryconf    <cluster name|props file location> <contents>"
    echo $"       $0 removerecoveryconf         <cluster name|props file location>"
    echo $"       $0 validatedatadir       <cluster name|props file location>"
    echo $"       $0 validatedbconf             <cluster name|props file location>"
    echo $"       $0 validatepgbin              <cluster name|props file location>"
    echo $"       $0 validatepgwaldir           <cluster name|props file location>"
    echo $"       $0 extrecconfexists           <cluster name|props file location>"
    echo $"       $0 recoveryfileexists         <cluster name|props file location>"
    echo $"       $0 fileexists                 <cluster name|props file location> <file path>"
    echo $"       $0 reconfigurerecconf         <cluster name|props file location> <host> <is switchover>"
    echo $"       $0 clearwalfiles              <cluster name|props file location>"
    echo $"       $0 startdb                    <cluster name|props file location>"
    echo $"       $0 stopdb                     <cluster name|props file location>"
    echo $"       $0 readpgversion              <cluster name|props file location>"
    echo $"       $0 readrecoveryconf           <cluster name|props file location>"
    echo $"       $0 touchfile                  <file path>"
    echo $"       $0 appendautoconf             <cluster name|props file location> <contents>"
    exit 1
}

#
# look for the last occurrence of a non-commented line. Leading and trailing
# white spaces trimmed.
#
# shell functions can't return string, so rely on clunky outvar
#
# Params
#   $1 outvar - this is the name of the variable to store the result in
#   $2 property name to look for
#   $3 property file to grep in
getProp() {
    local OUTVAR=$1
    local PROP_NAME=$2
    local PROP_FILE=$3
    eval "${OUTVAR}"="$(grep "${PROP_NAME}" "${PROP_FILE}" | grep -v \# | tail -1 | cut -d'=' -f2 | awk '{$1=$1};1')"
}

#
# Triggers promotion of a standby with promote command of pg_ctl utility
#
promoteStandby() {
    local PROP_FILE=$1
    getProp DB_BIN db.bin "${PROP_FILE}"
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -z "$DATA_DIR" ]; then
        # some kind of error grepping the recovery dir location from the prop file
        return 1
    else
        # The -W (--no-wait) option in below command makes the command asynchronous
        # and exists with exit code 0 even though database is still in recovery
        "${DB_BIN}/pg_ctl" promote -D $DATA_DIR -W
        return $?
    fi
}

#
# Touch file passed in from agent, for instance standby.signal.
# The file must not already exist.
#
touchFile() {
    local FILE=$1
    if [ -f "$FILE" ] ; then
        echo "File ${FILE} already exists."
        return 1
    fi
    touch "$1"
}

#
# read the PG_VERSION file to get version of database. This is needed to if db is not running.
#
readPGVersion() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -z "$DATA_DIR" ]; then
        # some kind of error grepping the data dir location from the prop file
        return 1
    else
        local PG_VERSION="${DATA_DIR}/PG_VERSION"
        if [ -e "${PG_VERSION}" ]; then
            cat ${PG_VERSION}
        else
            echo "ERROR: cannot find file ${PG_VERSION}."
            return 1
        fi
    fi
}

#
# read the recovery.conf file. this is needed during switchover to save on original primary
# for database versions before v12
#
readRecoveryConf() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -z "$DATA_DIR" ]; then
        # some kind of error grepping the recovery dir location from the prop file
        return 1
    else
        local RECOVERY_CONF="${DATA_DIR}/recovery.conf"
        if [ -e "${RECOVERY_CONF}" ]; then
            cat ${RECOVERY_CONF}
        else
            echo "ERROR: cannot find file ${RECOVERY_CONF}."
            return 1
        fi
    fi
}

#
# validate the db.config.dir property
# return success if:
#    db.config.dir dir exists and contains postgresql.conf file
#    db.config.dir value is not specified
#
validateDBConfig() {
    local PROP_FILE=$1
    getProp DB_CONFIG_DIR db.config.dir "${PROP_FILE}"
    if [ -z "$DB_CONFIG_DIR" ]; then
        # this is  not a required prop and can be kept blank
        return 0
    else
        local CONFIG_FILE="${DB_CONFIG_DIR}/postgresql.conf"
        if [ -w "$CONFIG_FILE" ] ; then
            # file exists and is writable
            return 0
        else
            echo "ERROR: db.config.dir must exist, be a directory, and contain postgresql.conf file: $DB_CONFIG_DIR"
            return 1
        fi
    fi
}

#
# will replace the host and application_name information in recovery.conf file with
# new host param and application_name set in properties file. this is used with
# database versions before v12
#
reconfigureRecConf() {
    local PROP_FILE=$1
    local NEW_HOST=$2
    local SWITCHOVER=$3

    source $EFM_CONFIG
    source $RUN_JAVA

    # passing in date value to be consistent across script
    runJREApplication -Xmx16m -cp $LIB $CLASS reconfigureRecConf "${PROP_FILE}" "${NEW_HOST}" "${SWITCHOVER}" "$(date +%F-%T)"  < /dev/null
}

#
# Append the passed-in text to the postgresql.auto.conf file. The file will be in the
# db.config.dir directory if specified, otherwise in db.data.dir directory.
#
appendAutoConf() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    local PG_AUTO_CONF="${DATA_DIR}/postgresql.auto.conf"
    if [ -f "${PG_AUTO_CONF}" ]; then
        # -e here enables the interpretation of backslash escapes
        # this option creates new line for '\n' from where it is used.
        echo -e "$2" >> ${PG_AUTO_CONF}
    else
        echo "Error: cannot find file ${PG_AUTO_CONF}"
        return 1
    fi
}

#
# backup existing pg_wal directory and then clear files from original
#
clearWalFiles() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [[ -z "$DATA_DIR" ]]; then
        # some kind of error grepping the recovery dir location from the prop file
        return 1
    else
        local WAL_DIR="pg_wal"
        if [[ -z "${WAL_DIR}" ]]; then
          echo "ERROR: Could not find pg_wal directory at ${DATA_DIR}."
          return 1
        fi

        local TARGET_WAL_DIR
        if [ -L "${DATA_DIR}/${WAL_DIR}" ]; then
            TARGET_WAL_DIR=$(readlink ${DATA_DIR}/${WAL_DIR})
        else
             TARGET_WAL_DIR="${DATA_DIR}/${WAL_DIR}"
        fi

        if [ -w "${TARGET_WAL_DIR}"/.. ]; then
          # First remove existing backups, e.g. pg_wal_<....>,
          # before taking a new one.
          echo "Removing existing backups ${TARGET_WAL_DIR}_* before creating new one."
          rm -rf "${TARGET_WAL_DIR}"_*
          cp -R "${TARGET_WAL_DIR}" "${TARGET_WAL_DIR}_$(date +%F-%T)"
          rm -rf  "${TARGET_WAL_DIR:?}"/*
        else
          # In this case we would not be removing the WALs because we can't take it's backup
          echo "ERROR: Parent dir of ${TARGET_WAL_DIR} is not writable."
          return 1
        fi
    fi
}

#
# start database
#
startDb() {
    local PROP_FILE=$1
    getProp PG_CTL_PATH db.bin "${PROP_FILE}"
    getProp DB_CONFIG_DIR db.config.dir "${PROP_FILE}"

     if [ -z "$DB_CONFIG_DIR" ]; then
        # if db.config.dir value not specified then default to db.data.dir
        getProp DATA_DIR db.data.dir "${PROP_FILE}"
        if [ -z "$DATA_DIR" ]; then
            # some kind of error grepping the recovery dir location from the prop file
            return 1
        else
            DB_CONFIG_DIR=$DATA_DIR
        fi
     fi

     "${PG_CTL_PATH}/pg_ctl" start -w -D ${DB_CONFIG_DIR}
}

#
# stop database
#
stopDb() {
    local PROP_FILE=$1
    getProp PG_CTL_PATH db.bin "${PROP_FILE}"
    getProp DB_CONFIG_DIR db.config.dir "${PROP_FILE}"

     if [ -z "$DB_CONFIG_DIR" ]; then
        # if db.config.dir value not specified then default to db.data.dir
        getProp DATA_DIR db.data.dir "${PROP_FILE}"
        if [ -z "$DATA_DIR" ]; then
            # some kind of error grepping the recovery dir location from the prop file
            return 1
        else
            DB_CONFIG_DIR=$DATA_DIR
        fi
     fi

     "${PG_CTL_PATH}/pg_ctl" stop -m fast -D ${DB_CONFIG_DIR}
}

#
# validate the recovery conf property
# return success if:
#    db.data.dir exists and is writable
#    db.data.dir is a dir
#
# Environment.java has already verified that the property is set in the prop file
#
# Note: this function no longer checks to see if the recovery.conf file actually exists
#       because we are now asking the db if it is in recovery mode or not at startup to
#       assign primary/standby roles.
#
validateDataDir() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -z "$DATA_DIR" ]; then
        # some kind of error grepping the recovery dir location from the prop file
        return 1
    else
        if [ -w "$DATA_DIR" ] && [ -d "$DATA_DIR" ]; then
             return 0
        else
            echo "ERROR: db.data.dir must exist, be a directory, and be writable: $DATA_DIR"
            return 1
        fi
    fi
}

#
#
#
validatePgBin() {
    local PROP_FILE=$1
    getProp BIN_DIR db.bin "${PROP_FILE}"
    if [ -z "$BIN_DIR" ]; then
        # some kind of error grepping the db.bin location from the prop file
        return 1
    else
        if [ -x "$BIN_DIR/pg_ctl" ]; then
             return 0
        else
            echo "ERROR: db.bin must exist, be a directory, and contain pg_ctl: $BIN_DIR"
            return 1
        fi
    fi
}

#
# Checks if the parent directory of the pg_wal directory is writable or not.
# Need to ensure it's writeable to avoid failure while taking WAL directory backup during promotion.
# This would be used if pg_wal is a symlink. Otherwise this directory would be present in
# PGDATA which is writeable anyways.
#
validatePgWALDir() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -z "$DATA_DIR" ]; then
        echo "ERROR: Could not read value of db.data.dir from $PROP_FILE"
        return 1
    else
        local WAL_DIR="pg_wal"
        if [[ -z "${WAL_DIR}" ]]; then
          echo "ERROR: Could not find pg_wal directory at ${DATA_DIR}."
          return 1
        fi

        local TARGET_WAL_DIR
        if [ -L "${DATA_DIR}/${WAL_DIR}" ]; then
            TARGET_WAL_DIR=$(readlink ${DATA_DIR}/${WAL_DIR})
            if [ ! -w "${TARGET_WAL_DIR}"/.. ]; then
              echo "ERROR: Parent dir of ${TARGET_WAL_DIR} is not writable."
              return 1
            fi
        fi
    fi
}

#
# write the recovery.conf file
#
writeRecoveryConfFile() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -z "$DATA_DIR" ]; then
        # some kind of error grepping the recovery dir location from the prop file
        return 1
    elif [ -e ${DATA_DIR}/recovery.conf ]; then
        grep "$EDB_FM" ${DATA_DIR}/recovery.conf >/dev/null 2>&1
        if [ $? -eq 0 ]; then
            # file exists, but it's ours, so delete it and re-write it (below)
            rm -f ${DATA_DIR}/recovery.conf
        else
            # file exists and it's not ours, so rename it and write ours (below)
            mv ${DATA_DIR}/recovery.conf "${DATA_DIR}/recovery.conf.$(date +%Y-%m-%d_%H:%M)"
        fi
    fi
    cat > ${DATA_DIR}/recovery.conf << EOF
# $EDB_FM
# This generated recovery.conf file prevents the db server from accidentally
# being restarted as a primary since a failover or promotion has occurred.
# For v12 and above the settings are ignored; the presence of the file
# prevents startup.
standby_mode = on
restore_command = 'echo 2>"recovery suspended on failed server node"; exit 1'
EOF
    return $?
}

#
# write custom recovery.conf file
#
writeCustomRecoveryConf() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -z "$DATA_DIR" ]; then
        # some kind of error grepping the recovery dir location from the prop file
        return 1
    elif [ -e ${DATA_DIR}/recovery.conf ]; then
        # file exists, so rename it and write ours (below)
        mv ${DATA_DIR}/recovery.conf "${DATA_DIR}/recovery.conf.$(date +%Y-%m-%d_%H:%M)"
    fi
    echo -e "$2" > ${DATA_DIR}/recovery.conf
    return $?
}

#
# remove recovery.conf file
#
removeRecoveryConf() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -z "$DATA_DIR" ]; then
        # some kind of error grepping the recovery dir location from the prop file
        return 1
    fi
    rm -f ${DATA_DIR}/recovery.conf
    return $?
}

#
# test if an externally written recovery.conf file exists (not created by EFM). We don't really
# care if we find a recovery.conf file created by EFM with this function.
#
extRecConfExists() {
    local PROP_FILE=$1
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    # Note: we aren't testing for -z $DATA_DIR here because the prop should
    #       have already been validated with validatedatadir().
    if [ -e ${DATA_DIR}/recovery.conf ]; then
        grep "$EDB_FM" ${DATA_DIR}/recovery.conf >/dev/null 2>&1
        if [ $? -eq 0 ]; then
            # file exists, but it's ours, so delete it and return false
            rm -f ${DATA_DIR}/recovery.conf
            return 1
        else
            # file exists and it's not ours, so return true
            return 0
        fi
    else
        # file doesn't exist
        return 1
    fi
}

#
# test to see if the given file exists
#
fileExists() {
    local FILE_PATH=$1
    if [ -f "${FILE_PATH}" ]; then
        # file exists
        return 0
    else
        return 1
    fi
}

#
# test to see if at least one file exists signifying the database will be in recovery
#
recoveryFileExists() {
    local PROP_FILE=$1
    getProp DIR db.data.dir "${PROP_FILE}"
    # Note: we aren't testing for -z $DIR here because the prop should
    #       have already been validated with validatedatadir().
    if [ ! -f "${DIR}/recovery.conf" ] && [ ! -f "${DIR}/standby.signal" ] && [ ! -f "${DIR}/recovery.signal" ]; then
        return 1
    else
        # at least one exists
        return 0
    fi
}

#
# process the command
#
# command names correlate to enum values in SudoFunctions.java. If you add new functions
# here, then also add a value in SudoFunctions...
#
if [ $# -gt 1 ]; then
    COMMAND=$1

    if [[ "$2" == *\.properties ]]; then
        # If the given value ends with ".properties" consider it as a properties file path
        PROPS=$2;
    elif [ -f ~/${EFM}/"$2".properties ]; then
        # If props file path not given then check if the properties file with given cluster
        #  name exists in user's home directory. If present, consider that as properties file.
        PROPS=~/${EFM}/$2.properties
    else
        # If not found in user's home dir too then use the default props file location
        PROPS="/etc/edb/${EFM}/$2.properties"
    fi

    case "$COMMAND" in
        promotestandby)
            promoteStandby "${PROPS}"
            exit $?
            ;;
        touchfile)
            touchFile "$2"
            exit $?
            ;;
        readpgversion)
            readPGVersion "${PROPS}"
            exit $?
            ;;
        readrecoveryconf)
            readRecoveryConf "${PROPS}"
            exit $?
            ;;
        validatedatadir)
            validateDataDir "${PROPS}"
            exit $?
            ;;
        validatedbconf)
            validateDBConfig "${PROPS}"
            exit $?
            ;;
        validatepgbin)
            validatePgBin "${PROPS}"
            exit $?
            ;;
        validatepgwaldir)
            validatePgWALDir "${PROPS}"
            exit $?
            ;;
        writerecoveryconf)
            writeRecoveryConfFile "${PROPS}"
            exit $?
            ;;
        removerecoveryconf)
            removeRecoveryConf "${PROPS}"
            exit $?
            ;;
        writecustomrecoveryconf)
            shift
            shift
            TEXT=$*
            writeCustomRecoveryConf "${PROPS}" "${TEXT}"
            exit $?
            ;;
        appendautoconf)
            shift
            shift
            TEXT=$*
            appendAutoConf "${PROPS}" "${TEXT}"
            exit $?
            ;;
        extrecconfexists)
            extRecConfExists "${PROPS}"
            exit $?
            ;;
        recoveryfileexists)
            recoveryFileExists "${PROPS}"
            exit $?
            ;;
        fileexists)
            fileExists "$3"
            exit $?
            ;;
        reconfigurerecconf)
            reconfigureRecConf "${PROPS}" "$3" "$4"
            exit $?
            ;;
        clearwalfiles)
            clearWalFiles "${PROPS}"
            exit $?
            ;;
        startdb)
            startDb "${PROPS}"
            exit $?
            ;;
        stopdb)
            stopDb "${PROPS}"
            exit $?
            ;;
        *)
            usage
    esac
else
    usage
fi
