Browse Source

[5078] Addressed review comments.

Marcin Siodelski 8 years ago
parent
commit
41b560cc8a

+ 84 - 81
src/bin/agent/ca_command_mgr.cc

@@ -15,12 +15,14 @@
 #include <cc/command_interpreter.h>
 #include <cc/command_interpreter.h>
 #include <cc/data.h>
 #include <cc/data.h>
 #include <boost/pointer_cast.hpp>
 #include <boost/pointer_cast.hpp>
+#include <iterator>
 #include <string>
 #include <string>
 #include <vector>
 #include <vector>
 
 
 using namespace isc::asiolink;
 using namespace isc::asiolink;
 using namespace isc::config;
 using namespace isc::config;
 using namespace isc::data;
 using namespace isc::data;
+using namespace isc::hooks;
 using namespace isc::process;
 using namespace isc::process;
 
 
 namespace isc {
 namespace isc {
@@ -40,66 +42,99 @@ ConstElementPtr
 CtrlAgentCommandMgr::handleCommand(const std::string& cmd_name,
 CtrlAgentCommandMgr::handleCommand(const std::string& cmd_name,
                                    const isc::data::ConstElementPtr& params,
                                    const isc::data::ConstElementPtr& params,
                                    const isc::data::ConstElementPtr& original_cmd) {
                                    const isc::data::ConstElementPtr& original_cmd) {
-    ConstElementPtr answer;
+    ConstElementPtr answer = handleCommandInternal(cmd_name, params, original_cmd);
 
 
-    try {
-        // list-commands is a special case. The Control Agent always supports this
-        // command but most of the time users don't want to list commands supported
-        // by the CA but by one of the Kea servers. The user would indicate that
-        // by specifying 'service' value.
-        if (cmd_name == "list-commands") {
-            if (original_cmd && original_cmd->contains("service")) {
-                ConstElementPtr services = original_cmd->get("service");
-                if (services && !services->empty()) {
-                    // The non-empty control command 'service' parameter exists which
-                    // means we will forward this command to the Kea server. Let's
-                    // cheat that Control Agent doesn't support this command to
-                    // avoid it being handled by CA.
-                    answer = createAnswer(CONTROL_RESULT_COMMAND_UNSUPPORTED,
-                                          "forwarding list-commands command");
-                }
-            }
+    if (answer->getType() == Element::list) {
+        return (answer);
+    }
+
+    // In general, the handlers should return a list of answers rather than a
+    // single answer, but in some cases we rely on the generic handlers,
+    // e.g. 'list-commands', which may return a single answer not wrapped in
+    // the list. Such answers need to be wrapped in the list here.
+    ElementPtr answer_list = Element::createList();
+    answer_list->add(boost::const_pointer_cast<Element>(answer));
+
+    return (answer_list);
+}
+
+
+ConstElementPtr
+CtrlAgentCommandMgr::handleCommandInternal(std::string cmd_name,
+                                           isc::data::ConstElementPtr params,
+                                           isc::data::ConstElementPtr original_cmd) {
+
+    ConstElementPtr services = Element::createList();
+
+    // Retrieve 'service' parameter to determine if we should forward the
+    // command or handle it on our own.
+    if (original_cmd && original_cmd->contains("service")) {
+        services = original_cmd->get("service");
+        // If 'service' value is not a list, this is a fatal error. We don't want
+        // to try processing commands that don't adhere to the required format.
+        if (services->getType() != Element::list) {
+            return (createAnswer(CONTROL_RESULT_ERROR, "service value must be a list"));
         }
         }
-    } catch (const std::exception& ex) {
-        answer = createAnswer(CONTROL_RESULT_ERROR, "invalid service parameter value: "
-                              + std::string(ex.what()));
     }
     }
 
 
-    if (!answer) {
-        // Try handling this command on our own.
-        answer = HookedCommandMgr::handleCommand(cmd_name, params, original_cmd);
+    // 'service' parameter hasn't been specified which indicates that the command
+    // is intended to be processed by the CA. The following command will try to
+    // process the command with hooks libraries (if available) or by one of the
+    // CA's native handlers.
+    if (services->empty()) {
+        return (HookedCommandMgr::handleCommand(cmd_name, params, original_cmd));
     }
     }
 
 
-    int rcode = 0;
-    static_cast<void>(parseAnswer(rcode, answer));
+    ElementPtr answer_list = Element::createList();
 
 
-    // We have tried handling the command on our own but it seems that neither
-    // the Control Agent nor a hook library can handle this command. We need
-    // to try forwarding the command to one of the Kea servers.
-    if (original_cmd && (rcode == CONTROL_RESULT_COMMAND_UNSUPPORTED)) {
-        try {
-            answer = tryForwardCommand(cmd_name, original_cmd);
+    // Before the command is forwarded it should be processed by the hooks libraries.
+    if (HookedCommandMgr::delegateCommandToHookLibrary(cmd_name, params, original_cmd,
+                                                       answer_list)) {
+        // If the hooks libraries set the 'skip' flag, they indicate that the
+        // commands have been processed. The answer_list should contain the list
+        // of answers with each answer pertaining to one service.
+        if (callout_handle_->getStatus() == CalloutHandle::NEXT_STEP_SKIP) {
+                LOG_DEBUG(agent_logger, isc::log::DBGLVL_COMMAND,
+                          CTRL_AGENT_COMMAND_PROCESS_SKIP)
+                    .arg(cmd_name);
+            return (answer_list);
+        }
+    }
 
 
-        } catch (const CommandForwardingError& ex) {
-            // This is apparently some configuration error or client's error.
-            // We have notify the client.
-            answer = createAnswer(CONTROL_RESULT_ERROR, ex.what());
+    // We don't know whether the hooks libraries modified the value of the
+    //  answer list, so let's be safe and re-create the answer_list.
+    answer_list = Element::createList();
+
+    // For each value within 'service' we have to try forwarding the command.
+    for (unsigned i = 0; i < services->size(); ++i) {
+        if (original_cmd) {
+            ConstElementPtr answer;
+            try {
+                LOG_DEBUG(agent_logger, isc::log::DBGLVL_COMMAND,
+                          CTRL_AGENT_COMMAND_FORWARD_BEGIN)
+                    .arg(cmd_name).arg(services->get(i)->stringValue());
+
+                answer = forwardCommand(services->get(i)->stringValue(),
+                                        cmd_name, original_cmd);
+
+            } catch (const CommandForwardingError& ex) {
+                LOG_DEBUG(agent_logger, isc::log::DBGLVL_COMMAND,
+                          CTRL_AGENT_COMMAND_FORWARD_FAILED)
+                    .arg(cmd_name).arg(ex.what());
+                answer = createAnswer(CONTROL_RESULT_ERROR, ex.what());
+            }
 
 
-        } catch (const CommandForwardingSkip& ex) {
-            // Command is not intended to be forwarded so do nothing.
+            answer_list->add(boost::const_pointer_cast<Element>(answer));
         }
         }
     }
     }
 
 
-    // We have a response, so let's wrap it in the list.
-    ElementPtr answer_list = Element::createList();
-    answer_list->add(boost::const_pointer_cast<Element>(answer));
-
     return (answer_list);
     return (answer_list);
 }
 }
 
 
 ConstElementPtr
 ConstElementPtr
-CtrlAgentCommandMgr::tryForwardCommand(const std::string& cmd_name,
-                                       const isc::data::ConstElementPtr& command) {
+CtrlAgentCommandMgr::forwardCommand(const std::string& service,
+                                    const std::string& cmd_name,
+                                    const isc::data::ConstElementPtr& command) {
     // Context will hold the server configuration.
     // Context will hold the server configuration.
     CtrlAgentCfgContextPtr ctx;
     CtrlAgentCfgContextPtr ctx;
 
 
@@ -126,42 +161,11 @@ CtrlAgentCommandMgr::tryForwardCommand(const std::string& cmd_name,
                   " Control Agent configuration information");
                   " Control Agent configuration information");
     }
     }
 
 
-    // If the service is not specified it means that the Control Agent is the
-    // intended receiver of this message. This is not a fatal error, we simply
-    // skip forwarding the command and rely on the internal logic of the
-    // Control Agent to generate response.
-    ConstElementPtr service_elements = command->get("service");
-    if (!service_elements) {
-        isc_throw(CommandForwardingSkip, "service parameter not specified");
-    }
-
-    // If the service exists it must be a list, even though we currently allow
-    // only one service.
-    std::vector<ElementPtr> service_vec;
-    try {
-        service_vec = service_elements->listValue();
-
-    } catch (const std::exception& ex) {
-        isc_throw(CommandForwardingError, "service parameter is not a list");
-    }
-
-    // service list may be empty in which case we treat it as it is not specified.
-    if (service_vec.empty()) {
-        isc_throw(CommandForwardingSkip, "service parameter is empty");
-    }
-
-    // Do not allow more than one service value. This will be allowed in the
-    // future.
-    if (service_vec.size() > 1) {
-        isc_throw(CommandForwardingError, "service parameter must contain 0 or 1"
-                  " service value");
-    }
-
     // Convert the service to the server type values. Make sure the client
     // Convert the service to the server type values. Make sure the client
     // provided right value.
     // provided right value.
     CtrlAgentCfgContext::ServerType server_type;
     CtrlAgentCfgContext::ServerType server_type;
     try {
     try {
-        server_type = CtrlAgentCfgContext::toServerType(service_vec.at(0)->stringValue());
+        server_type = CtrlAgentCfgContext::toServerType(service);
 
 
     } catch (const std::exception& ex) {
     } catch (const std::exception& ex) {
         // Invalid value in service list. Can't proceed.
         // Invalid value in service list. Can't proceed.
@@ -174,7 +178,7 @@ CtrlAgentCommandMgr::tryForwardCommand(const std::string& cmd_name,
     ConstElementPtr socket_info = ctx->getControlSocketInfo(server_type);
     ConstElementPtr socket_info = ctx->getControlSocketInfo(server_type);
     if (!socket_info) {
     if (!socket_info) {
         isc_throw(CommandForwardingError, "forwarding socket is not configured"
         isc_throw(CommandForwardingError, "forwarding socket is not configured"
-                  " for the server type " << service_vec.at(0)->stringValue());
+                  " for the server type " << service);
     }
     }
 
 
     // If the configuration does its job properly the socket-name must be
     // If the configuration does its job properly the socket-name must be
@@ -191,9 +195,9 @@ CtrlAgentCommandMgr::tryForwardCommand(const std::string& cmd_name,
         unix_socket.write(&wire_command[0], wire_command.size());
         unix_socket.write(&wire_command[0], wire_command.size());
         receive_len = unix_socket.receive(&receive_buf_[0], receive_buf_.size());
         receive_len = unix_socket.receive(&receive_buf_[0], receive_buf_.size());
 
 
-    } catch (...) {
+    } catch (const std::exception& ex) {
         isc_throw(CommandForwardingError, "unable to forward command to the "
         isc_throw(CommandForwardingError, "unable to forward command to the "
-                  + service_vec.at(0)->stringValue() + " service. The server "
+                  << service << " service: " << ex.what() << ". The server "
                   "is likely to be offline");
                   "is likely to be offline");
     }
     }
 
 
@@ -212,8 +216,7 @@ CtrlAgentCommandMgr::tryForwardCommand(const std::string& cmd_name,
         answer = Element::fromJSON(reply);
         answer = Element::fromJSON(reply);
 
 
         LOG_INFO(agent_logger, CTRL_AGENT_COMMAND_FORWARDED)
         LOG_INFO(agent_logger, CTRL_AGENT_COMMAND_FORWARDED)
-            .arg(cmd_name)
-            .arg(service_vec.at(0)->stringValue());
+            .arg(cmd_name).arg(service);
 
 
     } catch (const std::exception& ex) {
     } catch (const std::exception& ex) {
         isc_throw(CommandForwardingError, "internal server error: unable to parse"
         isc_throw(CommandForwardingError, "internal server error: unable to parse"

+ 37 - 30
src/bin/agent/ca_command_mgr.h

@@ -24,13 +24,6 @@ public:
         isc::Exception(file, line, what) { };
         isc::Exception(file, line, what) { };
 };
 };
 
 
-/// @brief Exception thrown when command forwarding has been skipped.
-class CommandForwardingSkip : public Exception {
-public:
-    CommandForwardingSkip(const char* file, size_t line, const char* what) :
-        isc::Exception(file, line, what) { };
-};
-
 /// @brief Command Manager for Control Agent.
 /// @brief Command Manager for Control Agent.
 ///
 ///
 /// This is an implementation of the Command Manager within Control Agent.
 /// This is an implementation of the Command Manager within Control Agent.
@@ -54,18 +47,24 @@ public:
     /// @brief Handles the command having a given name and arguments.
     /// @brief Handles the command having a given name and arguments.
     ///
     ///
     /// This method extends the base implementation with the ability to forward
     /// This method extends the base implementation with the ability to forward
-    /// commands to Kea servers if the Control Agent failed to handle it itself.
+    /// commands to Kea servers.
     ///
     ///
-    /// @todo Currently this method only wraps an answer within a list Element.
-    /// This will be later used to include multiple answers within this list.
-    /// For now it is just a single answer from the Control Agent.
+    /// If the received command doesn't include 'service' parameter or this
+    /// parameter is blank, the command is handled by the Control Agent or the
+    /// attached hooks libraries.
+    ///
+    /// If the non-blank 'service' parameter has been specified the callouts
+    /// are executed. If the callouts process the command the result is returned
+    /// to the controlling client. Otherwise, the command is forwarded to each
+    /// Kea server listed in the 'service' parameter.
     ///
     ///
     /// @param cmd_name Command name.
     /// @param cmd_name Command name.
     /// @param params Command arguments.
     /// @param params Command arguments.
     /// @param original_cmd Original command being processed.
     /// @param original_cmd Original command being processed.
     ///
     ///
-    /// @return Pointer to the const data element representing response
-    /// to a command.
+    /// @return Pointer to the const data element representing a list of
+    /// responses to the command. If the command has been handled by the CA,
+    /// this list includes one response.
     virtual isc::data::ConstElementPtr
     virtual isc::data::ConstElementPtr
     handleCommand(const std::string& cmd_name,
     handleCommand(const std::string& cmd_name,
                   const isc::data::ConstElementPtr& params,
                   const isc::data::ConstElementPtr& params,
@@ -73,34 +72,42 @@ public:
 
 
 private:
 private:
 
 
-    /// @brief Tries to forward received control command to Kea servers.
+    /// @brief Implements the logic for @ref CtrlAgentCommandMgr::handleCommand.
     ///
     ///
-    /// When the Control Agent was unable to process the control command
-    /// because it doesn't recognize it, the command should be forwarded to
-    /// the specific Kea services listed within a 'service' parameter.
+    /// All parameters are passed by value because they may be modified within
+    /// the method.
     ///
     ///
-    /// @todo Currently only one service per control command is supported.
-    /// Forwarding to multiple services should be allowed in the future.
+    /// @param cmd_name Command name.
+    /// @param params Command arguments.
+    /// @param original_cmd Original command being processed.
     ///
     ///
-    /// This method makes an attempt to forward the control command. If
-    /// the 'service' parameter is not specified or it is empty, the
-    /// command is not forwarded and the @ref CommandForwardingSkip exception
-    /// is thrown. The caller catching this exception should not treat
-    /// this situation as an error but this is normal situation when the
-    /// message is not intended to be forwarded.
+    /// @return Pointer to the const data element representing a list of responses
+    /// to the command or a single response (not wrapped in a list). The
+    /// @ref CtrlAgentCommandMgr::handleCommand will wrap non-list value returned
+    /// in a single element list.
+    isc::data::ConstElementPtr
+    handleCommandInternal(std::string cmd_name,
+                          isc::data::ConstElementPtr params,
+                          isc::data::ConstElementPtr original_cmd);
+
+    /// @brief Tries to forward received control command to Kea servers.
     ///
     ///
-    /// All other exceptions should be treated as an error.
+    /// When the Control Agent was unable to process the control command
+    /// because it doesn't recognize it, the command should be forwarded to
+    /// the specific Kea services listed within a 'service' parameter. This
+    /// method forwards the command to the specified Kea service.
     ///
     ///
+    /// @param service Contains name of the service where the command should be
+    /// forwarded.
     /// @param cmd_name Command name.
     /// @param cmd_name Command name.
     /// @param command Pointer to the object representing the forwarded command.
     /// @param command Pointer to the object representing the forwarded command.
     ///
     ///
     /// @return Response to forwarded command.
     /// @return Response to forwarded command.
     /// @throw CommandForwardingError when an error occurred during forwarding.
     /// @throw CommandForwardingError when an error occurred during forwarding.
-    /// @throw CommandForwardingSkip when 'service' parameter hasn't been
-    /// specified which means that the command should not be forwarded.
     isc::data::ConstElementPtr
     isc::data::ConstElementPtr
-    tryForwardCommand(const std::string& cmd_name,
-                      const isc::data::ConstElementPtr& command);
+    forwardCommand(const std::string& destination,
+                   const std::string& cmd_name,
+                   const isc::data::ConstElementPtr& command);
 
 
     /// @brief Private constructor.
     /// @brief Private constructor.
     ///
     ///

+ 0 - 2
src/bin/agent/ca_controller.h

@@ -21,8 +21,6 @@ namespace agent {
 class CtrlAgentController : public process::DControllerBase {
 class CtrlAgentController : public process::DControllerBase {
 public:
 public:
 
 
-    using DControllerBase::getIOService;
-
     /// @brief Static singleton instance method.
     /// @brief Static singleton instance method.
     ///
     ///
     /// This method returns the base class singleton instance member.
     /// This method returns the base class singleton instance member.

+ 15 - 1
src/bin/agent/ca_messages.mes

@@ -24,10 +24,24 @@ This is a debug message issued when the Control Agent exits its
 event loop.
 event loop.
 
 
 % CTRL_AGENT_STARTED Kea Control Agent version %1 started
 % CTRL_AGENT_STARTED Kea Control Agent version %1 started
-This informational message indicates that the DHCP-DDNS server has
+This informational message indicates that the Control Agent has
 processed all configuration information and is ready to begin processing.
 processed all configuration information and is ready to begin processing.
 The version is also printed.
 The version is also printed.
 
 
+% CTRL_AGENT_COMMAND_FORWARD_BEGIN begin forwarding command %1 to service %2
+This debug message is issued when the Control Agent starts forwarding a
+received command to one of the Kea servers.
+
+% CTRL_AGENT_COMMAND_FORWARD_FAILED failed forwarding command %1: %2
+This debug message is issued when the Control Agent failed forwarding a
+received command to one of the Kea servers. The second argument provides
+the details of the error.
+
+% CTRL_AGENT_COMMAND_PROCESS_SKIP command %1 already processed by hooks libraries, skipping
+This debug message is issued when the Control Agent skips processing
+received command because it has determined that the hooks libraries
+already processed the command.
+
 % CTRL_AGENT_CONFIG_FAIL Control Agent configuration failed: %1
 % CTRL_AGENT_CONFIG_FAIL Control Agent configuration failed: %1
 This error message indicates that the CA had failed configuration
 This error message indicates that the CA had failed configuration
 attempt. Details are provided. Additional details may be available
 attempt. Details are provided. Additional details may be available

+ 3 - 2
src/bin/agent/ca_process.cc

@@ -55,11 +55,12 @@ CtrlAgentProcess::run() {
                 CtrlAgentController::instance());
                 CtrlAgentController::instance());
         controller->registerCommands();
         controller->registerCommands();
 
 
-        CtrlAgentCfgContextPtr ctx =
-            boost::dynamic_pointer_cast<CtrlAgentCfgContext>(base_ctx);
         // Let's process incoming data or expiring timers in a loop until
         // Let's process incoming data or expiring timers in a loop until
         // shutdown condition is detected.
         // shutdown condition is detected.
         while (!shouldShutdown()) {
         while (!shouldShutdown()) {
+            // Remove unused listeners within the main loop because new listeners
+            // are created in within a callback method. This avoids removal the
+            // listeners within a callback.
             garbageCollectListeners();
             garbageCollectListeners();
             runIO();
             runIO();
         }
         }

+ 70 - 20
src/bin/agent/tests/ca_command_mgr_unittests.cc

@@ -21,6 +21,7 @@
 #include <boost/pointer_cast.hpp>
 #include <boost/pointer_cast.hpp>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 #include <cstdlib>
 #include <cstdlib>
+#include <vector>
 
 
 using namespace isc::agent;
 using namespace isc::agent;
 using namespace isc::asiolink;
 using namespace isc::asiolink;
@@ -68,18 +69,40 @@ public:
     /// verification of the response parameters.
     /// verification of the response parameters.
     ///
     ///
     /// @param answer answer to be verified
     /// @param answer answer to be verified
-    /// @param expected_code code expected to be returned in the answer
-    void checkAnswer(ConstElementPtr answer, int expected_code) {
+    /// @param expected_code0 code expected to be returned in first result within
+    /// the answer.
+    /// @param expected_code1 code expected to be returned in second result within
+    /// the answer.
+    /// @param expected_code2 code expected to be returned in third result within
+    /// the answer.
+    void checkAnswer(const ConstElementPtr& answer, const int expected_code0 = 0,
+                     const int expected_code1 = -1, const int expected_code2 = -1) {
+        std::vector<int> expected_codes;
+        if (expected_code0 >= 0) {
+            expected_codes.push_back(expected_code0);
+        }
+
+        if (expected_code1 >= 0) {
+            expected_codes.push_back(expected_code1);
+        }
+
+        if (expected_code2 >= 0) {
+            expected_codes.push_back(expected_code2);
+        }
+
         int status_code;
         int status_code;
         // There may be multiple answers returned within a list.
         // There may be multiple answers returned within a list.
         std::vector<ElementPtr> answer_list = answer->listValue();
         std::vector<ElementPtr> answer_list = answer->listValue();
-        // There must be at least one answer.
-        ASSERT_GE(answer_list.size(), 1);
-        // Check that all answers indicate success.
+
+        ASSERT_EQ(expected_codes.size(), answer_list.size());
+        // Check all answers.
         for (auto ans = answer_list.cbegin(); ans != answer_list.cend();
         for (auto ans = answer_list.cbegin(); ans != answer_list.cend();
              ++ans) {
              ++ans) {
-            ASSERT_NO_THROW(isc::config::parseAnswer(status_code, *ans));
-            EXPECT_EQ(expected_code, status_code);
+            ConstElementPtr text;
+            ASSERT_NO_THROW(text = isc::config::parseAnswer(status_code, *ans));
+            EXPECT_EQ(expected_codes[std::distance(answer_list.cbegin(), ans)],
+                      status_code)
+                << "answer contains text: " << text->stringValue();
         }
         }
     }
     }
 
 
@@ -148,32 +171,34 @@ public:
     ///
     ///
     /// @param response Stub response to be sent from the server socket to the
     /// @param response Stub response to be sent from the server socket to the
     /// client.
     /// client.
-    void bindServerSocket(const std::string& response) {
+    /// @param stop_after_count Number of received messages received over the
+    /// server socket after which the IO service should be stopped.
+    void bindServerSocket(const std::string& response,
+                          const unsigned int stop_after_count = 1) {
         server_socket_.reset(new test::TestServerUnixSocket(*getIOService(),
         server_socket_.reset(new test::TestServerUnixSocket(*getIOService(),
                                                             unixSocketFilePath(),
                                                             unixSocketFilePath(),
                                                             TEST_TIMEOUT,
                                                             TEST_TIMEOUT,
                                                             response));
                                                             response));
-        server_socket_->bindServerSocket();
+        server_socket_->bindServerSocket(stop_after_count);
     }
     }
 
 
     /// @brief Creates command with no arguments.
     /// @brief Creates command with no arguments.
     ///
     ///
     /// @param command_name Command name.
     /// @param command_name Command name.
-    /// @param service Service value to be added to the command. If this value
-    /// holds an empty string, the service parameter is not added.
+    /// @param service Service value to be added to the command. This value is
+    /// specified as a list of comma separated values, e.g. "dhcp4, dhcp6".
     ///
     ///
     /// @return Pointer to the instance of the created command.
     /// @return Pointer to the instance of the created command.
     ConstElementPtr createCommand(const std::string& command_name,
     ConstElementPtr createCommand(const std::string& command_name,
                                   const std::string& service) {
                                   const std::string& service) {
         ElementPtr command = Element::createMap();
         ElementPtr command = Element::createMap();
-
         command->set("command", Element::create(command_name));
         command->set("command", Element::create(command_name));
 
 
         // Only add the 'service' parameter if non-empty.
         // Only add the 'service' parameter if non-empty.
         if (!service.empty()) {
         if (!service.empty()) {
-            ElementPtr services = Element::createList();
-            services->add(Element::create(service));
-            command->set("service", services);
+            std::string s = boost::replace_all_copy(service, ",", "\",\"");
+            s = std::string("[ \"") + s + std::string("\" ]");
+            command->set("service", Element::fromJSON(s));
         }
         }
 
 
         command->set("arguments", Element::createMap());
         command->set("arguments", Element::createMap());
@@ -186,16 +211,23 @@ public:
     /// @param server_type Server for which the client socket should be
     /// @param server_type Server for which the client socket should be
     /// configured.
     /// configured.
     /// @param service Service to be included in the command.
     /// @param service Service to be included in the command.
-    /// @param expected_result Expected result in response from the server.
+    /// @param expected_result0 Expected first result in response from the server.
+    /// @param expected_result1 Expected second result in response from the server.
+    /// @param expected_result2 Expected third result in response from the server.
+    /// @param stop_after_count Number of received messages received over the
+    /// server socket after which the IO service should be stopped.
     /// @param server_response Stub response to be sent by the server.
     /// @param server_response Stub response to be sent by the server.
     void testForward(const CtrlAgentCfgContext::ServerType& server_type,
     void testForward(const CtrlAgentCfgContext::ServerType& server_type,
                      const std::string& service,
                      const std::string& service,
-                     const int expected_result,
+                     const int expected_result0,
+                     const int expected_result1 = -1,
+                     const int expected_result2 = -1,
+                     const unsigned stop_after_count = 1,
                      const std::string& server_response = "{ \"result\": 0 }") {
                      const std::string& server_response = "{ \"result\": 0 }") {
         // Configure client side socket.
         // Configure client side socket.
         configureControlSocket(server_type);
         configureControlSocket(server_type);
         // Create server side socket.
         // Create server side socket.
-        bindServerSocket(server_response);
+        bindServerSocket(server_response, stop_after_count);
 
 
         // The client side communication is synchronous. To be able to respond
         // The client side communication is synchronous. To be able to respond
         // to this we need to run the server side socket at the same time.
         // to this we need to run the server side socket at the same time.
@@ -208,7 +240,7 @@ public:
         ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(),
         ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(),
                                                     command);
                                                     command);
 
 
-        checkAnswer(answer, expected_result);
+        checkAnswer(answer, expected_result0, expected_result1, expected_result2);
     }
     }
 
 
     /// @brief a convenience reference to control agent command manager
     /// @brief a convenience reference to control agent command manager
@@ -249,6 +281,24 @@ TEST_F(CtrlAgentCommandMgrTest, forwardToDHCPv6Server) {
                 isc::config::CONTROL_RESULT_SUCCESS);
                 isc::config::CONTROL_RESULT_SUCCESS);
 }
 }
 
 
+/// Check that the same command is forwarded to multiple servers.
+TEST_F(CtrlAgentCommandMgrTest, forwardToBothDHCPServers) {
+    configureControlSocket(CtrlAgentCfgContext::TYPE_DHCP6);
+
+    testForward(CtrlAgentCfgContext::TYPE_DHCP4, "dhcp4,dhcp6",
+                isc::config::CONTROL_RESULT_SUCCESS,
+                isc::config::CONTROL_RESULT_SUCCESS,
+                -1, 2);
+}
+
+/// Check that the command may forwarded to the second server even if
+/// forwarding to a first server fails.
+TEST_F(CtrlAgentCommandMgrTest, failForwardToServer) {
+    testForward(CtrlAgentCfgContext::TYPE_DHCP6, "dhcp4,dhcp6",
+                isc::config::CONTROL_RESULT_ERROR,
+                isc::config::CONTROL_RESULT_SUCCESS);
+}
+
 /// Check that control command is not forwarded if the service is not specified.
 /// Check that control command is not forwarded if the service is not specified.
 TEST_F(CtrlAgentCommandMgrTest, noService) {
 TEST_F(CtrlAgentCommandMgrTest, noService) {
     testForward(CtrlAgentCfgContext::TYPE_DHCP6, "",
     testForward(CtrlAgentCfgContext::TYPE_DHCP6, "",
@@ -259,7 +309,7 @@ TEST_F(CtrlAgentCommandMgrTest, noService) {
 /// command was forwarded sent an invalid message.
 /// command was forwarded sent an invalid message.
 TEST_F(CtrlAgentCommandMgrTest, invalidAnswer) {
 TEST_F(CtrlAgentCommandMgrTest, invalidAnswer) {
     testForward(CtrlAgentCfgContext::TYPE_DHCP6, "dhcp6",
     testForward(CtrlAgentCfgContext::TYPE_DHCP6, "dhcp6",
-                isc::config::CONTROL_RESULT_ERROR,
+                isc::config::CONTROL_RESULT_ERROR, -1, -1, 1,
                 "{ \"result\": 0");
                 "{ \"result\": 0");
 }
 }
 
 

+ 29 - 6
src/lib/asiolink/testutils/test_server_unix_socket.cc

@@ -21,19 +21,21 @@ TestServerUnixSocket::TestServerUnixSocket(IOService& io_service,
       server_acceptor_(io_service_.get_io_service()),
       server_acceptor_(io_service_.get_io_service()),
       server_socket_(io_service_.get_io_service()),
       server_socket_(io_service_.get_io_service()),
       test_timer_(io_service_),
       test_timer_(io_service_),
-      custom_response_(custom_response) {
+      custom_response_(custom_response),
+      stop_after_count_(1),
+      read_count_(0) {
     test_timer_.setup(boost::bind(&TestServerUnixSocket::timeoutHandler, this),
     test_timer_.setup(boost::bind(&TestServerUnixSocket::timeoutHandler, this),
                       test_timeout, IntervalTimer::ONE_SHOT);
                       test_timeout, IntervalTimer::ONE_SHOT);
 }
 }
 
 
 void
 void
-TestServerUnixSocket::bindServerSocket() {
+TestServerUnixSocket::bindServerSocket(const unsigned int stop_after_count) {
     server_acceptor_.open();
     server_acceptor_.open();
     server_acceptor_.bind(server_endpoint_);
     server_acceptor_.bind(server_endpoint_);
     server_acceptor_.listen();
     server_acceptor_.listen();
-    server_acceptor_.async_accept(server_socket_,
-                                  boost::bind(&TestServerUnixSocket::
-                                              acceptHandler, this, _1));
+    accept();
+
+    stop_after_count_ = stop_after_count;
 }
 }
 
 
 void
 void
@@ -45,18 +47,39 @@ TestServerUnixSocket::acceptHandler(const boost::system::error_code&) {
 }
 }
 
 
 void
 void
+TestServerUnixSocket::accept() {
+    server_acceptor_.async_accept(server_socket_,
+                                  boost::bind(&TestServerUnixSocket::
+                                              acceptHandler, this, _1));
+}
+
+
+void
 TestServerUnixSocket::readHandler(const boost::system::error_code&,
 TestServerUnixSocket::readHandler(const boost::system::error_code&,
                                   size_t bytes_transferred) {
                                   size_t bytes_transferred) {
     if (!custom_response_.empty()) {
     if (!custom_response_.empty()) {
         boost::asio::write(server_socket_, boost::asio::buffer(custom_response_.c_str(),
         boost::asio::write(server_socket_, boost::asio::buffer(custom_response_.c_str(),
                                                                custom_response_.size()));
                                                                custom_response_.size()));
+
     } else {
     } else {
         std::string received(&raw_buf_[0], bytes_transferred);
         std::string received(&raw_buf_[0], bytes_transferred);
         std::string response("received " + received);
         std::string response("received " + received);
         boost::asio::write(server_socket_, boost::asio::buffer(response.c_str(),
         boost::asio::write(server_socket_, boost::asio::buffer(response.c_str(),
                                                                response.size()));
                                                                response.size()));
     }
     }
-    io_service_.stop();
+
+    // Close the connection as we might be expecting another connection over the
+    // same socket.
+    server_socket_.close();
+
+    // Stop IO service if we have reached the maximum number of read messages.
+    if (++read_count_ >= stop_after_count_) {
+        io_service_.stop();
+
+    } else {
+        // Previous connection is done, so let's accept another connection.
+        accept();
+    }
 }
 }
 
 
 void
 void

+ 14 - 2
src/lib/asiolink/testutils/test_server_unix_socket.h

@@ -32,10 +32,13 @@ public:
     TestServerUnixSocket(IOService& io_service,
     TestServerUnixSocket(IOService& io_service,
                          const std::string& socket_file_path,
                          const std::string& socket_file_path,
                          const long test_timeout,
                          const long test_timeout,
-                         const std::string& custom_response = "");
+                         const std::string& custom_respons_ = "");
 
 
     /// @brief Creates and binds server socket.
     /// @brief Creates and binds server socket.
-    void bindServerSocket();
+    ///
+    /// @param stop_after_count Number of received messages after which the
+    /// IO service should be stopped.
+    void bindServerSocket(const unsigned int stop_after_count = 1);
 
 
     /// @brief Server acceptor handler.
     /// @brief Server acceptor handler.
     ///
     ///
@@ -56,6 +59,9 @@ public:
 
 
 private:
 private:
 
 
+    /// @brief Asynchronously accept new connections.
+    void accept();
+
     /// @brief IO service used by the tests.
     /// @brief IO service used by the tests.
     IOService& io_service_;
     IOService& io_service_;
 
 
@@ -75,6 +81,12 @@ private:
 
 
     /// @brief Holds custom response to be sent to the client.
     /// @brief Holds custom response to be sent to the client.
     std::string custom_response_;
     std::string custom_response_;
+
+    /// @brief Number of messages received after which IO service gets stopped.
+    unsigned int stop_after_count_;
+
+    /// @brief Number of messages received so far.
+    unsigned int read_count_;
 };
 };
 
 
 /// @brief Pointer to the @ref TestServerUnixSocket.
 /// @brief Pointer to the @ref TestServerUnixSocket.

+ 0 - 4
src/lib/asiolink/unix_domain_socket.h

@@ -52,8 +52,6 @@ public:
     ///
     ///
     /// @param data Pointer to data to be written.
     /// @param data Pointer to data to be written.
     /// @param length Number of bytes to be written.
     /// @param length Number of bytes to be written.
-    /// @param [out] ec Error code returned as a result of an attempt to
-    /// write to socket.
     ///
     ///
     /// @return Number of bytes written.
     /// @return Number of bytes written.
     /// @throw UnixDomainSocketError if error occurs.
     /// @throw UnixDomainSocketError if error occurs.
@@ -64,8 +62,6 @@ public:
     /// @param [out] data Pointer to a location into which the read data should
     /// @param [out] data Pointer to a location into which the read data should
     /// be stored.
     /// be stored.
     /// @param length Length of the buffer.
     /// @param length Length of the buffer.
-    /// @param [out] ec Error code returned as a result of an attempt to
-    /// read from socket.
     ///
     ///
     /// @return Number of bytes read.
     /// @return Number of bytes read.
     /// @throw UnixDomainSocketError if error occurs.
     /// @throw UnixDomainSocketError if error occurs.

+ 42 - 26
src/lib/config/hooked_command_mgr.cc

@@ -42,18 +42,11 @@ HookedCommandMgr::HookedCommandMgr()
     : BaseCommandMgr(), callout_handle_(HooksManager::createCalloutHandle()) {
     : BaseCommandMgr(), callout_handle_(HooksManager::createCalloutHandle()) {
 }
 }
 
 
-ConstElementPtr
-HookedCommandMgr::handleCommand(const std::string& cmd_name,
-                                const ConstElementPtr& params,
-                                const ConstElementPtr& original_cmd) {
-    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 final_cmd = original_cmd;
+bool
+HookedCommandMgr::delegateCommandToHookLibrary(std::string& cmd_name,
+                                               ConstElementPtr& params,
+                                               ConstElementPtr& original_cmd,
+                                               ElementPtr& answer) {
 
 
     ConstElementPtr hook_response;
     ConstElementPtr hook_response;
     if (HooksManager::calloutsPresent(Hooks.hook_index_control_command_receive_)) {
     if (HooksManager::calloutsPresent(Hooks.hook_index_control_command_receive_)) {
@@ -74,35 +67,58 @@ HookedCommandMgr::handleCommand(const std::string& cmd_name,
         // The callouts should set the response.
         // The callouts should set the response.
         callout_handle_->getArgument("response", hook_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
         // The hook library can modify the command or arguments. Thus, we
         // retrieve the command returned by the callouts and use it as input
         // retrieve the command returned by the callouts and use it as input
         // to the local command handler.
         // to the local command handler.
         ConstElementPtr hook_command;
         ConstElementPtr hook_command;
         callout_handle_->getArgument("command", hook_command);
         callout_handle_->getArgument("command", hook_command);
-        final_cmd_name = parseCommand(final_params, hook_command);
-        final_cmd = hook_command;
+        cmd_name = parseCommand(params, hook_command);
+        original_cmd = hook_command;
+
+        answer = boost::const_pointer_cast<Element>(hook_response);
+
+        return (true);
+    }
+
+    return (false);
+}
+
+ConstElementPtr
+HookedCommandMgr::handleCommand(const std::string& cmd_name,
+                                const ConstElementPtr& params,
+                                const ConstElementPtr& original_cmd) {
+    if (!callout_handle_) {
+        isc_throw(Unexpected, "callout handle not configured for the Command "
+                  "Manager: this is a programming error");
+    }
+
+    std::string mutable_cmd_name = cmd_name;
+    ConstElementPtr mutable_params = params;
+    ConstElementPtr mutable_cmd = original_cmd;
+
+    ElementPtr hook_response;
+    if (delegateCommandToHookLibrary(mutable_cmd_name, mutable_params,
+                                     mutable_cmd, hook_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);
+        }
     }
     }
 
 
     // If we're here it means that the callouts weren't called or the 'skip'
     // 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'
     // 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
     // is being processed. Anyhow, we need to handle the command using local
     // Command Mananger.
     // Command Mananger.
-    ConstElementPtr response = BaseCommandMgr::handleCommand(final_cmd_name,
-                                                             final_params,
-                                                             final_cmd);
+    ConstElementPtr response = BaseCommandMgr::handleCommand(mutable_cmd_name,
+                                                             mutable_params,
+                                                             mutable_cmd);
 
 
     // For the 'list-commands' case we will have to combine commands supported
     // For the 'list-commands' case we will have to combine commands supported
     // by the hook libraries with the commands that this Command Manager supports.
     // by the hook libraries with the commands that this Command Manager supports.
-    if ((final_cmd_name == "list-commands") && hook_response && response) {
+    if ((mutable_cmd_name == "list-commands") && hook_response && response) {
         response = combineCommandsLists(hook_response, response);
         response = combineCommandsLists(hook_response, response);
     }
     }
 
 

+ 27 - 7
src/lib/config/hooked_command_mgr.h

@@ -36,15 +36,35 @@ protected:
         return (callout_handle_);
         return (callout_handle_);
     }
     }
 
 
-    /// @brief Handles the command having a given name and arguments.
+    /// @brief Handles the command within the hooks libraries.
     ///
     ///
-    /// This method checks if the hook library is installed which implements
+    /// This method checks if the hooks libraries are installed which implement
     /// callouts for the 'control_command_receive' hook point, and calls them
     /// 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 isc::data::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.
+    /// if they exist. If the hooks library supports the given command it creates
+    /// a response and returns it in the @c answer argument.
+    ///
+    /// Values of all arguments can be modified by the hook library.
+    ///
+    /// @param [out] cmd_name Command name.
+    /// @param [out] params Command arguments.
+    /// @param [out] original_cmd Original command received.
+    /// @param [out] answer Command processing result returned by the hook.
+    ///
+    /// @return Boolean value indicating if any callouts have been executed.
+    bool
+    delegateCommandToHookLibrary(std::string& cmd_name,
+                                 isc::data::ConstElementPtr& params,
+                                 isc::data::ConstElementPtr& original_cmd,
+                                 isc::data::ElementPtr& answer);
+
+    /// @brief Handles the command having a given name and arguments.
+    ///
+    /// This method calls @ref HookedCommandMgr::delegateCommandToHookLibrary to
+    /// try to process the command with the hook libraries, if they are installed.
+    /// If the returned @c skip value indicates that the callout set the 'skip' flag
+    /// the command is assumed to have been processed and the response is returned.
+    /// If the 'skip' flag is not set, the @ref BaseCommandMgr::handleCommand is
+    /// called.
     ///
     ///
     /// @param cmd_name Command name.
     /// @param cmd_name Command name.
     /// @param params Command arguments.
     /// @param params Command arguments.