Browse Source

[master] Merge branch 'trac5078_rebase'

Marcin Siodelski 8 years ago
parent
commit
19a50ed1cc
37 changed files with 1762 additions and 116 deletions
  1. 1 0
      configure.ac
  2. 15 0
      src/bin/agent/ca_cfg_mgr.cc
  3. 6 0
      src/bin/agent/ca_cfg_mgr.h
  4. 197 3
      src/bin/agent/ca_command_mgr.cc
  5. 60 7
      src/bin/agent/ca_command_mgr.h
  6. 5 0
      src/bin/agent/ca_controller.cc
  7. 4 0
      src/bin/agent/ca_controller.h
  8. 19 1
      src/bin/agent/ca_messages.mes
  9. 117 43
      src/bin/agent/ca_process.cc
  10. 52 0
      src/bin/agent/ca_process.h
  11. 1 0
      src/bin/agent/tests/Makefile.am
  12. 13 0
      src/bin/agent/tests/ca_cfg_mgr_unittests.cc
  13. 290 10
      src/bin/agent/tests/ca_command_mgr_unittests.cc
  14. 217 1
      src/bin/agent/tests/ca_controller_unittests.cc
  15. 1 1
      src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
  16. 1 1
      src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
  17. 2 1
      src/lib/asiolink/Makefile.am
  18. 5 2
      src/lib/asiolink/tests/Makefile.am
  19. 142 0
      src/lib/asiolink/tests/unix_domain_socket_unittest.cc
  20. 24 0
      src/lib/asiolink/testutils/Makefile.am
  21. 93 0
      src/lib/asiolink/testutils/test_server_unix_socket.cc
  22. 99 0
      src/lib/asiolink/testutils/test_server_unix_socket.h
  23. 97 0
      src/lib/asiolink/unix_domain_socket.cc
  24. 83 0
      src/lib/asiolink/unix_domain_socket.h
  25. 3 0
      src/lib/cc/command_interpreter.h
  26. 31 5
      src/lib/config/base_command_mgr.cc
  27. 38 2
      src/lib/config/base_command_mgr.h
  28. 5 0
      src/lib/config/config_messages.mes
  29. 45 27
      src/lib/config/hooked_command_mgr.cc
  30. 30 8
      src/lib/config/hooked_command_mgr.h
  31. 1 1
      src/lib/config/tests/command_mgr_unittests.cc
  32. 18 0
      src/lib/http/listener.cc
  33. 13 0
      src/lib/http/listener.h
  34. 3 1
      src/lib/http/tests/listener_unittests.cc
  35. 6 1
      src/lib/process/d_cfg_mgr.cc
  36. 9 1
      src/lib/process/d_cfg_mgr.h
  37. 16 0
      src/lib/process/tests/d_cfg_mgr_unittests.cc

+ 1 - 0
configure.ac

@@ -1722,6 +1722,7 @@ AC_CONFIG_FILES([Makefile
                  src/lib/asiodns/Makefile
                  src/lib/asiodns/tests/Makefile
                  src/lib/asiolink/Makefile
+                 src/lib/asiolink/testutils/Makefile
                  src/lib/asiolink/tests/Makefile
                  src/lib/cc/Makefile
                  src/lib/cc/tests/Makefile

+ 15 - 0
src/bin/agent/ca_cfg_mgr.cc

@@ -10,6 +10,7 @@
 #include <agent/simple_parser.h>
 #include <cc/simple_parser.h>
 #include <cc/command_interpreter.h>
+#include <exceptions/exceptions.h>
 
 using namespace isc::dhcp;
 using namespace isc::process;
@@ -34,6 +35,20 @@ CtrlAgentCfgContext::CtrlAgentCfgContext(const CtrlAgentCfgContext& orig)
     ctrl_sockets_[TYPE_DHCP6] = orig.ctrl_sockets_[TYPE_DHCP6];
 }
 
+CtrlAgentCfgContext::ServerType
+CtrlAgentCfgContext::toServerType(const std::string& service) {
+    if (service == "dhcp4") {
+        return (CtrlAgentCfgContext::TYPE_DHCP4);
+
+    } else if (service == "dhcp6") {
+        return (CtrlAgentCfgContext::TYPE_DHCP6);
+
+    } else if (service == "d2") {
+        return (CtrlAgentCfgContext::TYPE_D2);
+    }
+
+    isc_throw(isc::BadValue, "invalid service value " << service);
+}
 
 CtrlAgentCfgMgr::CtrlAgentCfgMgr()
     : DCfgMgrBase(DCfgContextBasePtr(new CtrlAgentCfgContext())) {

+ 6 - 0
src/bin/agent/ca_cfg_mgr.h

@@ -11,6 +11,7 @@
 #include <hooks/hooks_config.h>
 #include <process/d_cfg_mgr.h>
 #include <boost/pointer_cast.hpp>
+#include <string>
 
 namespace isc {
 namespace agent {
@@ -42,6 +43,11 @@ public:
     /// @brief Used check that specified ServerType is within valid range.
     static const uint32_t MAX_TYPE_SUPPORTED = TYPE_D2;
 
+    /// @brief Converts service specified as a string to ServerType.
+    ///
+    /// @param service Service value as a string: 'dhcp4', 'dhcp6', 'd2'.
+    static ServerType toServerType(const std::string& service);
+
     /// @brief Creates a clone of this context object.
     ///
     /// Note this method does not do deep copy the information about control sockets.

+ 197 - 3
src/bin/agent/ca_command_mgr.cc

@@ -4,11 +4,26 @@
 // 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 <agent/ca_cfg_mgr.h>
 #include <agent/ca_command_mgr.h>
+#include <agent/ca_controller.h>
+#include <agent/ca_log.h>
+#include <agent/ca_process.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/io_service.h>
+#include <asiolink/unix_domain_socket.h>
+#include <cc/command_interpreter.h>
 #include <cc/data.h>
 #include <boost/pointer_cast.hpp>
+#include <iterator>
+#include <string>
+#include <vector>
 
+using namespace isc::asiolink;
+using namespace isc::config;
 using namespace isc::data;
+using namespace isc::hooks;
+using namespace isc::process;
 
 namespace isc {
 namespace agent {
@@ -25,13 +40,192 @@ CtrlAgentCommandMgr::CtrlAgentCommandMgr()
 
 ConstElementPtr
 CtrlAgentCommandMgr::handleCommand(const std::string& cmd_name,
-                                   const isc::data::ConstElementPtr& params) {
+                                   const isc::data::ConstElementPtr& params,
+                                   const isc::data::ConstElementPtr& original_cmd) {
+    ConstElementPtr answer = handleCommandInternal(cmd_name, params, original_cmd);
+
+    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>(HookedCommandMgr::handleCommand(cmd_name, params)));
+    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"));
+        }
+    }
+
+    // '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));
+    }
+
+    ElementPtr answer_list = Element::createList();
+
+    // 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);
+        }
+    }
+
+    // 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());
+            }
+
+            answer_list->add(boost::const_pointer_cast<Element>(answer));
+        }
+    }
+
+    return (answer_list);
+}
+
+ConstElementPtr
+CtrlAgentCommandMgr::forwardCommand(const std::string& service,
+                                    const std::string& cmd_name,
+                                    const isc::data::ConstElementPtr& command) {
+    // Context will hold the server configuration.
+    CtrlAgentCfgContextPtr ctx;
+
+    // There is a hierarchy of the objects through which we need to pass to get
+    // the configuration context. We may simplify this at some point but since
+    // we're in the singleton we want to make sure that we're using most current
+    // configuration.
+    boost::shared_ptr<CtrlAgentController> controller =
+        boost::dynamic_pointer_cast<CtrlAgentController>(CtrlAgentController::instance());
+    if (controller) {
+        CtrlAgentProcessPtr process = controller->getCtrlAgentProcess();
+        if (process) {
+            CtrlAgentCfgMgrPtr cfgmgr = process->getCtrlAgentCfgMgr();
+            if (cfgmgr) {
+                ctx = cfgmgr->getCtrlAgentCfgContext();
+            }
+        }
+    }
+
+    // This is highly unlikely but keep the checks just in case someone messes up
+    // in the code.
+    if (!ctx) {
+        isc_throw(CommandForwardingError, "internal server error: unable to retrieve"
+                  " Control Agent configuration information");
+    }
+
+    // Convert the service to the server type values. Make sure the client
+    // provided right value.
+    CtrlAgentCfgContext::ServerType server_type;
+    try {
+        server_type = CtrlAgentCfgContext::toServerType(service);
+
+    } catch (const std::exception& ex) {
+        // Invalid value in service list. Can't proceed.
+        isc_throw(CommandForwardingError, ex.what());
+    }
+
+    // Now that we know what service it should be forwarded to, we should
+    // find a matching forwarding socket. If this socket is not configured,
+    // we have to communicate it to the client.
+    ConstElementPtr socket_info = ctx->getControlSocketInfo(server_type);
+    if (!socket_info) {
+        isc_throw(CommandForwardingError, "forwarding socket is not configured"
+                  " for the server type " << service);
+    }
+
+    // If the configuration does its job properly the socket-name must be
+    // specified and must be a string value.
+    std::string socket_name = socket_info->get("socket-name")->stringValue();
+
+    // Forward command and receive reply.
+    IOService io_service;
+    UnixDomainSocket unix_socket(io_service);
+    size_t receive_len;
+    try {
+        unix_socket.connect(socket_name);
+        std::string wire_command = command->toWire();
+        unix_socket.write(&wire_command[0], wire_command.size());
+        receive_len = unix_socket.receive(&receive_buf_[0], receive_buf_.size());
+
+    } catch (const std::exception& ex) {
+        isc_throw(CommandForwardingError, "unable to forward command to the "
+                  << service << " service: " << ex.what() << ". The server "
+                  "is likely to be offline");
+    }
+
+    // This is really not possible right now, but when we migrate to the
+    // solution using timeouts it is possible that the response is not
+    // received.
+    if (receive_len == 0) {
+        isc_throw(CommandForwardingError, "internal server error: no answer"
+                  " received from the server to the forwarded message");
+    }
+
+    std::string reply(&receive_buf_[0], receive_len);
+
+    ConstElementPtr answer;
+    try {
+        answer = Element::fromJSON(reply);
+
+        LOG_INFO(agent_logger, CTRL_AGENT_COMMAND_FORWARDED)
+            .arg(cmd_name).arg(service);
+
+    } catch (const std::exception& ex) {
+        isc_throw(CommandForwardingError, "internal server error: unable to parse"
+                  " server's answer to the forwarded message: " << ex.what());
+    }
+
+    return (answer);
+}
+
+
 } // end of namespace isc::agent
 } // end of namespace isc

+ 60 - 7
src/bin/agent/ca_command_mgr.h

@@ -8,12 +8,22 @@
 #define CTRL_AGENT_COMMAND_MGR_H
 
 #include <config/hooked_command_mgr.h>
+#include <exceptions/exceptions.h>
 #include <boost/noncopyable.hpp>
 #include <boost/shared_ptr.hpp>
+#include <array>
 
 namespace isc {
 namespace agent {
 
+/// @brief Exception thrown when an error occurred during control command
+/// forwarding.
+class CommandForwardingError : public Exception {
+public:
+    CommandForwardingError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
 /// @brief Command Manager for Control Agent.
 ///
 /// This is an implementation of the Command Manager within Control Agent.
@@ -37,28 +47,71 @@ public:
     /// @brief Handles the command having a given name and arguments.
     ///
     /// 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.
+    ///
+    /// If the received command doesn't include 'service' parameter or this
+    /// parameter is blank, the command is first handled by the attached hooks
+    /// libraries, and if still unhandled, the Control Agent itself.
     ///
-    /// @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 non-blank 'service' parameter has been specified the hooks
+    /// are executed. If the hooks 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 params Command arguments.
+    /// @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
     handleCommand(const std::string& cmd_name,
-                  const isc::data::ConstElementPtr& params);
+                  const isc::data::ConstElementPtr& params,
+                  const isc::data::ConstElementPtr& original_cmd);
 
 private:
+
+    /// @brief Implements the logic for @ref CtrlAgentCommandMgr::handleCommand.
+    ///
+    /// All parameters are passed by value because they may be modified within
+    /// the method.
+    ///
+    /// @param cmd_name Command name.
+    /// @param params Command arguments.
+    /// @param original_cmd Original command being processed.
+    ///
+    /// @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 a specified server.
+    ///
+    /// @param service Contains name of the service where the command should be
+    /// forwarded.
+    /// @param cmd_name Command name.
+    /// @param command Pointer to the object representing the forwarded command.
+    ///
+    /// @return Response to forwarded command.
+    /// @throw CommandForwardingError when an error occurred during forwarding.
+    isc::data::ConstElementPtr
+    forwardCommand(const std::string& service, const std::string& cmd_name,
+                   const isc::data::ConstElementPtr& command);
+
     /// @brief Private constructor.
     ///
     /// The instance should be created using @ref CtrlAgentCommandMgr::instance,
     /// thus the constructor is private.
     CtrlAgentCommandMgr();
 
+    /// @brief Buffer into which responses to forwarded commands are stored.
+    std::array<char, 8192> receive_buf_;
+
 };
 
 } // end of namespace isc::agent

+ 5 - 0
src/bin/agent/ca_controller.cc

@@ -86,5 +86,10 @@ CtrlAgentController::CtrlAgentController()
 CtrlAgentController::~CtrlAgentController() {
 }
 
+CtrlAgentProcessPtr
+CtrlAgentController::getCtrlAgentProcess() {
+    return (boost::dynamic_pointer_cast<CtrlAgentProcess>(getProcess()));
+}
+
 } // namespace isc::agent
 } // namespace isc

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

@@ -7,6 +7,7 @@
 #ifndef CTRL_AGENT_CONTROLLER_H
 #define CTRL_AGENT_CONTROLLER_H
 
+#include <agent/ca_process.h>
 #include <process/d_controller.h>
 
 namespace isc {
@@ -32,6 +33,9 @@ public:
     /// @brief Destructor
     virtual ~CtrlAgentController();
 
+    /// @brief Returns pointer to an instance of the underlying process object.
+    CtrlAgentProcessPtr getCtrlAgentProcess();
+
     /// @brief Defines the application name, this is passed into base class
     /// and appears in log statements.
     static const char* agent_app_name_;

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

@@ -6,6 +6,10 @@
 
 $NAMESPACE isc::agent
 
+% CTRL_AGENT_COMMAND_FORWARDED command %1 successfully forwarded to the service %2
+This informational message is issued when the CA successfully forwards
+the control message to the specified Kea service and receives a response.
+
 % CTRL_AGENT_HTTP_SERVICE_STARTED HTTP service bound to address %1:%2
 This informational message indicates that the server has started HTTP service
 on the specified address and port. All control commands should be sent to this
@@ -20,10 +24,24 @@ This is a debug message issued when the Control Agent exits its
 event loop.
 
 % 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.
 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
 This error message indicates that the CA had failed configuration
 attempt. Details are provided. Additional details may be available

+ 117 - 43
src/bin/agent/ca_process.cc

@@ -5,6 +5,7 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 #include <config.h>
+#include <asiolink/asio_wrapper.h>
 #include <agent/ca_process.h>
 #include <agent/ca_controller.h>
 #include <agent/ca_response_creator_factory.h>
@@ -12,10 +13,10 @@
 #include <asiolink/io_address.h>
 #include <asiolink/io_error.h>
 #include <cc/command_interpreter.h>
-#include <http/listener.h>
 #include <boost/pointer_cast.hpp>
 
 using namespace isc::asiolink;
+using namespace isc::data;
 using namespace isc::http;
 using namespace isc::process;
 
@@ -32,7 +33,8 @@ namespace agent {
 
 CtrlAgentProcess::CtrlAgentProcess(const char* name,
                                    const asiolink::IOServicePtr& io_service)
-    : DProcessBase(name, io_service, DCfgMgrBasePtr(new CtrlAgentCfgMgr())) {
+    : DProcessBase(name, io_service, DCfgMgrBasePtr(new CtrlAgentCfgMgr())),
+      http_listeners_() {
 }
 
 CtrlAgentProcess::~CtrlAgentProcess() {
@@ -47,53 +49,20 @@ CtrlAgentProcess::run() {
     LOG_INFO(agent_logger, CTRL_AGENT_STARTED).arg(VERSION);
 
     try {
-
         // Register commands.
         CtrlAgentControllerPtr controller =
             boost::dynamic_pointer_cast<CtrlAgentController>(
                 CtrlAgentController::instance());
         controller->registerCommands();
 
-        // Create response creator factory first. It will be used to generate
-        // response creators. Each response creator will be used to generate
-        // answer to specific request.
-        HttpResponseCreatorFactoryPtr rcf(new CtrlAgentResponseCreatorFactory());
-
-        DCfgContextBasePtr base_ctx = getCfgMgr()->getContext();
-        CtrlAgentCfgContextPtr ctx =
-            boost::dynamic_pointer_cast<CtrlAgentCfgContext>(base_ctx);
-        if (!ctx) {
-            isc_throw(Unexpected, "Interal logic error: bad context type");
-        }
-
-        /// @todo: If the parameter is a hostname, we need to resolve it.
-        IOAddress server_address("::");
-        try {
-            server_address = IOAddress(ctx->getHttpHost());
-        } catch (const IOError& e) {
-            isc_throw(BadValue, "Failed to convert " << ctx->getHttpHost()
-                      << " to IP address:" << e.what());
-        }
-
-        uint16_t server_port = ctx->getHttpPort();
-
-        // Create http listener. It will open up a TCP socket and be prepared
-        // to accept incoming connection.
-        HttpListener http_listener(*getIoService(), server_address,
-                                   server_port, rcf, REQUEST_TIMEOUT);
-
-        // Instruct the http listener to actually open socket, install callback
-        // and start listening.
-        http_listener.start();
-
-        // Ok, seems we're good to go.
-        LOG_INFO(agent_logger, CTRL_AGENT_HTTP_SERVICE_STARTED)
-            .arg(server_address.toText()).arg(server_port);
-
         // Let's process incoming data or expiring timers in a loop until
         // shutdown condition is detected.
         while (!shouldShutdown()) {
-            getIoService()->run_one();
+            // 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();
+            runIO();
         }
         stopIOService();
     } catch (const std::exception& ex) {
@@ -120,6 +89,15 @@ CtrlAgentProcess::run() {
     LOG_DEBUG(agent_logger, isc::log::DBGLVL_START_SHUT, CTRL_AGENT_RUN_EXIT);
 }
 
+size_t
+CtrlAgentProcess::runIO() {
+    size_t cnt = getIoService()->get_io_service().poll();
+    if (!cnt) {
+        cnt = getIoService()->get_io_service().run_one();
+    }
+    return (cnt);
+}
+
 isc::data::ConstElementPtr
 CtrlAgentProcess::shutdown(isc::data::ConstElementPtr /*args*/) {
     setShutdownFlag(true);
@@ -129,17 +107,113 @@ CtrlAgentProcess::shutdown(isc::data::ConstElementPtr /*args*/) {
 isc::data::ConstElementPtr
 CtrlAgentProcess::configure(isc::data::ConstElementPtr config_set,
                             bool check_only) {
+    // System reconfiguration often poses an interesting issue whereby the
+    // configuration parsing is successful, but an attempt to use a new
+    // configuration is not. This will leave us in the inconsistent state
+    // when the configuration is in fact only partially applied and the
+    // system's ability to operate is impaired. The use of C++ lambda is
+    // a way to resolve this problem by injecting the code to the
+    // simpleParseConfig which performs an attempt to open new instance
+    // of the listener (if required). The lambda code will throw an
+    // exception if it fails and cause the simpleParseConfig to rollback
+    // configuration changes and report an error.
+    ConstElementPtr answer = getCfgMgr()->simpleParseConfig(config_set,
+                                                            check_only,
+                                                            [this]() {
+        DCfgContextBasePtr base_ctx = getCfgMgr()->getContext();
+        CtrlAgentCfgContextPtr
+            ctx = boost::dynamic_pointer_cast<CtrlAgentCfgContext>(base_ctx);
+
+        if (!ctx) {
+            isc_throw(Unexpected, "Interal logic error: bad context type");
+        }
+
+        /// @todo: If the parameter is a hostname, we need to resolve it.
+        IOAddress server_address("::");
+        try {
+            server_address = IOAddress(ctx->getHttpHost());
+
+        } catch (const IOError& e) {
+            isc_throw(BadValue, "Failed to convert " << ctx->getHttpHost()
+                      << " to IP address:" << e.what());
+        }
+
+        uint16_t server_port = ctx->getHttpPort();
+
+        // Only open a new listener if the configuration has changed.
+        if (http_listeners_.empty() ||
+            (http_listeners_.back()->getLocalAddress() != server_address) ||
+            (http_listeners_.back()->getLocalPort() != server_port)) {
+            // Create response creator factory first. It will be used to
+            // generate response creators. Each response creator will be
+            // used to generate answer to specific request.
+            HttpResponseCreatorFactoryPtr rcf(new CtrlAgentResponseCreatorFactory());
+
+            // Create http listener. It will open up a TCP socket and be
+            // prepared to accept incoming connection.
+            HttpListenerPtr http_listener(new HttpListener(*getIoService(),
+                                                           server_address,
+                                                           server_port, rcf,
+                                                           REQUEST_TIMEOUT));
+
+            // Instruct the http listener to actually open socket, install
+            // callback and start listening.
+            http_listener->start();
+
+            // The new listener is running so add it to the collection of
+            // active listeners. The next step will be to remove all other
+            // active listeners, but we do it inside the main process loop.
+            http_listeners_.push_back(http_listener);
+        }
+
+        // Ok, seems we're good to go.
+        LOG_INFO(agent_logger, CTRL_AGENT_HTTP_SERVICE_STARTED)
+            .arg(server_address.toText()).arg(server_port);
+
+    });
+
     int rcode = 0;
-    isc::data::ConstElementPtr answer = getCfgMgr()->simpleParseConfig(config_set,
-                                                                       check_only);
     config::parseAnswer(rcode, answer);
     return (answer);
 }
 
+void
+CtrlAgentProcess::garbageCollectListeners() {
+    // We expect only one active listener. If there are more (most likely 2),
+    // it means we have just reconfigured the server and need to shut down all
+    // listeners execept the most recently added.
+    if (http_listeners_.size() > 1) {
+        // Stop no longer used listeners.
+        for (auto l = http_listeners_.begin(); l != http_listeners_.end() - 1;
+             ++l) {
+            (*l)->stop();
+        }
+        // We have stopped listeners but there may be some pending handlers
+        // related to these listeners. Need to invoke these handlers.
+        getIoService()->get_io_service().poll();
+        // Finally, we're ready to remove no longer used listeners.
+        http_listeners_.erase(http_listeners_.begin(),
+                              http_listeners_.end() - 1);
+    }
+}
+
 
 CtrlAgentCfgMgrPtr
 CtrlAgentProcess::getCtrlAgentCfgMgr() {
-    return(boost::dynamic_pointer_cast<CtrlAgentCfgMgr>(getCfgMgr()));
+    return (boost::dynamic_pointer_cast<CtrlAgentCfgMgr>(getCfgMgr()));
+}
+
+ConstHttpListenerPtr
+CtrlAgentProcess::getHttpListener() const {
+    // Return the most recent listener or null.
+    return (http_listeners_.empty() ? ConstHttpListenerPtr() :
+            http_listeners_.back());
+}
+
+bool
+CtrlAgentProcess::isListening() const {
+    // If there are is a listener, we're listening.
+    return (static_cast<bool>(getHttpListener()));
 }
 
 } // namespace isc::agent

+ 52 - 0
src/bin/agent/ca_process.h

@@ -8,7 +8,9 @@
 #define CTRL_AGENT_PROCESS_H
 
 #include <agent/ca_cfg_mgr.h>
+#include <http/listener.h>
 #include <process/d_process.h>
+#include <vector>
 
 namespace isc {
 namespace agent {
@@ -78,6 +80,23 @@ public:
     /// processing errors and return a success or failure answer as described
     /// below.
     ///
+    /// A usual problem related to the system reconfiguration is how to preserve
+    /// configuration integrity in case of errors. In this case, when the
+    /// HTTP listener's configuration is modified there is a need to close all
+    /// existing connections and gracefully shutdown the listener's instance.
+    /// This, however, makes it possible that the control agent looses
+    /// connectivity if opening a new listener is unsuccessful. In fact, this
+    /// is quite possible scenario when the user is setting up the listener to
+    /// use a restricted port range or non-existing IP address. In this case,
+    /// the configuration parser will not signal the problem because IP address
+    /// and/or port are syntactically correcect.
+    ///
+    /// This method deals with this problem by opening a new listener aside of
+    /// the currently running listener (if the new listener settings are
+    /// different than current settings). Both instances are held until the
+    /// CtrlAgentProcess::garbageCollectListeners is invoked, which
+    /// removes any listeners which are no longer used.
+    ///
     /// @param config_set a new configuration (JSON) for the process
     /// @param check_only true if configuration is to be verified only, not applied
     /// @return an Element that contains the results of configuration composed
@@ -89,6 +108,39 @@ public:
 
     /// @brief Returns a pointer to the configuration manager.
     CtrlAgentCfgMgrPtr getCtrlAgentCfgMgr();
+
+    /// @brief Returns a const pointer to the HTTP listener used by the process.
+    ///
+    /// @return Const pointer to the currently used listener or null pointer if
+    /// we're not listening. In fact, the latter should never be the case given
+    /// that we provide default listener configuration.
+    http::ConstHttpListenerPtr getHttpListener() const;
+
+    /// @brief Checks if the process is listening to the HTTP requests.
+    ///
+    /// @return true if the process is listening.
+    bool isListening() const;
+
+private:
+
+    /// @brief Removes listeners which are no longer in use.
+    ///
+    /// This method should be called after executing
+    /// @ref CtrlAgentProcess::configure to remove listeners used previously
+    /// (no longer used because the listening address and port has changed as
+    // a result of the reconfiguration). If there are no listeners additional
+    /// to the one that is currently in use, the method has no effect.
+    void garbageCollectListeners();
+
+    /// @brief Polls all ready handlers and then runs one handler if none
+    /// handlers have been executed as a result of polling.
+    ///
+    /// @return Number of executed handlers.
+    size_t runIO();
+
+    /// @brief Holds a list of pointers to the active listeners.
+    std::vector<http::HttpListenerPtr> http_listeners_;
+
 };
 
 /// @brief Defines a shared pointer to CtrlAgentProcess.

+ 1 - 0
src/bin/agent/tests/Makefile.am

@@ -69,6 +69,7 @@ ca_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
 ca_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
 ca_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
 ca_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la
+ca_unittests_LDADD += $(top_builddir)/src/lib/asiolink/testutils/libasiolinktest.la
 ca_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
 ca_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la
 ca_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la

+ 13 - 0
src/bin/agent/tests/ca_cfg_mgr_unittests.cc

@@ -7,6 +7,7 @@
 #include <config.h>
 #include <agent/ca_cfg_mgr.h>
 #include <agent/parser_context.h>
+#include <exceptions/exceptions.h>
 #include <process/testutils/d_test_stubs.h>
 #include <process/d_cfg_mgr.h>
 #include <agent/tests/test_libraries.h>
@@ -27,6 +28,18 @@ public:
     using CtrlAgentCfgMgr::parse;
 };
 
+// Tests conversion of the 'service' parameter to ServerType.
+TEST(CtrlAgentCfgContextTest, toServerType) {
+    EXPECT_EQ(CtrlAgentCfgContext::TYPE_DHCP4,
+              CtrlAgentCfgContext::toServerType("dhcp4"));
+    EXPECT_EQ(CtrlAgentCfgContext::TYPE_DHCP6,
+              CtrlAgentCfgContext::toServerType("dhcp6"));
+    EXPECT_EQ(CtrlAgentCfgContext::TYPE_D2,
+              CtrlAgentCfgContext::toServerType("d2"));
+    EXPECT_THROW(CtrlAgentCfgContext::toServerType("other"),
+                 isc::BadValue);
+}
+
 // Tests construction of CtrlAgentCfgMgr class.
 TEST(CtrlAgentCfgMgr, construction) {
     boost::scoped_ptr<CtrlAgentCfgMgr> cfg_mgr;

+ 290 - 10
src/bin/agent/tests/ca_command_mgr_unittests.cc

@@ -5,29 +5,54 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 #include <config.h>
+#include <agent/ca_cfg_mgr.h>
 #include <agent/ca_command_mgr.h>
+#include <agent/ca_controller.h>
+#include <agent/ca_process.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <asiolink/testutils/test_server_unix_socket.h>
 #include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <process/testutils/d_test_stubs.h>
+#include <util/threads/thread.h>
+#include <boost/bind.hpp>
+#include <boost/pointer_cast.hpp>
 #include <gtest/gtest.h>
+#include <cstdlib>
+#include <vector>
 
 using namespace isc::agent;
+using namespace isc::asiolink;
 using namespace isc::data;
+using namespace isc::process;
 
 namespace {
 
+/// @brief Test unix socket file name.
+const std::string TEST_SOCKET = "test-socket";
+
+/// @brief Test timeout in ms.
+const long TEST_TIMEOUT = 10000;
+
 /// @brief Test fixture class for @ref CtrlAgentCommandMgr.
 ///
 /// @todo Add tests for various commands, including the cases when the
 /// commands are forwarded to other servers via unix sockets.
 /// Meanwhile, this is just a placeholder for the tests.
-class CtrlAgentCommandMgrTest : public ::testing::Test {
+class CtrlAgentCommandMgrTest : public DControllerTest {
 public:
 
     /// @brief Constructor.
     ///
     /// Deregisters all commands except 'list-commands'.
     CtrlAgentCommandMgrTest()
-        : mgr_(CtrlAgentCommandMgr::instance()) {
+        : DControllerTest(CtrlAgentController::instance),
+          mgr_(CtrlAgentCommandMgr::instance()) {
         mgr_.deregisterAll();
+        removeUnixSocketFile();
+        initProcess();
     }
 
     /// @brief Destructor.
@@ -35,6 +60,7 @@ public:
     /// Deregisters all commands except 'list-commands'.
     virtual ~CtrlAgentCommandMgrTest() {
         mgr_.deregisterAll();
+        removeUnixSocketFile();
     }
 
     /// @brief Verifies received answer
@@ -43,23 +69,185 @@ public:
     /// verification of the response parameters.
     ///
     /// @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;
         // There may be multiple answers returned within a list.
         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();
              ++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();
         }
     }
 
+    /// @brief Returns socket file path.
+    ///
+    /// If the KEA_SOCKET_TEST_DIR environment variable is specified, the
+    /// socket file is created in the location pointed to by this variable.
+    /// Otherwise, it is created in the build directory.
+    static std::string unixSocketFilePath() {
+        std::ostringstream s;
+        const char* env = getenv("KEA_SOCKET_TEST_DIR");
+        if (env) {
+            s << std::string(env);
+        } else {
+            s << TEST_DATA_BUILDDIR;
+        }
+
+        s << "/" << TEST_SOCKET;
+        return (s.str());
+    }
+
+    /// @brief Removes unix socket descriptor.
+    void removeUnixSocketFile() {
+        static_cast<void>(remove(unixSocketFilePath().c_str()));
+    }
+
+    /// @brief Returns pointer to CtrlAgentProcess instance.
+    CtrlAgentProcessPtr getCtrlAgentProcess() {
+        return (boost::dynamic_pointer_cast<CtrlAgentProcess>(getProcess()));
+    }
+
+    /// @brief Returns pointer to CtrlAgentCfgMgr instance for a process.
+    CtrlAgentCfgMgrPtr getCtrlAgentCfgMgr() {
+        CtrlAgentCfgMgrPtr p;
+        if (getCtrlAgentProcess()) {
+            p = getCtrlAgentProcess()->getCtrlAgentCfgMgr();
+        }
+        return (p);
+    }
+
+    /// @brief Returns a pointer to the configuration context.
+    CtrlAgentCfgContextPtr getCtrlAgentCfgContext() {
+        CtrlAgentCfgContextPtr p;
+        if (getCtrlAgentCfgMgr()) {
+            p = getCtrlAgentCfgMgr()->getCtrlAgentCfgContext();
+        }
+        return (p);
+    }
+
+    /// @brief Adds configuration of the control socket.
+    ///
+    /// @param server_type Server type for which socket configuration is to
+    /// be added.
+    void
+    configureControlSocket(const CtrlAgentCfgContext::ServerType& server_type) {
+        CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
+        ASSERT_TRUE(ctx);
+
+        ElementPtr control_socket = Element::createMap();
+        control_socket->set("socket-name",
+                            Element::create(unixSocketFilePath()));
+        ctx->setControlSocketInfo(control_socket, server_type);
+    }
+
+    /// @brief Create and bind server side socket.
+    ///
+    /// @param response Stub response to be sent from the server socket to the
+    /// client.
+    /// @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(),
+                                                            unixSocketFilePath(),
+                                                            TEST_TIMEOUT,
+                                                            response));
+        server_socket_->bindServerSocket(stop_after_count);
+    }
+
+    /// @brief Creates command with no arguments.
+    ///
+    /// @param command_name Command name.
+    /// @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.
+    ConstElementPtr createCommand(const std::string& command_name,
+                                  const std::string& service) {
+        ElementPtr command = Element::createMap();
+        command->set("command", Element::create(command_name));
+
+        // Only add the 'service' parameter if non-empty.
+        if (!service.empty()) {
+            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());
+
+        return (command);
+    }
+
+    /// @brief Test forwarding the command.
+    ///
+    /// @param server_type Server for which the client socket should be
+    /// configured.
+    /// @param service Service to be included in the command.
+    /// @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.
+    void testForward(const CtrlAgentCfgContext::ServerType& server_type,
+                     const std::string& service,
+                     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 }") {
+        // Configure client side socket.
+        configureControlSocket(server_type);
+        // Create server side socket.
+        bindServerSocket(server_response, stop_after_count);
+
+        // 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.
+        // Running IO service in a thread guarantees that the server responds
+        // as soon as it receives the control command.
+        isc::util::thread::Thread(boost::bind(&IOService::run,
+                                              getIOService().get()));
+
+        ConstElementPtr command = createCommand("foo", service);
+        ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(),
+                                                    command);
+
+        checkAnswer(answer, expected_result0, expected_result1, expected_result2);
+    }
+
     /// @brief a convenience reference to control agent command manager
     CtrlAgentCommandMgr& mgr_;
+
+    /// @brief Pointer to the test server unix socket.
+    test::TestServerUnixSocketPtr server_socket_;
 };
 
 /// Just a basic test checking that non-existent command is handled
@@ -67,17 +255,109 @@ public:
 TEST_F(CtrlAgentCommandMgrTest, bogus) {
     ConstElementPtr answer;
     EXPECT_NO_THROW(answer = mgr_.handleCommand("fish-and-chips-please",
+                                                ConstElementPtr(),
                                                 ConstElementPtr()));
-    checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR);
+    checkAnswer(answer, isc::config::CONTROL_RESULT_COMMAND_UNSUPPORTED);
 };
 
 /// Just a basic test checking that 'list-commands' is supported.
 TEST_F(CtrlAgentCommandMgrTest, listCommands) {
     ConstElementPtr answer;
     EXPECT_NO_THROW(answer = mgr_.handleCommand("list-commands",
+                                                ConstElementPtr(),
                                                 ConstElementPtr()));
     checkAnswer(answer, isc::config::CONTROL_RESULT_SUCCESS);
 };
 
+/// Check that control command is successfully forwarded to the DHCPv4 server.
+TEST_F(CtrlAgentCommandMgrTest, forwardToDHCPv4Server) {
+    testForward(CtrlAgentCfgContext::TYPE_DHCP4, "dhcp4",
+                isc::config::CONTROL_RESULT_SUCCESS);
+}
+
+/// Check that control command is successfully forwarded to the DHCPv6 server.
+TEST_F(CtrlAgentCommandMgrTest, forwardToDHCPv6Server) {
+    testForward(CtrlAgentCfgContext::TYPE_DHCP6, "dhcp6",
+                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.
+TEST_F(CtrlAgentCommandMgrTest, noService) {
+    testForward(CtrlAgentCfgContext::TYPE_DHCP6, "",
+                isc::config::CONTROL_RESULT_COMMAND_UNSUPPORTED);
+}
+
+/// Check that error is returned to the client when the server to which the
+/// command was forwarded sent an invalid message.
+TEST_F(CtrlAgentCommandMgrTest, invalidAnswer) {
+    testForward(CtrlAgentCfgContext::TYPE_DHCP6, "dhcp6",
+                isc::config::CONTROL_RESULT_ERROR, -1, -1, 1,
+                "{ \"result\": 0");
+}
+
+/// Check that error is returned to the client if the forwarding socket is
+/// not configured for the given service.
+TEST_F(CtrlAgentCommandMgrTest, noClientSocket) {
+    ConstElementPtr command = createCommand("foo", "dhcp4");
+    ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(),
+                                                command);
+
+    checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR);
+}
+
+/// Check that error is returned to the client if the remote server to
+/// which the control command is to be forwarded is not available.
+TEST_F(CtrlAgentCommandMgrTest, noServerSocket) {
+    configureControlSocket(CtrlAgentCfgContext::TYPE_DHCP6);
+
+    ConstElementPtr command = createCommand("foo", "dhcp6");
+    ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(),
+                                                command);
+
+    checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR);
+}
+
+// Check that list-commands command is forwarded when the service
+// value is specified.
+TEST_F(CtrlAgentCommandMgrTest, forwardListCommands) {
+    // Configure client side socket.
+    configureControlSocket(CtrlAgentCfgContext::TYPE_DHCP4);
+    // Create server side socket.
+    bindServerSocket("{ \"result\" : 3 }");
+
+    // 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.
+    // Running IO service in a thread guarantees that the server responds
+    // as soon as it receives the control command.
+    isc::util::thread::Thread(boost::bind(&IOService::run,
+                                          getIOService().get()));
+
+    ConstElementPtr command = createCommand("list-commands", "dhcp4");
+    ConstElementPtr answer = mgr_.handleCommand("list-commands", ConstElementPtr(),
+                                                command);
+
+    // Answer of 3 is specific to the stub response we send when the
+    // command is forwarded. So having this value returned means that
+    // the command was forwarded as expected.
+    checkAnswer(answer, 3);
+}
 
 }

+ 217 - 1
src/bin/agent/tests/ca_controller_unittests.cc

@@ -7,11 +7,16 @@
 #include <config.h>
 #include <agent/ca_controller.h>
 #include <agent/ca_process.h>
+#include <cc/data.h>
 #include <process/testutils/d_test_stubs.h>
 #include <boost/pointer_cast.hpp>
+#include <sstream>
 
 using namespace isc::agent;
+using namespace isc::data;
+using namespace isc::http;
 using namespace isc::process;
+using namespace boost::posix_time;
 
 namespace {
 
@@ -19,7 +24,17 @@ namespace {
 const char* valid_agent_config =
     "{"
     "  \"http-host\": \"127.0.0.1\","
-    "  \"http-port\": 8081"
+    "  \"http-port\": 8081,"
+    "  \"control-sockets\": {"
+    "    \"dhcp4-server\": {"
+    "      \"socket-type\": \"unix\","
+    "      \"socket-name\": \"/first/dhcp4/socket\""
+    "    },"
+    "    \"dhcp6-server\": {"
+    "      \"socket-type\": \"unix\","
+    "      \"socket-name\": \"/first/dhcp6/socket\""
+    "    }"
+    "  }"
     "}";
 
 /// @brief test fixture class for testing CtrlAgentController class. This
@@ -35,6 +50,48 @@ public:
         : DControllerTest(CtrlAgentController::instance) {
     }
 
+    /// @brief Returns pointer to CtrlAgentProcess instance.
+    CtrlAgentProcessPtr getCtrlAgentProcess() {
+        return (boost::dynamic_pointer_cast<CtrlAgentProcess>(getProcess()));
+    }
+
+    /// @brief Returns pointer to CtrlAgentCfgMgr instance for a process.
+    CtrlAgentCfgMgrPtr getCtrlAgentCfgMgr() {
+        CtrlAgentCfgMgrPtr p;
+        if (getCtrlAgentProcess()) {
+            p = getCtrlAgentProcess()->getCtrlAgentCfgMgr();
+        }
+        return (p);
+    }
+
+    /// @brief Returns a pointer to the configuration context.
+    CtrlAgentCfgContextPtr getCtrlAgentCfgContext() {
+        CtrlAgentCfgContextPtr p;
+        if (getCtrlAgentCfgMgr()) {
+            p = getCtrlAgentCfgMgr()->getCtrlAgentCfgContext();
+        }
+        return (p);
+    }
+
+    /// @brief Tests that socket info structure contains 'unix' socket-type
+    /// value and the expected socket-name.
+    ///
+    /// @param type Server type.
+    /// @param exp_socket_name Expected socket name.
+    void testUnixSocketInfo(const CtrlAgentCfgContext::ServerType& type,
+                            const std::string& exp_socket_name) {
+        CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
+        ASSERT_TRUE(ctx);
+
+        ConstElementPtr sock_info = ctx->getControlSocketInfo(type);
+        ASSERT_TRUE(sock_info);
+        ASSERT_TRUE(sock_info->contains("socket-type"));
+        EXPECT_EQ("unix", sock_info->get("socket-type")->stringValue());
+        ASSERT_TRUE(sock_info->contains("socket-name"));
+        EXPECT_EQ(exp_socket_name,
+                  sock_info->get("socket-name")->stringValue());
+    }
+
 };
 
 // Basic Controller instantiation testing.
@@ -142,4 +199,163 @@ TEST_F(CtrlAgentControllerTest, sigtermShutdown) {
     EXPECT_TRUE(elapsed_time.total_milliseconds() < 300);
 }
 
+// Tests that the sockets settings are updated upon successful reconfiguration.
+TEST_F(CtrlAgentControllerTest, successfulConfigUpdate) {
+    // This configuration should be used to override the initial conifguration.
+    const char* second_config =
+        "{"
+        "  \"http-host\": \"127.0.0.1\","
+        "  \"http-port\": 8080,"
+        "  \"control-sockets\": {"
+        "    \"dhcp4-server\": {"
+        "      \"socket-type\": \"unix\","
+        "      \"socket-name\": \"/second/dhcp4/socket\""
+        "    },"
+        "    \"dhcp6-server\": {"
+        "      \"socket-type\": \"unix\","
+        "      \"socket-name\": \"/second/dhcp6/socket\""
+        "    }"
+        "  }"
+        "}";
+
+    // Schedule reconfiguration.
+    scheduleTimedWrite(second_config, 100);
+    // Schedule SIGHUP signal to trigger reconfiguration.
+    TimedSignal sighup(*getIOService(), SIGHUP, 200);
+
+    // Start the server.
+    time_duration elapsed_time;
+    runWithConfig(valid_agent_config, 500, elapsed_time);
+
+    CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
+    ASSERT_TRUE(ctx);
+
+    // The server should now hold the new listener configuration.
+    EXPECT_EQ("127.0.0.1", ctx->getHttpHost());
+    EXPECT_EQ(8080, ctx->getHttpPort());
+
+    // The forwarding configuration should have been updated too.
+    testUnixSocketInfo(CtrlAgentCfgContext::TYPE_DHCP4, "/second/dhcp4/socket");
+    testUnixSocketInfo(CtrlAgentCfgContext::TYPE_DHCP6, "/second/dhcp6/socket");
+
+    CtrlAgentProcessPtr process = getCtrlAgentProcess();
+    ASSERT_TRUE(process);
+
+    // Check that the HTTP listener still exists after reconfiguration.
+    ConstHttpListenerPtr listener = process->getHttpListener();
+    ASSERT_TRUE(listener);
+    EXPECT_TRUE(process->isListening());
+
+    // The listener should have been reconfigured to use new address and port.
+    EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText());
+    EXPECT_EQ(8080, listener->getLocalPort());
+}
+
+// Tests that the server continues to use an old configuration when the listener
+// reconfiguration is unsuccessful.
+TEST_F(CtrlAgentControllerTest, unsuccessfulConfigUpdate) {
+    // This is invalid configuration. We're using restricted port number and
+    // IP address of 1.1.1.1.
+    const char* second_config =
+        "{"
+        "  \"http-host\": \"1.1.1.1\","
+        "  \"http-port\": 1,"
+        "  \"control-sockets\": {"
+        "    \"dhcp4-server\": {"
+        "      \"socket-type\": \"unix\","
+        "      \"socket-name\": \"/second/dhcp4/socket\""
+        "    },"
+        "    \"dhcp6-server\": {"
+        "      \"socket-type\": \"unix\","
+        "      \"socket-name\": \"/second/dhcp6/socket\""
+        "    }"
+        "  }"
+        "}";
+
+    // Schedule reconfiguration.
+    scheduleTimedWrite(second_config, 100);
+    // Schedule SIGHUP signal to trigger reconfiguration.
+    TimedSignal sighup(*getIOService(), SIGHUP, 200);
+
+    // Start the server.
+    time_duration elapsed_time;
+    runWithConfig(valid_agent_config, 500, elapsed_time);
+
+    CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
+    ASSERT_TRUE(ctx);
+
+    // The reconfiguration should have been unsuccessful, and the server should
+    // still use the original configuration.
+    EXPECT_EQ("127.0.0.1", ctx->getHttpHost());
+    EXPECT_EQ(8081, ctx->getHttpPort());
+
+    // Same for forwarding.
+    testUnixSocketInfo(CtrlAgentCfgContext::TYPE_DHCP4, "/first/dhcp4/socket");
+    testUnixSocketInfo(CtrlAgentCfgContext::TYPE_DHCP6, "/first/dhcp6/socket");
+
+    CtrlAgentProcessPtr process = getCtrlAgentProcess();
+    ASSERT_TRUE(process);
+
+    // We should still be using an original listener.
+    ConstHttpListenerPtr listener = process->getHttpListener();
+    ASSERT_TRUE(listener);
+    EXPECT_TRUE(process->isListening());
+
+    EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText());
+    EXPECT_EQ(8081, listener->getLocalPort());
+}
+
+// Tests that it is possible to update the configuration in such a way that the
+// listener configuration remains the same. The server should continue using the
+// listener instance it has been using prior to the reconfiguration.
+TEST_F(CtrlAgentControllerTest, noListenerChange) {
+    // This configuration should be used to override the initial conifguration.
+    const char* second_config =
+        "{"
+        "  \"http-host\": \"127.0.0.1\","
+        "  \"http-port\": 8081,"
+        "  \"control-sockets\": {"
+        "    \"dhcp4-server\": {"
+        "      \"socket-type\": \"unix\","
+        "      \"socket-name\": \"/second/dhcp4/socket\""
+        "    },"
+        "    \"dhcp6-server\": {"
+        "      \"socket-type\": \"unix\","
+        "      \"socket-name\": \"/second/dhcp6/socket\""
+        "    }"
+        "  }"
+        "}";
+
+    // Schedule reconfiguration.
+    scheduleTimedWrite(second_config, 100);
+    // Schedule SIGHUP signal to trigger reconfiguration.
+    TimedSignal sighup(*getIOService(), SIGHUP, 200);
+
+    // Start the server.
+    time_duration elapsed_time;
+    runWithConfig(valid_agent_config, 500, elapsed_time);
+
+    CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
+    ASSERT_TRUE(ctx);
+
+    // The server should use a correct listener configuration.
+    EXPECT_EQ("127.0.0.1", ctx->getHttpHost());
+    EXPECT_EQ(8081, ctx->getHttpPort());
+
+    // The forwarding configuration should have been updated.
+    testUnixSocketInfo(CtrlAgentCfgContext::TYPE_DHCP4, "/second/dhcp4/socket");
+    testUnixSocketInfo(CtrlAgentCfgContext::TYPE_DHCP6, "/second/dhcp6/socket");
+
+    CtrlAgentProcessPtr process = getCtrlAgentProcess();
+    ASSERT_TRUE(process);
+
+    // The listener should keep listening.
+    ConstHttpListenerPtr listener = process->getHttpListener();
+    ASSERT_TRUE(listener);
+    EXPECT_TRUE(process->isListening());
+
+    EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText());
+    EXPECT_EQ(8081, listener->getLocalPort());
+}
+
 }

+ 1 - 1
src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc

@@ -413,7 +413,7 @@ TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelNegative) {
     std::string response;
 
     sendUnixCommand("{ \"command\": \"bogus\" }", response);
-    EXPECT_EQ("{ \"result\": 1,"
+    EXPECT_EQ("{ \"result\": 2,"
               " \"text\": \"'bogus' command not supported.\" }", response);
 
     sendUnixCommand("utter nonsense", response);

+ 1 - 1
src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc

@@ -780,7 +780,7 @@ TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelNegative) {
     std::string response;
 
     sendUnixCommand("{ \"command\": \"bogus\" }", response);
-    EXPECT_EQ("{ \"result\": 1,"
+    EXPECT_EQ("{ \"result\": 2,"
               " \"text\": \"'bogus' command not supported.\" }", response);
 
     sendUnixCommand("utter nonsense", response);

+ 2 - 1
src/lib/asiolink/Makefile.am

@@ -1,4 +1,4 @@
-SUBDIRS = . tests
+SUBDIRS = . testutils tests
 
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += $(BOOST_INCLUDES)
@@ -32,6 +32,7 @@ libkea_asiolink_la_SOURCES += tcp_endpoint.h
 libkea_asiolink_la_SOURCES += tcp_socket.h
 libkea_asiolink_la_SOURCES += udp_endpoint.h
 libkea_asiolink_la_SOURCES += udp_socket.h
+libkea_asiolink_la_SOURCES += unix_domain_socket.cc unix_domain_socket.h
 
 # Note: the ordering matters: -Wno-... must follow -Wextra (defined in
 # KEA_CXXFLAGS)

+ 5 - 2
src/lib/asiolink/tests/Makefile.am

@@ -1,6 +1,7 @@
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += $(BOOST_INCLUDES)
 AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(srcdir)/testdata\"
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/asiolink/tests\"
 
 AM_CXXFLAGS = $(KEA_CXXFLAGS)
 
@@ -8,7 +9,7 @@ if USE_STATIC_LINK
 AM_LDFLAGS = -static
 endif
 
-CLEANFILES = *.gcno *.gcda
+CLEANFILES = *.gcno *.gcda test-socket
 
 TESTS_ENVIRONMENT = \
 	$(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
@@ -28,10 +29,12 @@ run_unittests_SOURCES += udp_socket_unittest.cc
 run_unittests_SOURCES += io_service_unittest.cc
 run_unittests_SOURCES += dummy_io_callback_unittest.cc
 run_unittests_SOURCES += tcp_acceptor_unittest.cc
+run_unittests_SOURCES += unix_domain_socket_unittest.cc
 
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 
-run_unittests_LDADD = $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
+run_unittests_LDADD  = $(top_builddir)/src/lib/asiolink/testutils/libasiolinktest.la
+run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.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

+ 142 - 0
src/lib/asiolink/tests/unix_domain_socket_unittest.cc

@@ -0,0 +1,142 @@
+// 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 <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/io_service.h>
+#include <asiolink/unix_domain_socket.h>
+#include <asiolink/testutils/test_server_unix_socket.h>
+#include <gtest/gtest.h>
+#include <array>
+#include <cstdio>
+#include <cstdlib>
+#include <sstream>
+#include <string>
+
+using namespace isc::asiolink;
+
+namespace  {
+
+/// @brief Test unix socket file name.
+const std::string TEST_SOCKET = "test-socket";
+
+/// @brief Test timeout in ms.
+const long TEST_TIMEOUT = 10000;
+
+/// @brief Test fixture class for @ref UnixDomainSocket class.
+class UnixDomainSocketTest : public ::testing::Test {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Removes unix socket descriptor before the test.
+    UnixDomainSocketTest() : io_service_(),
+                             test_socket_(io_service_, unixSocketFilePath(),
+                                          TEST_TIMEOUT) {
+        removeUnixSocketFile();
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Removes unix socket descriptor after the test.
+    virtual ~UnixDomainSocketTest() {
+        removeUnixSocketFile();
+    }
+
+    /// @brief Returns socket file path.
+    ///
+    /// If the KEA_SOCKET_TEST_DIR environment variable is specified, the
+    /// socket file is created in the location pointed to by this variable.
+    /// Otherwise, it is created in the build directory.
+    static std::string unixSocketFilePath() {
+        std::ostringstream s;
+        const char* env = getenv("KEA_SOCKET_TEST_DIR");
+        if (env) {
+            s << std::string(env);
+        } else {
+            s << TEST_DATA_BUILDDIR;
+        }
+
+        s << "/" << TEST_SOCKET;
+        return (s.str());
+    }
+
+    /// @brief Removes unix socket descriptor.
+    void removeUnixSocketFile() {
+        static_cast<void>(remove(unixSocketFilePath().c_str()));
+    }
+
+    /// @brief IO service used by the tests.
+    IOService io_service_;
+
+    /// @brief Server side unix socket used in these tests.
+    test::TestServerUnixSocket test_socket_;
+};
+
+// This test verifies that the client can send data over the unix
+// domain socket and receive a response.
+TEST_F(UnixDomainSocketTest, sendReceive) {
+    // Start the server.
+    test_socket_.bindServerSocket();
+
+    // Setup client side.
+    UnixDomainSocket socket(io_service_);
+    ASSERT_NO_THROW(socket.connect(unixSocketFilePath()));
+
+    // Send "foo".
+    const std::string outbound_data = "foo";
+    size_t sent_size = 0;
+    ASSERT_NO_THROW(sent_size = socket.write(outbound_data.c_str(),
+                                             outbound_data.size()));
+    // Make sure all data have been sent.
+    ASSERT_EQ(outbound_data.size(), sent_size);
+
+    // Run IO service to generate server's response.
+    io_service_.run();
+
+    // Receive response from the socket.
+    std::array<char, 1024> read_buf;
+    size_t bytes_read = 0;
+    ASSERT_NO_THROW(bytes_read = socket.receive(&read_buf[0], read_buf.size()));
+    std::string response(&read_buf[0], bytes_read);
+
+    // The server should prepend "received" to the data we had sent.
+    EXPECT_EQ("received foo", response);
+}
+
+// This test verifies that UnixDomainSocketError exception is thrown
+// on attempt to connect, write or receive when the server socket
+// is not available.
+TEST_F(UnixDomainSocketTest, clientErrors) {
+    UnixDomainSocket socket(io_service_);
+    ASSERT_THROW(socket.connect(unixSocketFilePath()), UnixDomainSocketError);
+    const std::string outbound_data = "foo";
+    ASSERT_THROW(socket.write(outbound_data.c_str(), outbound_data.size()),
+                 UnixDomainSocketError);
+    std::array<char, 1024> read_buf;
+    ASSERT_THROW(socket.receive(&read_buf[0], read_buf.size()),
+                 UnixDomainSocketError);
+}
+
+// Check that native socket descriptor is returned correctly when
+// the socket is connected.
+TEST_F(UnixDomainSocketTest, getNative) {
+    // Start the server.
+    test_socket_.bindServerSocket();
+
+    // Setup client side.
+    UnixDomainSocket socket(io_service_);
+    ASSERT_NO_THROW(socket.connect(unixSocketFilePath()));
+    ASSERT_GE(socket.getNative(), 0);
+}
+
+// Check that protocol returned is 0.
+TEST_F(UnixDomainSocketTest, getProtocol) {
+    UnixDomainSocket socket(io_service_);
+    EXPECT_EQ(0, socket.getProtocol());
+}
+
+}

+ 24 - 0
src/lib/asiolink/testutils/Makefile.am

@@ -0,0 +1,24 @@
+SUBDIRS = .
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+
+CLEANFILES = *.gcno *.gcda
+
+if HAVE_GTEST
+
+noinst_LTLIBRARIES = libasiolinktest.la
+
+libasiolinktest_la_SOURCES  = test_server_unix_socket.cc test_server_unix_socket.h
+
+libasiolinktest_la_CXXFLAGS = $(AM_CXXFLAGS)
+libasiolinktest_la_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+libasiolinktest_la_LDFLAGS  = $(AM_LDFLAGS)
+
+libasiolinktest_la_LIBADD  = $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
+libasiolinktest_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
+libasiolinktest_la_LIBADD += $(BOOST_LIBS)
+
+endif

+ 93 - 0
src/lib/asiolink/testutils/test_server_unix_socket.cc

@@ -0,0 +1,93 @@
+// 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 <asiolink/asio_wrapper.h>
+#include <asiolink/testutils/test_server_unix_socket.h>
+#include <boost/bind.hpp>
+
+namespace isc {
+namespace asiolink {
+namespace test {
+
+TestServerUnixSocket::TestServerUnixSocket(IOService& io_service,
+                                           const std::string& socket_file_path,
+                                           const long test_timeout,
+                                           const std::string& custom_response)
+    : io_service_(io_service),
+      server_endpoint_(socket_file_path),
+      server_acceptor_(io_service_.get_io_service()),
+      server_socket_(io_service_.get_io_service()),
+      test_timer_(io_service_),
+      custom_response_(custom_response),
+      stop_after_count_(1),
+      read_count_(0) {
+    test_timer_.setup(boost::bind(&TestServerUnixSocket::timeoutHandler, this),
+                      test_timeout, IntervalTimer::ONE_SHOT);
+}
+
+void
+TestServerUnixSocket::bindServerSocket(const unsigned int stop_after_count) {
+    server_acceptor_.open();
+    server_acceptor_.bind(server_endpoint_);
+    server_acceptor_.listen();
+    accept();
+
+    stop_after_count_ = stop_after_count;
+}
+
+void
+TestServerUnixSocket::acceptHandler(const boost::system::error_code&) {
+    server_socket_.async_read_some(boost::asio::buffer(&raw_buf_[0],
+                                                       raw_buf_.size()),
+                                   boost::bind(&TestServerUnixSocket::
+                                               readHandler, this, _1, _2));
+}
+
+void
+TestServerUnixSocket::accept() {
+    server_acceptor_.async_accept(server_socket_,
+                                  boost::bind(&TestServerUnixSocket::
+                                              acceptHandler, this, _1));
+}
+
+
+void
+TestServerUnixSocket::readHandler(const boost::system::error_code&,
+                                  size_t bytes_transferred) {
+    if (!custom_response_.empty()) {
+        boost::asio::write(server_socket_, boost::asio::buffer(custom_response_.c_str(),
+                                                               custom_response_.size()));
+
+    } else {
+        std::string received(&raw_buf_[0], bytes_transferred);
+        std::string response("received " + received);
+        boost::asio::write(server_socket_, boost::asio::buffer(response.c_str(),
+                                                               response.size()));
+    }
+
+    // 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
+TestServerUnixSocket::timeoutHandler() {
+    ADD_FAILURE() << "Timeout occurred while running the test!";
+    io_service_.stop();
+}
+
+} // end of namespace isc::asiolink::test
+} // end of namespace isc::asiolink
+} // end of namespace isc

+ 99 - 0
src/lib/asiolink/testutils/test_server_unix_socket.h

@@ -0,0 +1,99 @@
+// 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 TEST_SERVER_UNIX_SOCKET_H
+#define TEST_SERVER_UNIX_SOCKET_H
+
+#include <config.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+#include <array>
+#include <string>
+
+namespace isc {
+namespace asiolink {
+namespace test {
+
+/// @brief Provides unix domain socket functionality for unit tests.
+class TestServerUnixSocket {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service IO service.
+    /// @param socket_file_path Socket file path.
+    /// @param test_timeout Test timeout in milliseconds.
+    /// @param custom_response Custom response to be sent to the client.
+    TestServerUnixSocket(IOService& io_service,
+                         const std::string& socket_file_path,
+                         const long test_timeout,
+                         const std::string& custom_respons_ = "");
+
+    /// @brief Creates and binds server socket.
+    ///
+    /// @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.
+    ///
+    /// @param ec Error code.
+    void acceptHandler(const boost::system::error_code& ec);
+
+    /// @brief Server read handler.
+    ///
+    /// @param ec Error code.
+    /// @param bytes_transferred Number of bytes read.
+    void readHandler(const boost::system::error_code& ec,
+                     size_t bytes_transferred);
+
+    /// @brief Callback function invoke upon test timeout.
+    ///
+    /// It stops the IO service and reports test timeout.
+    void timeoutHandler();
+
+private:
+
+    /// @brief Asynchronously accept new connections.
+    void accept();
+
+    /// @brief IO service used by the tests.
+    IOService& io_service_;
+
+    /// @brief Server endpoint.
+    boost::asio::local::stream_protocol::endpoint server_endpoint_;
+    /// @brief Server acceptor.
+    boost::asio::local::stream_protocol::acceptor server_acceptor_;
+
+    /// @brief Server side unix domain socket.
+    boost::asio::local::stream_protocol::socket server_socket_;
+
+    /// @brief Receive buffer.
+    std::array<char, 1024> raw_buf_;
+
+    /// @brief Asynchronous timer service to detect timeouts.
+    IntervalTimer test_timer_;
+
+    /// @brief Holds custom response to be sent to the client.
+    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.
+typedef boost::shared_ptr<TestServerUnixSocket> TestServerUnixSocketPtr;
+
+} // end of namespace isc::asiolink::test
+} // end of namespace isc::asiolink
+} // end of namespace isc
+
+#endif // TEST_SERVER_UNIX_SOCKET_H

+ 97 - 0
src/lib/asiolink/unix_domain_socket.cc

@@ -0,0 +1,97 @@
+// 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 <asiolink/asio_wrapper.h>
+#include <asiolink/unix_domain_socket.h>
+#include <iostream>
+using namespace boost::asio::local;
+
+namespace isc {
+namespace asiolink {
+
+/// @brief Implementation of the unix domain socket.
+class UnixDomainSocketImpl {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service IO service to be used by the socket class.
+    UnixDomainSocketImpl(IOService& io_service)
+        : socket_(io_service.get_io_service()) {
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Closes the socket.
+    ~UnixDomainSocketImpl() {
+        close();
+    }
+
+    /// @brief Closes the socket.
+    void close();
+
+    /// @brief Instance of the boost asio unix domain socket.
+    stream_protocol::socket socket_;
+};
+
+void
+UnixDomainSocketImpl::close() {
+    static_cast<void>(socket_.close());
+}
+
+UnixDomainSocket::UnixDomainSocket(IOService& io_service)
+    : impl_(new UnixDomainSocketImpl(io_service)) {
+}
+
+int
+UnixDomainSocket::getNative() const {
+    return (impl_->socket_.native());
+}
+
+int
+UnixDomainSocket::getProtocol() const {
+    return (0);
+}
+
+void
+UnixDomainSocket::connect(const std::string& path) {
+    boost::system::error_code ec;
+    impl_->socket_.connect(stream_protocol::endpoint(path.c_str()), ec);
+    if (ec) {
+        isc_throw(UnixDomainSocketError, ec.message());
+    }
+}
+
+size_t
+UnixDomainSocket::write(const void* data, size_t length) {
+    boost::system::error_code ec;
+    size_t res = boost::asio::write(impl_->socket_,
+                                    boost::asio::buffer(data, length),
+                                    boost::asio::transfer_all(),
+                                    ec);
+    if (ec) {
+        isc_throw(UnixDomainSocketError, ec.message());
+    }
+    return (res);
+}
+
+size_t
+UnixDomainSocket::receive(void* data, size_t length) {
+    boost::system::error_code ec;
+    size_t res = impl_->socket_.receive(boost::asio::buffer(data, length), 0, ec);
+    if (ec) {
+        isc_throw(UnixDomainSocketError, ec.message());
+    }
+    return (res);
+}
+
+void
+UnixDomainSocket::close() {
+    impl_->close();
+}
+
+}
+}

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

@@ -0,0 +1,83 @@
+// 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 UNIX_DOMAIN_SOCKET_H
+#define UNIX_DOMAIN_SOCKET_H
+
+#include <asiolink/io_service.h>
+#include <asiolink/io_socket.h>
+#include <boost/shared_ptr.hpp>
+#include <string>
+
+namespace isc {
+namespace asiolink {
+
+/// @brief Exception thrown upon socket error.
+class UnixDomainSocketError : public Exception {
+public:
+    UnixDomainSocketError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+class UnixDomainSocketImpl;
+
+/// @brief Represents unix domain socket implemented in terms
+/// of boost asio.
+class UnixDomainSocket : public IOSocket {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service Reference to IOService to be used by this
+    /// class.
+    UnixDomainSocket(IOService& io_service);
+
+    /// @brief Returns native socket representation.
+    virtual int getNative() const;
+
+    /// @brief Always returns 0.
+    virtual int getProtocol() const;
+
+    /// @brief Connects the socket to the specified endpoint.
+    ///
+    /// @param path Path to the unix socket to which we should connect.
+    ///
+    /// @throw UnixDomainSocketError if error occurs.
+    void connect(const std::string& path);
+
+    /// @brief Writes specified amount of data to a socket.
+    ///
+    /// @param data Pointer to data to be written.
+    /// @param length Number of bytes to be written.
+    ///
+    /// @return Number of bytes written.
+    /// @throw UnixDomainSocketError if error occurs.
+    size_t write(const void* data, size_t length);
+
+    /// @brief Receives data from a socket.
+    ///
+    /// @param [out] data Pointer to a location into which the read data should
+    /// be stored.
+    /// @param length Length of the buffer.
+    ///
+    /// @return Number of bytes read.
+    /// @throw UnixDomainSocketError if error occurs.
+    size_t receive(void* data, size_t length);
+
+    /// @brief Closes the socket.
+    void close();
+
+private:
+
+    /// @brief Pointer to the implementation of this class.
+    boost::shared_ptr<UnixDomainSocketImpl> impl_;
+
+};
+
+} // end of namespace isc::asiolink
+} // end of namespace isc
+
+#endif // UNIX_DOMAIN_SOCKET_H

+ 3 - 0
src/lib/cc/command_interpreter.h

@@ -38,6 +38,9 @@ const int CONTROL_RESULT_SUCCESS = 0;
 /// @brief Status code indicating a general failure
 const int CONTROL_RESULT_ERROR = 1;
 
+/// @brief Status code indicating that the specified command is not supported.
+const int CONTROL_RESULT_COMMAND_UNSUPPORTED = 2;
+
 /// @brief A standard control channel exception that is thrown if a function
 /// is there is a problem with one of the messages
 class CtrlChannelError : public isc::Exception {

+ 31 - 5
src/lib/config/base_command_mgr.cc

@@ -31,12 +31,34 @@ BaseCommandMgr::registerCommand(const std::string& cmd, CommandHandler handler)
                   << "' is already installed.");
     }
 
-    handlers_.insert(make_pair(cmd, handler));
+    HandlersPair handlers;
+    handlers.handler = handler;
+    handlers_.insert(make_pair(cmd, handlers));
 
     LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_REGISTERED).arg(cmd);
 }
 
 void
+BaseCommandMgr::registerExtendedCommand(const std::string& cmd,
+                                        ExtendedCommandHandler 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.");
+    }
+
+    HandlersPair handlers;
+    handlers.extended_handler = handler;
+    handlers_.insert(make_pair(cmd, handlers));
+
+    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_EXTENDED_REGISTERED).arg(cmd);
+}
+
+void
 BaseCommandMgr::deregisterCommand(const std::string& cmd) {
     if (cmd == "list-commands") {
         isc_throw(InvalidCommandName,
@@ -76,7 +98,7 @@ BaseCommandMgr::processCommand(const isc::data::ConstElementPtr& cmd) {
 
         LOG_INFO(command_logger, COMMAND_RECEIVED).arg(name);
 
-        return (handleCommand(name, arg));
+        return (handleCommand(name, arg, cmd));
 
     } catch (const Exception& e) {
         LOG_WARN(command_logger, COMMAND_PROCESS_ERROR2).arg(e.what());
@@ -88,16 +110,20 @@ BaseCommandMgr::processCommand(const isc::data::ConstElementPtr& cmd) {
 
 ConstElementPtr
 BaseCommandMgr::handleCommand(const std::string& cmd_name,
-                              const ConstElementPtr& params) {
+                              const ConstElementPtr& params,
+                              const ConstElementPtr& original_cmd) {
     auto it = handlers_.find(cmd_name);
     if (it == handlers_.end()) {
         // Ok, there's no such command.
-        return (createAnswer(CONTROL_RESULT_ERROR,
+        return (createAnswer(CONTROL_RESULT_COMMAND_UNSUPPORTED,
                              "'" + cmd_name + "' command not supported."));
     }
 
     // Call the actual handler and return whatever it returned
-    return (it->second(cmd_name, params));
+    if (it->second.handler) {
+        return (it->second.handler(cmd_name, params));
+    }
+    return (it->second.extended_handler(cmd_name, params, original_cmd));
 }
 
 isc::data::ConstElementPtr

+ 38 - 2
src/lib/config/base_command_mgr.h

@@ -81,6 +81,20 @@ public:
     typedef boost::function<isc::data::ConstElementPtr (const std::string& name,
         const isc::data::ConstElementPtr& params)> CommandHandler;
 
+    /// @brief Defines extended command handler type.
+    ///
+    /// This command handler includes third parameter which holds the
+    /// entire command control message. The handler can retrieve
+    /// additional information from this parameter, e.g. 'service'.
+    ///
+    /// @param name name of the commands
+    /// @param params parameters specific to the command
+    /// @param original original control command.
+    /// @return response (created with createAnswer())
+    typedef boost::function<isc::data::ConstElementPtr (const std::string& name,
+        const isc::data::ConstElementPtr& params,
+        const isc::data::ConstElementPtr& original)> ExtendedCommandHandler;
+
     /// @brief Constructor.
     ///
     /// Registers "list-commands" command.
@@ -105,6 +119,19 @@ public:
     /// @param handler Pointer to the method that will handle the command.
     void registerCommand(const std::string& cmd, CommandHandler handler);
 
+    /// @brief Registers specified command handler for a given command.
+    ///
+    /// This variant of the method uses extended command handler which, besides
+    /// command name and arguments, also has a third parameter 'original_cmd'
+    /// in its signature. Such handlers can retrieve additional parameters from
+    /// the command, e.g. 'service' indicating where the command should be
+    /// routed.
+    ///
+    /// @param cmd Name of the command to be handled.
+    /// @param handler Pointer to the method that will handle the command.
+    void registerExtendedCommand(const std::string& cmd,
+                                 ExtendedCommandHandler handler);
+
     /// @brief Deregisters specified command handler.
     ///
     /// @param cmd Name of the command that's no longer handled.
@@ -127,15 +154,24 @@ protected:
     ///
     /// @param cmd_name Command name.
     /// @param params Command arguments.
+    /// @param original_cmd Pointer to the entire command received. It may
+    /// be sometimes useful to retrieve additional parameters from this
+    /// command.
     ///
     /// @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);
+                  const isc::data::ConstElementPtr& params,
+                  const isc::data::ConstElementPtr& original_cmd);
+
+    struct HandlersPair {
+        CommandHandler handler;
+        ExtendedCommandHandler extended_handler;
+    };
 
     /// @brief Type of the container for command handlers.
-    typedef std::map<std::string, CommandHandler> HandlerContainer;
+    typedef std::map<std::string, HandlersPair> HandlerContainer;
 
     /// @brief Container for command handlers.
     HandlerContainer handlers_;

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

@@ -6,6 +6,11 @@
 
 $NAMESPACE isc::config
 
+% COMMAND_EXTENDED_REGISTERED Command %1 registered
+This debug message indicates that the daemon started supporting specified
+command. The handler for the registered command includes a parameter holding
+entire command to be processed.
+
 % COMMAND_DEREGISTERED Command %1 deregistered
 This debug message indicates that the daemon stopped supporting specified
 command. This command can no longer be issued. If the command socket is

+ 45 - 27
src/lib/config/hooked_command_mgr.cc

@@ -42,16 +42,11 @@ 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);
+bool
+HookedCommandMgr::delegateCommandToHookLibrary(std::string& cmd_name,
+                                               ConstElementPtr& params,
+                                               ConstElementPtr& original_cmd,
+                                               ElementPtr& answer) {
 
     ConstElementPtr hook_response;
     if (HooksManager::calloutsPresent(Hooks.hook_index_control_command_receive_)) {
@@ -59,13 +54,11 @@ HookedCommandMgr::handleCommand(const std::string& cmd_name,
         // 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);
+        ConstElementPtr command = original_cmd ? original_cmd :
+            createCommand(cmd_name, params);
 
         // And pass it to the hook library.
-        callout_handle_->setArgument("command", original_command);
+        callout_handle_->setArgument("command", command);
         callout_handle_->setArgument("response", hook_response);
 
         HooksManager::callCallouts(Hooks.hook_index_control_command_receive_,
@@ -74,33 +67,58 @@ HookedCommandMgr::handleCommand(const std::string& cmd_name,
         // 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);
+        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'
     // 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);
+    ConstElementPtr response = BaseCommandMgr::handleCommand(mutable_cmd_name,
+                                                             mutable_params,
+                                                             mutable_cmd);
 
     // 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) {
+    if ((mutable_cmd_name == "list-commands") && hook_response && response) {
         response = combineCommandsLists(hook_response, response);
     }
 

+ 30 - 8
src/lib/config/hooked_command_mgr.h

@@ -36,24 +36,46 @@ protected:
         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
-    /// 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 params Command arguments.
+    /// @param original_cmd Original command received.
     ///
     /// @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);
+                  const isc::data::ConstElementPtr& params,
+                  const isc::data::ConstElementPtr& original_cmd);
 
     /// @brief Pointer to a callout handle used by this class.
     isc::hooks::CalloutHandlePtr callout_handle_;

+ 1 - 1
src/lib/config/tests/command_mgr_unittests.cc

@@ -222,7 +222,7 @@ TEST_F(CommandMgrTest, bogusCommand) {
     ASSERT_TRUE(answer);
     int status_code;
     parseAnswer(status_code, answer);
-    EXPECT_EQ(CONTROL_RESULT_ERROR, status_code);
+    EXPECT_EQ(CONTROL_RESULT_COMMAND_UNSUPPORTED, status_code);
 }
 
 // Test checks whether handlers installation is sanitized. In particular,

+ 18 - 0
src/lib/http/listener.cc

@@ -46,6 +46,9 @@ public:
                      const HttpResponseCreatorFactoryPtr& creator_factory,
                      const long request_timeout);
 
+    /// @brief Returns reference to the current listener endpoint.
+    const TCPEndpoint& getEndpoint() const;
+
     /// @brief Starts accepting new connections.
     ///
     /// This method starts accepting and handling new HTTP connections on
@@ -126,6 +129,11 @@ HttpListenerImpl::HttpListenerImpl(IOService& io_service,
     }
 }
 
+const TCPEndpoint&
+HttpListenerImpl::getEndpoint() const {
+    return (*endpoint_);
+}
+
 void
 HttpListenerImpl::start() {
     try {
@@ -186,6 +194,16 @@ HttpListener::~HttpListener() {
     stop();
 }
 
+IOAddress
+HttpListener::getLocalAddress() const {
+    return (impl_->getEndpoint().getAddress());
+}
+
+uint16_t
+HttpListener::getLocalPort() const {
+    return (impl_->getEndpoint().getPort());
+}
+
 void
 HttpListener::start() {
     impl_->start();

+ 13 - 0
src/lib/http/listener.h

@@ -12,6 +12,7 @@
 #include <exceptions/exceptions.h>
 #include <http/response_creator_factory.h>
 #include <boost/shared_ptr.hpp>
+#include <stdint.h>
 
 namespace isc {
 namespace http {
@@ -80,6 +81,12 @@ public:
     /// Stops all active connections and closes TCP acceptor service.
     ~HttpListener();
 
+    /// @brief Returns local address on which server is listening.
+    asiolink::IOAddress getLocalAddress() const;
+
+    /// @brief Returns local port on which server is listening.
+    uint16_t getLocalPort() const;
+
     /// @brief Starts accepting new connections.
     ///
     /// This method starts accepting and handling new HTTP connections on
@@ -101,6 +108,12 @@ private:
 
 };
 
+/// @brief Pointer to the @ref HttpListener.
+typedef boost::shared_ptr<HttpListener> HttpListenerPtr;
+
+/// @brief Pointer to the const @ref HttpListener.
+typedef boost::shared_ptr<const HttpListener> ConstHttpListenerPtr;
+
 } // end of namespace isc::http
 } // end of namespace isc
 

+ 3 - 1
src/lib/http/tests/listener_unittests.cc

@@ -184,7 +184,7 @@ public:
 
     /// @brief Receive response from the server.
     void receivePartialResponse() {
-        socket_.async_read_some(boost::asio::buffer(buf_),
+        socket_.async_read_some(boost::asio::buffer(buf_.data(), buf_.size()),
                                 [this](const boost::system::error_code& ec,
                                        std::size_t bytes_transferred) {
             if (ec) {
@@ -309,6 +309,8 @@ TEST_F(HttpListenerTest, listen) {
     HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
                           factory_, REQUEST_TIMEOUT);
     ASSERT_NO_THROW(listener.start());
+    ASSERT_EQ(SERVER_ADDRESS, listener.getLocalAddress().toText());
+    ASSERT_EQ(SERVER_PORT, listener.getLocalPort());
     ASSERT_NO_THROW(startRequest(request));
     ASSERT_NO_THROW(io_service_.run());
     ASSERT_EQ(1, clients_.size());

+ 6 - 1
src/lib/process/d_cfg_mgr.cc

@@ -275,7 +275,8 @@ DCfgMgrBase::parseConfig(isc::data::ConstElementPtr config_set,
 
 isc::data::ConstElementPtr
 DCfgMgrBase::simpleParseConfig(isc::data::ConstElementPtr config_set,
-                               bool check_only) {
+                               bool check_only,
+                               const std::function<void()>& post_config_cb) {
     if (!config_set) {
         return (isc::config::createAnswer(1,
                                     std::string("Can't parse NULL config")));
@@ -301,6 +302,10 @@ DCfgMgrBase::simpleParseConfig(isc::data::ConstElementPtr config_set,
 
         // Everything was fine. Configuration set processed successfully.
         if (!check_only) {
+            if (post_config_cb) {
+                post_config_cb();
+            }
+
             LOG_INFO(dctl_logger, DCTL_CONFIG_COMPLETE).arg(getConfigSummary(0));
             answer = isc::config::createAnswer(0, "Configuration committed.");
         } else {

+ 9 - 1
src/lib/process/d_cfg_mgr.h

@@ -11,6 +11,7 @@
 #include <cc/cfg_to_element.h>
 #include <exceptions/exceptions.h>
 #include <dhcpsrv/parsers/dhcp_parsers.h>
+#include <functional>
 
 #include <stdint.h>
 #include <string>
@@ -326,12 +327,19 @@ public:
     ///
     /// @param config set of configuration elements to be parsed
     /// @param check_only true if the config is to be checked only, but not applied
+    /// @param post_config_cb Callback to be executed after the usual parsing stage.
+    /// This can be specified as a C++ lambda which configures other parts of the
+    /// system based on the parsed configuration information. The callback should
+    /// throw an exception to signal an error. This method will catch this
+    /// exception and place an exception string within the result returned.
+    ///
     /// @return an Element that contains the results of configuration composed
     /// of an integer status value (0 means successful, non-zero means failure),
     /// and a string explanation of the outcome.
     isc::data::ConstElementPtr
     simpleParseConfig(isc::data::ConstElementPtr config,
-                      bool check_only = false);
+                      bool check_only = false,
+                      const std::function<void()>& post_config_cb = nullptr);
 
     /// @brief Adds a given element id to the end of the parse order list.
     ///

+ 16 - 0
src/lib/process/tests/d_cfg_mgr_unittests.cc

@@ -8,6 +8,7 @@
 
 #include <cc/command_interpreter.h>
 #include <config/module_spec.h>
+#include <exceptions/exceptions.h>
 #include <dhcpsrv/parsers/dhcp_parsers.h>
 #include <process/testutils/d_test_stubs.h>
 #include <process/d_cfg_mgr.h>
@@ -563,4 +564,19 @@ TEST_F(DStubCfgMgrTest, simpleParseConfig) {
     EXPECT_TRUE(checkAnswer(0));
 }
 
+// This test checks that the post configuration callback function is
+// executed by the simpleParseConfig function.
+TEST_F(DStubCfgMgrTest, simpleParseConfigWithCallback) {
+    string config = "{ \"bool_test\": true , \n"
+                    "  \"uint32_test\": 77 , \n"
+                    "  \"string_test\": \"hmmm chewy\" }";
+    ASSERT_NO_THROW(fromJSON(config));
+
+    answer_ = cfg_mgr_->simpleParseConfig(config_set_, false,
+                                          [this]() {
+        isc_throw(Unexpected, "unexpected configuration error");
+    });
+    EXPECT_TRUE(checkAnswer(1));
+}
+
 } // end of anonymous namespace