#!/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=@PGVERSION@

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

# Directory containing the postmaster executable
PGENGINE=@PGENGINE@

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

# Directory containing the previous postmaster executable
PREVPGENGINE=@PREVPGENGINE@

# Distribution README file
README_DIST=@README_DIST@

# Home directory of postgres user
POSTGRES_HOMEDIR=@POSTGRES_HOMEDIR@

SYSCONFIG_DIR=@PKGCONFIG_DIR@

SU=@SU@

# The where PostgreSQL server listens by default
PGPORT_DEF=5432

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

Script is aimed to help sysadmin with basic database cluster administration.

The UNIT_NAME is used for selection of proper sysconfig or unit configuration
file; For more info and howto/when use this script please look at the docu file
$README_DIST.  The 'postgresql'
string is used when no UNIT_NAME is explicitly passed.

Available operation mode:
  --initdb      Create a new PostgreSQL database cluster.  This is usually the
                first action you perform after PostgreSQL server installation.
  --upgrade     Upgrade PostgreSQL database cluster to be usable with new
                server.  Use this if you upgraded your PostgreSQL server to
                newer major version (currently from $PREVMAJORVERSION \
to $PGMAJORVERSION).

Options:
  --unit        unit ID of PostgreSQL to run against
  --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 debugging output."


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


print_version()
{
    echo "postgresql@DISTSUFF@-setup @VERSION@"
    echo $"Built against PostgreSQL version @PGMAJORVERSION@ and configured"
    echo $"to upgrade from PostgreSQL version @PREVMAJORVERSION@."
}


# 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
        info $"Initializing database in $pgdata."
        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."
}


handle_sysconfig()
{
    local mode="$1"
    local service="$2"
    local sysconfig_file="$SYSCONFIG_DIR/$service"

    test -r "$sysconfig_file" || {
        warn "system config file '$sysconfig_file' not found or unreadable"
        return 1
    }

    unset PGPORT PGDATA
    . "$sysconfig_file"
    sysconfig_pgdata="$PGDATA"
    sysconfig_pgport="$PGPORT"
    unset PGPORT PGDATA

    test -n "$sysconfig_pgdata" && debug "sysconfig pgdata: '$sysconfig_pgdata'"
    test -n "$sysconfig_pgport" && debug "sysconfig pgport: $sysconfig_pgport"
}


# This is mostly for backward compatibility with version postgresql-setup from
# postgresql package <= 9.3.4-7 as this type of configuration is not adviced
# anymore.  But user still may override the /etc/sysconfig/* settings with
# Environment= statement in service file.   Note that this parsing technique
# fails for PGDATA pathnames containing spaces, but there's not much we can do
# about it given systemctl's output format.

handle_service_file()
{
    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_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_debug=0

# Content of /etc/sysconfig/$option_service fills those:
sysconfig_pgdata=
sysconfig_pgport=

# Configuration from (/etc/systemd/system/$option_service.service) fills those
# variables (this is here for compat, users should user rather sysconfig).
unit_pgdata=
unit_pgport=

# Configuration from postgresql.conf:
conf_pgport=

# Key variables.  Try to fill them postgresql.conf, sysconfig or unit file
# configuration (the later mentioned has more priority).
pgdata=default
pgport=default


short_opts=""
long_opts="\
initdb,upgrade,\
unit:,service:,port:,\
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
            ;;

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

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

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

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

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

handle_sysconfig    "$option_mode" "$option_service"
handle_service_file "$option_mode" "$option_service"

test -n "$sysconfig_pgdata" && pgdata="$sysconfig_pgdata"
test -n "$unit_pgdata" && pgdata="$unit_pgdata"

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

handle_pgconf "$option_mode" "$pgdata" || die "can not parse postgresql.conf"

test -n "$conf_pgport" && pgport="$conf_pgport"
test -n "$sysconfig_pgport" && pgport="$sysconfig_pgport"
test -n "$unit_pgport" && pgport="$unit_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
    info $"Using the default port '$PGPORT_DEF'."
    pgport=$PGPORT_DEF
fi

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

# These variables are read by underlying utilites, rather export them.
export PGDATA=$pgdata
export 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