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

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

# version name
EFM=efm-5.1

# Java files
EFM_CONFIG=/etc/sysconfig/$EFM
RUN_JAVA=/usr/edb/$EFM/bin/runJavaApplication.sh
LIB=/usr/edb/$EFM/lib/EFM-5.1.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 standbysignalexists        <cluster name|props file location>"
    echo $"       $0 fileexists                 <cluster name|props file location> <file path>"
    echo $"       $0 pgbasebackup               <cluster name|props file location> <params> <connection string>"
    echo $"       $0 pgrewind                   <cluster name|props file location> <connection string> <dry-run> <restore-wals>"
    echo $"       $0 archivereadywal            <cluster name|props file location>"
    echo $"       $0 reconfigurerecconf         <cluster name|props file location> <host> <is switchover>"
    echo $"       $0 clearwalfiles              <cluster name|props file location> <create backup>"
    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>"
    echo $"       $0 readparam                  <cluster name|props file location> <parameter>"
    echo $"       $0 pgcontroldata              <cluster name|props file location>"
    echo $"       $0 pgwaldump                  <cluster name|props file location> <timeline> <start>"
    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 run the archive_command for ready wal files
#
archiveReadyWal() {
    local PROP_FILE=$1

    source $EFM_CONFIG
    source $RUN_JAVA

    runJREApplication -Xmx16m -cp $LIB $CLASS archiveReadyWal "${PROP_FILE}" < /dev/null
}

#
# 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
}

#
# Use the postgres -C option to read the value of run-time parameter
#
readParam() {
    local PROP_FILE=$1
    getProp DB_BIN db.bin "${PROP_FILE}"
    getProp DATA_DIR db.data.dir "${PROP_FILE}"
    if [ -e ${DB_BIN}/postgres ]; then
        "${DB_BIN}/postgres" -D "${DATA_DIR}" -C "$2"
    elif [ -e ${DB_BIN}/edb-postgres ]; then
        "${DB_BIN}/edb-postgres" -D "${DATA_DIR}" -C "$2"
    else
        echo "ERROR: cannot find postgres or edb-postgres in ${DB_BIN}."
        return 1
    fi
}

#
# backup existing pg_wal directory and then clear files from original
#
clearWalFiles() {
    local PROP_FILE=$1
    local BACKUP=$2
    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}_*."
          rm -rf "${TARGET_WAL_DIR}"_*
          # Create new backup if specified with "true"
          if [ "$BACKUP" = "true" ]; then
            echo "Creating backup of pg_wal contents."
            cp -R "${TARGET_WAL_DIR}" "${TARGET_WAL_DIR}_$(date +%F-%T)"
          else
            echo "Not creating pg_wal backup."
          fi
          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
}

#
# Will remove the data dir and run pg_basebackup
#
pgBasebackup() {
    local PROP_FILE=$1
    getProp DB_BIN db.bin "${PROP_FILE}"
    getProp DATA_DIR db.data.dir "${PROP_FILE}"

    echo "Removing ${DATA_DIR}/pg_wal."
    local WAL_DIR="pg_wal"
    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
    rm -rf  "${TARGET_WAL_DIR:?}"

    echo "Removing data dir ${DATA_DIR}."
    rm -rf "${DATA_DIR}"
    echo "Running ${DB_BIN}/pg_basebackup."
    # quotes around $3 needed for -d param (string with spaces in it)
    "${DB_BIN}/pg_basebackup" ${2} -d "${3}"
}

#
# Will run pg_rewind
#
pgRewind() {
    local PROP_FILE=$1
    local DRY_RUN=$3
    local RESTORE_WALS=$4
    getProp DB_BIN db.bin "${PROP_FILE}"
    getProp DATA_DIR db.data.dir "${PROP_FILE}"

    if [ "$DRY_RUN" = "true" ]; then
      echo "Running ${DB_BIN}/pg_rewind with --dry-run option."
      "${DB_BIN}/pg_rewind" --source-server "${2}" --target-pgdata "${DATA_DIR}" -R --dry-run
    elif [ "$RESTORE_WALS" = "true" ]; then
      echo "Running ${DB_BIN}/pg_rewind with --restore-target-wal."
      "${DB_BIN}/pg_rewind" --source-server "${2}" --target-pgdata "${DATA_DIR}" -R --restore-target-wal
    else
      echo "Running ${DB_BIN}/pg_rewind."
      "${DB_BIN}/pg_rewind" --source-server "${2}" --target-pgdata "${DATA_DIR}" -R
    fi
}


#
# Run pg_waldump to calculate checkpoint end
#
pgWaldump() {
    local PROP_FILE=$1
    getProp DB_BIN db.bin "${PROP_FILE}"
    getProp DATA_DIR db.data.dir "${PROP_FILE}"

    "${DB_BIN}/pg_waldump" -t ${2} -s ${3} -p "${DATA_DIR}/pg_wal" -n 2
}

#
# Run pg_controldata to get the control file information
#
pgControldata() {
    local PROP_FILE=$1
    getProp DB_BIN db.bin "${PROP_FILE}"
    getProp DATA_DIR db.data.dir "${PROP_FILE}"

    echo "Running ${DB_BIN}/pg_controldata -D ${DATA_DIR}."
    "${DB_BIN}/pg_controldata" -D "${DATA_DIR}"
}

# 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
}

#
# test to see if standby.signal exists
#
standbySignalExists() {
    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}/standby.signal" ]; then
        return 0
    else
        # file does not exist
        return 1
    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 $?
            ;;
        readparam)
            shift
            shift
            PARAM=$*
            readParam "${PROPS}" "${PARAM}"
            exit $?
            ;;
        pgcontroldata)
            pgControldata "${PROPS}"
            exit $?
            ;;
        extrecconfexists)
            extRecConfExists "${PROPS}"
            exit $?
            ;;
        recoveryfileexists)
            recoveryFileExists "${PROPS}"
            exit $?
            ;;
        standbysignalexists)
            standbySignalExists "${PROPS}"
            exit $?
            ;;
        fileexists)
            fileExists "$3"
            exit $?
            ;;
        pgbasebackup)
            pgBasebackup "${PROPS}" "$3" "$4"
            exit $?
            ;;
        pgrewind)
            pgRewind "${PROPS}" "$3" "$4" "$5"
            exit $?
            ;;
        archivereadywal)
            archiveReadyWal "${PROPS}"
            exit $?
            ;;
        pgwaldump)
            pgWaldump "${PROPS}" "$3" "$4"
            exit $?
            ;;
        reconfigurerecconf)
            reconfigureRecConf "${PROPS}" "$3" "$4"
            exit $?
            ;;
        clearwalfiles)
            clearWalFiles "${PROPS}" "$3"
            exit $?
            ;;
        startdb)
            startDb "${PROPS}"
            exit $?
            ;;
        stopdb)
            stopDb "${PROPS}"
            exit $?
            ;;
        *)
            usage
    esac
else
    usage
fi
