#! /bin/bash

set -o errexit
set -o pipefail
set -o nounset
set -o xtrace

#CHANGELOG
#0.60	fix selection of UIDs ge 1000
#0.65	replace egrep with perl for shadow users enumeration
#0.66	protect synced passwd and shadow from accidental modifications
#0.67	add additional excludes for DHCP next-server-option and tftp-server-option
#0.68	add tftpboot syncing
#0.70	authentication
#0.71	exclude NetApp .snapshot (tftpboot)
#0.72	locks for individual sections
#0.73	users: reset group and shell; also include uids 500-999
#0.74	Nomad provisioning
#0.75	add rsync I/O timeout
#0.76	process changes in shadow and passwd files independently
#0.77	set UID/GID min/max using script variables according to the Debian Policy Manual
#0.78	exclude Joe's Own Editor backup files (*~) from DHCP synchronization
#0.79	exclude etc/localtime from tftpboot sync - preserves syslog timezone workaround
#0.80	exclude dhcpd.conf (provided by Bcfg2)
#0.81	exclude Vi IMproved editor backup files (*.swp) from DHCP synchronization
#0.82	use bash-specific syntax for command output capture
#0.83	interpolating and non-interpolating quotes
#0.84	removed use of deprecated egrep command
#0.85	use the "service" interface to run the DHCP System V init script
#0.86	auto-restart of DHCP for DST changes
#0.87	update of log facilities
#0.88	update of logged message (number of file changes)
#0.89	added the standard header for auto-abort on error conditions and removed redundant checks
#0.90	consolidated rsync password query function and streamlined to use just awk
#0.91	exclude "CableLabs" and "global" files (provided by Bcfg2)
#0.92	monitor by syscheck restrict/release API
#0.93	whitespace cleanups; new style arithmetic expressions
#0.94	change blocking mode on DHCP lock acquisition to timeout-based
#0.95	conform style to https://kb.clearcable.ca/KB/ProgrammingStyleStandards 
#0.96	reset UID range to Debian standards (Solaris support ended)
#0.97	restart DHCP only once if both time skew and rsync changes are detected
#0.98	accept half-hour interval as sufficient indicator of time skew
#0.99	move more path definitions to the script header
#1.00	split DHCP configuration sync into /etc/dhcp and modems/
#1.01	filter sync of DHCP modem groups by confirming their inclusion in the "group" file
#1.02	override nasty DHCP file ownership/permissions generated by NOMS
#1.03	refine permission filtering rules to avoid removing exeutable bit from /etc/dhcp directory
#1.04	move custom path definitions to /etc/cron.d/file-sync
#1.05	update directory locations for Debian 8+
#1.06   allow exclusion of TFTP sync, needed at Westman due to NFS read-only TFTP mount
#1.07	exclude any *.local files in the DHCP config to support multiple prov pairs
#1.08   refuse to run if file-sync-cluter is configured
#1.09   modifications to enable fwprov operation

FILE_SYNC_CLUSTER_PATH="/usr/local/sbin/file-sync-cluster"
FILE_SYNC_CRON_PATH="/etc/cron.d/file-sync"
if grep -q "${FILE_SYNC_CLUSTER_PATH}" ${FILE_SYNC_CRON_PATH}; then
	echo "This system is configured to use ${FILE_SYNC_CLUSTER_PATH}. Run that instead."
	exit 1
fi

SECRETS='/etc/rsyncd.secrets'
SRC_HOST='noms.localnet'
if [[ -f /usr/local/etc/file-sync.host ]]; then
	SRC_HOST="$( cat /usr/local/etc/file-sync.host )"
fi
DHCP_DIR='/etc/dhcp'
JOB_NAME="file-sync-$$"
if [ -d /run ]; then
	TFTP_DIR='/srv/tftp'
	RUNSTAMP='/run/file-sync.timestamp'
	LOCK_BASE='/run/lock/file-sync'
else
	TFTP_DIR='/var/lib/tftpboot'
	RUNSTAMP='/var/run/file-sync.timestamp'
	LOCK_BASE='/var/lock/file-sync'
fi

LOCK_DHCP="$LOCK_BASE.dhcp"
LOCK_USERS="$LOCK_BASE.passwd"
LOCK_NOMAD="$LOCK_BASE.nomad"

SYNC_TIMEOUT=180
LOCK_TIMEOUT=90
RESTART_RANDOMIZATION_INTERVAL=60
RSYNC="rsync --delete --delete-after --archive --itemize-changes --timeout=$SYNC_TIMEOUT"

# Debian Policy Manual
# Chapter 9 - The Operating System
# 9.2 Users and groups
# 9.2.2 UID and GID classes
# -> 1000-59999: Dynamically allocated user accounts.
UIDMIN=1000
UIDMAX=59999
GIDMIN=$UIDMIN
GIDMAX=$UIDMAX

LOG=$(mktemp)

syscheck restrict $JOB_NAME $(( 2 * $SYNC_TIMEOUT ))

function rsync_password_for {
	USER_QUERY="$1"
	awk --field-separator ':' --assign USER=$USER_QUERY '{if ($1==USER){print $2}}' $SECRETS
}

(
if [[ -d $DHCP_DIR && -d $TFTP_DIR ]]; then
	flock --exclusive --wait $LOCK_TIMEOUT 200	# also used by dhcpd syscheck plugin

	USER='prov'
	PASSWORD_FILE=$(mktemp)
	rsync_password_for $USER > $PASSWORD_FILE

	DHCP_RSYNC_OPTIONS="--password-file=$PASSWORD_FILE --no-owner --no-group"

	# synchronize main configuration without customer provisioning data
	EXCLUDE_FILE=$(mktemp)
	cat > $EXCLUDE_FILE << 'EOF'
dhcpd.conf
CableLabs
global
time-offset
failover
boot
modems/
*.local
*~
*.swp
*.bak
EOF

	$RSYNC $DHCP_RSYNC_OPTIONS --chmod='go-w' \
		--exclude-from=$EXCLUDE_FILE $USER@$SRC_HOST::dhcp/ $DHCP_DIR/ > $LOG
	rm $EXCLUDE_FILE

	# generate a list of modem groups that are actually referenced
	# (NOMS may generate unused groups in some cases)
	INCLUDE_FILE=$(mktemp)
	awk '
{
	if (!section) {
		if ($0~/^[[:space:]]*group[[:space:]]*{[[:space:]]*$/)
			section="group"
	} else if (section="group") {
		if (match($0, /^[[:space:]]*include[[:space:]]["][/]etc[/]dhcp[/]modems[/](.*)["][[:space:]]*;[[:space:]]*$/, filename))
				print filename[1]
		else if ($0~/^[[:space:]]*}[[:space:]]*$/)
			section=""
	}
}' $DHCP_DIR/group > $INCLUDE_FILE

	$RSYNC $DHCP_RSYNC_OPTIONS --chmod=Fugo=r --delete-excluded \
		--include-from=$INCLUDE_FILE --exclude='*' \
		$USER@$SRC_HOST::dhcp/modems/ $DHCP_DIR/modems/ >> $LOG
	rm $INCLUDE_FILE

	if [[ -w $TFTP_DIR ]]; then
		$RSYNC $DHCP_RSYNC_OPTIONS --exclude='etc' --exclude='device-firmware' --exclude='termsys-firmware' --no-itemize-changes \
			$USER@$SRC_HOST::tftpboot/ $TFTP_DIR/
	fi

	rm $PASSWORD_FILE

	function dhcp_restart {
		RESTART_REASON="$1"
		logger -p lpr.notice -t $(basename $0) "$RESTART_REASON"
		sleep $(( $RANDOM%$RESTART_RANDOMIZATION_INTERVAL ))
		service isc-dhcp-server restart >/dev/null 200>/dev/null
	}

	# detect daylight saving time changes
	# and automatically restart DHCP to force time-offset update
	DATE_CUR=$(date --utc +%s)
	TIMEDIFF_MAX=1800
	if [[ -s $RUNSTAMP ]]; then
		# subsequent run (since reboot)
		DATE_LAST=$(<$RUNSTAMP)
		TIMEDIFF=$(( $DATE_CUR - $DATE_LAST ))
	else
		TIMEDIFF=0
	fi
	echo $DATE_CUR > $RUNSTAMP

	if [[ -x /etc/init.d/isc-dhcp-server ]]; then
		if [[ -s $LOG ]]; then
			CHANGE_COUNT=$(wc --lines < $LOG)
			dhcp_restart "$CHANGE_COUNT change(s) in $DHCP_DIR directory"
		elif [[ $TIMEDIFF -gt $TIMEDIFF_MAX || $TIMEDIFF -lt 0 ]]; then
			dhcp_restart "time skew of $TIMEDIFF seconds"
		fi
	fi
fi
) 200>$LOCK_DHCP

PASSWD='/etc/passwd'
SHADOW='/etc/shadow'
PASSWD_CORE="$PASSWD.core"
SHADOW_CORE="$SHADOW.core"
PASSWD_SYNC="$PASSWD.sync"
SHADOW_SYNC="$SHADOW.sync"
(
if [[ -s $PASSWD_CORE && -s $SHADOW_CORE ]]; then
	flock --exclusive --nonblock 200

	USER='mail'
	PASSWORD_FILE=$(mktemp)
	rsync_password_for $USER > $PASSWORD_FILE

	$RSYNC --password-file=$PASSWORD_FILE $USER@$SRC_HOST::etc/passwd $PASSWD_SYNC  > $LOG
	$RSYNC --password-file=$PASSWORD_FILE $USER@$SRC_HOST::etc/shadow $SHADOW_SYNC >> $LOG
	rm $PASSWORD_FILE

	if [[ -s $LOG ]]; then
	PASSWD_PLUS=$(mktemp)
	PASSWD_PREP=$(mktemp)
	SHADOW_PREP=$(mktemp)

	cp --preserve $PASSWD $PASSWD_PREP
	cp --preserve $SHADOW $SHADOW_PREP

	cat $PASSWD_CORE > $PASSWD_PREP
	cat $SHADOW_CORE > $SHADOW_PREP

	awk -F: -v UIDMIN=$UIDMIN -v UIDMAX=$UIDMAX -v GIDMIN=$GIDMIN -v GIDMAX=$GIDMAX '
		{
		uid = $3
		gid = $4
		if ( gid < GIDMIN )
			gid = GIDMIN
		if ( uid>=UIDMIN && uid<UIDMAX && gid>=GIDMIN && gid<GIDMAX )
			print $1 ":x:" uid ":" gid ":" $5 ":" $6 ":" "/bin/false"
		}' $PASSWD_SYNC > $PASSWD_PLUS
	cat $PASSWD_PLUS >> $PASSWD_PREP

	cat $PASSWD_PLUS $SHADOW_SYNC |
	perl -e'
		while (<>) {
			chomp;
			@field = split(":",$_);
			if (@field[1] eq "x") {
				$users{@field[0]}=@field[2];
			} elsif (defined $users{$field[0]}) {
				print $_,"\n";
			}
		}' >> $SHADOW_PREP

	if cmp --silent $SHADOW $SHADOW_PREP; then
		rm $SHADOW_PREP
	else
		chattr -i $SHADOW
		mv $SHADOW_PREP $SHADOW
		chattr +i $SHADOW
	fi

	if cmp --silent $PASSWD $PASSWD_PREP; then
		rm $PASSWD_PREP
	else
		chattr -i $PASSWD
		mv $PASSWD_PREP $PASSWD
		chattr +i $PASSWD
	fi

	rm $PASSWD_PLUS
	fi
fi
) 200>$LOCK_USERS

(
if [[ -d '/etc/asterisk' ]]; then
	flock --exclusive --nonblock 200

	USER='nomad'
	PASSWORD_FILE=$(mktemp)
	rsync_password_for $USER > $PASSWORD_FILE

	$RSYNC --password-file=$PASSWORD_FILE $USER@$SRC_HOST::nomad/{sip.conf} /etc/asterisk/ > $LOG
	rm $PASSWORD_FILE

	if [[ -s $LOG ]]; then
		logger -p local0.notice -t $0 $(cat $LOG|tr '\n' ' ')
		sleep $(( $RANDOM%$RESTART_RANDOMIZATION_INTERVAL ))
		asterisk -rx 'sip reload'
	fi
fi
) 200>$LOCK_NOMAD

rm $LOG
syscheck release $JOB_NAME
# vim: cindent:shiftwidth=4:tabstop=4:smarttab:textwidth=100
