#!/bin/sh # Copyright (C) 2014-2023 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/. # shellcheck disable=SC1091 # SC1091: Not following: ... was not specified as input (see shellcheck -x). # shellcheck disable=SC2034 # SC2034: ... appears unused. Verify use (or export if used externally). # shellcheck disable=SC2039 # SC2039: In POSIX sh, 'local' is undefined. # shellcheck disable=SC2153 # SC2153: Possible misspelling: ... may not be assigned, but ... is. # shellcheck disable=SC2154 # SC2154: bin_path is referenced but not assigned. # shellcheck disable=SC3043 # SC3043: In POSIX sh, 'local' is undefined. # Exit with error if commands exit with non-zero and if undefined variables are # used. set -eu # Include XML reporting library. . "@abs_top_builddir@/src/lib/testutils/xml_reporting_test_lib.sh" prefix="@prefix@" # Expected version EXPECTED_VERSION="@PACKAGE_VERSION@" # Kea environment variables for shell tests. # KEA_LOGGER_DESTINATION is set per test with set_logger. export KEA_LFC_EXECUTABLE="@abs_top_builddir@/src/bin/lfc/kea-lfc" export KEA_LOCKFILE_DIR="@abs_top_builddir@/test_lockfile_dir" export KEA_PIDFILE_DIR="@abs_top_builddir@/test_pidfile_dir" KEA_DHCP4_LOAD_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/load_marker.txt" KEA_DHCP4_UNLOAD_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/unload_marker.txt" KEA_DHCP4_SRV_CONFIG_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/srv_config_marker_file.txt" KEA_DHCP6_LOAD_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/load_marker.txt" KEA_DHCP6_UNLOAD_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/unload_marker.txt" KEA_DHCP6_SRV_CONFIG_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/srv_config_marker_file.txt" # A list of Kea processes, mainly used by the cleanup functions. KEA_PROCS="kea-dhcp4 kea-dhcp6 kea-dhcp-ddns kea-ctrl-agent" ### Colors ### if test -t 1; then green='\033[92m' red='\033[91m' reset='\033[0m' fi ### Logging functions ### # Prints error message. test_lib_error() { local s="${1-}" # Error message. local no_new_line="${2-}" # If specified, the message is not terminated # with new line. printf "ERROR/test_lib: %s" "${s}" if [ -z "${no_new_line}" ]; then printf '\n' fi } # Prints info message. test_lib_info() { local s="${1-}" # Info message. local no_new_line="${2-}" # If specified, the message is not terminated # with new line. printf "INFO/test_lib: %s" "${s}" if [ -z "${no_new_line}" ]; then printf '\n' fi } ### Assertions ### # Assertion that checks if two numbers are equal. # If numbers are not equal, the mismatched values are presented and the # detailed error is printed. The detailed error must use the printf # formatting like this: # "Expected that some value 1 %d is equal to some other value %d". assert_eq() { val1=${1} # Reference value val2=${2} # Tested value detailed_err=${3-} # Optional detailed error format string # If nothing found, present an error an exit. if [ "${val1}" -ne "${val2}" ]; then printf 'Assertion failure: %s != %s, expected %s, got %s\n' \ "${val1}" "${val2}" "${val1}" "${val2}" # shellcheck disable=SC2059 # SC2059: Don't use variables in the printf format string. Use printf '..%s..' "$foo" ERROR=$(printf "${detailed_err}" "${val1}" "${val2}") printf '%s\n%s\n' "${ERROR}" "${OUTPUT}" >&2 clean_exit 1 fi } # Assertion that checks that two strings are equal. # If strings are not equal, the mismatched values are presented and the # detailed error is printed. The detailed error must use the printf # formatting like this: # "Expected that some value 1 %d is equal to some other value %d". assert_str_eq() { val1=${1} # Reference value val2=${2} # Tested value detailed_err=${3-} # Optional detailed error format string # If nothing found, present an error an exit. if [ "${val1}" != "${val2}" ]; then printf 'Assertion failure: %s != %s, expected "%s", got "%s"\n' \ "${val1}" "${val2}" "${val1}" "${val2}" # shellcheck disable=SC2059 # SC2059: SC2059: Don't use variables in the printf format string. Use printf '..%s..' "$foo". ERROR=$(printf "${detailed_err}" "${val1}" "${val2}") printf '%s\n%s\n' "${ERROR}" "${OUTPUT}" >&2 clean_exit 1 fi } # Assertion that checks that two strings are NOT equal. # If strings are equal, the mismatched values are presented and the # optional detailed error, if any, is printed. assert_str_neq() { reference=${1} # Reference value tested=${2} # Tested value detailed_error=${3-} # Optional detailed error format string if test "${reference}" = "${tested}"; then printf 'Assertion failure: expected different strings, but ' printf 'both variables have the value "%s".\n' "${reference}" printf '%s\n%s\n' "${detailed_error}" "${OUTPUT}" >&2 clean_exit 1 fi } # Assertion that checks if one string contains another string. # If assertion fails, both strings are displayed and the detailed # error is printed. The detailed error must use the printf formatting # like this: # "Expected some string to contain this string: %s". assert_string_contains() { pattern="${1}" # Substring or awk pattern text="${2}" # Text to be searched for substring detailed_err="${3}" # Detailed error format string # Search for a pattern match=$( printf "%s" "${text}" | awk /"${pattern}"/ ) # If nothing found, present an error and exit. if [ -z "${match}" ]; then ERROR=$(printf \ "Assertion failure: \"%s\" does not contain pattern: \"%s\" ${detailed_err} " "${text}" "${pattern}" "${pattern}") printf '%s\n%s\n' "${ERROR}" "${OUTPUT}" >&2 clean_exit 1 fi } # Runs all the given arguments as a single command. Maintains quoting. Places # output in ${OUTPUT} and exit code in ${EXIT_CODE}. Does not support pipes and # redirections. Support for them could be added through eval and single # parameter assignment, but eval is not recommended. # shellcheck disable=SC2034 # SC2034: ... appears unused. Verify use (or export if used externally). run_command() { if test -n "${DEBUG+x}"; then printf '%s\n' "${*}" >&2 fi set +e OUTPUT=$("${@}") EXIT_CODE=${?} set -e } # Enable traps to print FAILED status when a command fails unexpectedly or when # the user sends a SIGINT. Used in `test_start`. traps_on() { for t in HUP INT QUIT KILL TERM EXIT; do # shellcheck disable=SC2064 # SC2064: Use single quotes, otherwise this expands now rather than when signalled. # reason: we want ${red-} and ${reset-} to expand here, at trap-time # they will be empty or have other values trap " exit_code=\${?} printf '${red-}[ FAILED ]${reset-} %s (exit code: %d)\n' \ \"\${TEST_NAME}\" \"\${exit_code}\" " "${t}" done } # Disable traps so that a double status is not printed. Used in `test_finish` # after the status has been printed explicitly. traps_off() { for t in HUP INT QUIT KILL TERM EXIT; do trap - "${t}" done } # Print UNIX time with millisecond resolution. get_current_time() { local time time=$(date +%s%3N) # In some systems, particularly BSD-based, `+%3N` millisecond resolution is # not supported. It instead prints the literal '3N', but we check for any # alphabetical character. If we do find one, revert to second resolution and # convert to milliseconds. if printf '%s' "${time}" | grep -E '[A-Za-z]' > /dev/null 2>&1; then time=$(date +%s) time=$((1000 * time)) fi printf '%s' "${time}" } # Begins a test by printing its name. test_start() { TEST_NAME=${1-} if [ -z "${TEST_NAME}" ]; then test_lib_error "test_start requires test name as an argument" clean_exit 1 fi # Set traps first to fail if something goes wrong. traps_on # Announce test start. printf "${green-}[ RUN ]${reset-} %s\n" "${TEST_NAME}" # Remove dangling Kea instances and remove log files. cleanup # Make sure lockfile and pidfile directories exist. They are used in some # tests. mkdir -p "${KEA_LOCKFILE_DIR}" # There are certain tests that intentionally run without a KEA_PIDFILE_DIR # e.g. keactrl.status_test. Only create the directory if we test requires # one. if test -n "${KEA_PIDFILE_DIR+x}"; then mkdir -p "${KEA_PIDFILE_DIR}" fi # Start timer in milliseconds. START_TIME=$(get_current_time) } # Prints test result an cleans up after the test. test_finish() { # Exit code to be returned by the exit function local exit_code="${1}" # Stop timer and set duration. FINISH_TIME=$(get_current_time) local duration duration=$((FINISH_TIME - START_TIME)) # Add the test result to the XML. report_test_result_in_xml "${TEST_NAME}" "${exit_code}" "${duration}" if [ "${exit_code}" -eq 0 ]; then printf "${green-}[ OK ]${reset-} %s\n" "${TEST_NAME}" else # Dump log file for debugging purposes if specified and exists. # Otherwise the code below would simply call cat. # Use ${var+x} to test if ${var} is defined. if test -n "${LOG_FILE+x}" && test -s "${LOG_FILE}"; then printf 'Log file dump:\n' cat "${LOG_FILE}" fi printf "${red-}[ FAILED ]${reset-} %s\n" "${TEST_NAME}" fi # Remove dangling Kea instances and log files. cleanup # Reset traps. traps_off # Explicitly return ${exit_code}. The effect should be for `make check` to # return with the exit same code or at least another non-zero exit code thus # reporting a failure. return "${exit_code}" } # Stores the configuration specified as a parameter in the configuration # file which name has been set in the ${CFG_FILE} variable. create_config() { local cfg="${1-}" # Configuration string. if [ -z "${CFG_FILE+x}" ]; then test_lib_error "create_config requires CFG_FILE variable be set" clean_exit 1 elif [ -z "${cfg}" ]; then test_lib_error "create_config requires argument holding a configuration" clean_exit 1 fi printf 'Creating Kea configuration file: %s.\n' "${CFG_FILE}" printf '%b' "${cfg}" > "${CFG_FILE}" } # Stores the DHCP4 configuration specified as a parameter in the # configuration file which name has been set in the ${DHCP4_CFG_FILE} # variable. create_dhcp4_config() { local cfg="${1-}" # Configuration string. if [ -z "${DHCP4_CFG_FILE+x}" ]; then test_lib_error "create_dhcp4_config requires DHCP4_CFG_FILE \ variable be set" clean_exit 1 elif [ -z "${cfg}" ]; then test_lib_error "create_dhcp4_config requires argument holding a \ configuration" clean_exit 1 fi printf 'Creating Dhcp4 configuration file: %s.\n' "${DHCP4_CFG_FILE}" printf '%b' "${cfg}" > "${DHCP4_CFG_FILE}" } # Stores the DHCP6 configuration specified as a parameter in the # configuration file which name has been set in the ${DHCP6_CFG_FILE} # variable. create_dhcp6_config() { local cfg="${1-}" # Configuration string. if [ -z "${DHCP6_CFG_FILE+x}" ]; then test_lib_error "create_dhcp6_config requires DHCP6_CFG_FILE \ variable be set" clean_exit 1 elif [ -z "${cfg}" ]; then test_lib_error "create_dhcp6_config requires argument holding a \ configuration" clean_exit 1 fi printf 'Creating Dhcp6 configuration file: %s.\n' "${DHCP6_CFG_FILE}" printf '%b' "${cfg}" > "${DHCP6_CFG_FILE}" } # Stores the D2 configuration specified as a parameter in the # configuration file which name has been set in the ${D2_CFG_FILE} # variable. create_d2_config() { local cfg="${1-}" # Configuration string. if [ -z "${D2_CFG_FILE+x}" ]; then test_lib_error "create_d2_config requires D2_CFG_FILE \ variable be set" clean_exit 1 elif [ -z "${cfg}" ]; then test_lib_error "create_d2_config requires argument holding a \ configuration" clean_exit 1 fi printf 'Creating D2 configuration file: %s.\n' "${D2_CFG_FILE}" printf '%b' "${cfg}" > "${D2_CFG_FILE}" } # Stores the CA configuration specified as a parameter in the # configuration file which name has been set in the ${CA_CFG_FILE} # variable. create_ca_config() { local cfg="${1-}" # Configuration string. if [ -z "${CA_CFG_FILE+x}" ]; then test_lib_error "create_ca_config requires CA_CFG_FILE \ variable be set" clean_exit 1 elif [ -z "${cfg}" ]; then test_lib_error "create_ca_config requires argument holding a \ configuration" clean_exit 1 fi printf 'Creating Ca configuration file: %s.\n' "${CA_CFG_FILE}" printf '%b' "${cfg}" > "${CA_CFG_FILE}" } # Stores the NC configuration specified as a parameter in the # configuration file which name has been set in the ${NC_CFG_FILE} # variable. create_nc_config() { local cfg="${1-}" # Configuration string. if [ -z "${NC_CFG_FILE+x}" ]; then test_lib_error "create_nc_config requires NC_CFG_FILE \ variable be set" clean_exit 1 elif [ -z "${cfg}" ]; then test_lib_error "create_nc_config requires argument holding a \ configuration" clean_exit 1 fi printf 'Creating Nc configuration file: %s.\n' "${NC_CFG_FILE}" printf '%b' "${cfg}" > "${NC_CFG_FILE}" } # Stores the keactrl configuration specified as a parameter in the # configuration file which name has been set in the ${KEACTRL_CFG_FILE} # variable. create_keactrl_config() { local cfg="${1-}" # Configuration string. if [ -z "${KEACTRL_CFG_FILE+x}" ]; then test_lib_error "create_keactrl_config requires KEACTRL_CFG_FILE \ variable be set" clean_exit 1 elif [ -z "${cfg}" ]; then test_lib_error "create_keactrl_config requires argument holding a \ configuration" clean_exit 1 fi printf 'Creating keactrl configuration file: %s.\n' "${KEACTRL_CFG_FILE}" printf '%b' "${cfg}" > "${KEACTRL_CFG_FILE}" } # Sets Kea logger to write to the file specified by the global value # ${LOG_FILE}. set_logger() { if [ -z "${LOG_FILE+x}" ]; then test_lib_error "set_logger requires LOG_FILE variable be set" clean_exit 1 fi printf 'Kea log will be stored in %s.\n' "${LOG_FILE}" export KEA_LOGGER_DESTINATION=${LOG_FILE} } # Checks if specified process is running. # # This function uses PID file to obtain the PID and then calls # 'kill -0 ' to check if the process is alive. # The PID files are expected to be located in the ${KEA_PIDFILE_DIR}, # and their names should match the following pattern: # ..pid. If the is not # specified a 'test_config' is used by default. # # Return value: # _GET_PID: holds a PID if process is running # _GET_PIDS_NUM: holds 1 if process is running, 0 otherwise get_pid() { local proc_name="${1-}" # Process name local cfg_file_name="${2-}" # Configuration file name without extension. # Reset PID results. _GET_PID=0 _GET_PIDS_NUM=0 # PID file name includes process name. The process name is required. if [ -z "${proc_name}" ]; then test_lib_error "get_pid requires process name" clean_exit 1 fi # There are certain tests that intentionally run without a KEA_PIDFILE_DIR # e.g. keactrl.status_test. We can't get the PID if KEA_PIDFILE_DIR is not # defined. In this case, this function is reporting process not running # (_GET_PID == 0). if test -z "${KEA_PIDFILE_DIR+x}"; then return fi # PID file name includes server configuration file name. For most of # the tests it is 'test-config' (excluding .json extension). It is # possible to specify custom name if required. if [ -z "${cfg_file_name}" ]; then cfg_file_name="test_config" fi # Get the absolute location of the PID file for the specified process # name. abs_pidfile_path="${KEA_PIDFILE_DIR}/${cfg_file_name}.${proc_name}.pid" # If the PID file exists, get the PID and see if the process is alive. pid=$(cat "${abs_pidfile_path}" 2> /dev/null || true) if test -n "${pid}"; then if kill -0 "${pid}" > /dev/null 2>&1; then _GET_PID=${pid} _GET_PIDS_NUM=1 fi fi } # Get the name of the process identified by PID. get_process_name() { local pid="${1-}" if test -z "${pid}"; then test_lib_error 'expected PID parameter in get_process_name' clean_exit 1 fi ps "${pid}" | tr -s ' ' | cut -d ' ' -f 6- | head -n 2 | tail -n 1 } # Wait for file to be created. wait_for_file() { local file="${1-}" if test -z "${file}"; then test_lib_error 'expected file parameter in wait_for_file' clean_exit 1 fi local timeout='4' # seconds local deadline="$(($(date +%s) + timeout))" while ! test -f "${file}"; do if test "${deadline}" -lt "$(date +%s)"; then # Time is up. printf 'ERROR: file "%s" was not created in time.\n' "${file}" >&2 return 1 fi printf 'Waiting for file "%s" to be created...\n' "${file}" sleep 1 done } # Wait for process identified by PID to die. wait_for_process_to_stop() { local pid="${1-}" if test -z "${pid}"; then test_lib_error 'expected PID parameter in wait_for_process_to_stop' clean_exit 1 fi local timeout='4' # seconds local deadline="$(($(date +%s) + timeout))" while ps "${pid}" >/dev/null; do if test "${deadline}" -lt "$(date +%s)"; then # Time is up. printf 'ERROR: %s is not stopping.\n' "$(get_process_name "${pid}")" >&2 return 1 fi printf 'Waiting for %s to stop...\n' "$(get_process_name "${pid}")" sleep 1 done } # Kills processes specified by name. # # This function kills all processes having a specified name. # It uses 'pgrep' to obtain pids of those processes. # This function should be used when identifying process by # the value in its PID file is not relevant. # # Linux limitation for pgrep: The process name used for matching is # limited to the 15 characters. If you call this with long process # names, add this before pgrep: # proc_name=$(printf '%s' "${proc_name}" | cut -c1-15) kill_processes_by_name() { local proc_name="${1-}" # Process name if [ -z "${proc_name}" ]; then test_lib_error "kill_processes_by_name requires process name" clean_exit 1 fi # Obtain PIDs of running processes. local pids pids=$(pgrep "${proc_name}" || true) # For each PID found, send kill signal. for pid in ${pids}; do printf 'Shutting down Kea process %s with PID %d...\n' "${proc_name}" "${pid}" kill -9 "${pid}" || true done # Wait for all processes to stop. for pid in ${pids}; do printf 'Waiting for Kea process %s with PID %d to stop...\n' "${proc_name}" "${pid}" wait_for_process_to_stop "${pid}" done } # Returns the number of occurrences of the Kea log message in the log file. # Return value: # _GET_LOG_MESSAGES: number of log message occurrences. get_log_messages() { local msg="${1}" # Message id, e.g. DHCP6_SHUTDOWN if [ -z "${msg}" ]; then test_lib_error "get_log_messages require message identifier" clean_exit 1 fi _GET_LOG_MESSAGES=0 # If log file is not present, the number of occurrences is 0. # Use ${var+x} to test if ${var} is defined. if test -n "${LOG_FILE+x}" && test -s "${LOG_FILE}"; then # Grep log file for the logger message occurrences and remove # whitespaces, if any. _GET_LOG_MESSAGES=$(grep -Fo "${msg}" "${LOG_FILE}" | wc -w | tr -d " ") fi } # Returns the number of server configurations performed so far. Also # returns the number of configuration errors. # Return values: # _GET_RECONFIGS: number of configurations so far. # _GET_RECONFIG_ERRORS: number of configuration errors. get_reconfigs() { # Grep log file for CONFIG_COMPLETE occurrences. There should # be one occurrence per (re)configuration. _GET_RECONFIGS=$(grep -Fo CONFIG_COMPLETE "${LOG_FILE}" | wc -w) # Grep log file for CONFIG_LOAD_FAIL to check for configuration # failures. _GET_RECONFIG_ERRORS=$(grep -Fo CONFIG_LOAD_FAIL "${LOG_FILE}" | wc -w) # Remove whitespaces ${_GET_RECONFIGS##*[! ]} ${_GET_RECONFIG_ERRORS##*[! ]} } # Remove the given directories or files if they exist. remove_if_exists() { while test ${#} -gt 0; do if test -e "${1}"; then rm -rf "${1}" fi shift done } # Performs cleanup after test. # It shuts down running Kea processes and removes temporary files. # The location of the log file and the configuration files should be set # in the ${LOG_FILE}, ${CFG_FILE} and ${KEACTRL_CFG_FILE} variables # respectively, prior to calling this function. cleanup() { # If there is no KEA_PROCS set, just return if [ -z "${KEA_PROCS}" ]; then return fi # KEA_PROCS holds the name of all Kea processes. Shut down each # of them if running. for proc_name in ${KEA_PROCS} do get_pid "${proc_name}" # Shut down running Kea process. if [ "${_GET_PIDS_NUM}" -ne 0 ]; then printf 'Shutting down Kea process having pid %d.\n' "${_GET_PID}" kill -9 "${_GET_PID}" fi done # Kill any running LFC processes. Even though 'kea-lfc' creates PID # file we rather want to use 'pgrep' to find the process PID, because # kea-lfc execution is not controlled from the test and thus there # is possibility that process is already/still running but the PID # file doesn't exist for it. As a result, the process will not # be killed. This is not a problem for other processes because # tests control launching them and monitor when they are shut down. kill_processes_by_name "kea-lfc" # Remove temporary files. remove_if_exists \ "${CA_CFG_FILE-}" \ "${CFG_FILE-}" \ "${D2_CFG_FILE-}" \ "${DHCP4_CFG_FILE-}" \ "${KEA_DHCP4_LOAD_MARKER_FILE-}" \ "${KEA_DHCP4_UNLOAD_MARKER_FILE-}" \ "${KEA_DHCP4_SRV_CONFIG_MARKER_FILE-}" \ "${DHCP6_CFG_FILE-}" \ "${KEA_DHCP6_LOAD_MARKER_FILE-}" \ "${KEA_DHCP6_UNLOAD_MARKER_FILE-}" \ "${KEA_DHCP6_SRV_CONFIG_MARKER_FILE-}" \ "${KEACTRL_CFG_FILE-}" \ "${KEA_LOCKFILE_DIR-}" \ "${KEA_PIDFILE_DIR-}" \ "${NC_CFG_FILE-}" # Use ${var+x} to test if ${var} is defined. if test -n "${LOG_FILE+x}" && test -n "${LOG_FILE}"; then rm -rf "${LOG_FILE}" rm -rf "${LOG_FILE}.lock" fi # Use asterisk to remove all files starting with the given name, # in case the LFC has been run. LFC creates files with postfixes # appended to the lease file name. if test -n "${LEASE_FILE+x}" && test -n "${LEASE_FILE}"; then rm -rf "${LEASE_FILE}"* fi } # Exists the test in the clean way. # It performs the cleanup and prints whether the test has passed or failed. # If a test fails, the Kea log is dumped. clean_exit() { exit_code=${1-} # Exit code to be returned by the exit function. case ${exit_code} in ''|*[!0-9]*) test_lib_error "argument passed to clean_exit must be a number" ;; esac # Print test result and perform a cleanup test_finish "${exit_code}" exit "${exit_code}" } # Starts Kea process in background using a configuration file specified # in the global variable ${CFG_FILE}. start_kea() { local bin="${1-}" if [ -z "${bin}" ]; then test_lib_error "binary name must be specified for start_kea" clean_exit 1 fi printf "Running command %s.\n" "\"${bin} -c ${CFG_FILE}\"" "${bin}" -c "${CFG_FILE}" & } # Waits with timeout for Kea to start. # This function repeatedly checks if the Kea log file has been created # and is non-empty. If it is, the function assumes that Kea has started. # It doesn't check the contents of the log file though. # If the log file doesn't exist the function sleeps for a second and # checks again. This is repeated until timeout is reached or non-empty # log file is found. If timeout is reached, the function reports an # error. # Return value: # _WAIT_FOR_KEA: 0 if Kea hasn't started, 1 otherwise wait_for_kea() { local timeout="${1-}" # Desired timeout in seconds. if test -z "${timeout}"; then test_lib_error 'expected timeout parameter in wait_for_kea' clean_exit 1 fi case ${timeout} in ''|*[!0-9]*) test_lib_error "argument passed to wait_for_kea must be a number" clean_exit 1 ;; esac local loops=0 # Loops counter _WAIT_FOR_KEA=0 test_lib_info "wait_for_kea " "skip-new-line" while [ ! -s "${LOG_FILE}" ] && [ "${loops}" -le "${timeout}" ]; do printf "." sleep 1 loops=$(( loops + 1 )) done printf '\n' if [ "${loops}" -le "${timeout}" ]; then _WAIT_FOR_KEA=1 fi } # Waits for a specific message to occur in the Kea log file. # This function is called when the test expects specific message # to show up in the log file as a result of some action that has # been taken. Typically, the test expects that the message # is logged when the SIGHUP or SIGTERM signal has been sent to the # Kea process. # This function waits a specified number of seconds for the number # of message occurrences to show up. If the expected number of # message doesn't occur, the error status is returned. # Return value: # _WAIT_FOR_MESSAGE: 0 if the message hasn't occurred, 1 otherwise. wait_for_message() { local timeout="${1-}" # Expected timeout value in seconds. local message="${2-}" # Expected message id. local occurrences="${3-}" # Number of expected occurrences. # Validate timeout case ${timeout} in ''|*[!0-9]*) test_lib_error "argument timeout passed to wait_for_message must \ be a number" clean_exit 1 ;; esac # Validate message if [ -z "${message}" ]; then test_lib_error "message id is a required argument for wait_for_message" clean_exit 1 fi # Validate occurrences case ${occurrences} in ''|*[!0-9]*) test_lib_error "argument occurrences passed to wait_for_message \ must be a number" clean_exit 1 ;; esac local loops=0 # Number of loops performed so far. _WAIT_FOR_MESSAGE=0 test_lib_info "wait_for_message ${message}: " "skip-new-line" # Check if log file exists and if we reached timeout. while [ "${loops}" -le "${timeout}" ]; do printf "." # Check if the message has been logged. get_log_messages "${message}" if [ "${_GET_LOG_MESSAGES}" -ge "${occurrences}" ]; then printf '\n' _WAIT_FOR_MESSAGE=1 return fi # Message not recorded. Keep going. sleep 1 loops=$(( loops + 1 )) done printf '\n' # Timeout. } # Waits for server to be down. # Return value: # _WAIT_FOR_SERVER_DOWN: 1 if server is down, 0 if timeout occurred and the # server is still running. wait_for_server_down() { local timeout="${1-}" # Timeout specified in seconds. local proc_name="${2-}" # Server process name. if test -z "${proc_name}"; then test_lib_error 'expected process name parameter in wait_for_server_down' clean_exit 1 fi case ${timeout} in ''|*[!0-9]*) test_lib_error "argument passed to wait_for_server_down must be a number" clean_exit 1 ;; esac local loops=0 # Loops counter _WAIT_FOR_SERVER_DOWN=0 test_lib_info "wait_for_server_down ${proc_name}: " "skip-new-line" while [ "${loops}" -le "${timeout}" ]; do printf "." get_pid "${proc_name}" if [ "${_GET_PIDS_NUM}" -eq 0 ]; then printf '\n' _WAIT_FOR_SERVER_DOWN=1 return fi sleep 1 loops=$(( loops + 1 )) done printf '\n' } # Sends specified signal to the Kea process. send_signal() { local sig="${1-}" # Signal number. local proc_name="${2-}" # Process name # Validate signal case ${sig} in ''|*[!0-9]*) test_lib_error "signal number passed to send_signal \ must be a number" clean_exit 1 ;; esac # Validate process name if [ -z "${proc_name}" ]; then test_lib_error "send_signal requires process name be passed as argument" clean_exit 1 fi # Get Kea pid. get_pid "${proc_name}" if [ "${_GET_PIDS_NUM}" -ne 1 ]; then printf "ERROR: expected one Kea process to be started.\ Found %d processes started.\n" ${_GET_PIDS_NUM} clean_exit 1 fi printf "Sending signal %s to Kea process (pid=%s).\n" "${sig}" "${_GET_PID}" # Actually send a signal. kill "-${sig}" "${_GET_PID}" } # Verifies that a server is up running by its PID file # The PID file is constructed from the given config file name and # binary name. If it exists and the PID it contains refers to a # live process it sets _SERVER_PID_FILE and _SERVER_PID to the # corresponding values. Otherwise, it emits an error and exits. verify_server_pid() { local bin_name="${1-}" # binary name of the server local cfg_file="${2-}" # config file name # We will construct the PID file name based on the server config # and binary name if [ -z "${bin_name}" ]; then test_lib_error "verify_server_pid requires binary name" clean_exit 1 fi if [ -z "${cfg_file}" ]; then test_lib_error "verify_server_pid requires config file name" clean_exit 1 fi # Only the file name portion of the config file is used, try and # extract it. NOTE if this "algorithm" changes this code will need # to be updated. fname=$(basename "${cfg_file}") fname=$(echo "${fname}" | cut -f1 -d'.') if [ -z "${fname}" ]; then test_lib_error "verify_server_pid could not extract config name" clean_exit 1 fi # Now we can build the name: pid_file="${KEA_PIDFILE_DIR}/${fname}.${bin_name}.pid" if [ ! -e "${pid_file}" ]; then printf "ERROR: PID file:[%s] does not exist\n" "${pid_file}" clean_exit 1 fi # File exists, does its PID point to a live process? pid=$(cat "${pid_file}" 2> /dev/null || true) if ! kill -0 "${pid}"; then printf "ERROR: PID file:[%s] exists but PID:[%d] does not\n" \ "${pid_file}" "${pid}" clean_exit 1 fi # Make the values accessible to the caller _SERVER_PID="${pid}" _SERVER_PID_FILE="${pid_file}" } # This test verifies that the binary is reporting its version properly. version_test() { test_name=${1} # Test name long_version=${2-} # Test long version? # Log the start of the test and print test name. test_start "${test_name}" # If set to anything other than empty string, reset it to the long version # parameter. if test -n "${long_version}"; then long_version='--version' fi # Keep ${long_version} unquoted so that it is not included as an empty # string if not given as argument. for v in -v ${long_version}; do run_command \ "${bin_path}/${bin}" "${v}" if test "${OUTPUT}" != "${EXPECTED_VERSION}"; then printf 'ERROR: Expected version "%s", got "%s" when calling "%s"\n' \ "${EXPECTED_VERSION}" "${OUTPUT}" "${bin} ${v}" test_finish 1 fi done test_finish 0 } # This test verifies that the server is using logger variable # KEA_LOCKFILE_DIR properly (it should be used to point out to the directory, # where lockfile should be created. Also, "none" value means to not create # the lockfile at all). logger_vars_test() { test_name=${1} # Test name # Log the start of the test and print test name. test_start "${test_name}" # Create bogus configuration file. We don't really want the server to start, # just want it to log something and die. Empty config is an easy way to # enforce that behavior. create_config "{ }" printf "Please ignore any config error messages.\n" # Remember old KEA_LOCKFILE_DIR KEA_LOCKFILE_DIR_OLD=${KEA_LOCKFILE_DIR} # Set lockfile directory to current directory. KEA_LOCKFILE_DIR=. # Start Kea. start_kea "${bin_path}/${bin}" # Wait for Kea to process the invalid configuration and die. sleep 1 # Check if it is still running. It should have terminated. get_pid "${bin}" if [ "${_GET_PIDS_NUM}" -ne 0 ]; then printf 'ERROR: expected Kea process to not start. ' printf 'Found %d processes running.\n' "${_GET_PIDS_NUM}" # Revert to the old KEA_LOCKFILE_DIR value KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR_OLD} clean_exit 1 fi if [ ! -f "./logger_lockfile" ]; then printf 'ERROR: Expect %s to create logger_lockfile in the ' "${bin}" printf 'current directory, but no such file exists.\n' # Revert to the old KEA_LOCKFILE_DIR value KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR__OLD} clean_exit 1 fi # Remove the lock file rm -f ./logger_lockfile # Tell Kea to NOT create logfiles at all KEA_LOCKFILE_DIR="none" # Start Kea. start_kea "${bin_path}/${bin}" # Wait for Kea to process the invalid configuration and die. sleep 1 # Check if it is still running. It should have terminated. get_pid "${bin}" if [ "${_GET_PIDS_NUM}" -ne 0 ]; then printf 'ERROR: expected Kea process to not start. ' printf 'Found %d processes running.\n' "${_GET_PIDS_NUM}" # Revert to the old KEA_LOCKFILE_DIR value KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR_OLD} clean_exit 1 fi if [ -f "./logger_lockfile" ]; then printf 'ERROR: Expect %s to NOT create logger_lockfile in the ' "${bin}" printf 'current directory, but the file exists.\n' # Revert to the old KEA_LOCKFILE_DIR value KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR_OLD} clean_exit 1 fi # Revert to the old KEA_LOCKFILE_DIR value printf 'Reverting KEA_LOCKFILE_DIR to %s\n' "${KEA_LOCKFILE_DIR_OLD}" KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR_OLD} test_finish 0 } # This test verifies server PID file management # 1. It verifies that upon startup, the server creates a PID file # 2. It verifies the an attempt to start a second instance fails # due to pre-existing PID File/PID detection server_pid_file_test() { local server_cfg="${1}" local log_id="${2}" # Log the start of the test and print test name. test_start "${bin}.server_pid_file_test" # Create new configuration file. create_config "${CONFIG}" # Instruct server to log to the specific file. set_logger # Start server start_kea "${bin_path}/${bin}" # Wait up to 20s for server to start. wait_for_kea 20 if [ "${_WAIT_FOR_KEA}" -eq 0 ]; then printf 'ERROR: timeout waiting for %s to start.\n' "${bin}" clean_exit 1 fi # Verify server is still running verify_server_pid "${bin}" "${CFG_FILE}" printf 'PID file is [%s], PID is [%d]\n' "${_SERVER_PID_FILE}" "${_SERVER_PID}" # Now try to start a second one start_kea "${bin_path}/${bin}" wait_for_message 10 "${log_id}" 1 if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then printf 'ERROR: Second %s instance started? ' "${bin}" printf 'PID conflict not reported.\n' clean_exit 1 fi # Verify server is still running verify_server_pid "${bin}" "${CFG_FILE}" # All ok. Shut down the server and exit. test_finish 0 } # This test verifies that passwords are redacted in logs. # This function takes 2 parameters: # test_name # config - string with a content of the config (will be written to a file) # expected_code - expected exit code returned by kea (0 - success, 1 - failure) password_redact_test() { local test_name="${1}" local config="${2}" local expected_code="${3}" # Log the start of the test and print test name. test_start "${test_name}" # Create correct configuration file. create_config "${config}" # Instruct Control Agent to log to the specific file. set_logger # Check it printf "Running command %s.\n" "\"${bin_path}/${bin} -d -t ${CFG_FILE}\"" run_command \ "${bin_path}/${bin}" -d -t "${CFG_FILE}" if [ "${EXIT_CODE}" -ne "${expected_code}" ]; then printf 'ERROR: expected exit code %s, got %s\n' "${expected_code}" "${EXIT_CODE}" clean_exit 1 fi if grep -q 'sensitive' "${LOG_FILE}"; then printf "ERROR: sensitive is present in logs\n" clean_exit 1 fi if ! grep -q 'superadmin' "${LOG_FILE}"; then printf "ERROR: superadmin is not present in logs\n" clean_exit 1 fi test_finish 0 } # kea-dhcp[46] configuration with a password # used for redact tests: # - sensitive should be hidden # - superadmin should be visible kea_dhcp_config() { printf ' { "Dhcp%s": { "config-control": { "config-databases": [ { "password": "sensitive", "type": "mysql", "user": "keatest" } ] }, "hooks-libraries": [ { "library": "@abs_top_builddir@/src/bin/dhcp%s/tests/.libs/libco1.so", "parameters": { "password": "sensitive", "user": "keatest", "nested-map": { "password": "sensitive", "user": "keatest" } } } ], "hosts-database": { "password": "sensitive", "type": "mysql", "user": "keatest" }, "lease-database": { "password": "sensitive", "type": "mysql", "user": "keatest" }, "user-context": { "password": "superadmin", "secret": "superadmin", "shared-info": { "password": "superadmin", "secret": "superadmin" } } } } ' "${1}" "${1}" }