#!/bin/sh
# @description update policies on agent
# @man The agent will fetch the last version of its policies from its configured
# @man policy server.
# @man +
# @man *Options*:
# @man +
# @man *-C*: update the agent with the CFEngine protocol
# @man +
# @man *-H*: update the agent with the HTTPS protocol
# @man +
# @man *-i*: run the agent in information mode, prints basic information
# @man +
# @man *-v*: run the agent in verbose mode, prints detailed information
# @man +
# @man *-d*: run the agent in debug mode, prints low-level information
# @man +
# @man *-q*: run the agent in quiet mode (display only error messages)
# @man +
# @man *-c*: run the agent without color output
# @man +
# @man *-f*: force full update

. "${BASEDIR}/../lib/common.sh"

bootstrap_check

VERBOSITY=""
FORCE=false
QUIET=false
if is_https_only; then
  PROTOCOL=https
else
  PROTOCOL=cfengine
fi

while getopts "CHiIvdqcf" opt; do
  case $opt in
    C)
      PROTOCOL=cfengine
      ;;
    H)
      PROTOCOL=https
      ;;
    i|I)
      QUIET=false
      ;;
    v)
      VERBOSITY="-v ${VERBOSE_CLASS}"
      QUIET=false
      ;;
    d)
      VERBOSITY="-d ${DEBUG_CLASS}"
      QUIET=false
      ;;
    q)
      VERBOSITY=""
      QUIET=true
      ;;
    c)
      COLOR=""
      ;;
    f)
      FORCE=true
      ;;
  esac
done

if [ "${FORCE}" = "true" ]; then
  rm -f ${RUDDER_VAR}/cfengine-community/inputs/rudder-promises-generated
  rm -f ${RUDDER_VAR}/tools/rudder_tools_updated
  rm -f ${RUDDER_VAR}/ncf/common/ncf_hash_file
  rm -f ${RUDDER_VAR}/ncf/local/ncf_hash_file
  rm -f ${RUDDER_VAR}/tmp/*.etag
fi

# download a policy file and extract it in a .new directory
download_and_extract() {
  SOURCE="$1"
  DEST="$2"
  ARCHIVE="/var/rudder/tmp/$(basename ${SOURCE})"

  # try download if more recent
  /opt/rudder/bin/rudder-client -c -e "${SOURCE}" -t "${ARCHIVE}.etag" -- -o "${ARCHIVE}"
  code=$?
  if [ $code -ne 0 ]; then
    # in case of error, also cleanup etag to force a re-download
    rm -f "${ARCHIVE}" "${ARCHIVE}.etag"
    return $code
  fi
  [ "$QUIET" = false ] && printf "${GREEN}ok${NORMAL}: Checked policies for available updates.\n"

  # if download happened, extract
  if [ -f "${ARCHIVE}" ]; then
    [ "$QUIET" = false ] && printf "${GREEN}ok${NORMAL}: New policies available.\n"

    # cleanup old cruft
    rm -rf "${DEST}.old" "${DEST}.new"
    # extract in new place
    (umask 077 && tar xf "${ARCHIVE}" -C $(dirname "${DEST}") --no-same-owner --no-same-permissions --transform "s|policies_next|inputs|" --transform "s|^\([^/]\+\)|\1.new|")
    code=$?
    if [ $code -ne 0 ]; then
      # in case of error, also cleanup etag to force a re-download
      rm -rf "${ARCHIVE}" "${ARCHIVE}.etag" "${DEST}.new"
      return $code
    fi
    rm -r "${ARCHIVE}"
    [ "$QUIET" = false ] && printf "${GREEN}ok${NORMAL}: Policies correctly extracted.\n"

  fi
}

# replace provided directory with the .new version
swap_new_dir() {
  DEST="$1"
  # if "new" was extracted replace current with new
  if [ -d "${DEST}.new" ]; then
    mv "${DEST}" "${DEST}.old" && mv "${DEST}.new" "${DEST}"
    code=$?
    if [ $code -ne 0 ]; then
      return $code
    fi
  fi
}

# in case of error try to rollback to previous policies
rollback() {
  NCF_PATH="$1"
  POL_PATH="$2"
  if [ -d "${NCF_PATH}.old" ]; then
    rm -rf "${NCF_PATH}"
    mv "${NCF_PATH}.old" "${NCF_PATH}"
  fi
  if [ -d "${POL_PATH}.old" ]; then
    rm -rf "${POL_PATH}"
    mv "${POL_PATH}.old" "${POL_PATH}"
  fi
  # final check
  rudder agent check -f
  # there was an error, it should be updated before retrying
  rm -f "${NCF_AR}"
}

# The whole update process
update() {
  NCF_SOURCE="/common/policies-library-linux.tar.bz2"
  POL_SOURCE="/policies/${UUID}/rules/rudder.tar.bz2"
  NCF_PATH="/var/rudder/ncf"
  POL_PATH="/var/rudder/cfengine-community/inputs"

  # download and extract both ncf and the policy
  download_and_extract "${NCF_SOURCE}" "${NCF_PATH}" && download_and_extract "${POL_SOURCE}" "${POL_PATH}"
  if [ $? -ne 0 ]; then
    # if one has had an error, keep archives, the working one will be reused
    return 1
  fi

  # swap both ncf and policy directories
  swap_new_dir "${NCF_PATH}" && swap_new_dir "${POL_PATH}"
  if [ $? -ne 0 ]; then
    # rollback the move
    rollback "${NCF_PATH}" "${POL_PATH}"
    return 2
  fi

  # if we replaced the policy, test its validity
  if [ -d "${NCF_PATH}.old" ] || [ -d "${POL_PATH}.old" ]; then
    /opt/rudder/bin/cf-promises
    # if promises are valid, remove old ones
    if [ $? -eq 0 ]; then
      rm -rf "${NCF_PATH}.old" "${POL_PATH}.old"
    # if they are not, rollback
    else
      rollback "${NCF_PATH}" "${POL_PATH}"
    return 3
    fi
    [ "$QUIET" = false ] && printf "${GREEN}ok${NORMAL}: New policies enabled.\n"
  fi

  # always remove archive, it is not useful anymore
  # and we have the etag file for future updates
  rm -f "${NCF_AR}"
  return 0
}

# We use standard failsafe on root as it does local copies and we don't have policy archives for it.
if [ "${PROTOCOL}" = "https" ] && [ "${UUID}" != 'root' ]
then
  update
  code=$?
  if [ "${code}" = "0" ]; then
    touch /var/rudder/cfengine-community/last_successful_inputs_update
  fi
else
  # The awk part is the workaround for update not exiting with a 1 in case of error
  # It will be necessary to remove it once the exit is back in cf-agent
  # It must be changed if the error messages are back to stderr
  "${RUDDER_DIR}/bin/cf-agent" ${VERBOSITY} ${COLOR} -K -f failsafe.cf | awk 'BEGIN{e=0}{print; if(match($0,/You must accept this node/)){e=1} else if(match($0,/rudder-agent could not get an updated configuration|error:/)){e=2}}END{exit e}'
  code=$?
fi

if [ $code -eq 0 ]; then
  [ "$QUIET" = false ] && printf "${GREEN}ok${NORMAL}: Rudder agent policies were updated.\n"
  exit 0
elif [ $code -eq 1 ] && [ "${PROTOCOL}" != "https" ]; then
  printf "${GREEN}ok${NORMAL}: Rudder node waiting to be accepted.\n"
  exit 0
else
  printf "${RED}error${NORMAL}: Rudder agent policies could not be updated.\n" 1>&2
  exit $code
fi
