#!/bin/bash
#
# Bootstrap script for initial installation of the updater. Not
# intended to be run by end users.
#
# For agent registration, this script expects the environment
# variables USERNAME, DATACENTER, and API_KEY, which are passed to the
# corresponding arguments of driveclient --configure. AUTH_TOKEN may
# substitute for API_KEY if desired. These variables are normally set by
# servermill. They are not required when updating the legacy agent.
#
# For testing purposes, you may also supply any of the following. Note
# these are not Officially Supported and should not be relied on in
# production:
#
FLAVOR=${FLAVOR:-raxcloudserver}
CBU_AGENTREPO=${CBU_AGENTREPO:-https://agentrepo.drivesrvr.com/repo}
CBU_ANALYTICS=${CBU_ANALYTICS:-https://analytics.stag.cbu.rackspace.net}
CBU_DRIVECLIENT_CONF=/etc/driveclient/bootstrap.json
CBAU_EXTRA_ARGS=${CBAU_EXTRA_ARGS:-}  # these get passed to `cbau install`

# These vars are normally supplied by servermill for debugging purposes.
BASE_IMAGE=${BASE_IMAGE:-}
BUILD_IMAGE=${BUILD_IMAGE:-}
IMAGE_NAME=${IMAGE_NAME:-}
INSTANCE_ID=${INSTANCE_ID:-}

# FIXME: for debugging, remove in release
set -e
set -u
# API_KEY/AUTH_TOKEN are either/or; provide empty defaults so set -u
# doesn't trip on whichever one you're not using.
API_KEY=${API_KEY:-}
AUTH_TOKEN=${AUTH_TOKEN:-}

export DEBIAN_FRONTEND=noninteractive  # shut up, apt
# https://manpages.debian.org/bullseye/debconf-doc/debconf.7.en.html#Frontends

# these settings are missing when called from the legacy updater
umask 0022
export SHELL=/bin/bash

RETRY_CRONJOB=/etc/cron.d/cbu-retry-update  # used only if needed

pkgcmd () {
	for cmd in apt-get apt yum; do
		command -v $cmd 2>/dev/null && return
	done
}

installdeps () {
	local cmd
	cmd=$(pkgcmd)
	if [ -z "${cmd}" ]; then
		echo "failed to install missing sysdeps (can't find package manager)"
		return 1
	fi
	echo "installing missing sysdeps: $*"
	if [[ $cmd  == */apt* ]]; then
		$cmd update
	fi
	$cmd install -y "$@"
}

geturl () {
	# A few images don't have curl out of the box. This wrapper falls
	# back to wget in those cases. Expects to receive a URL to download
	# and a file to download to. If no file is provided, results go to
	# stdout.
	#
	# At the moment this is only used to download the standalone
	# updater. It might be simpler to pipe it to python rather than
	# saving it, though I'm not sure how to handle errors properly that
	# way.
	local url="${1}"
	local tgt="${2}"
	if command -v curl > /dev/null 2>&1; then
		# FIXME: the redirect here stops curl from inserting a bare carriage
		# return in the log. I am not sure if this hides legitimate errors
		# -- those *should* be on stderr, but verify.
		curl "${url}" \
			--output "${tgt}" \
			--silent \
			--show-error \
			--fail \
			> /dev/null || return
	elif command -v wget /dev/null 2>&1; then
		wget "${url}" \
			--output-document "${tgt}" \
			--no-verbose \
			|| return
	else
		fail "curl/wget not available"
	fi
}

bootstrap_updater () {
	# Allow overriding the repo path for local testing purposes
	local script=cbu-updater-standalone.py
	local url=${CBU_AGENTREPO}/${script}
	local path=${TMP}/${script}

	# Simplest way of getting a useful indicator of a preinstalled version
	type cbu-updater >/dev/null 2>&1 && cbu-updater install cbu-updater && return 0
	echo "downloading standalone installer from ${url} to ${path}"
	geturl "${url}" "${path}" || return
	# shellcheck disable=SC2086 # intentional, but should it be?
	python3 "${path}" install cbu-updater ${CBAU_EXTRA_ARGS} || return
}

kicksvc () {
	local svc="$1"
	systemctl restart "${svc}" ||
		service "${svc}" restart ||
		/etc/init.d/"${svc}" restart
}

register () {
	local autharg
	local authval
	local req_vars="USERNAME DATACENTER"
	local missing_vars=""
	for var in ${req_vars}; do
		[ -z "${!var}" ] && missing_vars="${missing_vars}${var} "
	done
	if [ -z "${API_KEY}${AUTH_TOKEN}" ]; then
		missing_vars="${missing_vars}(API_KEY|AUTH_TOKEN) "
	fi
	if [ -n "${missing_vars}" ]; then
		fail "can't register; missing servermill env vars: ${missing_vars}"
	fi
	if [ -n "${AUTH_TOKEN}" ]; then
		autharg=authtoken
		authval=${AUTH_TOKEN}
	elif [ -n "${API_KEY}" ]; then
		autharg=apikey
		authval=${API_KEY}
	else
		# shouldn't happen, precheck should catch this
		echo "must supply at least one of AUTH_TOKEN or API_KEY"
		return 1
	fi

	echo "configuring driveclient for ${USERNAME} in ${DATACENTER}"
	driveclient \
		--configure \
		--noninteractive \
		--flavor "${FLAVOR:-raxcloudserver}" \
		--datacenter "${DATACENTER}" \
		--user "${USERNAME}" \
		--${autharg} "${authval}" \
		|| return
		# --apihost api.${DATACENTER}.cbu.rackspace.net
		# may not need to specify apihost; it should get the right one
		# from the service catalog, I think?
	kicksvc driveclient > /dev/null 2>&1
}

report_result () {
	# Report success/failure and system metadata to server
	local result="$1"
	local message="${2:-}"
	local url="$CBU_ANALYTICS/report/bootstrap"
	local results="${TMP}/results.txt"
	cat <<- EOF > "${results}"
		result: ${result}
		message: ${message}
		img_base: ${BASE_IMAGE}
		img_build: ${BUILD_IMAGE}
		img_release: ${IMAGE_NAME:-$(detect_os)}
		instance: ${INSTANCE_ID}
		EOF
	# make sure the results get logged whether or not they're
	# successfully posted
	echo "dumping result report"
	cat "${results}"
	echo "sending results to ${url}"

	# We don't want bootstrap to hang just because analytics is down. This has
	# two layers of timeouts; those built in to the commands (which may produce
	# more useful errors), and the actual timeout command (in case the command
	# timeouts miss some cases -- I think wget's in particular isn't
	# end-to-end).
	if command -v curl > /dev/null 2>&1; then
		timeout 10 curl "${url}" \
			--max-time 8 \
			--silent \
			--fail \
			--show-error \
			--header "Content-Type: text/plain" \
			--data-binary @"${results}" \
			> /dev/null \
			|| echo "couldn't ship bootstrap results (curl failed)"
	elif command -v wget /dev/null 2>&1; then
		timeout 10 wget "${url}" \
			--timeout 8 \
			--no-verbose \
			--header "Content-Type: text/plain" \
			--post-file "${results}" \
			|| echo "couldn't ship bootstrap results (wget failed)"
	else
		echo "can't ship bootstrap results (curl/wget not available)"
	fi
}

create_migration_retry_job () {
	# Echoes a cron job that starts the v2 updater once a day
	#
	# When the v2 updater tries to migrate to v3 and fails, the v2 service
	# stays dead afterward. We don't want to restart it right away,
	# because it will just get stuck in a retry loop. Instead, create a
	# cron job that tries to start it once a day, and remove the job
	# when a successful migration happens.

	# Get pseudorandom hour/minute, to spread load
	local h="$(( $(hostname    | cksum | awk '{print $1}') % 24 ))"
	local m="$(( $(hostname -i | cksum | awk '{print $1}') % 60 ))"
	local sched
	sched=$(printf "%02d:%02d" "${h}" "${m}")

	# Rather than try to detect the init system, just try all possible
	# init systems.
	local cmd_sysd="systemctl start cloudbackup-updater"
	local cmd_svc="service cloudbackup-updater start"
	local cmd_init="/etc/init.d/cloudbackup-updater start"
	local force_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

	cat <<-EOF > ${RETRY_CRONJOB}
		# Retry cloudbackup-updater v3 migration once a day"
		# This job will be automatically removed on success"
		#
		# (note: init script relies on commands from sbin, which is
		# missing from cron's PATH on some platforms)
		${m} ${h} * * * root export PATH=${force_path}; ${cmd_sysd} || ${cmd_svc} || ${cmd_init}
		EOF
	echo "retry job created at ${RETRY_CRONJOB}, scheduled daily at ${sched}"
}

fail () {
	# Shorthand to exit with an error message
	report_result "failed" "${1}"
	if type -p cloudbackup-updater; then
		echo "failed to migrate updater to v3; restoring v2 and preparing retry job"
		# replace the v2 stub with the real one if possible
		mv -vf /usr/bin/cloudbackup-updater.real /usr/bin/cloudbackup-updater
		create_migration_retry_job
	fi
	exit 1;
}

detect_os () {
	# Figure out what OS we're on, for analytics reporting. For
	# servermill builds we can rely on IMAGE_NAME, but it is not
	# available during migrations of existing installs. In such cases,
	# try to farm out detection to whatever commands we can find. It's
	# remarkably annoying making this work across distros.
	( \
		cbu-updater sysinfo --get os ||
		lsb_release -sd ||
		( . /etc/os-release && echo "${PRETTY_NAME:-Linux}" ) ||
		cat /etc/system-release ||
		cat /etc/centos-release ||
		cat /etc/redhat-release ||
		cat /etc/arch-release ||
		cat /etc/gentoo-release ||
		cat /etc/issue ||
		echo UNKNOWN \
	) 2>/dev/null | head -n 1
}

detect_runmode () {
	# We may be migrating from the legacy updater, or installing from
	# scratch. These are `migrate` and `bootstrap` modes respectively.
	# While they can be specified explicitly for testing purposes, in
	# prod this script gets called by either servermill or the legacy
	# updater with arguments we don't control.
	#
	# We can detect which one called us by examining $0, the name it
	# called us by. Servermill looks for this script under the name
	# `cbu-installer`, and pipes that to sh; either name should be
	# interpreted as bootstrap mode. For legacy updates, we are
	# hijacking the legacy updater's `installstartup.sh` file; that name
	# is thus interpreted as migration mode.
	#
	# For testing purposes, either mode may be forced by specifying it
	# as the first argument.
	local runmode=${1:-}
	case $(basename "${0}") in
		installstartup.sh ) runmode=${runmode:-migrate}   ;;
		cbu-installer     ) runmode=${runmode:-bootstrap} ;;
		bash              ) runmode=${runmode:-bootstrap} ;;
		sh                ) runmode=${runmode:-bootstrap} ;;
		*                 ) runmode=${runmode:-bootstrap} ;;
	esac
	echo "${runmode}"
}

main () {
	local runmode
	runmode=$(detect_runmode "$@")
	case $runmode in
		bootstrap ) echo "initiating cbu agent bootstrap" ;;
		migrate   ) echo "migrating cbu legacy updater" ;;
	esac

	if [ "$EUID" -ne 0 ]; then
		local lines=("WARNING: we are not root. This is allowed for"
		             "testing purposes, but you should expect driveclient"
		             "--configure to fail")
		echo "${lines[*]}"
	fi

	type python3 > /dev/null || installdeps python3 ||
		fail "error installing python3 dependency, aborting"
	bootstrap_updater ||
		fail "couldn't bootstrap updater, aborting"
	cbu-updater install driveclient ||
		fail "failed to install driveclient, aborting"

	if [ "$runmode" = migrate ]; then
		# migrations can stop here, they don't need to register
		report_result "succeeded" "migration successful"
		return 0
	fi

	if [ -f ${CBU_DRIVECLIENT_CONF} ]; then
		report_msg="registration skipped (already registered)"
	elif register; then
		report_msg="bootstrap successful"
	elif [[ $? -eq 139 ]]; then
		echo "CBU-1003 segfault bug detected; attempting nscd workaround"
		installdeps nscd || fail "problem installing nscd (returned $?)"
		echo "retrying driveclient configuration"
		register || fail "agent configuration still failed; aborting"
		report_msg="succeeded after nscd workaround"
	else
		fail "failed to install driveclient, aborting"
	fi
	report_result "succeeded" "${report_msg:-(bug, not set)}"
	rm -f ${RETRY_CRONJOB}
}

# Haven't found a way around the global here...
TMP="$(mktemp -d)" || fail "Failed to create temporary directory"
trap 'rm -rvf -- "$TMP"' EXIT
main "$@" 2>&1 | while read -r line; do
	echo -e "$(date +'%Y-%m-%dT%H:%M:%S%:z')\t${line}"
done | tee -a /var/log/cbu-updater-bootstrap.log
