diff --git a/src/hooks/dhcp/Makefile.am b/src/hooks/dhcp/Makefile.am index 090bde7fd3..b40e08c3fd 100644 --- a/src/hooks/dhcp/Makefile.am +++ b/src/hooks/dhcp/Makefile.am @@ -4,4 +4,4 @@ if HAVE_MYSQL SUBDIRS += mysql_cb endif -SUBDIRS += stat_cmds user_chk +SUBDIRS += run_script stat_cmds user_chk diff --git a/src/hooks/dhcp/run_script/.gitattributes b/src/hooks/dhcp/run_script/.gitattributes new file mode 100644 index 0000000000..31e5a07093 --- /dev/null +++ b/src/hooks/dhcp/run_script/.gitattributes @@ -0,0 +1,2 @@ +/run_script_messages.cc -diff merge=ours +/run_script_messages.h -diff merge=ours diff --git a/src/hooks/dhcp/run_script/.gitignore b/src/hooks/dhcp/run_script/.gitignore new file mode 100644 index 0000000000..35b5e99aee --- /dev/null +++ b/src/hooks/dhcp/run_script/.gitignore @@ -0,0 +1 @@ +/html diff --git a/src/hooks/dhcp/run_script/Makefile.am b/src/hooks/dhcp/run_script/Makefile.am new file mode 100644 index 0000000000..0fa924c9bb --- /dev/null +++ b/src/hooks/dhcp/run_script/Makefile.am @@ -0,0 +1,74 @@ +SUBDIRS = . libloadtests tests + +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) +AM_CXXFLAGS = $(KEA_CXXFLAGS) + +# Ensure that the message file and doxygen file is included in the distribution +EXTRA_DIST = run_script_messages.mes +EXTRA_DIST += run_script.dox + +CLEANFILES = *.gcno *.gcda + +# convenience archive + +noinst_LTLIBRARIES = librun_script.la + +librun_script_la_SOURCES = run_script_callouts.cc +librun_script_la_SOURCES += run_script_log.cc run_script_log.h +librun_script_la_SOURCES += run_script_messages.cc run_script_messages.h +librun_script_la_SOURCES += version.cc + +librun_script_la_CXXFLAGS = $(AM_CXXFLAGS) +librun_script_la_CPPFLAGS = $(AM_CPPFLAGS) + +# install the shared object into $(libdir)/kea/hooks +lib_hooksdir = $(libdir)/kea/hooks +lib_hooks_LTLIBRARIES = libdhcp_run_script.la + +libdhcp_run_script_la_SOURCES = +libdhcp_run_script_la_LDFLAGS = $(AM_LDFLAGS) +libdhcp_run_script_la_LDFLAGS += -avoid-version -export-dynamic -module +libdhcp_run_script_la_LIBADD = librun_script.la +libdhcp_run_script_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la +libdhcp_run_script_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la +libdhcp_run_script_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la +libdhcp_run_script_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la +libdhcp_run_script_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +libdhcp_run_script_la_LIBADD += $(LOG4CPLUS_LIBS) +libdhcp_run_script_la_LIBADD += $(CRYPTO_LIBS) +libdhcp_run_script_la_LIBADD += $(BOOST_LIBS) + +# If we want to get rid of all generated messages files, we need to use +# make maintainer-clean. The proper way to introduce custom commands for +# that operation is to define maintainer-clean-local target. However, +# make maintainer-clean also removes Makefile, so running configure script +# is required. To make it easy to rebuild messages without going through +# reconfigure, a new target messages-clean has been added. +maintainer-clean-local: + rm -f run_script_messages.h run_script_messages.cc + +# To regenerate messages files, one can do: +# +# make messages-clean +# make messages +# +# This is needed only when a .mes file is modified. +messages-clean: maintainer-clean-local + +if GENERATE_MESSAGES + +# Define rule to build logging source files from message file +messages: run_script_messages.h run_script_messages.cc + @echo Message files regenerated + +run_script_messages.h run_script_messages.cc: run_script_messages.mes + $(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/hooks/dhcp/run_script/run_script_messages.mes + +else + +messages run_script_messages.h run_script_messages.cc: + @echo Messages generation disabled. Configure with --enable-generate-messages to enable it. + +endif + diff --git a/src/hooks/dhcp/run_script/run_script.dox b/src/hooks/dhcp/run_script/run_script.dox new file mode 100644 index 0000000000..45ecf765b6 --- /dev/null +++ b/src/hooks/dhcp/run_script/run_script.dox @@ -0,0 +1,65 @@ +// Copyright (C) 2021 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/. + +/** + +@page libdhcp_run_script Kea Run Script Hooks Library + +@section libdhcp_run_scriptIntro Introduction + +Welcome to Kea Run Script Hooks Library. This documentation is addressed to +developers who are interested in the internal operation of the Run Script +library. This file provides information needed to understand and +perhaps extend this library. + +This documentation is stand-alone: you should have read and understood +the Kea +Developer's Guide and in particular its section about hooks. + +@section libdhcp_run_scriptUser Now To Use libdhcp_run_script +## Introduction +libdhcp_run_script is a hooks library which allows an external script to +be run on specific hook points. + +## Configuring the DHCPv4 Module + +It must be configured as a hook library for the desired DHCP server +modules. Note that the bootp library is installed alongside the +Kea libraries in "/lib" where is determined +by the --prefix option of the configure script. It defaults to +"/usr/local". Assuming the default value then, configuring kea-dhcp4 +to load the bootp library could be done with the following Kea4 +configuration: + +@code +"Dhcp4": { + "hook_libraries": [ + { "library": "/usr/local/lib/libdhcp_run_script.so", + "parameters": { "name": "/path_to/script_name.sh", + "sync": false }}, + ... + ] +} +@endcode + +The parameters contain the 'name' which indicates the path and name of the +external script to be called on each hookpoint, and also the 'sync' option +to be able to wait synchronously for the script to finish execution. +If the 'sync' parameter is false, then the script will be launched and all +the OUT parameters of the script will be ignored. + +## Internal operation + +The first function called in @ref load() located in the +bootp_callouts.cc. It checks if the necessary parameter is passed and +decodes the option configurations. @ref unload() free the configuration. + +Kea engine checks if the library has functions that match known hook +point names. This library has several such functions: @ref pkt4_receive, +@ref pkt4_send, @ref pkt6_receive, @ref pkt6_send, etc... +located in run_script_callouts.cc. + +*/ diff --git a/src/hooks/dhcp/run_script/run_script_callouts.cc b/src/hooks/dhcp/run_script/run_script_callouts.cc new file mode 100644 index 0000000000..5478509d3f --- /dev/null +++ b/src/hooks/dhcp/run_script/run_script_callouts.cc @@ -0,0 +1,65 @@ +// Copyright (C) 2021 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/. + +#include + +#include +#include + +namespace isc { +namespace run_script { + +RunScriptPtr impl; + +} // end of namespace flex_option +} // end of namespace isc + +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::hooks; +using namespace isc::run_script; + +// Functions accessed by the hooks framework use C linkage to avoid the name +// mangling that accompanies use of the C++ compiler as well as to avoid +// issues related to namespaces. +extern "C" { + +/// @brief This function is called when the library is loaded. +/// +/// @param handle library handle +/// @return 0 when initialization is successful, 1 otherwise +int load(LibraryHandle& handle) { + try { + impl.reset(new RunScriptImpl()); + } catch (const std::exception& ex) { + LOG_ERROR(run_script_logger, RUN_SCRIPT_LOAD_ERROR) + .arg(ex.what()); + return (1); + } + + LOG_INFO(run_script_logger, RUN_SCRIPT_LOAD); + return (0); +} + +/// @brief This function is called when the library is unloaded. +/// +/// @return always 0. +int unload() { + impl.reset(); + LOG_INFO(run_script_logger, RUN_SCRIPT_UNLOAD); + return (0); +} + +/// @brief This function is called to retrieve the multi-threading compatibility. +/// +/// @return 1 which means compatible with multi-threading. +int multi_threading_compatible() { + return (1); +} + +} // end extern "C" + diff --git a/src/hooks/dhcp/run_script/run_script_log.cc b/src/hooks/dhcp/run_script/run_script_log.cc new file mode 100644 index 0000000000..62af75b466 --- /dev/null +++ b/src/hooks/dhcp/run_script/run_script_log.cc @@ -0,0 +1,17 @@ +// Copyright (C) 2021 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/. + +#include + +#include + +namespace isc { +namespace run_script { + +isc::log::Logger run_script_logger("run-script-hooks"); + +} // namespace bootp +} // namespace isc diff --git a/src/hooks/dhcp/run_script/run_script_log.h b/src/hooks/dhcp/run_script/run_script_log.h new file mode 100644 index 0000000000..10a23d92a6 --- /dev/null +++ b/src/hooks/dhcp/run_script/run_script_log.h @@ -0,0 +1,22 @@ +// Copyright (C) 2021 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/. + +#ifndef RUN_SCRIPT_LOG_H +#define RUN_SCRIPT_LOG_H + +#include +#include +#include +#include + +namespace isc { +namespace run_script { + +extern isc::log::Logger run_script_logger; + +} // end of namespace run_script +} // end of namespace isc +#endif diff --git a/src/hooks/dhcp/run_script/run_script_messages.mes b/src/hooks/dhcp/run_script/run_script_messages.mes new file mode 100644 index 0000000000..348b4ae73a --- /dev/null +++ b/src/hooks/dhcp/run_script/run_script_messages.mes @@ -0,0 +1,11 @@ +# Copyright (C) 2021 Internet Systems Consortium, Inc. ("ISC") + +% RUN_SCRIPT_LOAD Run Script hooks library has been loaded +This info message indicates that the Run Script hooks library has been loaded. + +% RUN_SCRIPT_LOAD_ERROR Run Script hooks library failed: %1 +This error message indicates an error during loading the Run Script hooks +library. The details of the error are provided as argument of the log message. + +% RUN_SCRIPT_UNLOAD Run Script hooks library has been unloaded +This info message indicates that the RunScript hooks library has been unloaded. diff --git a/src/hooks/dhcp/run_script/run_script_wrapper.sh b/src/hooks/dhcp/run_script/run_script_wrapper.sh new file mode 100755 index 0000000000..5687af8530 --- /dev/null +++ b/src/hooks/dhcp/run_script/run_script_wrapper.sh @@ -0,0 +1,17 @@ +if test ${#} -lt 2; then + echo "Usage: ${0} script_name function_name" + echo " All variables used by the script must be available" + echo " as environment variables." + echo " All variables specified in the 'hook_parameters'" + echo " variable will be printed to stdout." + exit 1 +fi + +script_name=${1} +function_name=${2} + +source ${script_name} ${function_name} + +for parameter in ${hook_parameters}; do + echo "${parameter}=${!parameter}" +done diff --git a/src/hooks/dhcp/run_script/tests/run_script_test.sh b/src/hooks/dhcp/run_script/tests/run_script_test.sh new file mode 100755 index 0000000000..eac4366264 --- /dev/null +++ b/src/hooks/dhcp/run_script/tests/run_script_test.sh @@ -0,0 +1,7 @@ +echo ${@} + +SKIP="false" +if test ! -z ${ADDRESS}; then + echo "${ADDRESS}" + SKIP="false" +fi diff --git a/src/lib/util/process_spawn.cc b/src/lib/util/process_spawn.cc index 1cc3717356..249bc96862 100644 --- a/src/lib/util/process_spawn.cc +++ b/src/lib/util/process_spawn.cc @@ -59,8 +59,10 @@ public: /// /// @param executable A path to the program to be executed. /// @param args Arguments for the program to be executed. + /// @param vars Environment variables for the program to be executed. ProcessSpawnImpl(const std::string& executable, - const ProcessArgs& args); + const ProcessArgs& args, + const ProcessEnvVars& vars); /// @brief Destructor. ~ProcessSpawnImpl(); @@ -146,13 +148,18 @@ private: std::string executable_; /// @brief An array holding arguments for the executable. - char** args_; + boost::shared_ptr args_; + + /// @brief An array holding environment variables for the executable. + boost::shared_ptr vars_; }; ProcessSpawnImpl::ProcessSpawnImpl(const std::string& executable, - const ProcessArgs& args) + const ProcessArgs& args, + const ProcessEnvVars& vars) : signals_(new SignalSet(SIGCHLD)), process_state_(), - executable_(executable), args_(new char*[args.size() + 2]) { + executable_(executable), args_(new char*[args.size() + 2]), + vars_(new char*[vars.size() + 1]) { // Set the handler which is invoked immediately when the signal // is received. signals_->setOnReceiptHandler(std::bind(&ProcessSpawnImpl::waitForProcess, @@ -160,12 +167,17 @@ ProcessSpawnImpl::ProcessSpawnImpl(const std::string& executable, // Conversion of the arguments to the C-style array we start by setting // all pointers within an array to NULL to indicate that they haven't // been allocated yet. - memset(args_, 0, (args.size() + 2) * sizeof(char*)); + memset(args_.get(), 0, (args.size() + 2) * sizeof(char*)); + memset(vars_.get(), 0, (vars.size() + 1) * sizeof(char*)); // By convention, the first argument points to an executable name. args_[0] = allocateArg(executable_); // Copy arguments to the array. for (int i = 1; i <= args.size(); ++i) { - args_[i] = allocateArg(args[i-1]); + args_[i] = allocateArg(args[i - 1]); + } + // Copy environment variables to the array. + for (int i = 0; i < vars.size(); ++i) { + vars_[i] = allocateArg(vars[i]); } } @@ -176,8 +188,13 @@ ProcessSpawnImpl::~ProcessSpawnImpl() { delete[] args_[i]; ++i; } - // Deallocate the array. - delete[] args_; + + i = 0; + // Deallocate strings in the array of environment variables. + while (vars_[i] != NULL) { + delete[] vars_[i]; + ++i; + } } std::string @@ -217,8 +234,8 @@ ProcessSpawnImpl::spawn() { // We're in the child process. sigprocmask(SIG_SETMASK, &osset, 0); // Run the executable. - if (execvp(executable_.c_str(), args_) != 0) { - // We may end up here if the execvp failed, e.g. as a result + if (execvpe(executable_.c_str(), args_.get(), vars_.get()) != 0) { + // We may end up here if the execvpe failed, e.g. as a result // of issue with permissions or invalid executable name. _exit(EXIT_FAILURE); } @@ -332,12 +349,12 @@ ProcessSpawnImpl::clearState(const pid_t pid) { } ProcessSpawn::ProcessSpawn(const std::string& executable, - const ProcessArgs& args) - : impl_(new ProcessSpawnImpl(executable, args)) { + const ProcessArgs& args, + const ProcessEnvVars& vars) + : impl_(new ProcessSpawnImpl(executable, args, vars)) { } ProcessSpawn::~ProcessSpawn() { - delete impl_; } std::string diff --git a/src/lib/util/process_spawn.h b/src/lib/util/process_spawn.h index dffd365e9b..c4e931047d 100644 --- a/src/lib/util/process_spawn.h +++ b/src/lib/util/process_spawn.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace isc { namespace util { @@ -27,10 +28,17 @@ public: /// class. class ProcessSpawnImpl; +/// @brief Pointer to a ProcessSpawnImpl class. +typedef boost::shared_ptr ProcessSpawnImplPtr; + /// @brief Type of the container holding arguments of the executable /// being run as a background process. typedef std::vector ProcessArgs; +/// @brief Type of the container holding environment variables of the executable +/// being run as a background process. +typedef std::vector ProcessEnvVars; + /// @brief Utility class for spawning new processes. /// /// This class is used to spawn new process by Kea. It forks the current @@ -64,8 +72,10 @@ public: /// /// @param executable A path to the program to be executed. /// @param args Arguments for the program to be executed. + /// @param vars Environment variables for the program to be executed. ProcessSpawn(const std::string& executable, - const ProcessArgs& args = ProcessArgs()); + const ProcessArgs& args = ProcessArgs(), + const ProcessEnvVars& = ProcessEnvVars()); /// @brief Destructor. ~ProcessSpawn(); @@ -132,9 +142,8 @@ public: private: - /// @brief A pointer to the implementation of this class. - ProcessSpawnImpl* impl_; - + /// @brief A smart pointer to the implementation of this class. + ProcessSpawnImplPtr impl_; }; } diff --git a/src/lib/util/tests/process_spawn_app.sh.in b/src/lib/util/tests/process_spawn_app.sh.in index c0b4a54b4c..806e7bd702 100644 --- a/src/lib/util/tests/process_spawn_app.sh.in +++ b/src/lib/util/tests/process_spawn_app.sh.in @@ -45,6 +45,16 @@ do shift sleep "${1}" ;; + -v) + shift + VAR_NAME=${1} + shift + VAR_VALUE=${1} + EXPECTED=$(env | grep ${VAR_NAME}) + if ! test "${VAR_NAME}=${VAR_VALUE}" = "${EXPECTED}"; then + exit 123 + fi + ;; *) exit 123 ;; diff --git a/src/lib/util/tests/process_spawn_unittest.cc b/src/lib/util/tests/process_spawn_unittest.cc index 016fbbebcb..6137af9d7f 100644 --- a/src/lib/util/tests/process_spawn_unittest.cc +++ b/src/lib/util/tests/process_spawn_unittest.cc @@ -122,6 +122,25 @@ TEST(ProcessSpawn, spawnWithArgs) { EXPECT_EQ(64, process.getExitStatus(pid)); } +// This test verifies that the external application can be ran with +// arguments and environment variables that the exit code is gathered. +TEST(ProcessSpawn, spawnWithArgsAndEnvVars) { + std::vector args; + std::vector vars; + args.push_back("-v"); + args.push_back("TEST_VARIABLE_NAME"); + args.push_back("TEST_VARIABLE_VALUE"); + vars.push_back("TEST_VARIABLE_NAME=TEST_VARIABLE_VALUE"); + + ProcessSpawn process(getApp(), args, vars); + pid_t pid = 0; + ASSERT_NO_THROW(pid = process.spawn()); + + ASSERT_TRUE(waitForProcess(process, pid, 2)); + + EXPECT_EQ(32, process.getExitStatus(pid)); +} + // This test verifies that the single ProcessSpawn object can be used // to start two processes and that their status codes can be gathered. // It also checks that it is possible to clear the status of the @@ -164,7 +183,6 @@ TEST(ProcessSpawn, spawnNoArgs) { EXPECT_EQ(32, process.getExitStatus(pid)); } - // This test verifies that the EXIT_FAILURE code is returned when // application can't be executed. TEST(ProcessSpawn, invalidExecutable) { @@ -247,5 +265,4 @@ TEST(ProcessSpawn, errnoInvariance) { EXPECT_EQ(123, errno); } - } // end of anonymous namespace