#!/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}: '

# Full PostgreSQL version, e.g. 9.0.2
PGVERSION=9.4.7

# Major version of PostgreSQL, e.g. 9.0
PGMAJORVERSION=9.4

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

# Previous major version, e.g., 8.4, for upgrades
PREVMAJORVERSION=9.3

# Directory containing the previous postmaster executable
PREVPGENGINE=/usr/lib/pgsql/postgresql-9.3/bin

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

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

SU=/usr/sbin/runuser

# The where PostgreSQL server listens by default
PGPORT_DEF=5432

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      Create a 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.  This installation is configured to perform upgrade
                from $PREVMAJORVERSION.X to $PGMAJORVERSION.X.

Options:
  --unit=UNIT_NAME           The UNIT_NAME is used to select proper systemd's
                             unit configuration.  For example, if you want to
                             work with unit '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 server will listen for connections
  --datadir=PATH             specify absolute path to DB data directory
  --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.
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."


die()     { echo >&2 $"FATAL: $@" ; exit 1 ; }
error()   { echo >&2 $"ERROR: $@" ; }
error_q() { echo >&2 $"       $@" ; }
warn()    { echo >&2 $"WARNING: $@" ; }
warn_q()  { echo >&2 $"         $@" ; }
info()    { echo >&2 $" * $@" ; }
info_q()  { echo >&2 $"   $@" ; }
debug()   { test "$option_debug" = "1" && echo >&2 $"DEBUG: $@";  }


print_version()
{
    echo "postgresql-setup 2.0"
    echo $"Built against PostgreSQL version 9.4 and configured"
    echo $"to upgrade from PostgreSQL version 9.3."
}


# 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="$PGENGINE/initdb --pgdata='$pgdata' --auth='ident'"
    initdbcmd+=" $PGSETUP_INITDB_OPTIONS"

    $SU -l 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

    if [ -f "$pgdata/PG_VERSION" ]; then
        return 0
    fi

    return 1
}


initdb()
{
    if [ -f "$pgdata/PG_VERSION" ]; then
        error $"Data directory $pgdata is not empty!"
        script_result=1
    else
        port_info=
        test "$pgport" != "$PGPORT_DEF" \
            && port_info=$", listening on port '$pgport'"

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


upgrade()
{
    # must see previous version in PG_VERSION
    if [ ! -f "$pgdata/PG_VERSION" -o \
         x`cat "$pgdata/PG_VERSION"` != x"$PREVMAJORVERSION" ]
    then
        error   $"Cannot upgrade because the database in $pgdata is not of"
        error_q $"compatible previous version $PREVMAJORVERSION."
        exit 1
    fi
    if [ ! -x "$PGENGINE/pg_upgrade" ]; then
        error $"Please install the postgresql-upgrade package."
        exit 5
    fi

    # Set up log file for pg_upgrade
    rm -f "$upgrade_log"
    touch "$upgrade_log" || exit 1
    chown postgres:postgres "$upgrade_log"
    chmod go-rwx "$upgrade_log"
    [ -x /sbin/restorecon ] && /sbin/restorecon "$upgrade_log"

    # Move old DB to pgdataold
    pgdataold="${pgdata}-old"
    rm -rf "$pgdataold"
    mv "$pgdata" "$pgdataold" || exit 1

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

    # Create empty new-format database
    if perform_initdb; then
        # Do the upgrade
        $SU -l postgres -c "$PGENGINE/pg_upgrade \
                        '--old-bindir=$PREVPGENGINE' \
                        '--new-bindir=$PGENGINE' \
                        '--old-datadir=$pgdataold' \
                        '--new-datadir=$pgdata' \
                        --link \
                        '--old-port=$PGPORT' '--new-port=$PGPORT' \
                        --user=postgres \
                        $PGSETUP_PGUPGRADE_OPTIONS" \
                                >> "$upgrade_log" 2>&1 < /dev/null
        if [ $? -ne 0 ]; then
            # pg_upgrade failed
            script_result=1
        fi
    else
        # 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
        rm -rf "$pgdata"
        mv "$pgdataold" "$pgdata"
        error $"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'"

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


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

    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_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
        debug "trying to read '$line' env file"

        if test ! -r "$line"; then
            error   "Can not read EnvironmentFile '$line' 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
        . "$line"
        envfile_pgdata="$PGDATA"
        envfile_pgport="$PGPORT"
        unset PGPORT PGDATA

    done <<<"$envfiles"
}


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

    test "$mode" = initdb && return 0

    debug "postgresql.conf: $conffile"

    test -r "$conffile" || {
        error "config file $conffile is not readable or does not exist"
        return 1
    }

    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
}


# <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_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,\
unit:,service:,port:,datadir:,\
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
            ;;

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

# Well, until the bug #1139148 is not resolved somehow, we need to stay ugly
# and parse Environment= and EnvironmentFile= statements.
handle_service_env "$option_mode" "$option_service"
handle_service_envfiles "$option_mode" "$option_service"

## DEAL WITH PGDATA ##

# EnvironmentFile has bigger priority then Environment in systemd
test -n "$unit_pgdata" && pgdata="$unit_pgdata"
test -n "$envfile_pgdata" && pgdata="$envfile_pgdata"

# Check that nothing breaks --new-systemd-unit
if test "$option_systemd_config" = yes; then
    if test "$option_service" = postgresql; then
        die $"Default unit '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?"
    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 ##

# for upgrade only
handle_pgconf "$option_mode" "$pgdata" || die "can not parse postgresql.conf"

## DEAL WITH PGPORT ##

test -n "$option_port" && pgport=$option_port
test -n "$conf_pgport" && pgport="$conf_pgport"
test -n "$unit_pgport" && pgport="$unit_pgport"
test -n "$envfile_pgport" && pgport="$envfile_pgport"

test -n "$option_port" -a "$option_port" != "$pgport" \
    && warn $"--pgport ignored, by configuration pgport='$pgport'"

# 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 (only 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

test "$pgport" = default \
    && die $"\
Port is not set by postgresql.conf nor by --port."

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

## LAST CHECK THE SETUP ##

nr_option=NeedDaemonReload
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

# 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
