#!/usr/bin/bash
# @file
# @brief CII Postinstall
#
# @copyright
#   SPDX-FileCopyrightText: 2020-2026 European Southern Observatory (ESO) @n
#   SPDX-License-Identifier: LGPL-3.0-only


# This is an end-user tool for self-managed CII installations.
# It works in two steps:
#   1) create an ansible playbook from command line args, and
#   2) run the playbook via ansible
#
#   By default, it runs both steps in one go. But the steps can also be run
#   separately to cater for the following use-cases:
#   Step 1 only: So user can modify the created playbook before applying it.
#   Step 2 only: Repair/Modify existing CII installation, or
#                Perform newly added setup tasks after an upgrade of CII.


ROLESDIR="/usr/share/ansible/roles"

THIS=`basename $0` # cii-postinstall

KNOWN_ROLE=/etc/cii/postinstall-knownrole       # old style (puppet), expected by some cii tools
KNOWN_PLAY=/etc/cii/postinstall-knownrole.yaml  # new style (ansible), for use by this tool


# Doc
# -----------------------------------------------------------------------

function help {
    cat <<EOF

Configures a CII-Installation on an ELT DevEnv host.
This command must be run with super-user privileges.

Usages:
  $THIS [opts] <role> <params>   # create configuration and apply it
  $THIS [opts] knownrole         # (re-)apply existing configuration
  $THIS [opts] info              # show/check existing configuration

  role/params:
    ownserver           # For isolated single-host development/testing
    groupserver [nic]   # Serve CII services on the given network interface
    groupclient <nic>   # Use CII services served by the groupserver at <nic>

  opts:
    -c|--create-only    # create configuration but do not apply it
    -s|--switch-yes     # allow role-switch (not generally supported)
    -v|-vv|...          # run ansible with -v... option (up to 5)
    -e var=value        # pass extra variable to ansible
    --dbg               # show ansible output on terminal

Examples:
  a) $THIS ownserver
  b) $THIS groupserver
  c) $THIS groupclient 10.10.10.10

Advanced:
  You can tweak your set-up, e.g. how many log files to retain: modify
  $KNOWN_PLAY, then run $THIS knownrole.
  You can do this even before first application by using --create-only.

Notes:
   For backward compatibility, roles names are also accepted when they are
   prefixed with "role_", e.g. role_ownserver is understood as ownserver.

   This utility is meant to choose a role only once. Role-switching is not
   a generally supported feature, some role switches will work, others won't.

EOF
}


# Generate playbook content
# -----------------------------------------------------------------------

# will be set by the factory functions below
generated=
messages=()

common_header="
# Created by cii-postinstall
# ANSIBLE_ROLES_PATH: $ROLESDIR
- name: cii-postinstall knownrole
  hosts: localhost"

# Factory method for playbook content
# 
function generate_ownserver {

read -d '' -r generated <<EOF
$common_header
  vars:
    cii_role: ownserver
  roles:
    - role: cii-log
      vars:
        #log_rotate_keep: 10
        #log_rotate_attic: 500M
    - role: cii-config-read
    - role: cii-oldb
EOF
: # i seemingly discovered a bash bug, if i don't put a statement after the EOF bash exits with 1
}


# Factory method for playbook content
#
function generate_groupserver {
  local nic=$1
  local ip=

  if [ -n "$nic" ]; then
    # optional arg given
    if ipcalc --check --silent $nic ; then
        ip=$nic
    else
        echo "Error. Usage: groupserver [<IP>]" >&2
        return 2
    fi
  else
    # look up own ip (picks one if there are multiple)
    ip=`hostname -I | cut -d" " -f1`
    if [ -z "$ip" ]; then
      echo "Error. Failed to look up IP Addr for this host" >&2
      return 2
    else
      messages+=("Info: This host resolved to IP Addr: $ip")
      messages+=("Info: To use a different IP, run: groupserver <IP Addr>")
    fi
  fi

read -d '' -r generated <<EOF
$common_header
  vars:
    cii_role: groupserver $nic
  roles:
    - role: cii-log
      vars:
        log_collect: true
        #log_rotate_keep: 10
        #log_rotate_attic: 2.5G
        #log_collector_forward_further: 10.10.10.10
    - role: cii-config-read
    - role: cii-oldb
      vars:
        oldb_server_nic: $ip
        oldb_accessible_from_other_hosts: true
EOF
: # i seemingly discovered a bash bug, if i don't put a statement after the EOF bash exits with 1
}


# Factory method for playbook content
# 
function generate_groupclient {
  local target=$1
  local ip=

  if [ -z "$target" ]; then
    echo "Error. Usage: groupclient <name|IP>" >&2
    return 2
  fi

  # name|IP -> IP
  if ipcalc --check --silent $target ; then
    ip=$target
  else
    # look up ip (picks one if there are multiple)
    ip=`dig +short +time=5 $target | tail -n1`
    if [ -z "$ip" ]; then
      echo "Error. Failed to look up IP Addr for: $target" >&2
      return 2
    else
      messages+=("Info: $target resolved to IP Addr: $ip")
      messages+=("Info: To use a different IP, run: groupclient <IP Addr>")
    fi
  fi

read -d '' -r generated <<EOF
$common_header
  vars:
    cii_role: groupclient $target
  roles:
    - role: cii-log
      vars:
        log_forward: $ip
        #log_rotate_keep: 10
    - role: cii-config-read
      vars:
        config_server: $ip
EOF
: # i seemingly discovered a bash bug, if i don't put a statement after the EOF bash exits with 1
}



# Info about Host / Known Role / Playbook
# -----------------------------------------------------------------------

is_puppet_style_host() {
  [ ! -e $KNOWN_PLAY ] && [ -e $KNOWN_ROLE ]
}

get_knownrole() {

  # back-compat: support puppet-style hosts (installed by puppet-postinstall)
  is_puppet_style_host && {
    read -a role_curr <$KNOWN_ROLE
    role_curr[0]=${role_curr[0]#"role_"}
    echo ${role_curr[@]}
    return 0
  }

  # yield cii_role from playbook, or empty string
  read -a role_curr < <(grep cii_role $KNOWN_PLAY 2>/dev/null | cut -d: -f2) \
    || { echo ""; return 1; }
  echo ${role_curr[@]}
}




# Execution of Playbook
# -----------------------------------------------------------------------

ansible_options=

run_playbook() {

  echo "Applying known configuration for this host"


  if [ -z ${LOGFILE} ]; then
    LOGFILE=/tmp/cii-postinstall-ansible-`date +%Y%m%d`.log
  fi
  echo "Ansible detailed output goes to: $LOGFILE"

  # Not enforcing, so user can use custom one
  # export ANSIBLE_LOAD_CALLBACK_PLUGINS=1
  # export ANSIBLE_STDOUT_CALLBACK=default

  # Playbooks can reference roles by absolute paths, but relative role names are nicer
  # I found no documentation if "first match wins" or "last match wins", I assume the latter.
  export ANSIBLE_ROLES_PATH=$ANSIBLE_ROLES_PATH:$ROLESDIR

  # Suppress "[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
  export ANSIBLE_LOCALHOST_WARNING=false

  # get ansible's output without buffering (hopefully)
  export PYTHONUNBUFFERED=true

  echo "Running ansible (this may take several minutes) ..."
  {
    echo -e "\n++++++++++++++++++++++++"
    command="ansible-playbook --syntax-check $ansible_options $KNOWN_PLAY"
    echo "PRE-CHECK: $command"
    stdbuf -o0 -e0 $command 2>&1
    rcPrecheck=$?
    if [ $rcPrecheck != 0 ]; then
      echo "Error. Known configuration is invalid
        For more information, see ansible detailed output" >&2
      return $rcPrecheck
    fi

    echo START `date "+%Y-%m-%d %H:%M:%S"`
    for var in "${!ANSIBLE_@}"; do printf 'CONFIG: %s=%s\n' "$var" "${!var}" ; done

    command="ansible-playbook $ansible_options $KNOWN_PLAY"
    echo "COMMAND: $command"
    stdbuf -o0 -e0 $command 2>&1
    ANSIBLERC=$?

    echo END `date "+%Y-%m-%d %H:%M:%S"`
  } >> $LOGFILE


  # Print summary
  echo "Ansible detailed exit code = ${ANSIBLERC}"
  if [ "${ANSIBLERC}" -ne "0"  ]; then
    echo
    echo "Ansible exit code indicates a problem in the execution."
    echo "Please check the installation log file in ${LOGFILE}"
    return ${ANSIBLERC}
  fi

  echo "Ansible exit code indicates a successful execution!"
  return 0
}



# Main
# -----------------------------------------------------------------------

if [ `whoami` != "root" ]; then
  echo "Warning: Running $THIS as non-root might not succeed due to permission problems"
fi


do_run=true
no_switch=true

# parse flags
while true; do
case "$1" in
  -x)                       set -x ; shift ;;
  --dbg)                    LOGFILE=/dev/stdout ; shift ;;
  ""|-h|--help)             help ; exit 0 ;;
  -c|--create-only)         do_run= ; shift ;;
  -s|--switch-yes)          no_switch= ; shift ;;
  -v|-vv|-vvv|-vvvv|-vvvvv) ansible_options="$ansible_options $1" ; shift ;;
  -e)                       ansible_options="$ansible_options -e $2" ; shift 2 ;;
  *)  break ;;
esac
done


# parse args
SUBCMDS="ownserver groupserver groupclient knownrole info"

# back-compat: ansible-postinstall dismisses the "role_"-prefix
# naming convention used in puppet-postinstall, but still accepts it.
role_arg=${1#"role_"}
role_params="${@:2}"

case "$role_arg" in

  ownserver|groupserver|groupclient)

    # warn about role-switches
    role_curr=($(get_knownrole)) && {
      if [ "$role_arg" != "${role_curr[0]}" ] && [ "$no_switch" ]; then
        echo "Error. You are trying to switch role from ${role_curr[0]} to $role_arg
       Be aware that role-switching is not a supported feature, and may fail.
       If you are determined to continue, use the --switch-yes flag. You may
       then also want to override all safety checks with '-e force=true'." >&2
        exit 2
      fi
    }

    # generate
    generate_${role_arg} $role_params || exit $?  # will set "$generated" and "$messages"
    for msg in "${messages[@]}"; do echo "$msg"; done

    # write
    echo "Creating configuration for this host"
    mkdir -p $(dirname $KNOWN_PLAY) $(dirname $KNOWN_ROLE)
    echo "$generated"                  > $KNOWN_PLAY
    echo "role_$role_arg $role_params" > $KNOWN_ROLE

    # run
    [ $do_run ] &&
      run_playbook || exit $?
    ;;

  knownrole)

    # back-compat: support puppet-style hosts (installed by puppet-postinstall)
    is_puppet_style_host && {
      echo "Note: This host was previously configured with old postinstall"
      echo "Note: Upgrading your old configuration to new postinstall ..."
      role_curr=($(get_knownrole))
      generate_${role_curr[0]} ${role_curr[@]:1} || exit $?
      for msg in "${messages[@]}"; do echo "$msg"; done
      echo "$generated" > $KNOWN_PLAY
      echo "Note: Upgrade completed."
    }

    if [ ! -e $KNOWN_PLAY ]; then
      echo "Error. Known configuration not found" >&2
      exit 1
    fi

    # run
    [ $do_run ] &&
      run_playbook || exit $?
    ;;

  info)

    # Role info
    role_curr=($(get_knownrole)) || {
      echo "Role: NONE, this host is not post-installed";
      exit 3;
    }
    echo "Role: ${role_curr[@]}"

    # back-compat: support puppet-style hosts (installed by puppet-postinstall)
    is_puppet_style_host && {
      echo "Customisations: N/A, as this host was configured with old postinstall"
      echo "Note: To upgrade to new postinstall, run 'cii-postinstall -c knownrole'"
      exit 0
    }

    # Diff between what we would generate and what the user has
    generate_${role_curr[0]} ${role_curr[@]:1} || exit $?
    nDiff=$(diff --ignore-blank-lines --side-by-side --suppress-common-lines <(echo "$generated") $KNOWN_PLAY | grep '^' | wc -l)
    echo "Customisations: $nDiff"

    # Show differences in detail
    [ $nDiff -gt 0 ] && {
      printf "\n%-65s%s\n" "Predefined Standard"  "Your Setup";  # 65 = 130/2 (diff command uses default width 130)
      diff --ignore-blank-lines --side-by-side --left-column --expand-tabs --tabsize=2 <(echo "$generated") $KNOWN_PLAY
      echo
    }
    exit 0
    ;;

  *)
    echo "Available subcommands: $SUBCMDS" ; exit 2 ;;
esac

echo "Done."
