Browse Source

[master] Merge branch 'trac5100'

Marcin Siodelski 8 years ago
parent
commit
d0c7cb29a7

+ 34 - 1
src/bin/dhcp4/dhcp4_hooks.dox

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-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
@@ -292,6 +292,39 @@ to the end of this list.
   expired leases will remain in the database and their recovery will
   be attempted during the next reclaim cycle.
 
+@subsection dhcpv4HooksControlCommandReceive control_command_receive
+
+ - @b Arguments:
+   - name: @b command, type: isc::data::ConstElementPtr, direction: <b>in/out</b>
+   - name: @b response, type: isc::data::ConstElementPtr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when DHCPv4 server receives a
+   control command over the command channel (typically unix domain socket).
+   The "command" argument is a pointer to the parsed JSON structure
+   including command name and command arguments. If the callout implements
+   the specified command, it handles the command and creates appropriate
+   response. The response should be returned in the "response" argument.
+   In most cases, the callout which handles the command will set the next
+   step action to SKIP, to prevent the server from trying to handle the
+   command on its own and overriding the response created by the callouts.
+   A notable exception is the 'list-commands' command for which the callouts
+   should not set the next step action to SKIP. The server has a special
+   code path for this command which combines the list of commands returned
+   by the callouts with the list of commands supported by the server. If
+   the callout sets the next step action to SKIP in this case, the server
+   will only return the list of commands supported by the hook library.
+   The callout can modify the command arguments to influence the command
+   processing by the Command Manager. For example, it may freely modify
+   the configuration received in 'set-config' before it is processed by
+   the server. The SKIP action is not set in this case.
+
+ - <b>Next step status</b>: if any callout sets the next step action to SKIP,
+   the server will assume that the command has been handled by the callouts
+   and will expect that the response is provided in the "response" argument.
+   The server will not handle the command in this case but simply return the
+   response returned by the callout to the caller.
+
+
 @section dhcpv4HooksOptionsAccess Accessing DHCPv4 Options within a Packet
 When the server constructs a response message to a client it includes
 DHCP options configured for this client in a response message. Apart

+ 34 - 1
src/bin/dhcp6/dhcp6_hooks.dox

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-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
@@ -334,6 +334,39 @@ to the end of this list.
   expired leases will remain in the database and their recovery will
   be attempted during the next reclaim cycle.
 
+@subsection dhcpv6HooksControlCommandReceive control_command_receive
+
+ - @b Arguments:
+   - name: @b command, type: ConstElementPtr, direction: <b>in/out</b>
+   - name: @b response, type: ConstElementPtr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when DHCPv4 server receives a
+   control command over the command channel (typically unix domain socket).
+   The "command" argument is a pointer to the parsed JSON structure
+   including command name and command arguments. If the callout implements
+   the specified command, it handles the command and creates appropriate
+   response. The response should be returned in the "response" argument.
+   In most cases, the callout which handles the command will set the next
+   step action to SKIP, to prevent the server from trying to handle the
+   command on its own and overriding the response created by the callouts.
+   A notable exception is the 'list-commands' command for which the callouts
+   should not set the next step action to SKIP. The server has a special
+   code path for this command which combines the list of commands returned
+   by the callouts with the list of commands supported by the server. If
+   the callout sets the next step action to SKIP in this case, the server
+   will only return the list of commands supported by the hook library.
+   The callout can modify the command arguments to influence the command
+   processing by the Command Manager. For example, it may freely modify
+   the configuration received in 'set-config' before it is processed by
+   the server. The SKIP action is not set in this case.
+
+ - <b>Next step status</b>: if any callout sets the next step action to SKIP,
+   the server will assume that the command has been handled by the callouts
+   and will expect that the response is provided in the "response" argument.
+   The server will not handle the command in this case but simply return the
+   response returned by the callout to the caller.
+
+
 @section dhcpv6HooksOptionsAccess Accessing DHCPv6 Options within a Packet
 When the server constructs a response message to a client it includes
 DHCP options configured for this client in a response message. Apart

+ 3 - 0
src/lib/config/Makefile.am

@@ -15,16 +15,19 @@ BUILT_SOURCES = config_messages.h config_messages.cc
 lib_LTLIBRARIES = libkea-cfgclient.la
 libkea_cfgclient_la_SOURCES = config_data.h config_data.cc
 libkea_cfgclient_la_SOURCES += module_spec.h module_spec.cc
+libkea_cfgclient_la_SOURCES += base_command_mgr.cc base_command_mgr.h
 libkea_cfgclient_la_SOURCES += command_mgr.cc command_mgr.h
 libkea_cfgclient_la_SOURCES += command_socket.cc command_socket.h
 libkea_cfgclient_la_SOURCES += command_socket_factory.cc command_socket_factory.h
 libkea_cfgclient_la_SOURCES += config_log.h config_log.cc
+libkea_cfgclient_la_SOURCES += hooked_command_mgr.cc hooked_command_mgr.h
 
 libkea_cfgclient_la_LIBADD = $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
+libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la

+ 169 - 0
src/lib/config/base_command_mgr.cc

@@ -0,0 +1,169 @@
+// Copyright (C) 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/.
+
+#include <cc/command_interpreter.h>
+#include <config/base_command_mgr.h>
+#include <config/config_log.h>
+#include <boost/bind.hpp>
+#include <set>
+
+using namespace isc::data;
+
+namespace isc {
+namespace config {
+
+BaseCommandMgr::BaseCommandMgr() {
+    registerCommand("list-commands", boost::bind(&BaseCommandMgr::listCommandsHandler,
+                                                 this, _1, _2));
+}
+
+void
+BaseCommandMgr::registerCommand(const std::string& cmd, CommandHandler handler) {
+    if (!handler) {
+        isc_throw(InvalidCommandHandler, "Specified command handler is NULL");
+    }
+
+    HandlerContainer::const_iterator it = handlers_.find(cmd);
+    if (it != handlers_.end()) {
+        isc_throw(InvalidCommandName, "Handler for command '" << cmd
+                  << "' is already installed.");
+    }
+
+    handlers_.insert(make_pair(cmd, handler));
+
+    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_REGISTERED).arg(cmd);
+}
+
+void
+BaseCommandMgr::deregisterCommand(const std::string& cmd) {
+    if (cmd == "list-commands") {
+        isc_throw(InvalidCommandName,
+                  "Can't uninstall internal command 'list-commands'");
+    }
+
+    HandlerContainer::iterator it = handlers_.find(cmd);
+    if (it == handlers_.end()) {
+        isc_throw(InvalidCommandName, "Handler for command '" << cmd
+                  << "' not found.");
+    }
+    handlers_.erase(it);
+
+    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_DEREGISTERED).arg(cmd);
+}
+
+void
+BaseCommandMgr::deregisterAll() {
+
+    // No need to log anything here. deregisterAll is not used in production
+    // code, just in tests.
+    handlers_.clear();
+    registerCommand("list-commands",
+        boost::bind(&BaseCommandMgr::listCommandsHandler, this, _1, _2));
+}
+
+isc::data::ConstElementPtr
+BaseCommandMgr::processCommand(const isc::data::ConstElementPtr& cmd) {
+    if (!cmd) {
+        return (createAnswer(CONTROL_RESULT_ERROR,
+                             "Command processing failed: NULL command parameter"));
+    }
+
+    try {
+        ConstElementPtr arg;
+        std::string name = parseCommand(arg, cmd);
+
+        LOG_INFO(command_logger, COMMAND_RECEIVED).arg(name);
+
+        return (handleCommand(name, arg));
+
+    } catch (const Exception& e) {
+        LOG_WARN(command_logger, COMMAND_PROCESS_ERROR2).arg(e.what());
+        return (createAnswer(CONTROL_RESULT_ERROR,
+                             std::string("Error during command processing: ")
+                             + e.what()));
+    }
+}
+
+ConstElementPtr
+BaseCommandMgr::combineCommandsLists(const ConstElementPtr& response1,
+                                     const ConstElementPtr& response2) const {
+    // Usually when this method is called there should be two non-null
+    // responses. If there is just a single response, return this
+    // response.
+    if (!response1 && response2) {
+        return (response2);
+
+    } else if (response1 && !response2) {
+        return (response1);
+
+    } else if (!response1 && !response2) {
+        return (ConstElementPtr());
+
+    } else {
+        // Both responses are non-null so we need to combine the lists
+        // of supported commands if the status codes are 0.
+        int status_code;
+        ConstElementPtr args1 = parseAnswer(status_code, response1);
+        if (status_code != 0) {
+            return (response1);
+        }
+
+        ConstElementPtr args2 = parseAnswer(status_code, response2);
+        if (status_code != 0) {
+            return (response2);
+        }
+
+        const std::vector<ElementPtr> vec1 = args1->listValue();
+        const std::vector<ElementPtr> vec2 = args2->listValue();
+
+        // Storing command names in a set guarantees that the non-unique
+        // command names are aggregated.
+        std::set<std::string> combined_set;
+        for (auto v = vec1.cbegin(); v != vec1.cend(); ++v) {
+            combined_set.insert((*v)->stringValue());
+        }
+        for (auto v = vec2.cbegin(); v != vec2.cend(); ++v) {
+            combined_set.insert((*v)->stringValue());
+        }
+
+        // Create a combined list of commands.
+        ElementPtr combined_list = Element::createList();
+        for (auto s = combined_set.cbegin(); s != combined_set.cend(); ++s) {
+            combined_list->add(Element::create(*s));
+        }
+        return (createAnswer(CONTROL_RESULT_SUCCESS, combined_list));
+    }
+}
+
+ConstElementPtr
+BaseCommandMgr::handleCommand(const std::string& cmd_name,
+                              const ConstElementPtr& params) {
+    auto it = handlers_.find(cmd_name);
+    if (it == handlers_.end()) {
+        // Ok, there's no such command.
+        return (createAnswer(CONTROL_RESULT_ERROR,
+                             "'" + cmd_name + "' command not supported."));
+    }
+
+    // Call the actual handler and return whatever it returned
+    return (it->second(cmd_name, params));
+}
+
+isc::data::ConstElementPtr
+BaseCommandMgr::listCommandsHandler(const std::string& name,
+                                    const isc::data::ConstElementPtr& ) {
+    using namespace isc::data;
+    ElementPtr commands = Element::createList();
+    for (HandlerContainer::const_iterator it = handlers_.begin();
+         it != handlers_.end(); ++it) {
+        commands->add(Element::create(it->first));
+    }
+    return (createAnswer(CONTROL_RESULT_SUCCESS, commands));
+}
+
+
+} // namespace isc::config
+} // namespace isc

+ 184 - 0
src/lib/config/base_command_mgr.h

@@ -0,0 +1,184 @@
+// Copyright (C) 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/.
+
+#ifndef BASE_COMMAND_MGR_H
+#define BASE_COMMAND_MGR_H
+
+#include <cc/data.h>
+#include <exceptions/exceptions.h>
+#include <boost/function.hpp>
+#include <map>
+#include <string>
+
+namespace isc {
+namespace config {
+
+/// @brief Exception indicating that the handler specified is not valid
+class InvalidCommandHandler : public Exception {
+public:
+    InvalidCommandHandler(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception indicating that the command name is not valid
+class InvalidCommandName : public Exception {
+public:
+    InvalidCommandName(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Commands Manager, responsible for processing external commands.
+///
+/// Commands Manager is a generic interface for handling external commands.
+/// Commands are received over control sockets. Derivations of this class
+/// provide implementations of the control socket layers, e.g. unix domain
+/// sockets, TCP sockets etc. This base class merely provides methods to manage
+/// command handling functions, i.e. register commands, deregister commands.
+/// It also includes a @ref BaseCommandMgr::processCommand method which
+/// uses the command as an input and invokes appropriate handlers.
+///
+/// The commands and responses are formatted using JSON.
+/// See http://kea.isc.org/wiki/StatsDesign for details.
+///
+/// Below is an example of the command using JSON format:
+/// @code
+/// {
+///     "command": "statistic-get",
+///     "arguments": {
+///         "name": "received-packets"
+///     }
+/// }
+/// @endcode
+///
+/// And the response is:
+///
+/// @code
+/// {
+///     "result": 0,
+///     "arguments": {
+///         "received-packets": [ [ 1234, "2015-04-15 12:34:45.123" ] ]
+///     }
+/// }
+/// @endcode
+///
+/// BaseCommandsMgr does not implement the commands (except one,
+/// "list-commands") itself, but rather provides an interface
+/// (see @ref registerCommand, @ref deregisterCommand, @ref processCommand)
+/// for other components to use it.
+class BaseCommandMgr {
+public:
+
+    /// @brief Defines command handler type
+    ///
+    /// Command handlers are expected to use this format.
+    ///
+    /// @param name name of the commands
+    /// @param params parameters specific to the command
+    /// @return response (created with createAnswer())
+    typedef boost::function<isc::data::ConstElementPtr (const std::string& name,
+        const isc::data::ConstElementPtr& params)> CommandHandler;
+
+    /// @brief Constructor.
+    ///
+    /// Registers "list-commands" command.
+    BaseCommandMgr();
+
+    /// @brief Destructor.
+    virtual ~BaseCommandMgr() { };
+
+    /// @brief Triggers command processing.
+    ///
+    /// This method processes specified command. The command is specified using
+    /// a single Element. See @ref BaseCommandMgr for description of its syntax.
+    ///
+    /// @param cmd Pointer to the data element representing command in JSON
+    /// format.
+    isc::data::ConstElementPtr
+    processCommand(const isc::data::ConstElementPtr& cmd);
+
+    /// @brief Registers specified command handler for a given command
+    ///
+    /// @param cmd Name of the command to be handled.
+    /// @param handler Pointer to the method that will handle the command.
+    void registerCommand(const std::string& cmd, CommandHandler handler);
+
+    /// @brief Deregisters specified command handler.
+    ///
+    /// @param cmd Name of the command that's no longer handled.
+    void deregisterCommand(const std::string& cmd);
+
+    /// @brief Auxiliary method that removes all installed commands.
+    ///
+    /// The only unwipeable method is list-commands, which is internally
+    /// handled at all times.
+    void deregisterAll();
+
+protected:
+
+    /// @brief Combines lists of commands carried in two responses.
+    ///
+    /// This method is used to combine list of commands returned by the
+    /// hook library with the commands supported by the local Command
+    /// Manager. This method should also be used within the hook library
+    /// to combine commands supported by this hook library with the
+    /// commands returned by other hook libraries attached to the server
+    /// at the same time.
+    ///
+    /// If the same command appears in two responses only a single
+    /// instance is returned in the combined response.
+    ///
+    /// @param response1 First command response.
+    /// @param response2 Second command response.
+    ///
+    /// @return Pointer to the 'list-commands' response holding combined
+    /// list of commands.
+    isc::data::ConstElementPtr
+    combineCommandsLists(const isc::data::ConstElementPtr& response1,
+                         const isc::data::ConstElementPtr& response2) const;
+
+    /// @brief Handles the command having a given name and arguments.
+    ///
+    /// This method can be overridden in the derived classes to provide
+    /// custom logic for processing commands. For example, the
+    /// @ref HookedCommandMgr extends this method to delegate commands
+    /// processing to a hook library.
+    ///
+    /// @param cmd_name Command name.
+    /// @param params Command arguments.
+    ///
+    /// @return Pointer to the const data element representing response
+    /// to a command.
+    virtual isc::data::ConstElementPtr
+    handleCommand(const std::string& cmd_name,
+                  const isc::data::ConstElementPtr& params);
+
+    /// @brief Type of the container for command handlers.
+    typedef std::map<std::string, CommandHandler> HandlerContainer;
+
+    /// @brief Container for command handlers.
+    HandlerContainer handlers_;
+
+private:
+
+    /// @brief 'list-commands' command handler.
+    ///
+    /// This method implements command 'list-commands'. It returns a list of all
+    /// currently supported commands.
+    ///
+    /// @param name Name of the command (should always be 'list-commands').
+    /// @param params Additional parameters (ignored).
+    ///
+    /// @return Pointer to the structure that includes all currently supported
+    /// commands.
+    isc::data::ConstElementPtr
+    listCommandsHandler(const std::string& name,
+                        const isc::data::ConstElementPtr& params);
+};
+
+} // end of namespace isc::config
+} // end of namespace isc
+
+#endif

+ 2 - 88
src/lib/config/command_mgr.cc

@@ -18,9 +18,8 @@ using namespace isc::data;
 namespace isc {
 namespace config {
 
-CommandMgr::CommandMgr() {
-    registerCommand("list-commands",
-        boost::bind(&CommandMgr::listCommandsHandler, this, _1, _2));
+CommandMgr::CommandMgr()
+    : HookedCommandMgr() {
 }
 
 CommandSocketPtr
@@ -77,48 +76,6 @@ CommandMgr::instance() {
     return (cmd_mgr);
 }
 
-void CommandMgr::registerCommand(const std::string& cmd, CommandHandler handler) {
-
-    if (!handler) {
-        isc_throw(InvalidCommandHandler, "Specified command handler is NULL");
-    }
-
-    HandlerContainer::const_iterator it = handlers_.find(cmd);
-    if (it != handlers_.end()) {
-        isc_throw(InvalidCommandName, "Handler for command '" << cmd
-                  << "' is already installed.");
-    }
-
-    handlers_.insert(make_pair(cmd, handler));
-
-    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_REGISTERED).arg(cmd);
-}
-
-void CommandMgr::deregisterCommand(const std::string& cmd) {
-    if (cmd == "list-commands") {
-        isc_throw(InvalidCommandName,
-                  "Can't uninstall internal command 'list-commands'");
-    }
-
-    HandlerContainer::iterator it = handlers_.find(cmd);
-    if (it == handlers_.end()) {
-        isc_throw(InvalidCommandName, "Handler for command '" << cmd
-                  << "' not found.");
-    }
-    handlers_.erase(it);
-
-    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_DEREGISTERED).arg(cmd);
-}
-
-void CommandMgr::deregisterAll() {
-
-    // No need to log anything here. deregisterAll is not used in production
-    // code, just in tests.
-    handlers_.clear();
-    registerCommand("list-commands",
-        boost::bind(&CommandMgr::listCommandsHandler, this, _1, _2));
-}
-
 void
 CommandMgr::commandReader(int sockfd) {
 
@@ -217,48 +174,5 @@ CommandMgr::commandReader(int sockfd) {
     }
 }
 
-isc::data::ConstElementPtr
-CommandMgr::processCommand(const isc::data::ConstElementPtr& cmd) {
-    if (!cmd) {
-        return (createAnswer(CONTROL_RESULT_ERROR,
-                             "Command processing failed: NULL command parameter"));
-    }
-
-    try {
-        ConstElementPtr arg;
-        std::string name = parseCommand(arg, cmd);
-
-        LOG_INFO(command_logger, COMMAND_RECEIVED).arg(name);
-
-        HandlerContainer::const_iterator it = handlers_.find(name);
-        if (it == handlers_.end()) {
-            // Ok, there's no such command.
-            return (createAnswer(CONTROL_RESULT_ERROR,
-                                 "'" + name + "' command not supported."));
-        }
-
-        // Call the actual handler and return whatever it returned
-        return (it->second(name, arg));
-
-    } catch (const Exception& e) {
-        LOG_WARN(command_logger, COMMAND_PROCESS_ERROR2).arg(e.what());
-        return (createAnswer(CONTROL_RESULT_ERROR,
-                             std::string("Error during command processing:")
-                             + e.what()));
-    }
-}
-
-isc::data::ConstElementPtr
-CommandMgr::listCommandsHandler(const std::string& name,
-                                const isc::data::ConstElementPtr& params) {
-    using namespace isc::data;
-    ElementPtr commands = Element::createList();
-    for (HandlerContainer::const_iterator it = handlers_.begin();
-         it != handlers_.end(); ++it) {
-        commands->add(Element::create(it->first));
-    }
-    return (createAnswer(CONTROL_RESULT_SUCCESS, commands));
-}
-
 }; // end of isc::config
 }; // end of isc

+ 6 - 99
src/lib/config/command_mgr.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 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
@@ -8,72 +8,21 @@
 #define COMMAND_MGR_H
 
 #include <cc/data.h>
+#include <config/hooked_command_mgr.h>
 #include <config/command_socket.h>
 #include <boost/noncopyable.hpp>
-#include <boost/function.hpp>
-#include <string>
 #include <list>
-#include <map>
 
 namespace isc {
 namespace config {
 
-/// @brief CommandMgr exception indicating that the handler specified is not valid
-class InvalidCommandHandler : public Exception {
-public:
-    InvalidCommandHandler(const char* file, size_t line, const char* what) :
-        isc::Exception(file, line, what) { };
-};
-
-/// @brief CommandMgr exception indicating that the command name is not valid
-class InvalidCommandName : public Exception {
-public:
-    InvalidCommandName(const char* file, size_t line, const char* what) :
-        isc::Exception(file, line, what) { };
-};
-
-/// @brief Commands Manager, responsible for processing external commands
+/// @brief Commands Manager implementation for the Kea servers.
 ///
-/// Commands Manager is a generic interface for handling external commands.
-/// Commands can be received over control sockets. Currently unix socket is
-/// supported, but additional type (udp, tcp, https etc.) may be added later.
-/// The commands and responses are sent in JSON format.
-/// See http://kea.isc.org/wiki/StatsDesign for details.
-///
-/// In general, the command has the following format:
-/// {
-///     "command": "statistic-get",
-///     "arguments": {
-///         "name": "received-packets"
-///     }
-/// }
-///
-/// And the response is:
-///
-/// {
-///     "result": 0,
-///     "observations": {
-///         "received-packets": [ [ 1234, "2015-04-15 12:34:45.123" ] ]
-///     }
-/// }
-///
-/// CommandsMgr does not implement the commands (except one, "list-commands")
-/// itself, but rather provides an interface (see @ref registerCommand,
-/// @ref deregisterCommand, @ref processCommand) for other components to use
-/// it. The @ref CommandHandler type is specified in a way to easily use
-/// existing command handlers in DHCPv4 and DHCPv6 components.
-class CommandMgr : public boost::noncopyable {
+/// This class extends @ref BaseCommandMgr with the ability to receive and
+/// respond to commands over unix domain sockets.
+class CommandMgr : public HookedCommandMgr, public boost::noncopyable {
 public:
 
-    /// @brief Defines command handler type
-    ///
-    /// Command handlers are expected to use this format.
-    /// @param name name of the commands
-    /// @param params parameters specific to the command
-    /// @return response (created with createAnswer())
-    typedef boost::function<isc::data::ConstElementPtr (const std::string& name,
-        const isc::data::ConstElementPtr& params)> CommandHandler;
-
     /// @brief CommandMgr is a singleton class. This method returns reference
     /// to its sole instance.
     ///
@@ -98,26 +47,6 @@ public:
     /// @brief Shuts down any open control sockets
     void closeCommandSocket();
 
-    /// @brief Registers specified command handler for a given command
-    ///
-    /// @param cmd name of the command to be handled
-    /// @param handler pointer to the method that will handle the command
-    void registerCommand(const std::string& cmd, CommandHandler handler);
-
-    /// @brief Deregisters specified command handler
-    ///
-    /// @param cmd name of the command that's no longer handled
-    void deregisterCommand(const std::string& cmd);
-
-    /// @brief Triggers command processing
-    ///
-    /// This method processes specified command. The command is specified using
-    /// a single Element. See @ref CommandMgr for description of its syntax.
-    /// Typically, this method is called internally, when there's a new data
-    /// received over control socket. However, in some cases (e.g. signal received)
-    /// it may be called by external code explicitly. Hence this method is public.
-    isc::data::ConstElementPtr processCommand(const isc::data::ConstElementPtr& cmd);
-
     /// @brief Reads data from a socket, parses as JSON command and processes it
     ///
     /// This method is used to handle traffic on connected socket. This callback
@@ -130,12 +59,6 @@ public:
     /// @param sockfd socket descriptor of a connected socket
     static void commandReader(int sockfd);
 
-    /// @brief Auxiliary method that removes all installed commands.
-    ///
-    /// The only unwipeable method is list-commands, which is internally
-    /// handled at all times.
-    void deregisterAll();
-
     /// @brief Adds an information about opened connection socket
     ///
     /// @param conn Connection socket to be stored
@@ -161,22 +84,6 @@ private:
     /// Registers internal 'list-commands' command.
     CommandMgr();
 
-    /// @brief 'list-commands' command handler
-    ///
-    /// This method implements command 'list-commands'. It returns a list of all
-    /// currently supported commands.
-    /// @param name name of the command (should always be 'list-commands')
-    /// @param params additional parameters (ignored)
-    /// @return structure that includes all currently supported commands
-    isc::data::ConstElementPtr
-    listCommandsHandler(const std::string& name,
-                        const isc::data::ConstElementPtr& params);
-
-    typedef std::map<std::string, CommandHandler> HandlerContainer;
-
-    /// @brief Container for command handlers
-    HandlerContainer handlers_;
-
     /// @brief Control socket structure
     ///
     /// This is the socket that accepts incoming connections. There can be at

+ 7 - 0
src/lib/config/config_messages.mes

@@ -11,6 +11,13 @@ This debug message indicates that the daemon stopped supporting specified
 command. This command can no longer be issued. If the command socket is
 open and this command is issued, the daemon will not be able to process it.
 
+% COMMAND_HOOK_RECEIVE_SKIP command %1 has been handled by the hook library which returned the skip state
+This debug message is issued when a hook library has processed the control
+command and returned the skip status. The callout should have set the
+'response' argument which contains the result of processing the command.
+The Command Manager skips processing of this command and simply returns
+the response generated by the hook library.
+
 % COMMAND_PROCESS_ERROR1 Error while processing command: %1
 This warning message indicates that the server encountered an error while
 processing received command. Additional information will be provided, if

+ 112 - 0
src/lib/config/hooked_command_mgr.cc

@@ -0,0 +1,112 @@
+// Copyright (C) 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/.
+
+#include <cc/command_interpreter.h>
+#include <config/hooked_command_mgr.h>
+#include <config/config_log.h>
+#include <hooks/hooks_manager.h>
+#include <boost/pointer_cast.hpp>
+
+using namespace isc::data;
+using namespace isc::hooks;
+
+namespace {
+
+/// @brief Structure that holds registered hook indexes.
+struct CommandMgrHooks {
+    /// @brief Index for "control_command_receive" hook point.
+    int hook_index_control_command_receive_;
+
+    /// @brief Constructor that registers hook points for HookedCommandMgr
+    CommandMgrHooks() {
+        hook_index_control_command_receive_ =
+            HooksManager::registerHook("control_command_receive");
+    }
+};
+
+// Declare a hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+CommandMgrHooks Hooks;
+
+} // end of anonymous namespace
+
+namespace isc {
+namespace config {
+
+HookedCommandMgr::HookedCommandMgr()
+    : BaseCommandMgr(), callout_handle_(HooksManager::createCalloutHandle()) {
+}
+
+ConstElementPtr
+HookedCommandMgr::handleCommand(const std::string& cmd_name,
+                                const ConstElementPtr& params) {
+    if (!callout_handle_) {
+        isc_throw(Unexpected, "callout handle not configured for the Command "
+                  "Manager: this is a programming error");
+    }
+
+    std::string final_cmd_name = cmd_name;
+    ConstElementPtr final_params = boost::const_pointer_cast<Element>(params);
+
+    ConstElementPtr hook_response;
+    if (HooksManager::calloutsPresent(Hooks.hook_index_control_command_receive_)) {
+
+        // Delete previously set arguments.
+        callout_handle_->deleteAllArguments();
+
+        // Being in this function we don't have access to the original data
+        // object holding the whole command (name and arguments). Let's
+        // recreate it.
+        ConstElementPtr original_command = createCommand(cmd_name, params);
+
+        // And pass it to the hook library.
+        callout_handle_->setArgument("command", original_command);
+        callout_handle_->setArgument("response", hook_response);
+
+        HooksManager::callCallouts(Hooks.hook_index_control_command_receive_,
+                                   *callout_handle_);
+
+        // The callouts should set the response.
+        callout_handle_->getArgument("response", hook_response);
+
+        // If the hook return 'skip' status, simply return the response.
+        if (callout_handle_->getStatus() == CalloutHandle::NEXT_STEP_SKIP) {
+            LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_HOOK_RECEIVE_SKIP)
+                .arg(cmd_name);
+
+            return (hook_response);
+
+        }
+
+        // The hook library can modify the command or arguments. Thus, we
+        // retrieve the command returned by the callouts and use it as input
+        // to the local command handler.
+        ConstElementPtr hook_command;
+        callout_handle_->getArgument("command", hook_command);
+        final_cmd_name = parseCommand(final_params, hook_command);
+    }
+
+    // If we're here it means that the callouts weren't called or the 'skip'
+    // status wasn't returned. The latter is the case when the 'list-commands'
+    // is being processed. Anyhow, we need to handle the command using local
+    // Command Mananger.
+    ConstElementPtr response = BaseCommandMgr::handleCommand(final_cmd_name,
+                                                             final_params);
+
+    // For the 'list-commands' case we will have to combine commands supported
+    // by the hook libraries with the commands that this Command Manager supports.
+    if ((final_cmd_name == "list-commands") && hook_response && response) {
+        response = combineCommandsLists(hook_response, response);
+    }
+
+    return (response);
+}
+
+
+} // end of namespace isc::config
+} // end of namespace isc

+ 70 - 0
src/lib/config/hooked_command_mgr.h

@@ -0,0 +1,70 @@
+// Copyright (C) 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/.
+
+#ifndef HOOKED_COMMAND_MGR_H
+#define HOOKED_COMMAND_MGR_H
+
+#include <cc/data.h>
+#include <config/base_command_mgr.h>
+#include <hooks/callout_handle.h>
+
+namespace isc {
+namespace config {
+
+/// @brief Command Manager which can delegate commands to a hook library.
+///
+/// This class extends @ref BaseCommandMgr with the logic to delegate the
+/// commands to a hook library if the hook library is installed and provides
+/// callouts for the control API.
+class HookedCommandMgr : public BaseCommandMgr {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Initializes callout handle used by the Command Manager.
+    HookedCommandMgr();
+
+protected:
+
+    /// @brief Returns callout handle to the derived class.
+    ///
+    /// @return const pointer to the callout handle.
+    const isc::hooks::CalloutHandlePtr& getCalloutHandle() const {
+        return (callout_handle_);
+    }
+
+private:
+
+    /// @brief Handles the command having a given name and arguments.
+    ///
+    /// This method checks if the hook library is installed which implements
+    /// callouts for the 'control_command_receive' hook point, and calls them
+    /// if they exist. If the hook library supports the given command it creates
+    /// a response and returns it in the 'response' argument of the
+    /// @ref ConstElementPtr type. If the callout also sets the 'skip' status,
+    /// the response created by the callout is returned. Otherwise, the
+    /// @ref BaseCommandMgr::handleCommand is called.
+    ///
+    /// This method is private because it is its final implementation which
+    /// should not be overridden in the derived classes.
+    ///
+    /// @param cmd_name Command name.
+    /// @param params Command arguments.
+    ///
+    /// @return Pointer to the const data element representing response
+    /// to a command.
+    virtual isc::data::ConstElementPtr
+    handleCommand(const std::string& cmd_name,
+                  const isc::data::ConstElementPtr& params);
+
+    /// @brief Pointer to a callout handle used by this class.
+    isc::hooks::CalloutHandlePtr callout_handle_;
+};
+
+} // end of namespace isc::config
+} // end of namespace isc
+
+#endif

+ 1 - 0
src/lib/config/tests/Makefile.am

@@ -32,6 +32,7 @@ run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
 run_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
 run_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
 run_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
+run_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
 run_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
 run_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
 run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la

+ 280 - 3
src/lib/config/tests/command_mgr_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 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
@@ -6,11 +6,19 @@
 
 #include <gtest/gtest.h>
 
+#include <config/base_command_mgr.h>
 #include <config/command_mgr.h>
+#include <config/hooked_command_mgr.h>
 #include <cc/command_interpreter.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_handle.h>
+#include <hooks/library_handle.h>
+#include <string>
+#include <vector>
 
-using namespace isc::data;
 using namespace isc::config;
+using namespace isc::data;
+using namespace isc::hooks;
 using namespace std;
 
 // Test class for Command Manager
@@ -25,12 +33,23 @@ public:
 
         CommandMgr::instance().deregisterAll();
         CommandMgr::instance().closeCommandSocket();
+
+        resetCalloutIndicators();
     }
 
     /// Default destructor
-    ~CommandMgrTest() {
+    virtual ~CommandMgrTest() {
         CommandMgr::instance().deregisterAll();
         CommandMgr::instance().closeCommandSocket();
+        resetCalloutIndicators();
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+                "control_command_receive");
+    }
+
+    /// @brief Resets indicators related to callout invocation.
+    static void resetCalloutIndicators() {
+        callout_name = "";
+        callout_argument_names.clear();
     }
 
     /// @brief A simple command handler that always returns an eror
@@ -44,6 +63,105 @@ public:
         return (createAnswer(123, "test error message"));
     }
 
+    /// @brief A simple command handler used from within hook library.
+    ///
+    /// @param name Command name.
+    /// @param params Command arguments.
+    static ConstElementPtr my_hook_handler(const std::string& name,
+                                           const ConstElementPtr& params) {
+        return (createAnswer(234, "text generated by hook handler"));
+    }
+
+    /// @brief Test callback which stores callout name and passed arguments.
+    ///
+    /// This callout doesn't indicate that the command has been processed,
+    /// allowing the Command Manager to process it.
+    ///
+    /// @param callout_handle Handle passed by the hooks framework.
+    /// @return Always 0.
+    static int
+    control_command_receive_callout(CalloutHandle& callout_handle) {
+        callout_name = "control_command_receive";
+
+        ConstElementPtr response;
+        callout_handle.setArgument("response", response);
+
+        callout_argument_names = callout_handle.getArgumentNames();
+        // Sort arguments alphabetically, so as we can access them on
+        // expected positions and verify.
+        std::sort(callout_argument_names.begin(), callout_argument_names.end());
+        return (0);
+    }
+
+    /// @brief Test callback which stores callout name and passed arguments and
+    /// which handles the command.
+    ///
+    /// This callout returns the skip status to indicate the the command has
+    /// been handled.
+    ///
+    /// @param callout_handle Handle passed by the hooks framework.
+    /// @return Always 0.
+    static int
+    control_command_receive_handle_callout(CalloutHandle& callout_handle) {
+        callout_name = "control_command_receive";
+
+        // Create a hooks specific command manager.
+        BaseCommandMgr callout_command_mgr;
+        callout_command_mgr.registerCommand("my-command", my_hook_handler);
+
+        ConstElementPtr command;
+        callout_handle.getArgument("command", command);
+
+        ConstElementPtr arg;
+        std::string command_name = parseCommand(arg, command);
+
+        ConstElementPtr response = callout_command_mgr.processCommand(command);
+        callout_handle.setArgument("response", response);
+
+        // Set 'skip' status to indicate that the command has been handled.
+        if (command_name != "list-commands") {
+            callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+        }
+
+        callout_argument_names = callout_handle.getArgumentNames();
+        // Sort arguments alphabetically, so as we can access them on
+        // expected positions and verify.
+        std::sort(callout_argument_names.begin(), callout_argument_names.end());
+        return (0);
+    }
+
+    /// @brief Test callback which modifies parameters of the command and
+    /// does not return skip status.
+    ///
+    /// This callout is used to test the case when the callout modifies the
+    /// received command and does not set next state SKIP to propagate the
+    /// command with modified parameters to the local command handler.
+    ///
+    /// @param callout_handle Handle passed by the hooks framework.
+    /// @return Always 0.
+    static int
+    control_command_receive_modify_callout(CalloutHandle& callout_handle) {
+        callout_name = "control_command_receive";
+
+        ConstElementPtr command;
+        callout_handle.getArgument("command", command);
+
+        ConstElementPtr arg;
+        std::string command_name = parseCommand(arg, command);
+
+        ElementPtr new_arg = Element::createList();
+        new_arg->add(Element::create("hook-param"));
+        command = createCommand(command_name, new_arg);
+
+        callout_handle.setArgument("command", command);
+
+        callout_argument_names = callout_handle.getArgumentNames();
+        // Sort arguments alphabetically, so as we can access them on
+        // expected positions and verify.
+        std::sort(callout_argument_names.begin(), callout_argument_names.end());
+        return (0);
+    }
+
     /// @brief Name of the command (used in my_handler)
     static std::string handler_name;
 
@@ -52,6 +170,12 @@ public:
 
     /// @brief Indicates whether my_handler was called
     static bool handler_called;
+
+    /// @brief Holds invoked callout name.
+    static std::string callout_name;
+
+    /// @brief Holds a list of arguments passed to the callout.
+    static std::vector<std::string> callout_argument_names;
 };
 
 /// Name passed to the handler (used in my_handler)
@@ -63,6 +187,12 @@ ConstElementPtr CommandMgrTest::handler_params;
 /// Indicates whether my_handler was called
 bool CommandMgrTest::handler_called(false);
 
+/// Holds invoked callout name.
+std::string CommandMgrTest::callout_name("");
+
+/// Holds a list of arguments passed to the callout.
+std::vector<std::string> CommandMgrTest::callout_argument_names;
+
 // Test checks whether the internal command 'list-commands'
 // is working properly.
 TEST_F(CommandMgrTest, listCommandsEmpty) {
@@ -186,6 +316,11 @@ TEST_F(CommandMgrTest, deregisterAll) {
 // runs through processCommand to check that it's indeed called.
 TEST_F(CommandMgrTest, processCommand) {
 
+    // Register callout so as we can check that it is called before
+    // processing the command by the manager.
+    HooksManager::preCalloutsLibraryHandle().registerCallout(
+        "control_command_receive", control_command_receive_callout);
+
     // Install my handler
     EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command",
                                                            my_handler));
@@ -212,4 +347,146 @@ TEST_F(CommandMgrTest, processCommand) {
     EXPECT_EQ("my-command", handler_name);
     ASSERT_TRUE(handler_params);
     EXPECT_EQ("[ \"just\", \"some\", \"data\" ]", handler_params->str());
+
+    EXPECT_EQ("control_command_receive", callout_name);
+
+    // Check that the appropriate arguments have been set. Include the
+    // 'response' which should have been set by the callout.
+    ASSERT_EQ(2, callout_argument_names.size());
+    EXPECT_EQ("command", callout_argument_names[0]);
+    EXPECT_EQ("response", callout_argument_names[1]);
+}
+
+// Verify that processing a command can be delegated to a hook library.
+TEST_F(CommandMgrTest, delegateProcessCommand) {
+    // Register callout so as we can check that it is called before
+    // processing the command by the manager.
+    HooksManager::preCalloutsLibraryHandle().registerCallout(
+        "control_command_receive", control_command_receive_handle_callout);
+
+    // Install local handler
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command",
+                                                           my_handler));
+
+    // Now tell CommandMgr to process a command 'my-command' with the
+    // specified parameter.
+    ElementPtr my_params = Element::fromJSON("[ \"just\", \"some\", \"data\" ]");
+    ConstElementPtr command = createCommand("my-command", my_params);
+    ConstElementPtr answer;
+    ASSERT_NO_THROW(answer = CommandMgr::instance().processCommand(command));
+
+    // There should be an answer.
+    ASSERT_TRUE(answer);
+
+    // Local handler shouldn't be called because the command is handled by the
+    // hook library.
+    ASSERT_FALSE(handler_called);
+
+    // Returned status should be unique for the hook library.
+    ConstElementPtr answer_arg;
+    int status_code;
+    ASSERT_NO_THROW(answer_arg = parseAnswer(status_code, answer));
+    EXPECT_EQ(234, status_code);
+
+    EXPECT_EQ("control_command_receive", callout_name);
+
+    // Check that the appropriate arguments have been set. Include the
+    // 'response' which should have been set by the callout.
+    ASSERT_EQ(2, callout_argument_names.size());
+    EXPECT_EQ("command", callout_argument_names[0]);
+    EXPECT_EQ("response", callout_argument_names[1]);
+}
+
+// Verify that 'list-command' command returns combined list of supported
+// commands from hook library and from the Kea Command Manager.
+TEST_F(CommandMgrTest, delegateListCommands) {
+    // Register callout so as we can check that it is called before
+    // processing the command by the manager.
+    HooksManager::preCalloutsLibraryHandle().registerCallout(
+        "control_command_receive", control_command_receive_handle_callout);
+
+    // Create my-command-bis which is unique for the local Command Manager,
+    // i.e. not supported by the hook library. This command should also
+    // be returned as a result of processing 'list-commands'.
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command-bis",
+                                                           my_handler));
+
+    // Process command. The command should be routed to the hook library
+    // and the hook library should return the commands it supports.
+    ConstElementPtr command = createCommand("list-commands");
+    ConstElementPtr answer;
+    ASSERT_NO_THROW(answer = CommandMgr::instance().processCommand(command));
+
+    // There should be an answer.
+    ASSERT_TRUE(answer);
+
+    ConstElementPtr answer_arg;
+    int status_code;
+    ASSERT_NO_THROW(answer_arg = parseAnswer(status_code, answer));
+    EXPECT_EQ(0, status_code);
+
+    // The hook library supports: my-command and list-commands commands. The
+    // local Command Manager supports list-commands and my-command-bis. The
+    // combined list should include 3 unique commands.
+    const std::vector<ElementPtr>& commands_list = answer_arg->listValue();
+    ASSERT_EQ(3, commands_list.size());
+    std::vector<std::string> command_names_list;
+    for (auto cmd = commands_list.cbegin(); cmd != commands_list.cend();
+         ++cmd) {
+        command_names_list.push_back((*cmd)->stringValue());
+    }
+    std::sort(command_names_list.begin(), command_names_list.end());
+    EXPECT_EQ("list-commands", command_names_list[0]);
+    EXPECT_EQ("my-command", command_names_list[1]);
+    EXPECT_EQ("my-command-bis", command_names_list[2]);
+}
+
+// This test verifies the scenario in which the hook library influences the
+// command processing by the Kea server. In this test, the callout modifies
+// the arguments of the command and passes the command on to the Command
+// Manager for processing.
+TEST_F(CommandMgrTest, modifyCommandArgsInHook) {
+    // Register callout so as we can check that it is called before
+    // processing the command by the manager.
+    HooksManager::preCalloutsLibraryHandle().registerCallout(
+        "control_command_receive", control_command_receive_modify_callout);
+
+    // Install local handler
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command",
+                                                           my_handler));
+
+    // Now tell CommandMgr to process a command 'my-command' with the
+    // specified parameter.
+    ElementPtr my_params = Element::fromJSON("[ \"just\", \"some\", \"data\" ]");
+    ConstElementPtr command = createCommand("my-command", my_params);
+    ConstElementPtr answer;
+    ASSERT_NO_THROW(answer = CommandMgr::instance().processCommand(command));
+
+    // There should be an answer.
+    ASSERT_TRUE(answer);
+
+    // Returned status should be unique for the my_handler.
+    ConstElementPtr answer_arg;
+    int status_code;
+    ASSERT_NO_THROW(answer_arg = parseAnswer(status_code, answer));
+    EXPECT_EQ(123, status_code);
+
+    // Local handler should have been called after the callout.
+    ASSERT_TRUE(handler_called);
+    EXPECT_EQ("my-command", handler_name);
+    ASSERT_TRUE(handler_params);
+    // Check that the local handler received the command with arguments
+    // set by the callout.
+    EXPECT_EQ("[ \"hook-param\" ]", handler_params->str());
+
+
+    // Check that the callout has been called with appropriate parameters.
+    EXPECT_EQ("control_command_receive", callout_name);
+
+    // Check that the appropriate arguments have been set. Include the
+    // 'response' which should have been set by the callout.
+    ASSERT_EQ(2, callout_argument_names.size());
+    EXPECT_EQ("command", callout_argument_names[0]);
+    EXPECT_EQ("response", callout_argument_names[1]);
+
 }