#!/sbin/openrc-run
#
# FRR OpenRC init script.
#
# Copyright (C) 2020 Rafael F. Zalamena
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; only version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

description="FRR initialization script."
extra_started_commands="reload save"
description_reload="Stop/start daemons removed/added to config and reload config for running ones"

# FRR variables.
frr_dir="/usr/lib/frr"
frr_state_dir="/run/frr"
config_file="/etc/frr/frr.conf"
daemon_file="/etc/frr/daemons"
daemon_db="/run/frrdb"
vty_config_file="/etc/frr/vtysh.conf"
frr_reload="$frr_dir/frr-reload.py"
frr_reload_log="$frr_state_dir/reload.log"

# Don't change profile here, use $daemon_file. This is the default.
frr_profile="traditional"

# watchfrr variables.
watchfrr_daemons=''
watchfrr_pidfile="$frr_state_dir/watchfrr.pid"

#
# Helpers.
#
_check_daemon_binary() {
	local daemon=$1

	[ -x "$frr_dir/$daemon" ] && return 0

	[ -n "${2}" ] || eerror "No binary found for $daemon in $frr_dir"
	return 1
}

_load_daemon_list() {
	# Load FRR daemons configuration file.
	while read line <&3 ; do
		case $line in
			""|"#"*)
				# Skip empty/commented lines.
				continue
				;;

			*d=*|*_instances=*|*_options=*|*_wrap=*)
				# Load daemon options.
				eval "$line"
				;;

			MAX_FDS=*|frr_profile=*|vtysh_enable=*)
				# Load misc configuration.
				eval "$line"
				;;
		esac
	done 3< $daemon_file

	# `zebra` and `staticd` are mandatory.
	_check_daemon_binary 'zebra' || return 1
	_check_daemon_binary 'staticd' || return 1
	watchfrr_daemons='zebra staticd'

	# Newer versions has mgmtd ... which is if it exists, mandatory.
	if _check_daemon_binary 'mgmtd' optional; then
		watchfrr_daemons="$watchfrr_daemons mgmtd"
	fi

	# Create the watchfrr command line.
	for daemon in \
		babeld bfdd bgpd eigrpd fabricd isisd ldpd nhrpd ospfd ospf6d pbrd \
		pimd pim6d ripd ripngd sharpd vrrpd \
	; do
		# Trick to read variable name with variable.
		cdaemon=$(eval echo \$$daemon)
		cdaemon_instances=$(eval echo \$${daemon}_instances)

		# Add daemon to command line if specified.
		if [ ! -z $cdaemon ] && [ $cdaemon = 'yes' ]; then
			_check_daemon_binary $daemon || return 1

			# Multi instance daemon handling.
			if [ ! -z $cdaemon_instances ]; then
				for instance in $(echo $cdaemon_instances | tr ',' ' '); do
					watchfrr_daemons="$watchfrr_daemons $daemon-$instance"
				done
				continue
			fi

			# Single instance daemon handling.
			watchfrr_daemons="$watchfrr_daemons $daemon"
			continue
		fi
	done
}

_frr_start() {
	# Apply MAX_FDS configuration if set.
	if [ ! -z $MAX_FDS ]; then
		veinfo "  Setting maximum file descriptors to ${MAX_FDS}"
		prlimit -n $MAX_FDS >/dev/null 2>/dev/null
	fi

	# Save started daemons to state database.
	rm -f -- $daemon_db
	for daemon in $watchfrr_daemons; do
		echo $daemon >> $daemon_db
		veinfo "  Starting $daemon..."
	done

	veinfo "  Starting watchfrr..."

	# Start watchfrr which will start all configured daemons.
	eval $all_wrap $frr_dir/watchfrr -d -F $frr_profile $watchfrr_daemons

	veinfo "  Loading configuration..."

	# After starting the daemons, lets load the configuration.
	if [ $vtysh_enable = 'yes' ]; then
		vtysh -b -n
	else
		veinfo "  Configuration loading disabled (vtysh_enable=$vtysh_enable)"
	fi
}

_get_pid() {
	local daemon=$1
	local pid_file="$frr_state_dir/$daemon.pid"

	# Test for file existence.
	if [ ! -r "$pid_file" ]; then
		[ -n "${2}" ] || eerror "Failed to find or read $daemon pid file"
		return 1
	fi

	# Get PID if any.
	pid=$(cat $pid_file)
	if [ -z $pid ]; then
		[ -n "${2}" ] || eerror "$daemon PID file empty"
		return 1
	fi

	return 0
}

_stop_daemon() {
	local daemon=$1
	local pid_file="$frr_state_dir/$daemon.pid"

	# Get daemon pid.
	_get_pid $daemon || return 0

	# Ask daemon to quit.
	kill -2 "$pid" || return 0

	# Test if daemon is still running.
	attempts=1200
	while kill -0 "$pid" 2>/dev/null; do
		sleep 0.5
		[ $((attempts - 1)) -gt 0 ] || break
	done

	# Tell user about our situation.
	if kill -0 "$pid" 2>/dev/null ; then
		eerror "Failed to stop $daemon (PID=${pid})"
		return 1
	else
		rm -f -- $pid_file
	fi
}

_frr_stop() {
	local failures=0

	# Stop watchfrr first so it doesn't restart anyone.
	veinfo "  Stopping watchfrr..."
	_stop_daemon watchfrr || failures=1

	# Read started daemon database.
	while read line <&3 ; do
		case $line in
			""|"#"*)
				# Skip empty/commented lines.
				continue
				;;

			*)
				# Get daemon name.
				veinfo "  Stopping $line..."
				_stop_daemon $line || failures=1
				;;
		esac
	done 3< $daemon_db

	# Remove daemon database file.
	rm -f -- $daemon_db

	return $failures
}

_stop_unconfigured_daemons() {
	# Load daemon list.
	_load_daemon_list

	# We must restart 'watchfrr' in order to start new daemons.
	veinfo "  Stopping watchfrr..."
	_stop_daemon watchfrr

	# Stop daemons that are no longer in configuration file.
	for daemon in $(ls -1 /run/frr/*.pid | cut -d '.' -f 1); do
		# Filter daemon name.
		daemon=$(basename "$daemon")

		# Skip watchfrr.
		[ "$daemon" = 'watchfrr' ] && continue

		echo "$watchfrr_daemons" | grep "$daemon" >/dev/null
		if [ $? -ne 0 ]; then
			veinfo "  Stopping $daemon..."
			_stop_daemon $daemon
		fi
	done
}

_reload_frr_config() {
	"$frr_reload" --reload "$config_file" 2>/run/frr/reload.log
	local ec=$?
	[ $ec -ne 0 ] && ewarn "  Failed to reload (check $frr_reload_log)"
	return $ec
}

#
# Main.
#
depend() {
	# We need root to write logs.
	need localmount
	# Optionally wait for network to start.
	use net
	# Expect /run to be ready.
	after bootmisc
}

start_pre() {
	# Check configuration file readability.
	checkpath -f -m 0640 -o frr:frr $vty_config_file
	checkpath -f -m 0640 -o frr:frr $daemon_file
	checkpath -f -m 0640 -o frr:frr $config_file

	# Check run state directory.
	checkpath -d -o frr $frr_state_dir

	# Load daemon list and peform checks.
	_load_daemon_list
}

start() {
	# Load daemon list.
	_load_daemon_list

	ebegin 'Starting FRR'
	_frr_start
	eend 0
}

stop() {
	local failures=0

	ebegin 'Stopping FRR'
	_frr_stop || failures=1
	eend $failures 'some daemons failed to stop'
}

service_crashed() {
	_get_pid watchfrr silent || return 1
	kill -0 "$pid" >/dev/null 2>&1 || return 0
	return 1
}

reload() {
	ebegin "Updating daemon start/stop state and reloading configuration"
	_stop_unconfigured_daemons
	# NOTE: _s_u_daemons calls _load_daemon_list, so _frr_start *should* work.
	_frr_start
	_reload_frr_config
	eend $?
}

save() {
	ebegin "Saving frr configuration."
	vtysh -c "wr mem"
	eend $?
}
