# Copyright (C) 2014-2015,2017 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/. # A list of Kea processes, mainly used by the cleanup functions. KEA_PROCS="kea-dhcp4 kea-dhcp6 kea-dhcp-ddns kea-ctrl-agent" ### Logging functions ### # Prints error message. test_lib_error() { local s=${1} # Error message. local no_new_line=${2} # If specified, the message not terminated with # new line. printf "ERROR/test_lib: %s" "${s}" if [ -z ${no_new_line} ]; then printf "%s" "\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 "%s" "\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} # Detailed error format string # If nothing found, present an error an exit. if [ ${val1} -ne ${val2} ]; then printf "Assertion failure: ${val1} != ${val2}, for val1=${val1}, val2=${val2}\n" printf "${detailed_err}\n" ${val1} ${val2} clean_exit 1 fi } # Assertion that checks if two strings 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_str_eq() { val1=${1} # Reference value val2=${2} # Tested value detailed_err=${3} # Detailed error format string # If nothing found, present an error an exit. if [ "${val1}" != "${val2}" ]; then printf "Assertion failure: ${val1} != ${val2}, for val1=${val1}, val2=${val2}\n" printf "${detailed_err}\n" ${val1} ${val2} 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 printf "Assertion failure: \n\"%s\"\n\ndoesn't contain pattern:\n \"%s\"\n\n" "${text}" "${pattern}" printf "${detailed_err}\n" "\"${pattern}\"" clean_exit 1 fi } # Begins a test by prining 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 printf "\nSTART TEST ${TEST_NAME}\n" } # Prints test result an cleans up after the test. test_finish() { local exit_code=${1} # Exit code to be returned by the exit function. if [ ${exit_code} -eq 0 ]; then cleanup printf "PASSED ${TEST_NAME}\n\n" else # Dump log file for debugging purposes if specified and exists. # Otherwise the code below would simply call cat. if [ -n "${LOG_FILE}" -a -s "${LOG_FILE}" ]; then printf "Log file dump:\n" cat ${LOG_FILE} fi cleanup printf "FAILED ${TEST_NAME}\n\n" fi } # 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} ]; 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 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} ]; 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} ]; then test_lib_error "set_logger requies 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} } # PID file path is by default /var/kea, but can be # overridden by the environmental variable. PID_FILE_PATH=@localstatedir@/@PACKAGE@/ if [ ! -z ${KEA_PIDFILE_DIR} ]; then PID_FILE_PATH="${KEA_PIDFILE_DIR}" fi # 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 ${PID_FILE_PATH}, # 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. # 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 # 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="${PID_FILE_PATH}/${cfg_file_name}.${proc_name}.pid" _GET_PID=0 _GET_PIDS_NUM=0 # If the PID file exists, get the PID and see if the process is alive. if [ -e ${abs_pidfile_path} ]; then pid=$( cat $abs_pidfile_path ) kill -0 ${pid} > /dev/null 2>&1 if [ $? -eq 0 ]; then _GET_PID=${pid} _GET_PIDS_NUM=1 fi fi } # 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. kill_processes_by_name() { local proc_name=${1} # Process name if [ -z ${proc_name} ]; then test_lib_error "get_pids requires process name" clean_exit 1 fi # Obtain PIDs of running processes. local pids=$( pgrep ${proc_name} ) # For each PID found, send kill signal. for pid in ${pids} do printf "Shutting down Kea process ${proc_name} having pid %d.\n" ${pid} kill -9 ${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. if [ -s ${LOG_FILE} ]; then # Grep log file for the logger message occurrences and remove # whitespaces, if any. _GET_LOG_MESSAGES=$( grep -o ${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 -o CONFIG_COMPLETE ${LOG_FILE} | wc -w ) # Grep log file for CONFIG_LOAD_FAIL to check for configuration # failures. _GET_RECONFIG_ERRORS=$( grep -o CONFIG_LOAD_FAIL ${LOG_FILE} | wc -w ) # Remove whitespaces ${_GET_RECONFIGS##*[! ]} ${_GET_RECONFIG_ERRORS##*[! ]} } # 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 # recpectively, 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 proccess 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. rm -rf ${LOG_FILE} # 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 [ ! -z "${LEASE_FILE}" ]; then rm -rf ${LEASE_FILE}* fi rm -rf ${CFG_FILE} rm -rf ${KEACTRL_CFG_FILE} } # 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. 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=$( expr $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=$( expr ${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. 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=$( expr $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 ${sig} to Kea process (pid=%s).\n" ${_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}` kill -0 ${pid} if [ $? -ne 0 ]; 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 # Log the start of the test and print test name. test_start ${test_name} # Remove dangling Kea instances and remove log files. cleanup REPORTED_VERSION="`${bin_path}/${bin} -v`" if test "${REPORTED_VERSION}" == "${EXPECTED_VERSION}"; then test_finish 0 else printf "ERROR: Expected version ${EXPECTED_VERSION}, got ${REPORTED_VERSION}\n" test_finish 1 fi } # 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} # Remove dangling Kea instances and remove log files. cleanup # 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. Found %d processes" printf " 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 ${bin} to create logger_lockfile in the\n" 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. Found %d processes" printf " 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 ${bin} to NOT create logger_lockfile in the\n" printf "current directory, but the file exists." # 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 ${KEA_LOCKFILE_DIR_OLD}\n" 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" # Remove dangling DHCP4 instances and remove log files. cleanup # 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]" ${_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? PID conflict not reported.\n" ${bin} 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 }