#!/usr/bin/env bash
# © Copyright EnterpriseDB UK Limited 2015-2024 - All rights reserved.
#
# The dispatcher for all TPA functionality, including initial setup,
# and the configuration, provision, deployment, and testing of clusters.

IFS=$' \t\n'
set -eu
: "${PAGER:=less}"

error() {
    echo "ERROR: $*" >&2
    exit 1
}

# Enable user-specified $TMPDIR
export TMPDIR=${TMPDIR:-/tmp}
if [[ ! -d $TMPDIR ]]; then
    error "Can't find $TMPDIR; please set TMPDIR correctly"
fi

# If $TPA_DIR is not already set, we guess TPA_DIR based on $0 (so that
# a git checkout takes precedence over a package installation), or use
# /opt/EDB/TPA if it exists.

TOP=/opt/EDB/TPA
if [[ -z "${TPA_DIR:-}" ]]; then
    if [[ -h "${self:=$0}" ]]; then
        self=$(readlink "$0")
    fi
    SRC=$(dirname "$self")/..
    if command -v realpath &>/dev/null; then
        SRC=$(realpath "$SRC")
    else
        # If we can't use realpath, we probably can't depend on using
        # readlink -f either, so we just remove "/bin/.." from the end
        # of the path and see if that leaves us with something sensible
        # (which may be the current directory).
        SRC=${SRC/%\/bin\/../}
        if [[ $SRC == "bin/.." || $SRC == "." ]]; then
            SRC=$(pwd)
        fi
    fi
    if [[ -d $SRC/roles ]]; then
        TOP=$SRC
    fi
fi

export TPA_DIR=${TPA_DIR:-$TOP}
if [[ ! -d $TPA_DIR/roles ]]; then
    error "Can't find $TPA_DIR/roles; please set TPA_DIR correctly"
fi

ansible=$TPA_DIR/ansible/ansible
TPA_VENV=${TPA_VENV:-$TPA_DIR/tpa-venv}

export PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}$TPA_DIR/lib

## Command functions
#
# These functions handle the various tpaexec commands.

version() {
    if [[ -f $TPA_DIR/VERSION ]]; then
        VERSION=$(cat "$TPA_DIR/VERSION")
    elif [[ -e $TPA_DIR/.git ]]; then
        if command -v git &>/dev/null; then
            VERSION=$(cd "$TPA_DIR" && echo "$(git describe) (branch: $(git branch | sed -n 's/^\* //p'))")
        else
            VERSION="(unknown version; git not installed)"
        fi
    else
        VERSION="(unknown version; no $TPA_DIR/VERSION or .git)"
    fi
    echo "# TPAexec $VERSION"
}

verify_checksums() {
   $PYTHON "$TPA_DIR"/lib/tpaexec/compare_checksums.py "$TPA_DIR" "$TPA_DIR"/checksums.json
}

_python_version() {
    $PYTHON -c 'import sys; print("%d.%d.%d" % (sys.version_info[0:3]))'
}

_python39_or_later() {
    $PYTHON -c 'import sys; print(sys.version_info[0:2] >= (3, 9))'
}

_detect_venv() {
    $PYTHON -c "import sys; print('venv' if hasattr(sys, 'real_prefix') or sys.base_prefix != sys.prefix else 'no venv')"
}

_detect_site_packages() {
    $PYTHON -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])'
}

_ansible_version() {
    $PYTHON -c 'import ansible.release; print(ansible.release.__version__)'
}

_activate_venv() {
    set +u
    if ! source "$1/bin/activate"; then
        error "couldn't source $1/bin/activate (is $1 a venv?)"
    fi
    set -u
}

_create_and_activate_venv() {
    # If no venv exists, we have to create it before we try to
    # activate it below.

    if [[ ! -d $vpath ]]; then
        echo "Creating venv $vpath"
        if ! mkdir -p "$vpath"; then
            error "couldn't create $vpath (try '--venv /other/path' or use sudo)"
        fi
        if ! $PYTHON -m venv "$vpath"; then
            rm -rf "$vpath"
            error "couldn't initialise $vpath using '$PYTHON -m venv'"
        fi
    fi

    _activate_venv "$vpath"
}

_install_tpaexec_pth() {
    # Install a .pth file in the site-packages directory of the venv.
    # The .pth file contains a single line, which is the path to the
    # $TPA_DIR/lib directory. The .pth file makes venv add the $TPA_DIR/lib
    # to the PYTHONPATH when the venv is activated.

    # This allows us to use "import tpaexec" in the Ansible modules,
    # without having to install TPA itself into the venv.

    if [[ $(_detect_venv) == "venv" ]]; then
        echo "${TPA_DIR}/lib" > "$(_detect_site_packages)"/tpaexec.pth
    fi
}

_check_ansible_version() {
    case "$ansible_version" in
        "8"|"9")
            ;;
        *)
            error "Version $ansible_version of ansible is not supported"
            ;;
    esac
}

_install_pip_packages() {
    pip_args=(--upgrade)

    if [ -n "${pip_cache_dir:-}" ]; then
        pip_args+=(--cache-dir)
        pip_args+=("${pip_cache_dir}")
    else
        pip_args+=(--no-cache-dir)
    fi

    # If pip packages are bundled (i.e., the tpaexec-deps package has
    # been installed), we configure pip to use them instead of trying
    # to download them from the network.

    PIP_DIR=$TPA_DIR/pip-packages
    if [[ -d $PIP_DIR ]]; then
        pip_args+=(--no-index)
        pip_args+=("--find-links=file:$PIP_DIR")
    fi

    pip3 install "${pip_args[@]}" "pip>=10.0.1" wheel

    # We need to install Ansible and its dependencies into the venv,
    # either from the bundled package from tpaexec-deps if present, or
    # directly from github/PyPI

    ANSIBLE_REQUIREMENTS="$TPA_DIR"/requirements-ansible-${ansible_version}.txt
    TPA_REQUIREMENTS="$TPA_DIR"/requirements.txt

    if [[ -d $PIP_DIR && -f $PIP_DIR/requirements-ansible-${ansible_version}.txt ]]; then
        ANSIBLE_REQUIREMENTS="$PIP_DIR/requirements-ansible-${ansible_version}.txt"
        TPA_REQUIREMENTS="$PIP_DIR/requirements.txt"
    fi

    # If we have ansible already installed and we're going to install a different
    # one, we must remove the old one first
    existing_ansible_version=$(pip3 show ansible-core | grep Version | awk '{print $2}')
    desired_ansible_version=$(grep 'ansible-core==' $ANSIBLE_REQUIREMENTS|grep -Eo '[[:digit:].]+')

    if [[ $existing_ansible_version != $desired_ansible_version ]]; then
        pip3 uninstall -y ansible-core
    fi

    pip_args+=(-r "$ANSIBLE_REQUIREMENTS")
    pip_args+=(-r "$TPA_REQUIREMENTS")
    pip3 install "${pip_args[@]}"

    # TPA-21: added to clean leftover from previous tpaexec version
    # remove old ec2.py script.
    if [[ -f "$TPA_DIR"/platforms/aws/inventory/ec2.py ]]; then
        rm "$TPA_DIR"/platforms/aws/inventory/ec2.py
    fi

}

_apply_patch_if_not_applied() {
    local file=$1
    local patch=$2

    # We want to apply $patch to $file and display the output from
    # patch(1) if it either successfully applies or fails to apply the
    # patch. If the patch has already been applied, we don't want any
    # output (certainly not the confusingly error-like message patch
    # sees fit to issue in this case).
    #
    # Unfortunately, we cannot rely on the exit code, because patch will
    # return 1 if it detects a patch that has already been applied, but
    # also if it fails to apply a patch with a single hunk. Exit code 2
    # or above is always an error.
    #
    # Here we run patch in a subshell that translates exit code 1 to 0
    # and disables translation/localisation so that we can examine the
    # output afterwards to determine if the patch was already applied.

    (
        set +eE;
        trap '' ERR;
        LC_ALL=C LANG=C patch --forward -r - -sp1 "$file" < "$patch" 2>&1;
        if [[ $? -ge 2 ]]; then
            exit 2;
        fi
        exit 0;
    ) | awk '
        /FAILED/                { print "'"$(basename "$patch")"': " $0; exit 2; }
        /Reversed.*Skipping/    { exit 0; }
        //                      { print; }
    '
}

_install_ansible_collections() {

    # If ansible collections are bundled (i.e. the tpaexec-deps package has
    # been installed), we configure ansible-galaxy to use them instead of trying
    # to download them from the network.
    echo "Installing ansible collections"
    if [ -d "${TPA_DIR}/collections-static" ] ;then
        ANSIBLE_COLLECTIONS="${TPA_DIR}/collections-static"
    else
        ANSIBLE_COLLECTIONS="${TPA_DIR}/collections"
    fi
    (
        cd "$ANSIBLE_COLLECTIONS" && \
        "${ansible}"-galaxy collection install -p "${TPA_VENV}/collections" -r requirements.yml
    )
}

setup() {
    use_community=true
    ansible_version=8
    while [[ $# -gt 0 ]]; do
        opt=$1
        shift

        case "$opt" in
            --venv)
                vpath=${1:?venv location not specified}
                shift
                ;;
            --pip-cache-dir)
                pip_cache_dir=${1:?pip cache directory not specified}
                shift
                ;;
            --ansible-version)
                ansible_version=${1:?ansible version not specified}
                shift
                ;;
            *)
                error "unrecognised option: $opt"
                ;;
        esac
    done
    _display_notification
    _check_ansible_version
    _create_and_activate_venv "${vpath:=$TPA_VENV}"
    _install_pip_packages
    _install_ansible_collections
    _install_tpaexec_pth
}
_display_notification() {
    if [[ $use_community == false ]]; then
        cat <<NOTIFICATION

You have selected to use the 2ndQuadrant ansible fork. This is now
deprecated in favour of community ansible and will cease to be supported
in a future TPA release.

If you choose to re-run 'tpaexec setup' to use community ansible, please
be aware that the behaviour of the '--skip-tags' option is different and
this option is not supported by TPA with community ansible. An alternative
means of skipping tasks will be provided before support for 2ndQuadrant
ansible is removed.

NOTIFICATION

    fi
}

_with_ansible_env() {
    ANSIBLE_FILTER_PLUGINS=${ANSIBLE_FILTER_PLUGINS:+$ANSIBLE_FILTER_PLUGINS:}$TPA_DIR/lib/filter_plugins \
    ANSIBLE_LOOKUP_PLUGINS=${ANSIBLE_LOOKUP_PLUGINS:+$ANSIBLE_LOOKUP_PLUGINS:}$TPA_DIR/lib/lookup_plugins \
    ANSIBLE_TEST_PLUGINS=${ANSIBLE_TEST_PLUGINS:+$ANSIBLE_TEST_PLUGINS:}$TPA_DIR/lib/test_plugins \
    ANSIBLE_COLLECTIONS_PATHS=${ANSIBLE_COLLECTIONS_PATHS:+$ANSIBLE_COLLECTIONS_PATHS:}$TPA_VENV/collections \
    "$@"
}

configure() {
    _with_ansible_env "${TPA_DIR}/architectures/lib/configure" "$@"
}

reconfigure() {
    _with_ansible_env "${TPA_DIR}/architectures/lib/commands/reconfigure" "$@"
}

provision() {
    REMAINDER=()
    while [[ $# -gt 0 ]]; do
        opt=$1
        shift

        case "$opt" in
            --owner)
                owner=${1:?Owner not specified}
                shift
                ;;
            --cache*)
                cache=1
                ;;
            --force-reprovision)
                reprovision=1
                ;;
            --)
                break
                ;;
            *)
                REMAINDER+=("$opt")
                ;;
        esac
    done

    # TPA-21: added to clean leftover from previous tpaexec version
    # remove old ec2.py script configuration file
    if [[ -f "$(pwd)"/inventory/ec2.ini ]];then
        rm "$(pwd)"/inventory/ec2.ini
    fi
    if [[ -f "$(pwd)"/inventory/ec2.py ]];then
        rm "$(pwd)"/inventory/ec2.py
    fi

    set -- "${REMAINDER[@]:+${REMAINDER[@]}}" "$@"

    "$ansible"-playbook "$TPA_DIR/platforms/provision.yml" \
        ${reprovision:+-e force_reprovision=$reprovision} \
        ${cache:+-e use_cached_vars=$cache} \
        ${owner:+-e Owner=$owner} \
        -e cluster_dir="$(pwd)" "$@"
}

cmd() {
    # first ensure that vault_pass is defined for the cluster
    # otherwise don't add it to the args
    if "${TPA_DIR}/architectures/lib/use-vault" >> /dev/null 2>&1; then
        args=--vault-password-file="${TPA_DIR}/architectures/lib/use-vault"
    fi
    $ansible \
        -i inventory "$args"\
        -e cluster_dir="$(pwd)" "$@"
}

ping() {
    cmd all -m ping "$@"
}

playbook() {
    args=(-e "tpa_dir=$TPA_DIR")
    args+=(-e "cluster_dir=$(pwd)")

    if [ -d inventory ]; then
        args+=(-i inventory)
    fi

    for arg in "$@"; do
        shift
        if [[ $arg == --excluded?tasks=* ]]; then
            exclusion="${arg#*=}"
            args+=(-e excluded_tasks=$exclusion)
        elif [[ $arg == --included?tasks=* ]]; then
            inclusion="${arg#*=}"
            args+=(-e included_tasks=$inclusion)
        else
            set -- "$@" "$arg"
        fi
    done

    if "${TPA_DIR}/architectures/lib/use-vault" >> /dev/null 2>&1; then
        args+=(--vault-password-file "${TPA_DIR}/architectures/lib/use-vault")
    fi

    "$ansible"-playbook "${args[@]}" "$@"
}

deploy() {
    NO_PROVISION_OPTIONS="(--help)|(--list-hosts)|(--list-tasks)|(--list-tags)"
    if [[ ! $@ =~ $NO_PROVISION_OPTIONS ]] ; then
        provision
    fi
    playbook deploy.yml "$@"
}

upgrade() {
    if [[ ! -d inventory ]]; then
       error "No inventory found. Did you run 'tpaexec deploy $(pwd)'?"
    fi

    if [[ -h commands/update-postgres.yml && ! -h commands/upgrade.yml ]]; then
        echo "Creating link to upgrade.yml"
        "$TPA_DIR"/architectures/lib/commands/relink
    elif [[ -h commands/upgrade.yml ]]; then
        echo "Recreating link to upgrade.yml"
        rm commands/upgrade.yml
        "$TPA_DIR"/architectures/lib/commands/relink
    fi

    provision
    playbook commands/upgrade.yml "$@"
}

deprovision() {
    playbook "$TPA_DIR/platforms/deprovision.yml" "$@"
}

try_as_playbook_or_script() {
    # If you run `tpaexec xyzzy …`, you end up here to check if there
    # is a matching custom command under architectures/lib/commands or
    # $cluster/commands. If so, we execute it as if it were a built-in
    # command. As a safety measure, we require that custom commands be
    # executed with a cluster directory as the first argument.

    if [[ "${1:-}" && -d $1 && -f $1/config.yml ]]; then
        cd "${cluster:=$1}"
        shift
        for dir in ./commands $TPA_DIR/architectures/lib/commands; do
            playbook=$dir/$command.yml
            script=$dir/$command.sh
            exec=$dir/$command

            if [[ -x $exec ]]; then
                case $command in
                    store-password|show-password)
                        _with_ansible_env "$exec" "$@" "--vault_password_file=${TPA_DIR}/architectures/lib/use-vault"
                        return $?
                    ;;
                    *)
                        _with_ansible_env "$exec" "$@"
                        return $?
                    ;;
                esac
            elif [[ -f $script ]]; then
                source "$script"
                if [[ $(type -t _tpaexec_command) != function ]]; then
                    error "$script did not define _tpaexec_command"
                fi
                _with_ansible_env _tpaexec_command "$@"
                return $?
            elif [[ -f $playbook ]]; then
                playbook "$playbook" "$@"
                return $?
            fi
        done
    fi

    # If there's a matching command in architectures/lib and we didn't
    # run it above, a cluster directory must not have been specified
    # (e.g., `tpaexec test`). Let's try to be helpful.

    C=$TPA_DIR/architectures/lib/commands
    if [[ -x $C/$command || -f $C/$command.sh || -f $C/$command.yml ]]; then
        dir="clusterdir"
        if [[ -f config.yml ]]; then
            dir="."
        fi
        error "tpaexec $command needs a cluster directory (try 'tpaexec $command $dir')"
    fi

    error "unrecognised tpaexec command: $command (try 'tpaexec help')"
}

_tpaexec_info() {
    version
    echo tpaexec="$0"
    echo TPA_DIR="$TPA_DIR"

    if command -v "$PYTHON" &>/dev/null; then
        python_bin=$(command -v "$PYTHON")
        if [[ $(_python39_or_later) == "True" ]]; then
            echo -n PYTHON="$python_bin"
            echo " (v$(_python_version), $(_detect_venv))"
        else
            echo -n PYTHON=none "(need 3.9+, won't use $python_bin"
            echo " (v$(_python_version), $(_detect_venv)))"
        fi
    else
        echo PYTHON=none
    fi

    if [[ -d $TPA_VENV ]]; then
        echo TPA_VENV="$TPA_VENV"
    else
        echo TPA_VENV=none "(did you run tpaexec setup?)"
    fi

    if [[ "$(command -v ansible)" == $TPA_VENV/bin/ansible ]]; then
        echo -n ANSIBLE="$(command -v ansible)"
        echo " (v$(_ansible_version))"
    else
        echo -n ANSIBLE=none
        if command -v ansible &>/dev/null; then
            echo " (won't use $(command -v ansible); run tpaexec setup)"
        else
            echo " (did you run tpaexec setup?)"
        fi
    fi

    if [[ -f $TPA_DIR/checksums.json ]]; then
        verify_checksums
    elif [[ -d $TPA_DIR/.git ]]; then
        git -C "$TPA_DIR" status
    fi
}

selftest() {
    _tpaexec_info
    "$TPA_DIR/architectures/lib/selftest.py"
    $ansible --version
    "$ansible"-playbook "$TPA_DIR/architectures/lib/selftest.yml" -e tpa_dir="$TPA_DIR"
}

info() {
    subcommand=${1:-""}
    case "$subcommand" in
        "")
            _tpaexec_info
            ;;
        version)
            version
            ;;
        platforms|architectures)
            echo "Available $subcommand:"
            echo ""

            while IFS= read -r d
            do
                (
                    source "$d";
                    if [[ "${STATUS:=experimental}" != "experimental" || ${2:-""} == "-a" ]]; then
                        echo "${NAME:-$(basename "$(dirname "$d")")} [$STATUS]"
                        if [[ ${DESCRIPTION:-""} ]]; then
                            if type fmt &>/dev/null; then
                                echo "    $DESCRIPTION" | fmt -c
                            else
                                echo "    $DESCRIPTION"
                            fi
                        fi
                        echo ""
                    fi
                )
            done < <(find "$TPA_DIR/$subcommand" -name _metadata -print)

            echo "Run 'tpaexec info $subcommand/<name>' for more information."
            case "$subcommand" in
                platforms)
                    echo
                    echo "Run 'tpaexec configure --architecture <name> --platform <name> --help'"
                    echo "to see available options for an architecture and platform."
                    ;;
                architectures)
                    echo
                    echo "Run 'tpaexec configure --architecture <name> --help' to see available"
                    echo "options for an architecture."
                    ;;
            esac
            ;;
        platforms/*|architectures/*)
            doc=$TPA_DIR/$subcommand/README.md
            if [[ ! -f $doc ]]; then
                echo "Sorry, no documentation available for $subcommand"
                echo "Please contact tpa@enterprisedb.com for assistance"
                exit 1
            fi
            $PAGER "$doc"
            ;;
        *)
            error "unrecognised info subcommand: $subcommand"
            ;;
    esac
}

help() {
    topic=${1:-""}
    case "$topic" in
        "")
            cat <<TOPICS
Available help topics:

  Installation and setup

    info        Show information about this installation
    setup       Install tpaexec dependencies
    selftest    Test that tpaexec is installed properly

  Main commands

    configure   Generate an initial cluster configuration (config.yml)
    provision   Create instances and Ansible inventory from config.yml
    deploy      Install and configure software on a cluster
    test        Test the deployed cluster

  Utility commands

    ping        Test ssh connectivity to cluster instances
    cmd         Execute arbitrary Ansible commands
    playbook    Execute arbitrary Ansible playbooks

  Cluster management

    download-packages
                Download all packages required for a cluster (useful when
                cluster instances don't have outside connectivity)

    upgrade
                Perform a node-by-node upgrade of Postgres and related
                components installed on a cluster (e.g, BDR, pglogical,
                HARP, pgbouncer)

    show-password
    store-password
                Commands to manage passwords for cluster users
                (e.g., postgres_password, pgbouncer_password)
    show-vault
                Command to show the vault password of the cluster
                This is used to encrypt other passwords.

  Miscellaneous

    relink      Relink symbolic links within a cluster directory (useful
                when someone sends you their cluster directory)

    deprovision Destroy the cluster and resources it uses

Run 'tpaexec help <topic>' for more details.
TOPICS
            ;;

        info*)
            cat <<INFO
Available info subcommands:

    tpaexec info
        Displays some information about this installation

    tpaexec info version
        Displays current TPA version

    tpaexec info platforms
        Displays available deployment platforms

    tpaexec info architectures
        Displays available deployment architectures

    tpaexec info platforms/xxx
        Displays information about a particular platform

    tpaexec info architectures/xxx
        Displays information about a particular architecture

INFO
            ;;

        setup)
            cat <<SETUP
Command: tpaexec setup

Installs all Python dependencies (Ansible, modules) in an isolated
virtual environment (using the Python 3 builtin venv module). The
venv will be activated automatically whenever you run tpaexec.

Synopsis:

    $TPA_DIR/bin/tpaexec setup

The default location is $TPA_VENV
SETUP
            ;;

        selftest)
            cat <<SELFTEST
Command: tpaexec selftest

Performs some tests to verify that TPA is installed correctly. You need
to run this only once, after 'tpaexec setup'.

If this succeeds (all green, failed=0), you're good to go.

Synopsis:

    tpaexec selftest

Next, try 'tpaexec help configure'
SELFTEST
            ;;

        config*)
            cat <<CONFIG
Command: tpaexec configure

Generates an initial cluster configuration based on the options you
specify for the selected architecture and platform. This command will
create 'clustername/config.yml' and other files for you.

The next step is 'tpaexec provision'.

Synopsis:

    tpaexec configure --architecture '<x>' --help

    tpaexec configure /path/to/clustername …options…

Example:

    tpaexec configure clustername           \\
        --architecture PGD-Always-ON        \\
        --postgresql 14 --platform docker

Options:

    --architecture Name

        See 'tpaexec info architectures' for a list of supported
        architectures, and 'tpaexec info architectures/Name' for
        more details.

    --platform Name

        See 'tpaexec info platforms' for a list of supported platforms,
        and 'tpaexec info platforms/Name' for more details.

    --help

        Displays all other options available.

Docs: $TPA_DIR/docs/src/tpaexec-configure.md
CONFIG
            ;;

        reconfigure)
            cat <<RECONFIG
Command: tpaexec reconfigure

Changes the configuration of an existing cluster by modifying config.yml
according to the options specified.

At present, the only operation available is to upgrade a BDR-Always-ON
cluster to a PGD-Always-ON cluster (i.e., going from BDR4 to PGD5).

Synopsis:

    tpaexec configure /path/to/clustername  \\
        [--describe] [--output FILENAME]    \\
        …options…

    tpaexec reconfigure /path/to/clustername --help

Example:

    # Change an existing BDR-Always-ON cluster to PGD-Always-ON

    tpaexec reconfigure clustername         \\
        --architecture PGD-Always-ON        \\
        --pgd-proxy-routing global

Options:

    --architecture NAME

        Change the cluster's architecture. At present, PGD-Always-ON is
        the only supported architecture

    --describe

        Describe what would be changed without changing anything

    --check

        Check the cluster configuration against the proposed changes,
        and report any problems without changing anything

    --output FILENAME

        Write the modified config.yml to the given file, rather than
        overwriting config.yml in-place

    --help

        Displays all other options available.

Docs: $TPA_DIR/docs/src/tpaexec-reconfigure.md
RECONFIG
            ;;

        provision)
            cat <<PROVISION
Command: tpaexec provision

Reads clustername/config.yml (as generated by 'tpaexec configure') and
creates any resources required (e.g., AWS EC2 VMs or Docker containers)
to host the cluster, and generates an Ansible inventory for the cluster.

(If you are using existing instances with '--platform bare', it will not
need to create any servers, but just write the connection details you
specify into the generated inventory.)

Note: you must run 'tpaexec provision' every time you change config.yml,
to regenerate the inventory.

The next step is 'tpaexec deploy'.

Synopsis:

    tpaexec provision clustername [-vv]

Options:

    --owner OwnerName

        Override the cluster owner (default: your login name, ${USER:-$(id -u -n)}).

    --cached

        Speed up repeated provision runs in development by relying on
        cached information and skipping retests of resources like VPCs
        and subnets.

    --force-reprovision

        Repeat the process even if there are no changes to config.yml

Any other arguments will be passed through to ansible-playbook.
PROVISION
            ;;

        command|adhoc|cmd)
            cat <<COMMAND
Command: tpaexec cmd

Runs an ansible(1) ad-hoc command on the instances in a cluster (as
opposed to ansible-playbook(1); see also 'tpaexec help playbook').

You can specify a list of hosts (or 'all'), a module name, and arguments
to the module. These will be passed to ansible(1) along with options to
load the correct inventory for your cluster.

Synopsis:

    tpaexec cmd clustername all -m module -a "arguments"

Examples:

    # Print the value of an ansible variable
    tpaexec cmd clustername all -m debug -a var=inventory_hostname

    # Install a package
    tpaexec cmd clustername all -sm package -a 'name=pkgname state=latest'

    # Run arbitrary shell commands
    tpaexec cmd clustername host1,host2 -m shell -a 'ps -ef|grep repmgr'

    # Ensure key is present in ~admin/.ssh/authorized_keys
    tpaexec cmd clustername all -m authorized_key -a \\
        "user=admin key=\"{{ lookup('file', 'id_someone.pub') }}\""

See also 'tpaexec help playbook'.
COMMAND
            ;;

        ping)
            cat <<PING
Command: tpaexec ping

Tests connectivity (usually via SSH) to cluster instances (by running
the Ansible 'ping' module against all the hosts). If this fails, you
should investigate and fix the problem before 'tpaexec deploy'.

(You must have already run 'tpaexec provision' first.)

Synopsis:

    tpaexec ping clustername [-vvv]

Options:

    -vvv    Increase verbosity enough to show the exact ssh commands
            used. Helpful for debugging.
PING
            ;;

        playbook)
            cat <<PLAYBOOK
Command: tpaexec playbook

Runs an Ansible playbook against the instances in the given cluster.

(You must have already run 'tpaexec provision' first.)

Synopsis:

    tpaexec playbook clustername/playbook.yml
    tpaexec playbook clustername /path/to/playbook.yml

Any options you specify will be passed through to ansible-playbook.

See also 'tpaexec help cmd' to execute ad-hoc commands.
PLAYBOOK
            ;;

        deploy)
            cat <<DEPLOY
Command: tpaexec deploy

Starts with an already-provisioned cluster (i.e., an Ansible inventory
with server connection details and information derived from config.yml)
and installs and configures software on the cluster instances.

(You must have already run 'tpaexec provision' first.)

Synopsis:

    tpaexec deploy clustername [-vv] [-e var=value]

Options:

    -vv     Increase verbosity (recommended to debug any failures).

    -e var=value

            Override the value of a variable during the deploy.

Any options you specify will be passed through to ansible-playbook.
DEPLOY
            ;;

        test)
            cat <<TEST
Command: tpaexec test

Runs tests for a cluster according to its architecture.

You may specify the name of a test to run as the first argument instead
of the built-in test named 'default'. (Depending on the test, you may
need to provide additional options on the command line.)

The 'tpaexec test clustername test' command will search for test.yml
in the cluster's tests subdirectory first, then fall back to built-in
tests. You may create new tests (as Ansible playbooks) under the
cluster's tests subdirectory.

Synopsis:

    tpaexec test clustername [testname] [options]

Options:

    --include-tests-from /other/path

        Also look for tests defined under /other/path (in addition to
        the default locations).

The different kinds of tests executed by default have different tags,
e.g., postgres, repmgr, barman, pgbench. Use '--tags x,y' to select
tests to execute, or '--skip-tags p,q' to exclude certain tests.

Examples:

    tpaexec test mycluster --tags barman
    tpaexec test mycluster --skip-tags repmgr,pgbench
TEST
            ;;

        deprovision)
            cat <<DEPROVISION
Command: tpaexec deprovision

Removes the resources created for this cluster by 'tpaexec provision': aws
instances or docker containers if the cluster uses those platforms, and
local files such as ssh keys and ansible inventory. The cluster directory
is thus returned to the state it would be in after 'tpaexec configure'.

Synopsis:

    tpaexec deprovision clustername

Any options you specify will be passed through to ansible-playbook.
DEPROVISION
            ;;

        rebuild-sources)
            cat <<REBUILD-SOURCES
Command: tpaexec rebuild-sources clustername

Rebuilds components that were built from source on the instances in a
cluster. If you're using source from git repositories on the instances
themselves, this will do 'git pull' before rebuilding; if your instances
are Docker containers and you're using source directories mounted from
the host, those directories will be read-only on the containers and so
it will build from whatever state of the source it can see.

Restarts postgres after rebuilds are complete.

REBUILD-SOURCES

            ;;

        *)
            # We can treat the argument as the name of a custom command.
            # If we can find a matching executable, we can run it with
            # --help; or if there's a shell script, it might define a
            # _tpaexec_help function that we can call.

            declare -a dirs

            dirs=()
            if [[ -f ./config.yml && -d ./commands ]]; then
                dirs+=(./commands)
            fi
            dirs+=("$TPA_DIR"/architectures/lib/commands)

            for dir in "${dirs[@]}"; do
                script=$dir/$topic.sh
                exec=$dir/$topic

                if [[ -x $exec ]]; then
                    _with_ansible_env "$exec" --help
                    return $?
                elif [[ -f $script ]]; then
                    source "$script"
                    if [[ $(type -t _tpaexec_help) == function ]]; then
                        _with_ansible_env _tpaexec_help
                        return $?
                    fi
                fi
            done

            echo "Sorry, no help available for '$topic'"
            if [[ ! -d ./commands ]]; then
                echo "(Try running this command inside a cluster directory)"
            fi
            exit 1
            ;;
    esac
}

## Command handling
#
# We may be invoked as "tpaexec command …", or through a symbolic link
# from command → tpaexec (for backwards compatibility).

exec=$(basename "$0")
case "$exec" in
    tpaexec)
        command=${1:?No command specified (try 'tpaexec help')}
        shift
        ;;
    provision|deprovision|deploy|rehydrate)
        command=$exec
        ;;
    *)
        error "unrecognised tpaexec command link: $exec"
        ;;
esac

# We support some command aliases as a convenience.

case "$command" in
    config)
        command=configure
        ;;
    command|adhoc|cmd)
        command=cmd
        ;;
    --version)
        command=version
        ;;
esac

# Some commands take the path to a cluster directory as an argument, and
# we may need to chdir to it beforehand.

case "$command" in
    playbook)
        arg=${1:?No cluster specified}
        shift

        # We accept either a cluster directory followed by a playbook,
        # or a path to a playbook within a cluster directory.

        if [[ -d $arg ]]; then
            cluster=$arg
            if [[ ! ( ( -f $1 || -f $arg/$1 ) && $1 =~ .yml$ ) ]]; then
                error "argument is not a playbook: $1"
            fi
        elif [[ -f $arg ]]; then
            cluster=$(dirname "$arg")
            file=$(basename "$arg")
            set -- "$file" "$@"
        else
            error "argument is neither directory nor file: $arg"
        fi

        cd "$cluster"
        ;;

    cmd|ping|provision|deploy|upgrade|deprovision)
        cluster=${1:?No cluster specified}
        shift

        if [[ ! -d $cluster ]]; then
            error "cluster directory does not exist: $cluster"
        fi

        cd "$cluster"
        ;;
esac

# If we can find a venv located relative to TPA_DIR, we activate it as a
# convenience.

if [[ -f $TPA_VENV/bin/activate ]]; then
    _activate_venv "$TPA_VENV"
    PYTHON=python3
elif [[ -z ${PYTHON:-""} ]]; then
    EDB_PYTHON=/usr/libexec/edb-python39/bin/python3
    if [ -f $EDB_PYTHON ]; then
        PYTHON=$EDB_PYTHON
    else
        PYTHON=python3
    fi
fi

case "$command" in
    *help|info|version)
        # Let through some basic commands, info in particular, without
        # checking the Python version first.
        ;;

    *)
        if command -v "$PYTHON" &>/dev/null; then
            if [[ $(_python39_or_later) == "False" ]]; then
                error "TPA requires Python 3.9+, but found only" \
                    "$(_python_version) ($(command -v $PYTHON));" \
                    "please set PYTHON=/path/to/python3 or install edb-python"
            fi
        else
            error "TPA requires at least Python 3.9; please " \
                "set PYTHON=/path/to/python3 or install edb-python"
        fi
        ;;
esac

# Now we can look at the command-line arguments and decide which
# function to call.

case "$command" in
    *help)
        help "$*"
        ;;

    update-postgres)
        error "Please use 'tpaexec upgrade' instead"
        ;;

    info|version|setup|configure|reconfigure|selftest)
        $command "$@"
        ;;

    cmd|ping|provision|deploy|upgrade|playbook|deprovision)
        time $command "$@"
        real_exit_status=$?
        playbook "${TPA_DIR}/architectures/lib/commands/check-repositories.yml"
        exit $real_exit_status
        ;;

    *)
        try_as_playbook_or_script "$@"
        real_exit_status=$?
        playbook "${TPA_DIR}/architectures/lib/commands/check-repositories.yml"
        exit $real_exit_status
        ;;
esac
