#!/bin/sh

# Copyright (C) 2014-2025 Internet Systems Consortium, Inc. ("ISC")
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# This is keactrl script responsible for starting up Kea processes.
# This script is used to run Kea from installation directory,
# as well as for running tests.

# shellcheck disable=SC2034
# SC2034: ... appears unused. Verify use (or export if used externally).

# shellcheck disable=SC2154
# SC2154: ... is referenced but not assigned.
# Reason: some variables are taken from keactrl.conf
#
# shellcheck disable=SC2317
# SC2317: Command appears to be unreachable. Check usage (or ignore if invoked indirectly).
# Reason: shellcheck is deceived by the 'if test "${HAVE_NETCONF}" = 'yes'' condition which it
# evaluates to always false and thinks the rest of the script is never executed.

# Exit with error if commands exit with non-zero and if undefined variables are
# used.
set -eu

# Set no group write and no other access umask
orig_umask=$(umask)
umask $((orig_umask | 0027))

HAVE_NETCONF='@HAVE_NETCONF@'
EXTENDED_VERSION="@EXTENDED_VERSION@"
PACKAGE_VERSION="@PACKAGE_VERSION@"

# Set the have_netconf flag to know if netconf is available.
if test "${HAVE_NETCONF}" = 'yes'; then
    have_netconf=true
else
    have_netconf=false
fi

### Logging functions ###

# Logs message at the error level.
log_error() {
    printf "ERROR/keactrl: %s\n" "${1}"
}

# Logs message at the warning level.
log_warning() {
    printf "WARNING/keactrl: %s\n" "${1}"
}

# Logs message at the info level.
log_info() {
    printf "INFO/keactrl: %s\n" "${1}"
}

### Convenience functions ###

# Checks if the value is in the list. An example usage of this function
# is to determine whether the keactrl command belongs to the list of
# supported commands.
is_in_list() {
    local member="${1-}"  # Value to be checked
    local list="${2-}"    # Comma separated list of items
    _inlist=0             # Return value: 0 if not in list, 1 otherwise.
    if [ -z "${member}" ]; then
        log_error "missing ${member}"
    fi
    # Iterate over all items on the list and compare with the member.
    # If they match, return, otherwise log error and exit.
    for item in ${list}
    do
        if [ "${item}" = "${member}" ]; then
            _inlist=1
            return
        fi
    done
}

# Prints keactrl usage.
usage() {
    printf "usage is %s command [-c keactrl-config-file] [-s server[,server,..]]\n" \
           "$(basename -- "${0}")"
    printf "commands: start stop reload status version\n"
}

### Functions managing Kea processes ###
# Constructs a server's PID file based on its binary name, the config file,
# and the --localstatedir and returns the contents as $_pid.   If the file
# does not exist, the value of $_pid is 0.  If the file exists but cannot
# be read the function exists with a error message. Note the PID file name
# is always returned in $_pid_file.

# There are some variables set in /etc/kea/keactrl.conf that's included here.
# Since we run shellcheck against keactrl.in rather than an installed file,
# we get false warnings about the variable being referenced but not assigned.
get_pid_from_file() {
    local proc_name="${1}"  # Process name.

    local kea_config_file=
    case ${proc_name} in
    kea-dhcp4)
        kea_config_file=${kea_dhcp4_config_file}
        ;;
    kea-dhcp6)
        kea_config_file=${kea_dhcp6_config_file}
        ;;
    kea-dhcp-ddns)
        kea_config_file=${kea_dhcp_ddns_config_file}
        ;;
    kea-ctrl-agent)
        kea_config_file=${kea_ctrl_agent_config_file}
        ;;
    kea-netconf)
        kea_config_file=${kea_netconf_config_file}
        ;;
    esac

    # Extract the name portion (from last slash to last dot) of the config file name.
    local conf_name
    conf_name=${kea_config_file##*/}
    conf_name=${conf_name%.*}

    # Default the directory to --localstatedir / run
    local pid_file_dir
    pid_file_dir="@runstatedir@/@PACKAGE@"

    # Use directory override if set (primarily for testing only)
    if test -n "${KEA_PIDFILE_DIR+x}"; then
        pid_file_dir=${KEA_PIDFILE_DIR}
    fi

    # construct the PID file name
    _pid_file="${pid_file_dir}/${conf_name}.${proc_name}.pid"

    # Grab the PID if the file exists
    _pid=$(cat "${_pid_file}" 2> /dev/null || true)
    if test -z "${_pid}"; then
        # No file, means no pid
        _pid=0;
    fi
}


# Checks if the specified process is running by reading its
# PID file and checking the PID it contains.  If the file does
# not exist, the process is assumed to not be running.
check_running() {
    local proc_name="${1}"  # Process name.
    # Initially mark the process as not running.
    _running=0

    # Get the PID from the PID file (if it exists)
    get_pid_from_file "${proc_name}"
    if [ "${_pid}" -gt 0 ]; then
        # Use ps to check if PID is alive
        if ps -p "${_pid}" 1>/dev/null; then
            # No error, so PID IS ALIVE
            _running=1
        fi
    fi
}

# Sends a signal to a process based on its PID file
send_signal() {
    local sig="${1}"        # Signal number
    local proc_name="${2}"  # Process name.

    get_pid_from_file "${proc_name}"
    if [ "${_pid}" -eq 0 ]; then
        log_info "Skip sending signal ${sig} to process ${proc_name}: \
process is not running"
    else
        if ! kill "-${sig}" "${_pid}"; then
            log_error "Failed to send signal ${sig} to process ${proc_name}, PID {$_pid}."
        fi
    fi
}

# Start the Kea process. Do not start the process if there is an instance
# already running.
start_server() {
    binary_path=${1}    # Full path to the binary.
    # Extract the name of the binary from the path.
    local binary_name
    binary_name=$(basename -- "${binary_path}")
    # Use the binary name to check if the process is already running.
    check_running "${binary_name}"
    # If process is running, don't start another one. Just log a message.
    if [ "${_running}" -ne 0 ]; then
        log_info "${binary_name} appears to be running, see: \
PID ${_pid}, PID file: ${_pid_file}."
    else
        log_info "Starting ${*}"
        # Start the process.
        "${@}" &
    fi
}

# Instruct Kea process to shutdown by sending it signal 15
stop_server() {
    binary_path=${1}   # Full path to the binary.
    local sig=15
    # Extract the name of the binary from the path.
    local binary_name
    binary_name=$(basename -- "${binary_path}")

    # Use the binary name to check if the process is already running.
    check_running "${binary_name}"
    # If process isn't running, don't start another one. Just log a message.
    if [ "${_running}" -eq 0 ]; then
        log_info "${binary_name} isn't running."
    else
        log_info "Stopping ${binary_name}..."
        if ! kill "-${sig}" "${_pid}"; then
            log_error "Stop failed, could not send signal ${sig} \
to process ${proc_name}, PID ${_pid}."
        fi
    fi
}

# Instruct Kea process to reload config by sending it signal 1
reload_server() {
    binary_path=${1}   # Full path to the binary.
    local sig=1
    # Extract the name of the binary from the path.
    local binary_name
    binary_name=$(basename -- "${binary_path}")

    # Use the binary name to check if the process is already running.
    check_running "${binary_name}"
    # If process isn't running, don't start another one. Just log a message.
    if [ "${_running}" -eq 0 ]; then
        log_info "${binary_name} isn't running."
    else
        log_info "Reloading ${binary_name}..."
        if ! kill "-${sig}" "${_pid}"; then
            log_error "Reload failed, could not send signal ${sig} \
to process ${proc_name}, PID ${_pid}."
        fi
    fi
}

# Print Kea daemon version
print_version() {
    name=${1}
    binary_path=${2}

    if [ -e "${binary_path}" ]; then
        if ! ver=$(${binary_path} -v); then
            log_error "Error checking version of binary file: ${binary_path}"
        fi
    else
        # No file, means no pid
        ver="unknown, ${binary_path} missing";
    fi

    echo "${name}: ${ver}"
}

### Functions testing the existence of the Kea config file

# Check if the Kea configuration file location has been specified in the
# keactrl configuration file. If not, it is a warning or a fatal error.
check_kea_conf() {
    local conf_file="${1-}"     # Kea config file name.
    if [ -z "${conf_file}" ]; then
        log_error "Configuration file for Kea not specified."
        exit 1
    elif [ ! -f "${conf_file}" ]; then
        log_error "Configuration file for Kea does not exist: ${conf_file}."
        exit 1
    fi
}

# Run the specified command if the server has been enabled.
# In order for the command to run, the following conditions have to be met:
# - server must be on the list of servers (e.g. specified from command line)
#   or servers must contain all
# - if check_file_cfg is non zero, the server must be enabled in the
#   configuration file, so the variable named after server name should exist
#   and be set to yes, e.g. ${dhcp4} should be equal to yes if server name
#   is dhcp4
run_conditional() {
    local server="${1}"             # Server name: dhcp4, dhcp6, dhcp_ddns, ctrl_agent, netconf
    local commands="${2}"           # Commands to execute
    local check_file_cfg="${3}"     # Check if server enabled in the configuration file
    local is_all=0                  # is all servers or a specific one

    # If keyword "all" is not on the list of servers we will have to check
    # if our specific server is on the list. If, not return.
    is_in_list "all" "${servers}"
    if [ "${_inlist}" -eq 0 ]; then
        is_in_list "${server}" "${servers}"
        if [ "${_inlist}" -eq 0 ]; then
            return
        fi
    else
        is_all=1
    fi
    # Return for for netconf when not available.
    if [ "${server}" = "netconf" ]; then
        if ! ${have_netconf}; then
            return
        fi
        # reload is not supported for netconf.
        if [ "${command}" = "reload" ]; then
            if [ "${is_all}" -eq 1 ]; then
                return
            fi
            log_warning "netconf does not support reload"
            return
        fi
    fi

    # Get the configuration value of the keactrl which indicates whether
    # the server should be enabled or not. Variables that hold these values
    # are: ${dhcp4}, ${dhcp6}, ${dhcp_ddns}.
    local file_config
    file_config=$( eval printf "%s" "\${$server}" )
    # Run the commands if we ignore the configuration setting or if the
    # setting is "yes".
    if [ "${check_file_cfg}" -eq 0 ] || [ "${file_config}" = "yes" ]; then
        ${commands}
    fi
}

### Script starts here ###

# Configure logger to log messages into the file.
# Do not set destination if the KEA_LOGGER_DESTINATION is set,
# because a unit test could have set this to some other location.
# Note that when the configuration is applied this location may be
# altered and only the handful of initial messages will be logged
# to the default file.
if [ -z "${KEA_LOGGER_DESTINATION+x}" ]; then
    prefix="@prefix@"
    export KEA_LOGGER_DESTINATION="@localstatedir@/log/kea.log"
fi

command=${1-}
if [ -z "${command}" ]; then
    log_error "missing command"
    usage
    exit 1
fi

# Check if this is a simple question about version.
if test "${command}" = "-v" || test "${command}" = "--version"; then
    echo "${PACKAGE_VERSION}"
    exit 0
fi

if test "${command}" = "-V"; then
    echo "${EXTENDED_VERSION}"
    exit 0
fi

is_in_list "${command}" "start stop reload status version"
if [ "${_inlist}" -eq 0 ]; then
    log_error "invalid command: ${command}"
    exit 1
fi

# Get the location of the keactrl configuration file.
prefix="@prefix@"
localstatedir="@localstatedir@"
keactrl_conf="@sysconfdir@/@PACKAGE@/keactrl.conf"

servers="all"

shift
while test ${#} -gt 0
do
    option=${1}
    case ${option} in
        # Override keactrl configuration file.
        -c|--ctrl-config)
            shift
            keactrl_conf=${1-}
            if [ -z "${keactrl_conf}" ]; then
                log_error "keactrl-config-file not specified"
                usage
                exit 1
            fi
            ;;
        # Get the specific servers for which the command will be
        # executed.
        -s|--server)
            shift
            servers=$(printf '%s' "${1-}" | tr ',' '\n')
            if [ -z "${servers}" ]; then
                log_error "servers not specified"
                usage
                exit 1
            fi
            # Validate that the specified server names are correct.
            for s in ${servers}
            do
                server_list="all dhcp4 dhcp6 dhcp_ddns ctrl_agent"
                if ${have_netconf}; then
                    server_list="${server_list} netconf"
                fi
                is_in_list "${s}" "${server_list}"
                if [ "${_inlist}" -eq 0 ]; then
                    log_error "invalid server name: ${s}"
                    exit 1
                fi
            done
            ;;
        *)
            log_error "invalid option: ${option}"
            usage
            exit 1
    esac
    shift
done

# Check if the file exists. If it doesn't, it is a fatal error.
if [ ! -f "${keactrl_conf}" ]; then
    log_error "keactrl configuration file doesn't exist in ${keactrl_conf}."
    exit 1
fi

# Include the configuration file.
# shellcheck source=src/bin/keactrl/keactrl.conf.in
. "${keactrl_conf}"

# Get location of the DHCPv4 server binary.
if [ -z "${dhcp4_srv+x}" ]; then
    log_error "dhcp4_srv parameter not specified"
    exit 1
fi

# Get location of the DHCPv6 server binary.
if [ -z "${dhcp6_srv+x}" ]; then
    log_error "dhcp6_srv parameter not specified"
    exit 1
fi

# Get location of the DHCP DDNS server binary.
if [ -z "${dhcp_ddns+x}" ]; then
    log_error "dhcp_ddns parameter not specified"
    exit 1
fi

# Get location of the Control Agent binary.
if [ -z "${ctrl_agent_srv+x}" ]; then
    log_error "ctrl_agent_srv parameter not specified"
    exit 1
fi

# Get location of the Netconf binary.
if ${have_netconf}; then
    if [ -z "${netconf_srv+x}" ]; then
        log_error "netconf_srv parameter not specified"
        exit 1
    fi
fi

# dhcp4 and dhcp6 (=yes) indicate if we should start DHCPv4 and DHCPv6 server
# respectively. The same is true for ddns, ctrl-agent and netconf.
dhcp4=$( printf "%s" "${dhcp4}" | tr '[:upper:]' '[:lower:]' )
dhcp6=$( printf "%s" "${dhcp6}" | tr '[:upper:]' '[:lower:]' )
dhcp_ddns=$( printf "%s" "${dhcp_ddns}" | tr '[:upper:]' '[:lower:]' )
ctrl_agent=$( printf "%s" "${ctrl_agent}" | tr '[:upper:]' '[:lower:]' )
if ${have_netconf}; then
    netconf=$( printf "%s" "${netconf}" | tr '[:upper:]' '[:lower:]' )
fi

case ${command} in
    # Start the servers.
    start)
        args=""
        # kea_verbose is set in keactrl.conf that shellcheck is unable to load.
        if [ "${kea_verbose}" = "yes" ]; then
            args="-d"
        fi

        # Run servers if they are on the list of servers from the command line
        # and if they are enabled in the keactrl configuration file.
        # The variables (dhcp4_srv, dhcp6_serv, dhcp_ddns_srv etc) are set in the
        # keactrl.conf file that shellcheck is unable to read.
        run_conditional "dhcp4" "start_server ${dhcp4_srv} -c ${kea_dhcp4_config_file} ${args}" 1
        run_conditional "dhcp6" "start_server ${dhcp6_srv} -c ${kea_dhcp6_config_file} ${args}" 1
        run_conditional "dhcp_ddns" "start_server ${dhcp_ddns_srv} -c ${kea_dhcp_ddns_config_file} \
${args}" 1
        run_conditional "ctrl_agent" "start_server ${ctrl_agent_srv} -c ${kea_ctrl_agent_config_file} \
${args}" 1
        if ${have_netconf}; then
            run_conditional "netconf" "start_server ${netconf_srv} -c ${kea_netconf_config_file} \
${args}" 1
        fi

        exit 0 ;;

    # Stop running servers.
    stop)
        # Stop all servers or servers specified from the command line.
        run_conditional "dhcp4" "stop_server ${dhcp4_srv}" 0
        run_conditional "dhcp6" "stop_server ${dhcp6_srv}" 0
        run_conditional "dhcp_ddns" "stop_server ${dhcp_ddns_srv}" 0
        run_conditional "ctrl_agent" "stop_server ${ctrl_agent_srv}" 0
        if ${have_netconf}; then
            run_conditional "netconf" "stop_server ${netconf_srv}" 0
        fi

        exit 0 ;;

    # Reconfigure the servers.
    reload)
        # Reconfigure all servers or servers specified from the command line.
        run_conditional "dhcp4" "reload_server ${dhcp4_srv}" 0
        run_conditional "dhcp6" "reload_server ${dhcp6_srv}" 0
        run_conditional "dhcp_ddns" "reload_server ${dhcp_ddns_srv}" 0
        run_conditional "ctrl_agent" "reload_server ${ctrl_agent_srv}" 0
        if ${have_netconf}; then
            run_conditional "netconf" "reload_server ${netconf_srv}" 0
        fi

        exit 0 ;;

    status)
        if [ -t 1 ]; then
            inactive="\033[91minactive\033[0m"
            active="\033[92mactive\033[0m"
        else
            inactive="inactive"
            active="active"
        fi

        kea4_status=$inactive
        # This case of nested double quotes looks confusing, but it is actually
        # correct. For details, see this fine explanation:
        # https://unix.stackexchange.com/questions/443989/whats-the-right-way-to-quote-command-arg
        check_running "$(basename -- "${dhcp4_srv}")"
        if [ "${_running}" -eq 1 ]; then
            kea4_status=$active
        fi
        printf "DHCPv4 server: %b\n" "${kea4_status}"

        kea6_status=$inactive
        check_running "$(basename -- "${dhcp6_srv}")"
        if [ "${_running}" -eq 1 ]; then
            kea6_status=$active
        fi
        printf "DHCPv6 server: %b\n" "${kea6_status}"

        d2_status=$inactive
        check_running "$(basename -- "${dhcp_ddns_srv}")"
        if [ "${_running}" -eq 1 ]; then
            d2_status=$active
        fi
        printf "DHCP DDNS: %b\n" "${d2_status}"

        agent_status=$inactive
        check_running "$(basename -- "${ctrl_agent_srv}")"
        if [ "${_running}" -eq 1 ]; then
            agent_status=$active
        fi
        printf "Control Agent: %b\n" "${agent_status}"

        if ${have_netconf}; then
            netconf_status=$inactive
            check_running "$(basename -- "${netconf_srv}")"
            if [ "${_running}" -eq 1 ]; then
                netconf_status=$active
            fi
            printf "Netconf agent: %b\n" "${netconf_status}"
        fi

        printf "Kea DHCPv4 configuration file: %s\n" "${kea_dhcp4_config_file}"
        printf "Kea DHCPv6 configuration file: %s\n" "${kea_dhcp6_config_file}"
        printf "Kea DHCP DDNS configuration file: %s\n" "${kea_dhcp_ddns_config_file}"
        printf "Kea Control Agent configuration file: %s\n" "${kea_ctrl_agent_config_file}"
        if ${have_netconf}; then
            printf "Kea Netconf configuration file: %s\n" "${kea_netconf_config_file}"
        fi
        printf "keactrl configuration file: %s\n" "${keactrl_conf}"

        check_kea_conf "${kea_dhcp4_config_file}"
        check_kea_conf "${kea_dhcp6_config_file}"
        check_kea_conf "${kea_dhcp_ddns_config_file}"
        check_kea_conf "${kea_ctrl_agent_config_file}"
        if ${have_netconf}; then
            check_kea_conf "${kea_netconf_config_file}"
        fi

        exit 0 ;;

    version)
        echo "keactrl: ${PACKAGE_VERSION}"
        run_conditional "dhcp4" "print_version kea-dhcp4 ${dhcp4_srv}" 0
        run_conditional "dhcp6" "print_version kea-dhcp6 ${dhcp6_srv}" 0
        run_conditional "dhcp_ddns" "print_version kea-dhcp-ddns ${dhcp_ddns_srv}" 0
        run_conditional "ctrl_agent" "print_version kea-ctrl-agent ${ctrl_agent_srv}" 0
        if ${have_netconf}; then
            run_conditional "netconf" "print_version kea-netconf ${netconf_srv}" 0
        fi

        exit 0 ;;

    # No other commands are supported.
    *)
        log_error "Invalid command:  ${command}."
        exit 1 ;;
esac
