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

if test "$(id -u)" -eq 0; then
    cmd=
    for v in PGSETUP_DEBUG PGSETUP_INITDB_OPTIONS PGSETUP_PGUPGRADE_OPTIONS; do
        eval var_content=\$$v
        test -z "$var_content" && continue
        cmd+=$v="$(printf %q "$var_content") "
    done
    cmd+=$(printf %q "$(readlink -f "$0")")
    for arg; do cmd+=" $(printf %q "$arg")" ; done
    # Drop root privileges asap.  It's not recommended to run postgresql-setup
    # script under root nowadays; so we take the liberty to switch to the
    # PostgreSQL admin user (by default 'postgres') without any other option.
    exec /usr/sbin/runuser -s /bin/sh -l postgres -c "$cmd"
fi

# ensure privacy
umask 0077

: ${RESTORECON=/sbin/restorecon}
test -x $RESTORECON || RESTORECON=:

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

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

# The current user name.
USER=$(id -u -n)

# Directory containing the postmaster executable
PGENGINE=/usr/bin

# Distribution README file
README_DIST=/usr/share/doc/postgresql/README.rpm-dist

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

# The where PostgreSQL server listens by default
PGPORT_DEF=5432


. "/usr/share/postgresql-setup/library.sh"

:

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

srvsuff=
test 0 -eq 0 && srvsuff=".service"

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+="
  --new-systemd-unit         We dropped this option for security reasons.
                             Nowadays, please use the root-only script
                             /usr/sbin/postgresql-new-systemd-unit.
  --datadir                  Dropped with --new-systemd-unit."

USAGE_STRING+="
  --upgrade-from-unit=UNIT   Select proper unit name to upgrade from.  This
                             has similar semantics as --unit option.
  --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 8.2"
    echo $"Built against PostgreSQL version 10.5."
}


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
    fi
    $RESTORECON "$pgdata"
    test -w "$pgdata" || die "$pgdata is not writeable by $USER"

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

    # Create the initdb log file if needed
    if [ ! -e "$initdb_log" ]; then
        touch "$initdb_log" || return 1
    fi
    $RESTORECON "$initdb_log"
    test -w "$initdb_log" || echo "$initdb_log is not writeable by $USER"

    # Initialize the database
    initdbcmd+=( "$PGENGINE"/initdb --pgdata="$pgdata" --auth=ident )
    eval "initdbcmd+=( $PGSETUP_INITDB_OPTIONS )"
    "${initdbcmd[@]}" >> "$initdb_log" 2>&1 < /dev/null

    # Create directory for postmaster log files
    mkdir "$pgdata/pg_log"
    $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}"
    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 "$upgradefrom_engine/postgres" ]; then
        error $"Please install the $upgradefrom_package package."
        exit 5
    fi
    if [ ! -x "$PGENGINE/pg_upgrade" ]; then
        # The "$PGENGINE/postgres" depends transitively on
        # pg_upgrade binary in rather newer packaging, but SCL with PostgreSQL
        # 9.4 provides '*-upgrade' package having `pg_upgrade` inside.  We need
        # to have this installed, too.  Keep till {rh,sclo}-postgresql94 is
        # still a thing.
        error $"Please install the 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"
    $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"
        $RESTORECON "$pgdataold"
    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 x"$upgradefrom_redhat_sockets_hack" = xyes && {
        debug "upgrading from redhat server"
        socket_hacks="export REDHAT_PGUPGRADE_FROM_RHEL=yes"
    }

    test -n "$upgradefrom_pghost_override" && {
        pghost_override="export 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
        (
        cd # pg_upgrade writes to $PWD
        eval "
            $scls_upgrade_hacks
            $socket_hacks
            $pghost_override
        "
        eval "add_options=( $PGSETUP_PGUPGRADE_OPTIONS )"

        "$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 \
            "${add_options[@]}" \
            >>"$upgrade_log" 2>>"$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."
}


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 $service, mode: $mode"

    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" = "postgresql" \
            && set_var "$datavar" "/var/lib/pgsql/data"
        handle_envfile "/etc/sysconfig/pgsql/$service"
    else
        # We ship two service files, postgresql.service and
        # 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"
}

# <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="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="postgresql"
option_port=
option_debug=0
option_upgradefrom_unit=

# 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:,upgrade-from-unit:,\
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
            ;;

        --datadir|--new-systemd-unit)
            error   $"Removed option --new-systemd-unit/--datadir, please use"
            error_q $"/usr/sbin/postgresql-new-systemd-unit script"
            exit 1
            ;;

        --debug)
            option_debug=1
            shift
            ;;

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

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

        --upgrade-from-unit)
            option_upgradefrom_unit="$2"
            shift 2
            ;;

        --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"

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

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

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

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

## GATHER DATA FROM INITIALIZED DATADIR ##

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

if test upgrade = "$option_mode"; then
    upgradefrom_data="$upgradefrom_data_default"

    if test -z "$option_upgradefrom_unit"; then
        if test "postgresql" = "postgresql"; then
            # Fedora usecase -> upgrade while keeping the same name of
            # service/unit.
            option_upgradefrom_unit=$option_service
        else
            # PGRPMs/RHSCL usecase -> we upgrade from one service/unit name to
            # a different one, e.g. from postgresql92 to postgresql93, or from
            # postgresql (system version) to postgresql94 (scl).
            option_upgradefrom_unit=$upgradefrom_id

            # Try to predict situations: postgresql93@second -> postgresql94@second
            if [[ "$option_service" =~ ^postgresql@(.*)$ ]]; then
                option_upgradefrom_unit="$upgradefrom_id@${BASH_REMATCH[1]}"
            fi
        fi
    fi

    test "$option_service" = "$option_upgradefrom_unit" \
        || info "upgrading from '$option_upgradefrom_unit$srvsuff'" \
                "to '$option_service$srvsuff'"

    service_configuration upgrade upgradefrom_data pgport \
                          "$option_upgradefrom_unit"
    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" = "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

test -w "/var/lib/pgsql" || {
    # pg_upgrade binary needs to have write-able $PWD (and we use 'su -')
    error   $"The /var/lib/pgsql directory has wrong permissions."
    error_q $"Please make sure the directory is writable by $USER."
    exit 1
}

if /usr/bin/mountpoint -q "$pgdata" || /usr/bin/mountpoint -q "$(dirname "$pgdata")"; then
    warn   $"Note that either your data directory '$pgdata' or"
    warn_q $"the parent directory '$(dirname "$pgdata")'"
    warn_q $"is a direct mountpoint.  This is usually a bad idea and your"
    warn_q $"filesystem layout should ideally look like:"
    warn_q $"/ROOT_OWNED_MOUNTPOINT/POSTGRES_OWNED_DIRECTORY/DATADIR."
    warn_q $"See the upstream documentation for more info:"
    warn_q $"http://www.postgresql.org/docs/10/static/creating-cluster.html"
fi

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

exit $script_result
