#!/bin/bash
#
# postgresql-setup - Initialization and upgrade operations for PostgreSQL

test -z "$PATH" && export PATH="/sbin:/usr/sbin:/bin:/usr/bin"

test x"$PGSETUP_DEBUG" != x && set -x && PS4='${LINENO}: '

# Directory containing the postmaster executable
PGENGINE=/opt/rh/rh-postgresql94/root/usr/bin

# Distribution README file
README_DIST=/opt/rh/rh-postgresql94/root/usr/share/doc/rh-postgresql94-postgresql-9.4.9/README.rpm-dist

# Home directory of postgres user
POSTGRES_HOMEDIR=/var/lib/pgsql

# Convenient tool-aliases
SU_POSTGRES="/usr/sbin/runuser -s /bin/sh -l postgres"

# The where PostgreSQL server listens by default
PGPORT_DEF=5432

. "/opt/rh/rh-postgresql94/root/usr/share/postgresql-setup/library.sh"

source scl_source enable rh-postgresql94 ;

# We upgrade by default from system's default PostgreSQL installation
option_upgradefrom="postgresql"

USAGE_STRING=$"\
Usage: $0 MODE_OPTION [--unit=UNIT_NAME] [OPTION...]

Script is aimed to help sysadmin with basic database cluster administration.
Usually, \"postgresql-setup --initdb\" and \"postgresql-setup --upgrade\" is
enough, however there are other options described below.

For more info and howto/when use this script please look at the documentation
file $README_DIST.

Available operation mode:
  --initdb      Initialize new PostgreSQL database cluster.  This is usually the
                first action you perform after PostgreSQL server installation.
  --upgrade     Upgrade database cluster for new major version of PostgreSQL
                server.  See the --upgrade-from option for more info.

Options:
  --unit=UNIT_NAME           The UNIT_NAME is used to select proper unit
                             configuration (unit == service or initscript name
                             on non-systemd systems).  For example, if you want
                             to work with unit called
                             'postgresql@com_example.service', you should use
                             'postgresql@com_example' (without trailing .service
                             string).  When no UNIT_NAME is explicitly passed,
                             the 'postgresql' string is used by default.
  --port=PORT                port where the initialized server will listen for
                             connections"

test 0 -eq 0 && \
USAGE_STRING+="
  --datadir=PATH             Specify absolute path to DB data directory, only
                             use with --new-systemd-unit.
  --new-systemd-unit         Pre-generate system'd configuration in drop-in
                             directory if the unit is not yet configured,
                             requires non-default --unit specified and explicit
                             --datadir and --port."

USAGE_STRING+="
  --upgrade-ids              Print list of available IDs of upgrade scenarios to
                             standard output.
  --upgrade-from=ID          Specify id \"old\" postgresql stack to upgrade
                             from.  List of available IDs can be listed by
                             --upgrade-ids.  Default is '$option_upgradefrom'.

Other options:
  --help                     show this help
  --version                  show version of this package
  --debug                    show basic debugging information

Environment:
  PGSETUP_INITDB_OPTIONS     Options carried by this variable are passed to
                             subsequent call of \`initdb\` binary (see man
                             initdb(1)).  This variable is used also during
                             'upgrade' mode because the new cluster is actually
                             re-initialized from the old one.
  PGSETUP_PGUPGRADE_OPTIONS  Options in this variable are passed next to the
                             subsequent call of \`pg_upgrade\`.  For more info
                             about possible options please look at man
                             pg_upgrade(1).
  PGSETUP_DEBUG              Set to '1' if you want to see very verbose shell
                             debugging output."


print_version()
{
    echo "postgresql-setup 3.3"
    echo $"Built against PostgreSQL version 9.4.9."
}


check_not_initialized()
{
    if test -f "$pgdata/PG_VERSION"; then
        error $"Data directory $pgdata is not empty!"
        return 1
    fi
    return 0
}


# code shared between initdb and upgrade actions
perform_initdb()
{
    if [ ! -e "$pgdata" ]; then
        mkdir "$pgdata" || return 1
        chown postgres:postgres "$pgdata"
        chmod go-rwx "$pgdata"
    fi

    # Clean up SELinux tagging for pgdata
    [ -x /sbin/restorecon ] && /sbin/restorecon "$pgdata"

    # Create the initdb log file if needed
    if [ ! -e "$initdb_log" -a ! -h "$initdb_log" ]; then
        touch "$initdb_log" || return 1
        chown postgres:postgres "$initdb_log"
        chmod go-rwx "$initdb_log"
        [ -x /sbin/restorecon ] && /sbin/restorecon "$initdb_log"
    fi

    # Initialize the database
    initdbcmd="source scl_source enable rh-postgresql94 ;"
    initdbcmd+=" $PGENGINE/initdb --pgdata='$pgdata' --auth='ident'"
    initdbcmd+=" $PGSETUP_INITDB_OPTIONS"

    $SU_POSTGRES -c "$initdbcmd" >> "$initdb_log" 2>&1 < /dev/null

    # Create directory for postmaster log files
    mkdir "$pgdata/pg_log"
    chown postgres:postgres "$pgdata/pg_log"
    chmod go-rwx "$pgdata/pg_log"
    [ -x /sbin/restorecon ] && /sbin/restorecon "$pgdata/pg_log"

    # This if-fork is just to not unnecessarily overwrite what upstream
    # generates by initdb (upstream implicitly uses PGPORT_DEF).
    if test "$pgport" != "$PGPORT_DEF"; then
        local pgconf="$pgdata/postgresql.conf"
        sed -i "s|^[[:space:]#]*port[[:space:]]=[^#]*|port = $pgport |g" \
                "$pgconf" \
            && grep "^port = " "$pgconf" >/dev/null

        if test $? -ne 0; then
            error "can not change port in $pgdata/postgresql.conf"
            return 1
        fi
    fi

    test -f "$pgdata/PG_VERSION"
}


initdb()
{
    port_info=
    test "$pgport" != "$PGPORT_DEF" \
        && port_info=$", listening on port '$pgport'"

    info $"Initializing database in '$pgdata'$port_info"
    if check_not_initialized && perform_initdb; then
        info $"Initialized, logs are in ${initdb_log}"
        cleanup_dropin=
    else
        error $"Initializing database failed, possibly see $initdb_log"
        script_result=1
    fi
}


old_data_in_use()
{
    local pidfile="$pgdataold/postmaster.pid"
    test -f "$pidfile" || return 1
    error   $"The pidfile '$pidfile' exists.  Verify that there is no postmaster"
    error_q $"running the $pgdataold directory."
}


upgrade()
{
    local inplace=false
    test "$pgdata" = "$upgradefrom_data" && inplace=true

    debug "running inplace upgrade: $inplace"

    # must see previous version in PG_VERSION
    local old_data_version="`cat "$upgradefrom_data/PG_VERSION"`"
    if [ ! -f "$upgradefrom_data/PG_VERSION" -o \
         x"$old_data_version" != x"$upgradefrom_major" ]
    then
        error $"Cannot upgrade because the database in $upgradefrom_data is of"
        error_q $"version $old_data_version but it should be $upgradefrom_major"
        exit 1
    fi
    if [ ! -x "$PGENGINE/pg_upgrade" ]; then
        error $"Please install the rh-postgresql94-postgresql-upgrade package."
        exit 5
    fi

    # Set up log file for pg_upgrade
    rm -f "$upgrade_log"
    touch "$upgrade_log" || die "can't write into $upgrade_log file"

    chown postgres:postgres "$upgrade_log"
    chmod go-rwx "$upgrade_log"
    [ -x /sbin/restorecon ] && /sbin/restorecon "$upgrade_log"

    # Move old DB to pgdataold

    if $inplace; then
        pgdataold="${pgdata}-old"
        rm -rf "$pgdataold"
        mv "$pgdata" "$pgdataold" || exit 1
    else
        pgdataold="$upgradefrom_data"
    fi

    # Create configuration file for upgrade process
    HBA_CONF_BACKUP="$pgdataold/pg_hba.conf.postgresql-setup.`date +%s`"
    HBA_CONF_BACKUP_EXISTS=0

    if [ ! -f $HBA_CONF_BACKUP ]; then
        mv "$pgdataold/pg_hba.conf" "$HBA_CONF_BACKUP"
        HBA_CONF_BACKUP_EXISTS=1

        # For fluent upgrade 'postgres' user should be able to connect
        # to any database without password.  Temporarily, no other type
        # of connection is needed.
        echo "local all postgres ident" > "$pgdataold/pg_hba.conf"
    fi

    info $"Upgrading database."

    scls_upgrade_hacks=
    test -n "$upgradefrom_scls" && {
        debug "scls [$upgradefrom_scls] will be enabled"
        scls_upgrade_hacks="source scl_source enable $upgradefrom_scls ;"
    }

    test -n "$upgradefrom_redhat_sockets_hack" && {
        debug "upgrading from redhat server"
        socket_hacks="export REDHAT_PGUPGRADE_FROM_RHEL=yes ;"
    }

    test -n "$upgradefrom_pghost_override" && {
        pghost_override="PGHOST='$upgradefrom_pghost_override'"
    }

    local failure_cleanup=true
    if old_data_in_use; then
        script_result=1
        # Cleanup makes sense once perform_initdb gets called.
        failure_cleanup=false
    elif ! check_not_initialized; then
        # Don't try to re-init initialized data directory and also do not
        # remove it after this unsuccessful upgrade.
        script_result=1
        failure_cleanup=false
    elif perform_initdb; then
        $inplace && link_option=--link

        # After creating the empty new-format database, do the upgrade
        run_cmd_as_dbadmin "\
                        $scls_upgrade_hacks \
                        $socket_hacks \
                        $pghost_override \
                        $PGENGINE/pg_upgrade \
                        '--old-bindir=$upgradefrom_engine' \
                        '--new-bindir=$PGENGINE' \
                        '--old-datadir=$pgdataold' \
                        '--new-datadir=$pgdata' \
                        $link_option \
                        '--old-port=$PGPORT' '--new-port=$PGPORT' \
                        --username=postgres \
                        $PGSETUP_PGUPGRADE_OPTIONS" \
                        "$upgrade_log" "$upgrade_log"
        if [ $? -ne 0 ]; then
            # pg_upgrade failed
            error $"pg_upgrade tool failed"
            script_result=1
        fi
    else
        error $"initdb failed"
        script_result=1
    fi

    # Move back the backed-up pg_hba.conf regardless of the script_result.
    if [ x$HBA_CONF_BACKUP_EXISTS = x1 ]; then
        mv -f "$HBA_CONF_BACKUP" "$pgdataold/pg_hba.conf"
    fi

    if [ $script_result -eq 0 ]; then
        info $"Upgraded OK."
        warn $"The configuration files were replaced by default configuration."
        warn $"The previous configuration and data are stored in folder"
        warn $pgdataold.
    else
        # Clean up after failure.
        $failure_cleanup && rm -rf "$pgdata"
        $inplace && mv "$pgdataold" "$pgdata"
        error $"Upgrade failed."
    fi
    info $"See $upgrade_log for details."
}


generate_systemd_dropin()
{
    local service="$1"
    local dropindir="/etc/systemd/system/$service.service.d"
    local dropin="$dropindir/30-postgresql-setup.conf"

    test -e "$dropindir" \
        && die "The systemd drop-in directory '$dropindir' exists already"

    mkdir -p "$dropindir" \
        || die "Can not create '$dropindir'"

    cleanup_dropin="rm -rf \"$dropindir\""

    cat <<EOF > "$dropin" || die "Can not write to '$dropin'"
[Service]
Environment=PGDATA=$pgdata
EOF

    reload_systemd="systemctl daemon-reload"
    $reload_systemd || die $"Can not perform '$reload_systemd'"

    info $"The '$option_service' configured in '$dropindir' directory"
}


check_daemon_reload()
{
    local nr_option=NeedDaemonReload

    test 0 = 1 && return 0

    local nr_out="`systemctl show -p $nr_option $option_service.service 2>/dev/null`"
    if [[ "$nr_out" != "$nr_option=no" ]]; then
        error   $"Note that systemd configuration for '$option_service' changed."
        error_q $"You need to perform 'systemctl daemon-reload' otherwise the"
        error_q $"results of this script can be inadequate."
        exit 1
    fi
}


handle_service_env()
{
    local service="$1"

    local systemd_env="$(systemctl show -p Environment "${service}.service")" \
        || { return; }

    for env_var in `echo "$systemd_env" | sed 's/^Environment=//'`; do
        # If one variable name is defined multiple times the last definition wins.
        case "$env_var" in
            PGDATA=*)
                unit_pgdata="${env_var##PGDATA=}"
                debug "unit's datadir: '$unit_pgdata'"
                ;;
            PGPORT=*)
                unit_pgport="${env_var##PGPORT=}"
                debug "unit's pgport: $unit_pgport"
                ;;
        esac
    done
}


handle_envfile()
{
    local file="$1"

    debug "trying to read '$file' env file"
    if test ! -r "$file"; then
        if test 0 = 1; then
            return
        fi
        error   "Can not read EnvironmentFile '$file' specified"
        error_q "in ${service}.service"
    fi

    # Note that the env file parser in systemd does not perform exactly the
    # same job.
    unset PGPORT PGDATA
    . "$file"
    envfile_pgdata="$PGDATA"
    envfile_pgport="$PGPORT"
    unset PGPORT PGDATA
}


handle_service_envfiles()
{
    local mode="$1"
    local service="$2"

    local envfiles="$(systemctl show -p EnvironmentFiles "${service}.service")"\
        || return

    test -z "$envfiles" && return

    envfiles=$(echo $envfiles | \
        sed -e 's/^EnvironmentFile=//' \
            -e 's| ([^)]*)$||'
    )

    # Read the file names line-by-line (spaces may be inside)
    while read line; do
        handle_envfile "$line"
    done <<<"$envfiles"
}


handle_pgconf()
{
    local datadir="$1"
    local conffile="$datadir/postgresql.conf"

    debug "postgresql.conf: $conffile"

    test -r "$conffile" || {
        error "config file $conffile is not readable or does not exist"
        die "Old cluster in '$data' does not seem to be initialized"
    }

    local sp='[[:space:]]'
    local sed_expr="s/^$sp*port$sp*=$sp\([0-9]\+\).*/\1/p"

    rv=0
    conf_pgport=`sed -n "$sed_expr" $conffile | tail -1` || rv=1
    test -n "$conf_pgport" && debug "postgresql.conf pgport: $conf_pgport"
    return $rv
}


service_configuration()
{
    local data=
    local port=
    local unit_pgport=
    local unit_pgdata=
    local envfile_pgport=
    local envfile_pgdata=

    # 'mode' is 'initdb' or 'upgrade'.  Basically, if called with mode=initdb, we
    # parse configuration of the current (maybe already configured) service.
    # When run with mode=upgrade, we try to parse the configuration of the old
    # PostgreSQL configuration that we try to upgrade from.

    local mode="$1" datavar="$2" portvar="$3" service="$4"

    debug "running service_configuration() for $mode"

    local service="$service"
    test upgrade = "$mode" && service="$option_upgradefrom"

    if test "0" = 1; then
        # Sysvinit has the default PGDATA (for default unit name only)
        # configured directly in the initscript, so no additional configuration
        # must exist.  Set the default value of pgdata here to match whats in
        # initscript for the cases when no additional configuration file exists.
        # This is done to avoid parsing of whole initscript (for the real value)
        # and mainly to not fail in the logic following 'service_configuration'
        # call, where we usually want to error that pgdata is not defined..
        # Don't set the default pgdata for upgrade case, however, as we must
        # upgrade only from already properly configured & working stack (missing
        # pgdata here is a good reason to die later).
        test initdb = "$mode" && test "$service" = "rh-postgresql94-postgresql" \
            && set_var "$datavar" "/opt/rh/rh-postgresql94/root/var/lib/pgsql/data"
        handle_envfile "/etc/sysconfig/pgsql/$service"
    else
        # We ship two service files, rh-postgresql94-postgresql.service and
        # rh-postgresql94-postgresql@.service.  The former has PGDATA set by default
        # similarly to sysvinit case.
        handle_service_env "$service"
        handle_service_envfiles "$option_mode" "$service"
    fi

    # EnvironmentFile beats Environment configuration in systemd.  In sysvinit
    # there is no "unit_pgdata".  So make sure the envfile_gpdata is used later
    # than unit_pgdata.
    test -n "$unit_pgdata"      && set_var "$datavar" "$unit_pgdata"
    test -n "$envfile_pgdata"   && set_var "$datavar" "$envfile_pgdata"

    # skip for the first run
    test initdb = "$mode" && return

    set_var data "\$$datavar"
    handle_pgconf "$data"

    test -n "$conf_pgport"    && set_var "$portvar" "$conf_pgport"
    test -n "$unit_pgport"    && set_var "$portvar" "$unit_pgport"
    test -n "$envfile_pgport" && set_var "$portvar" "$envfile_pgport"
}

cleanup_dropin=
exit_handler()
{
    test -n "$cleanup_dropin" && {
        info "cleaning up created dropin directory"
        eval "$cleanup_dropin"
    }
}

trap exit_handler 0

# <Compat>
# Alow users to use the old style arguments like
# 'postgresql-setup initdb $SERVICE_NAME'.
case "$1" in initdb|upgrade)
    action="--$1"
    shift

    warn "using obsoleted argument syntax, try --help"
    old_long_args="help,usage,version,debug"
    oldargs=`getopt -o "" -l "$old_long_args" -n "old-options" -- "$@"` \
        || die "can't parse old arguments"
    eval set -- "$oldargs"
    additional_opts=
    while true; do
        case "$1" in
            --version|--help|--usage|--debug)
                additional_opts="$additional_opts $1"
                shift
                ;;
            --)
                shift
                break
                ;;
        esac
    done

    service="rh-postgresql94-postgresql"
    if test -n "$1"; then
        service=$1
        shift
    fi

    set -- $additional_opts "$action" --unit "$service" "$@"
    warn "arguments transformed to: ${0##*/} $*"
esac
# </Compat>


# postgresql-setup arguments are parsed into those variables
option_mode=none
option_service="rh-postgresql94-postgresql"
option_port=
option_pgdata=
option_debug=0

# Content of EnvironmentFile= files fills those:
envfile_pgdata=
envfile_pgport=

# Configuration from (/etc/systemd/system/$option_service.service) fills those
# variables.
unit_pgdata=
unit_pgport=

# Configuration from postgresql.conf:
conf_pgport=

# Key variables.  Try to fill them by postgresql.conf, Environment= statement in
# service file or EnvironmentFile= content (the later mentioned has more
# priority).
pgdata=default
pgport=default

## PARSE SCRIPT ARGUMENTS ##

short_opts=""
long_opts="\
initdb,upgrade,\
new-systemd-unit,upgrade-ids,\
unit:,service:,port:,datadir:,upgrade-from:,\
debug,\
version,help,usage"

args=`getopt -o "$short_opts" -l "$long_opts" -n "postgresql-setup" -- "$@"` \
    || die "can't parse arguments"
eval set -- "$args"
parse_fail=0
while true; do
    case "$1" in
        --initdb|--upgrade)
            if test "$option_mode" != none; then
                error "bad argument $1, mode already specified: --$option_mode"
                parse_fail=1
            else
                option_mode=${1##--}
            fi
            shift
            ;;

        --unit|--service)
            option_service=$2
            shift 2
            ;;

        --port)
            option_port=$2
            shift 2
            ;;

        --new-systemd-unit)
            option_systemd_config=yes
            shift
            ;;

        --datadir)
            option_pgdata=$2
            shift 2
            ;;

        --debug)
            option_debug=1
            shift
            ;;

        --help|--usage)
            echo "$USAGE_STRING"
            exit 0
            ;;

        --upgrade-from)
            option_upgradefrom="$2"
            shift 2
            ;;

        --upgrade-ids)
            parse_upgrade_setup help
            exit 0
            ;;

        --version)
            print_version
            exit 0
            ;;

        --)
            shift
            break
            ;;

        *)
            die "author's fault: option $1 not handled"
            break
            ;;
    esac
done

test $parse_fail -ne 0 && die "can't parse arguments"

test "$option_mode" = none \
    && die "no mode specified, use --initdb or --upgrade, or --help"

if ! parse_upgrade_setup config "$option_upgradefrom"; then
    if test upgrade = "$option_mode"; then
        die $"bad --upgrade-from parameter '$option_upgradefrom'"
    fi
fi

## GATHER THE SETUP FIRST ##

initdb_log="$POSTGRES_HOMEDIR/initdb_${option_service}.log"
upgrade_log="$POSTGRES_HOMEDIR/upgrade_${option_service}.log"

debug "mode used: $option_mode"
debug "service name: $option_service"

root_prereq

# load service's pgdata
service_configuration initdb pgdata UNUSED "$option_service"

# Check that nothing breaks --new-systemd-unit
if test "$option_systemd_config" = yes; then
    if test "$option_service" = "rh-postgresql94-postgresql"; then
        die $"Default unit 'rh-postgresql94-postgresql.service' should not need --new-systemd-unit"
    elif test "$pgdata" != default; then
        die $"Option --new-systemd-unit failed, is '$option_service.service'"\
            $"already configured to '$pgdata'?"
    elif test -z "$option_pgdata"; then
        die $"Option --new-systemd-unit requires --datadir"
    fi

    # pgdata == default && option_pgdata is set
    pgdata="$option_pgdata"

elif test -n "$option_pgdata"; then
    warn   $"--datadir option is ignored, either use --new-systemd-unit"
    warn_q $"option, or configure the systemd unit manually."
fi

test "$pgdata" = default \
    && die $"no db datadir (PGDATA) configured for '$option_service.service'"

[[ "$pgdata" =~ ^/.* ]] \
    || die $"the PostgreSQL datadir not absolute path: '$pgdata', try --debug"

test "$option_systemd_config" = yes \
    && generate_systemd_dropin "$option_service"

## GATHER DATA FROM INITIALIZED DATADIR ##

test -n "$option_port" && pgport=$option_port

if test upgrade = "$option_mode"; then
    upgradefrom_data="$upgradefrom_data_default"
    service_configuration upgrade upgradefrom_data pgport "$option_upgradefrom"
    test -n "$option_port" -a "$option_port" != "$pgport" \
        && warn "Old pgport $pgport has bigger priority than --pgport value."
fi

# We expect that for upgrade - the previous stack was in working state (thus
# running on the default port).
test "$option_mode" = upgrade -a "$pgport" = default \
    && pgport=$PGPORT_DEF

# This is mostly for 'initdb'.  We assume that the default port is $PGPORT_DEF
# if not set explicitly for default service name 'postgresql'.
if test "$pgport" = default -a "$option_service" = "rh-postgresql94-postgresql"; then
    debug $"Using the default port '$PGPORT_DEF'"
    pgport=$PGPORT_DEF
fi

if test "$pgport" = default; then
    # initdb case..  Note that this may be called by initscripts.  If this gets
    # called by legacy script, we can't help too much because systemd does not
    # allow passing additional arguments to 'service XX initdb' command.
    die $"For non-default unit names you must specify port by --port option."
fi

[[ "$option_port" =~ ^[0-9]*$ ]] \
    || die $"port set to '$option_port', must be integer number"

## LAST CHECK THE SETUP ##

check_daemon_reload

# These variables are read by underlying utilites, rather export them.
export PGDATA=$pgdata
export PGPORT=$pgport

debug "final pgdata: $pgdata"
debug "final pgport: $pgport"

script_result=0

# See how we were called.
case "$option_mode" in
    initdb)
        initdb
        ;;
    upgrade)
        upgrade
        ;;
    *)
        echo >&2 "$USAGE_STRING"
        exit 2
esac

exit $script_result
