#!/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."

# 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

  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'

  # Create the watchfrr command line.
  for daemon in \
    babeld bfdd bgpd eigrpd fabricd isisd ldpd nhrpd ospfd ospf6d pbrd \
    pimd 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
    eerror "Failed to find or read $daemon pid file"
    return 1
  fi

  # Get PID if any.
  pid=$(cat $pid_file)
  if [ -z $pid ]; then
    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

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

  # 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
}

_check_watchfrr() {
  _get_pid watchfrr || return 1
  return 0
}

#
# 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

  # Handle restarts.
  if [ "$RC_CMD" = 'restart' ]; then
    ebegin 'Reloading FRR configuration'
  else
    ebegin 'Starting FRR'
  fi

  # Start FRR.
  _frr_start

  # New daemons and watchfrr started, apply new configuration.
  if [ "$RC_CMD" = 'restart' ]; then
    "$frr_reload" --reload "$config_file" 2>/run/frr/reload.log
    [ $? -ne 0 ] && ewarn "  Failed to reload (check $frr_reload_log)"
    # NOTE: we can't return bad status otherwise OpenRC will think we
    # failed to start, lets print a helpful message instead.
  fi

  eend 0
}

stop() {
  local failures=0

  # Handle restarts.
  if [ "$RC_CMD" = 'restart' ]; then
    # 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

    return 0
  fi

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

status() {
  _check_watchfrr || return 1
}