#!/bin/bash
NAME='perunv3'
SCRIPTS_DIR=`dirname $0`
LIB_DIR='/opt/perun/lib/'
LIB_SYNC="$LIB_DIR/base/sync.pl"
CACHE_DIR='/var/lib/perun/'
CUSTOM_SCRIPTS_DIR="/etc/perun/"
OLD_CUSTOM_SCRIPTS_DIR="/opt/perun/bin/"
LOCK_DIR=${LOCK_DIR:=/var/lock}
SERVICE_BLACKLIST=()	# syntax: (item1 item2 item3)
SERVICE_WHITELIST=()

# accept configuration only if it was send to one of these hostnames
# prevent someone to configure perun to send malicious configuration via dns alias or ip address
DNS_ALIAS_WHITELIST=( `hostname -f` )
FACILITY_WHITELIST=()        # from which facilities this host accept configuration

# check if lock dir is writable
if ! [ -w $LOCK_DIR ] ; then
	# If not - redirect lock to /tmp as most compatible non-persistent place for locks.
	LOCK_DIR="/tmp";
fi

if [ -f "/etc/${NAME}.conf" ]; then
	. "/etc/${NAME}.conf"
fi

# read custom configurations
if [ -d "/etc/${NAME}.d/" ]; then
	for F in /etc/${NAME}.d/* ;
	do
	    [[ -e "$F" ]] || break # skip if no files present
	    . "$F" ;
	done
fi

# Temporarily set umask to 077 in order to have all temp configuration files private
umask 077

### Status codes
I_STARTED=(0 'Service ${SERVICE} processing started')
I_FINISHED=(0 'Service ${SERVICE} processing done')
I_PROTOCOL_MINOR_DIFF=(0 'Difference in protocol minor version')
I_SERVICE_DISABLED=(0 'Service ${SERVICE} is disabled')
I_FILE_RECOVERED=(0 'File ${BACKUP_DIR}/${LOCAL} was recovered successfully')
I_FILE_CANT_RECOVER=(0 'File ${BACKUP_DIR}/${LOCAL} cannot be recovered because backup not exists, was skipped')
I_BACKUP_COMPLETE=(0 'Backup files and lock file created in directory ${BACKUP_DIR}')

E_WORK_DIR=(1 'Problem with working directory')
E_TAR_FILES=(2 'Problem with extracting received files')
E_LOCK_FILE=(3 'Lock file already exists')
E_DIFF_UPDATE=(4 'Diff between old and new file failed')
E_IO=(5 'IO operation failed')
E_CONCURRENT_PROCESS=(6 'Concurrent process is running right now')
E_LOCK_DELETE=(7 'Lock file cannot be deleted')
E_LOCK_PIDFILE=(8 'Lock pid file cannot be created')
E_DNS_ALIAS_NOT_WHITELISTED=(9 'Perun send configuration to this host via dns alias which was not whitelisted (Facility=${FACILITY},DNS_Alias=${SEND_TO_HOSTNAME})')
E_BACKUP_DIR=(10 'Problem with backup directory')
E_REMOVE_SERVICE_LOCK_FILE=(11 'Cannot remove service lock file and backup files while recovering process')
E_FILE_CANT_RECOVER=(12 'File ${BACKUP_DIR}/${LOCAL} cannot be recovered. Moving file problems')
E_CREATE_BACKUP_FILE=(13 'Cannot copy file ${FILE} to backup directory ${BACKUP_DIR}')
E_CREATE_LOCK_FILE=(14 'Cannot create lock file in backup directory ${BACKUP_DIR}')
E_LOCK_DIR_NOT_WRITABLE=(15 'Lock dir ${LOCK_DIR} is not writable')
E_PROTOCOL_VERSION=(200 'Wrong version of received files - (local=${PROTOCOL_VERSION},remote=${RECEIVED_PROTOCOL_VERSION})')
E_PROTOCOL_VERSION_FILE=(201 'Remote protocol version file missing')
E_PROTOCOL_VERSION_VARIABLE=(202 'PROTOCOL_VERSION variable not set')
E_UNSUPPORTED_SERVICE=(203 'Unsupported service')
E_MOVE_ERROR=(205 'Could not move ${SRC} to ${DST}')
E_STATE_FILE=(206 'State file parameter (for diff_update function) is missing')
E_FROM_PERUN_FILE=(208 'FROM_PERUN file (to diff_update) does not exists or it is not readable')
E_DESTINATION_FILE=(209 'Destination file (to diff_update) does not exists or do not have right permissions')
E_PERMISSIONS=(210 'Cannot set permissions')
E_CHANGE_OWNER=(211 'Cannot set owner')

### declare list of all items in trap on exit
declare -a on_exit_items

### Functions

### for purpose of evaluating all on_exit items
function on_exit()
{
	for i in "${on_exit_items[@]}"
	do
		eval $i
	done
}

### add new on_exit item
function add_on_exit()
{
	local n=${#on_exit_items[*]}
	on_exit_items[$n]="$*"
	if [ $n -eq 0 ]; then
		trap on_exit EXIT
		trap 'on_exit; exit 129' HUP
		trap 'on_exit; exit 130' INT
		trap 'on_exit; exit 131' QUIT
		trap 'on_exit; exit 143' TERM
	fi
}

function log_msg {
	CODE=`eval echo '${'$1'[0]}'`
	TEXT=`eval echo '${'$1'[1]}" ("$2")"'`
	TEXT=`eval echo \"${TEXT}\"`	# expand variables in message
	CODE=${CODE:=255}
	TEXT=${TEXT:=Unknown error $1}
	TIME=`date "+%H:%M:%S"`

	if [ "${CODE}" -eq 0 ]; then
		MSG="Info: ${TEXT}"
		echo "${TIME} ${MSG}"
		logger -t "${NAME}" -p daemon.info "${SERVICE}: ${MSG}" &>/dev/null
	else
		MSG="Error $1 (code=${CODE}): ${TEXT}"
		echo "${TIME} ${MSG}" >&2
		logger -t "${NAME}" -p daemon.error "${SERVICE}: ${MSG}" &>/dev/null
		exit "${CODE}"
	fi
}

function log_msg_without_exit {
	CODE=`eval echo '${'$1'[0]}'`
	TEXT=`eval echo '${'$1'[1]}" ("$2")"'`
	TEXT=`eval echo \"${TEXT}\"`  # expand variables in message
	CODE=${CODE:=255}
	TEXT=${TEXT:=Unknown error $1}
	TIME=`date "+%H:%M:%S"`

	if [ "${CODE}" -eq 0 ]; then
		MSG="Info: ${TEXT}"
		echo "${TIME} ${MSG}"
		logger -t "${NAME}" -p daemon.info "${SERVICE}: ${MSG}" &>/dev/null
	else
		MSG="Error $1 (code=${CODE}): ${TEXT}"
		echo "${TIME} ${MSG}" >&2
		logger -t "${NAME}" -p daemon.error "${SERVICE}: ${MSG}" &>/dev/null
	fi
}

### Log message to stderr and exit with 0 return code
function log_warn_to_err_exit {
	TEXT="$1"
	TIME=`date "+%H:%M:%S"`

	MSG="Info: ${TEXT}"
	echo "${TIME} ${MSG}" >&2
	logger -t "${NAME}" -p daemon.error "${SERVICE}: ${MSG}" &>/dev/null
	exit 0
}

### Log message to stderr, but do not exit
function log_warn_to_err {
	TEXT="$1"
	TIME=`date "+%H:%M:%S"`

	MSG="Info: ${TEXT}"
	echo "${TIME} ${MSG}" >&2
	logger -t "${NAME}" -p daemon.error "${SERVICE}: ${MSG}" &>/dev/null
}

### Log debug only if variable DEBUG exists and is the length of this variable is nonzero
function log_debug {
	MSG="$1"
	if [ -n "${DEBUG}" ]; then
		logger -t "${NAME}" -p daemon.debug "${SERVICE}: ${MSG}" &>/dev/null
	fi
}

function catch_error {
	ERROR_NAME="$1"
	shift

	exec 3>&1
	ERR_TXT=`"$@" 2>&1 1>&3`
	ERR=$?
	exec 3>&-
	if [ "$ERR_TXT" ]; then echo "$ERR_TXT" >&2; fi
	if [ $ERR -ne 0 ]; then log_msg ${ERROR_NAME} "${ERR_TXT}"; fi
}

function create_lock {

	catch_error E_LOCK_DIR_NOT_WRITABLE test -w $LOCK_DIR

	if mkdir "${LOCK_FILE}"; then
		add_on_exit "rm -rf ${LOCK_FILE}"
		catch_error E_LOCK_PIDFILE echo $$ > "$LOCK_PIDFILE"
	else
		# lock file exists, check for existence of concurrent process
		if pidof perun | grep "\(^\| \)`cat $LOCK_PIDFILE`\( \|$\)"; then
			# concurrent process is running - this skript must terminate
			log_msg E_CONCURRENT_PROCESS
		else
			# lock is not valid; it should be deleted
			catch_error E_LOCK_DELETE rm -r "$LOCK_FILE"
			echo "Invalid lock file found and deleted: $LOCK_FILE" >&2
			catch_error E_LOCK_FILE mkdir "${LOCK_FILE}"
			add_on_exit "rm -rf ${LOCK_FILE}"
			catch_error E_LOCK_PIDFILE echo $$ > "$LOCK_PIDFILE"
		fi
	fi
}

function version_check {
	SERVICE_VERSION_FILE="${WORK_DIR}/VERSION"
	[ -n "${PROTOCOL_VERSION}" ] || log_msg E_PROTOCOL_VERSION_VARIABLE
	[ -r "$SERVICE_VERSION_FILE" ] || log_msg E_PROTOCOL_VERSION_FILE
	RECEIVED_PROTOCOL_VERSION=`head -n 1 "$SERVICE_VERSION_FILE"`
	[ "${RECEIVED_PROTOCOL_VERSION%.*}" = "${PROTOCOL_VERSION%.*}" ] || log_msg E_PROTOCOL_VERSION
	[ ${RECEIVED_PROTOCOL_VERSION} = ${PROTOCOL_VERSION} ] || log_msg I_PROTOCOL_MINOR_DIFF
}

function sync_files {
	SRC=$@

	for i in $SRC
	do
		perl $LIB_SYNC $i || sync
	done
}

function mv_sync {
	SRC="$1"
	DST="$2"

	sync_files "$SRC"
	catch_error E_MOVE_ERROR mv -f "$SRC" "$DST"
	sync_files "$DST"
}

function diff_mv_sync {
	SRC="$1"
	DST="$2"

	sync_files "$SRC"
	diff_mv "$SRC" "$DST"
	RET=$?;
	sync_files "$DST"

	return $RET;
}

function diff_mv {
	SRC="$1"
	DST="$2"

	diff -q "${SRC}" "${DST}" &>/dev/null || {
		# Read permissions of the destination file
		if [ -f "${DST}" ]; then
			DST_PERM=`stat -L -c %a "${DST}"`
			# Set the original permissions on the source file
			catch_error E_PERMISSIONS chmod $DST_PERM "${SRC}"
			# Set also original owners on the source file
			DST_USER=`ls -ld "${DST}" | awk '{ print $3; }'`
			DST_GROUP=`ls -ld "${DST}" | awk '{ print $4; }'`
			catch_error E_CHANGE_OWNER chown ${DST_USER}:${DST_GROUP} "${SRC}"
		fi
		catch_error E_MOVE_ERROR mv -f "${SRC}" "${DST}"

		# If SElinux is present and set to enforcing then restore contexts
		which sestatus > /dev/null 2>&1  && if [ `sestatus | grep "SELinux status" | grep -c enabled` -eq 1 -a `sestatus | grep "Current mode" | grep -c enforcing` -eq 1 ]; then
			restorecon "${DST}"
		fi

		return 0
	}

	return 1
}

function mv_chmod {
	SRC="$1"
	DST="$2"

	# Read permissions of the destination file
	if [ -f "${DST}" ]; then
		DST_PERM=`stat -L -c %a "${DST}"`
		# Set the original permissions on the source file
		catch_error E_PERMISSIONS chmod $DST_PERM "${SRC}"
	fi

	diff_mv_sync "${SRC}" "${DST}"

	return $?
}

function in_array {
	ITEM=$1
	shift

	for ELEMENT in "$@"; do
		[ "x${ITEM}" == "x${ELEMENT}" ] && return 0
	done

	return 1
}

# This function is used for updating files where Perun doesn't have absolute control of content of the file.
# It can only add lines which are not already present. Also can delete only lines which this function added in previsous runs.
#
# This function removes from DESTINATION_FILE lines which it added last time (these lines are stored in STATE_FILE),
# then it add there new lines from FROM_PERUN_FILE unless where are already present.
#
# usage:  diff_update FROM_PERUN_FILE DESTINATION_FILE STATE_FILE
# Params: FROM_PERUN_FILE - file from perun
#         DESTINATION_FILE - file into which FROM_PERUN_FILE will be merged
#         STATE_FILE - file where the state from previous run is hold. This file do not need to exists - it will be ctreated in such a case.
function diff_update {
	local FROM_PERUN="$1"
	local DESTINATION="$2"
	local STATE="$3"

	TMP_DESTINATION_FILE="${WORK_DIR}/diff_update-destination"

	[ -r "$FROM_PERUN" ] || log_msg E_FROM_PERUN_FILE
	[ -r "$DESTINATION" -a -w "$DESTINATION" ] || log_msg E_DESTINATION_FILE
	[ "$STATE" ] || log_msg E_STATE_FILE

	if [ -f "$STATE" ]; then
		DESTINATION_WITHOUT_STATE="$WORK_DIR/diff_update-destination_without_state"
		#filter out lines that are already present in state file
		grep -F -x -v -f "$STATE" "$DESTINATION" > "$DESTINATION_WITHOUT_STATE"
	else
		DESTINATION_WITHOUT_STATE="$DESTINATION"
	fi

	#filter out lines that are already present in destination file
	grep -F -x -v -f "$DESTINATION_WITHOUT_STATE" "$FROM_PERUN" > "$STATE"

	catch_error E_IO cat "$DESTINATION_WITHOUT_STATE" "$STATE" > "$TMP_DESTINATION_FILE"
	diff_mv_sync "$TMP_DESTINATION_FILE" "$DESTINATION"
}

# If lock file exits, recover all existing files
#
# Recover only those files, which are represented in new
# list of files to backup (need to know path to recovering files)
#
# If lock not exists, remove everything from backup directory and
# then backup new files (copy them to backup directory)
#
# Last step is to create lock file
#
# usage: backup_and_recover_files FILE FILE FILE ...
# Params: FILE - absolute path to file (more files can be specified)
function backup_and_recover_files {
	FILES="$@"
	SERVICE_LOCK_FILE="${BACKUP_DIR}/lock"

	#If backup dir for service not exists, create it
	if [ ! -d "${BACKUP_DIR}" ]; then
		catch_error E_BACKUP_DIR mkdir -p "${BACKUP_DIR}"
	fi

	#Service Lock file exists, first do recovering process
	if [ -f "${SERVICE_LOCK_FILE}" ]; then

		#recover existing backup files
		for FILE in FILES; do
			LOCAL=`echo $FILE | sed -e 's/^.*\///'`
			if [ -f "${BACKUP_DIR}/${LOCAL}" ]; then
				catch_error E_FILE_CANT_RECOVER  diff_mv_sync "${BACKUP_DIR}/${LOCAL}" "${FILE}"
				log_msg I_FILE_RECOVERED
			else
				#if not, skip it and info about it
				log_msg I_FILE_CANT_RECOVER
			fi
		done
	fi

	#remove all backup files and also lock file (and artefacts)
	catch_error E_REMOVE_SERVICE_LOCK_FILE rm -r -f "${BACKUP_DIR}"
	#create new empty backup directory
	catch_error E_BACKUP_DIR mkdir -p "${BACKUP_DIR}"

	#Create backup files
	for FILE in FILES; do
		catch_error E_CREATE_BACKUP_FILE cp -p "${FILE}" "${BACKUP_DIR}"
	done

	#Create lock file
	catch_error E_CREATE_LOCK_FILE touch SERVICE_LOCK_FILE

	log_msg I_BACKUP_COMPLETE
}

function remove_backup_files {
	 if [ -d "${BACKUP_DIR}" ]; then
		catch_error E_REMOVE_SERVICE_LOCK_FILE rm -r -f "${BACKUP_DIR}"
	 fi
}

function run_pre_hooks {
	for F in `ls "${CUSTOM_SCRIPTS_DIR}/${SERVICE}.d"/pre_* "${OLD_CUSTOM_SCRIPTS_DIR}/${SERVICE}.d"/pre_* 2>/dev/null | perl -e ' @_ = <>;  $, = " "; $\ = "\n"; print sort { $a =~ m#^.*/(pre|post|mid)_([^/]*)# ; $a1 = $2 ; $b =~ m#^.*/(pre|post|mid)_([^/]*)# ; $b1 = $2 ; $a1 cmp $b1 } grep { m|^(.*)/(.*)$| ; $1 =~ m|^/etc/perun/| || ! ( grep { m|^/etc/perun/.*$2$| } @_ ) } @_;'` ;do . $F ; done
}

function run_mid_hooks {
	for F in `ls "${CUSTOM_SCRIPTS_DIR}/${SERVICE}.d"/mid_* "${OLD_CUSTOM_SCRIPTS_DIR}/${SERVICE}.d"/mid_* 2>/dev/null | perl -e ' @_ = <>;  $, = " "; $\ = "\n"; print sort { $a =~ m#^.*/(pre|post|mid)_([^/]*)# ; $a1 = $2 ; $b =~ m#^.*/(pre|post|mid)_([^/]*)# ; $b1 = $2 ; $a1 cmp $b1 } grep { m|^(.*)/(.*)$| ; $1 =~ m|^/etc/perun/| || ! ( grep { m|^/etc/perun/.*$2$| } @_ ) } @_;'` ;do . $F ; done
}

function run_post_hooks {
	for F in `ls "${CUSTOM_SCRIPTS_DIR}/${SERVICE}.d"/post_* "${OLD_CUSTOM_SCRIPTS_DIR}/${SERVICE}.d"/post_* 2>/dev/null | perl -e ' @_ = <>;  $, = " "; $\ = "\n"; print sort { $a =~ m#^.*/(pre|post|mid)_([^/]*)# ; $a1 = $2 ; $b =~ m#^.*/(pre|post|mid)_([^/]*)# ; $b1 = $2 ; $a1 cmp $b1 } grep { m|^(.*)/(.*)$| ; $1 =~ m|^/etc/perun/| || ! ( grep { m|^/etc/perun/.*$2$| } @_ ) } @_;'` ;do . $F ; done
}


#################################################

WORK_DIR=`mktemp -d ${TMPDIR:-/tmp}/${NAME}.XXXXXXXXXX`
[ $? -ne 0 ] && log_msg E_WORK_DIR
add_on_exit "rm -rf ${WORK_DIR}"

### Receive and process data
catch_error E_TAR_FILES tar --no-same-owner --no-same-permissions --warning=no-timestamp -x -C "${WORK_DIR}" <&0
SERVICE=`head -n 1 "${WORK_DIR}/SERVICE"`
SCRIPT_NAME="$SERVICE"
if [[ -f "${WORK_DIR}/GEN_LOOKUP" ]] && grep -Fxq "$SERVICE" "${WORK_DIR}/GEN_LOOKUP"; then
	ORIGINAL_SERVICE="$SERVICE"
	SCRIPT_NAME="generic_json_gen"
fi
SEND_TO_HOSTNAME=`head -n 1 "${WORK_DIR}/HOSTNAME"`
FACILITY=`head -n 1 "${WORK_DIR}/FACILITY"`
LOCK_FILE="${LOCK_DIR}/${NAME}-${SERVICE}.lock"
LOCK_PIDFILE="$LOCK_FILE/pid"
BACKUP_DIR="/var/tmp/perun/${SERVICE}/backup-state/"

log_msg I_STARTED

# export needed variables
export PERUN_SERVICE=${SERVICE}
export PERUN_LIB_DIR=${LIB_DIR}
export PERUN_CUSTOM_SCRIPTS_DIR=${CUSTOM_SCRIPTS_DIR}

# write to stderr if old path for scripts is still used
WARN_USING_OLD_PATH_FOR_SCRIPTS="Warning: Old configuration dir ${OLD_CUSTOM_SCRIPTS_DIR}/${SERVICE}.d/ is still used."
ls "${OLD_CUSTOM_SCRIPTS_DIR}/${SERVICE}.d/" 2>/dev/null | grep '^pre_\|^post_\|^mid_' 1>/dev/null && echo "${WARN_USING_OLD_PATH_FOR_SCRIPTS}" 1>&2

DNS_ALIAS_OK=0
# check if perun send via allowed hostname
if [ "${#DNS_ALIAS_WHITELIST[@]}" -gt 0  ]; then
	if in_array "${SEND_TO_HOSTNAME}" "${DNS_ALIAS_WHITELIST[@]}"; then
		DNS_ALIAS_OK=1
	fi
fi

if [ "${#FACILITY_WHITELIST[@]}" -gt 0  ]; then
	if in_array "${FACILITY}" "${FACILITY_WHITELIST[@]}"; then
		DNS_ALIAS_OK=1
	fi
fi

[ "$DNS_ALIAS_OK" -ne 1 ] && log_msg E_DNS_ALIAS_NOT_WHITELISTED



# check if the service is not disabled
if [ "${#SERVICE_WHITELIST[@]}" -gt 0  ]; then
	if in_array "${SERVICE}" "${SERVICE_WHITELIST[@]}"; then
		true
	else
		log_msg I_SERVICE_DISABLED
		exit 0;
	fi
fi

if [ "${#SERVICE_BLACKLIST[@]}" -gt 0  ]; then
	if in_array "${SERVICE}" "${SERVICE_BLACKLIST[@]}"; then
		log_msg I_SERVICE_DISABLED
		exit 0;
	fi
fi

SERVICE_PROCESS_FILE="${SCRIPTS_DIR}/process-${SCRIPT_NAME}.sh";
catch_error E_UNSUPPORTED_SERVICE [ -r "$SERVICE_PROCESS_FILE" ]

. "$SERVICE_PROCESS_FILE"

version_check        #check the received version with version from slave script

run_pre_hooks
process              #execute slave skript (e.g. runs function process in process-passwd.sh script)
run_post_hooks

remove_backup_files

log_msg I_FINISHED
