Browse Source

[master] Implement set-config command in kea-dhcp4/6

    Merges in branch 'trac5046'
Thomas Markwalder 8 years ago
parent
commit
4afbdcf89c

+ 61 - 0
doc/guide/ctrl-channel.xml

@@ -184,6 +184,65 @@ will be sent to Kea and the responses received from Kea printed to standard outp
       </para>
     </section> <!-- end of command-list-commands -->
 
+    <section id="command-set-config">
+      <title>set-config</title>
+
+      <para>
+    The <emphasis>set-config</emphasis> command instructs the server to replace
+    its current configuration with the new configuration supplied in the
+    command's arguments. The supplied configuration is expected to be the full
+    configuration for the target server along with an optional Logger
+    configuration.  While optional, the Logger configuration is highly
+    recommended as without it the server will revert to its default logging
+    configuration. The structure of the command is as follows:
+      </para>
+<screen>
+{
+    "command": "set-config",
+    "arguments":  {
+        "&#60;server&#62;": {
+        },
+        "Logging": {
+        }
+     }
+}
+</screen>
+      <para>
+    where &#60;server&#62; is the configuration element name for a given server
+    such as "Dhcp4" or "Dhcp6".  For example:
+      </para>
+<screen>
+{
+    "command": "set-config",
+    "arguments":  {
+        "Dhcp6": {
+            :
+        },
+        "Logging": {
+            :
+        }
+     }
+}
+</screen>
+      <para>
+    If the new configuration proves to be invalid the server will retain
+    its current configuration.  Please note that the new configuration is
+    retained in memory only.  If the server is restarted or a configuration
+    reload is triggered via a signal, the server will use the configuration
+    stored in its configuration file.
+
+	The server's response will contain a numeric code, "result" (0 for success,
+    non-zero on failure), and  a string, "text", describing the outcome:
+<screen>
+    {"result": 0, "text": "Configuration successful." }
+
+    or
+
+    {"result": 1, "text": "unsupported parameter: BOGUS (&#60;string&#62;:16:26)" }
+</screen>
+      </para>
+    </section> <!-- end of command-set-config -->
+
     <section id="command-shutdown">
       <title>shutdown</title>
 
@@ -205,6 +264,8 @@ will be sent to Kea and the responses received from Kea printed to standard outp
       </para>
     </section> <!-- end of command-shutdown -->
 
+
+
     </section> <!-- end of commands supported by both servers -->
 
   </chapter>

+ 22 - 9
doc/guide/dhcp4-srv.xml

@@ -3649,17 +3649,30 @@ src/lib/dhcpsrv/cfg_host_operations.cc -->
 
       <para>
         Communication over control channel is conducted using JSON structures.
-        See the Control Channel section in the Kea Developer's Guide for more details.
+        See the Control Channel section in the Kea Developer's Guide for more
+        details.
+      </para>
+
+      <para>The DHCPv4 server supports the following operational commands:
+        <itemizedlist>
+            <listitem>leases-reclaim</listitem>
+            <listitem>list-commands</listitem>
+            <listitem>set-config</listitem>
+            <listitem>shutdown</listitem>
+        </itemizedlist>
+         as described in <xref linkend="commands-common"/>.  In addition,
+         it supports the following statistics related commands:
+        <itemizedlist>
+            <listitem>statistic-get</listitem>
+            <listitem>statistic-reset</listitem>
+            <listitem>statistic-remove</listitem>
+            <listitem>statistic-get-all</listitem>
+            <listitem>statistic-reset-all</listitem>
+            <listitem>statistic-remove-all</listitem>
+        </itemizedlist>
+        as described here <xref linkend="command-stats"/>.
       </para>
 
-      <para>The DHCPv4 server supports <command>statistic-get</command>,
-      <command>statistic-reset</command>, <command>statistic-remove</command>,
-      <command>statistic-get-all</command>, <command>statistic-reset-all</command>
-      and <command>statistic-remove-all</command>, specified in
-      <xref linkend="command-stats"/>. It also supports
-      <command>list-commands</command> and <command>shutdown</command>,
-      specified in <xref linkend="command-list-commands" /> and
-      <xref linkend="command-shutdown" />, respectively.</para>
     </section>
 
     <section id="dhcp4-std">

+ 20 - 9
doc/guide/dhcp6-srv.xml

@@ -4062,16 +4062,27 @@ If not specified, the default value is:
         See the Control Channel section in the Kea Developer's Guide for more details.
       </para>
 
-      <para>The DHCPv6 server supports <command>statistic-get</command>,
-      <command>statistic-reset</command>, <command>statistic-remove</command>,
-      <command>statistic-get-all</command>, <command>statistic-reset-all</command>
-      and <command>statistic-remove-all</command>, specified in
-      <xref linkend="command-stats"/>. It also supports
-      <command>list-commands</command> and <command>shutdown</command>,
-      specified in <xref linkend="command-list-commands" /> and
-      <xref linkend="command-shutdown" />, respectively.</para>
-    </section>
+      <para>The DHCPv6 server supports the following operational commands:
+        <itemizedlist>
+            <listitem>leases-reclaim</listitem>
+            <listitem>list-commands</listitem>
+            <listitem>set-config</listitem>
+            <listitem>shutdown</listitem>
+        </itemizedlist>
+         as described in <xref linkend="commands-common"/>.  In addition,
+         it supports the following statistics related commands:
+        <itemizedlist>
+            <listitem>statistic-get</listitem>
+            <listitem>statistic-reset</listitem>
+            <listitem>statistic-remove</listitem>
+            <listitem>statistic-get-all</listitem>
+            <listitem>statistic-reset-all</listitem>
+            <listitem>statistic-remove-all</listitem>
+        </itemizedlist>
+        as described here <xref linkend="command-stats"/>.
+      </para>
 
+    </section>
 
       <section>
         <title>User context in IPv6 pools</title>

+ 71 - 4
src/bin/dhcp4/ctrl_dhcp4_srv.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-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
@@ -62,7 +62,69 @@ ControlledDhcpv4Srv::commandLibReloadHandler(const string&, ConstElementPtr) {
 ConstElementPtr
 ControlledDhcpv4Srv::commandConfigReloadHandler(const string&,
                                                 ConstElementPtr args) {
-    return (processConfig(args));
+    // Use set-config as it handles logging and server config
+    return (commandSetConfigHandler("set-config", args));
+}
+
+ConstElementPtr
+ControlledDhcpv4Srv::commandSetConfigHandler(const string&,
+                                             ConstElementPtr args) {
+    const int status_code = 1; // 1 indicates an error
+    ConstElementPtr dhcp4;
+    string message;
+
+    // Command arguments are expected to be:
+    // { "Dhcp4": { ... }, "Logging": { ... } }
+    // The Logging component is technically optional. If it's not supplied
+    // logging will revert to default logging.
+    if (!args) {
+        message = "Missing mandatory 'arguments' parameter.";
+    } else {
+        dhcp4 = args->get("Dhcp4");
+        if (!dhcp4) {
+            message = "Missing mandatory 'Dhcp4' parameter.";
+        } else if (dhcp4->getType() != Element::map) {
+            message = "'Dhcp4' parameter expected to be a map.";
+        }
+    }
+
+    if (!message.empty()) {
+        // Something is amiss with arguments, return a failure response.
+        ConstElementPtr result = isc::config::createAnswer(status_code,
+                                                           message);
+        return (result);
+    }
+
+    // We are starting the configuration process so we should remove any
+    // staging configuration that has been created during previous
+    // configuration attempts.
+    CfgMgr::instance().rollback();
+
+    // Logging is a sibling element and must be parsed explicitly.
+    // The call to configureLogger parses the given Logging element if
+    // not null, into the staging config.  Note this does not alter the
+    // current loggers, they remain in effect until we apply the
+    // logging config below.  If no logging is supplied logging will
+    // revert to default logging.
+    Daemon::configureLogger(args->get("Logging"),
+                            CfgMgr::instance().getStagingCfg());
+
+    // Now we configure the server proper.
+    ConstElementPtr result = processConfig(dhcp4);
+
+    // If the configuration parsed successfully, apply the new logger
+    // configuration and the commit the new configuration.  We apply
+    // the logging first in case there's a configuration failure.
+    int rcode = 0;
+    isc::config::parseAnswer(rcode, result);
+    if (rcode == 0) {
+        CfgMgr::instance().getStagingCfg()->applyLoggingCfg();
+
+        // Use new configuration.
+        CfgMgr::instance().commit();
+    }
+
+    return (result);
 }
 
 ConstElementPtr
@@ -116,6 +178,9 @@ ControlledDhcpv4Srv::processCommand(const string& command,
         } else if (command == "config-reload") {
             return (srv->commandConfigReloadHandler(command, args));
 
+        } else if (command == "set-config") {
+            return (srv->commandSetConfigHandler(command, args));
+
         } else if (command == "leases-reclaim") {
             return (srv->commandLeasesReclaimHandler(command, args));
         }
@@ -235,8 +300,6 @@ ControlledDhcpv4Srv::processConfig(isc::data::ConstElementPtr config) {
         }
     }
 
-
-
     return (answer);
 }
 
@@ -257,6 +320,9 @@ ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t port /*= DHCP4_SERVER_PORT*/)
     CommandMgr::instance().registerCommand("libreload",
         boost::bind(&ControlledDhcpv4Srv::commandLibReloadHandler, this, _1, _2));
 
+    CommandMgr::instance().registerCommand("set-config",
+        boost::bind(&ControlledDhcpv4Srv::commandSetConfigHandler, this, _1, _2));
+
     CommandMgr::instance().registerCommand("leases-reclaim",
         boost::bind(&ControlledDhcpv4Srv::commandLeasesReclaimHandler, this, _1, _2));
 
@@ -300,6 +366,7 @@ ControlledDhcpv4Srv::~ControlledDhcpv4Srv() {
         // Deregister any registered commands
         CommandMgr::instance().deregisterCommand("shutdown");
         CommandMgr::instance().deregisterCommand("libreload");
+        CommandMgr::instance().deregisterCommand("set-config");
         CommandMgr::instance().deregisterCommand("leases-reclaim");
         CommandMgr::instance().deregisterCommand("statistic-get");
         CommandMgr::instance().deregisterCommand("statistic-reset");

+ 14 - 1
src/bin/dhcp4/ctrl_dhcp4_srv.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -143,6 +143,19 @@ private:
     commandConfigReloadHandler(const std::string& command,
                                isc::data::ConstElementPtr args);
 
+    /// @brief handler for processing 'set-config' command
+    ///
+    /// This handler processes set-config command, which processes
+    /// configuration specified in args parameter.
+    /// @param command (parameter ignored)
+    /// @param args configuration to be processed. Expected format:
+    /// map containing Dhcp4 map that contains DHCPv4 server configuration.
+    /// May also contain Logging map that specifies logging configuration.
+    ///
+    /// @return status of the command
+    isc::data::ConstElementPtr
+    commandSetConfigHandler(const std::string& command,
+                            isc::data::ConstElementPtr args);
 
     /// @brief Handler for processing 'leases-reclaim' command
     ///

+ 44 - 20
src/bin/dhcp4/json_config_parser.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -497,6 +497,47 @@ void setGlobalParameters4() {
     }
 }
 
+/// @brief Initialize the command channel based on the staging configuration
+///
+/// Only close the current channel, if the new channel configuration is
+/// different.  This avoids disconnecting a client and hence not sending them
+/// a command result, unless they specifically alter the channel configuration.
+/// In that case the user simply has to accept they'll be disconnected.
+///
+void configureCommandChannel() {
+    // Get new socket configuration.
+    ConstElementPtr sock_cfg =
+        CfgMgr::instance().getStagingCfg()->getControlSocketInfo();
+
+    // Get current socket configuration.
+    ConstElementPtr current_sock_cfg =
+            CfgMgr::instance().getCurrentCfg()->getControlSocketInfo();
+
+    // Determine if the socket configuration has changed. It has if
+    // both old and new configuration is specified but respective
+    // data elements are't equal.
+    bool sock_changed = (sock_cfg && current_sock_cfg &&
+                         !sock_cfg->equals(*current_sock_cfg));
+
+    // If the previous or new socket configuration doesn't exist or
+    // the new configuration differs from the old configuration we
+    // close the exisitng socket and open a new socket as appropriate.
+    // Note that closing an existing socket means the clien will not
+    // receive the configuration result.
+    if (!sock_cfg || !current_sock_cfg || sock_changed) {
+        // Close the existing socket (if any).
+        isc::config::CommandMgr::instance().closeCommandSocket();
+
+        if (sock_cfg) {
+            // This will create a control socket and install the external
+            // socket in IfaceMgr. That socket will be monitored when
+            // Dhcp4Srv::receivePacket() calls IfaceMgr::receive4() and
+            // callback in CommandMgr will be called, if necessary.
+            isc::config::CommandMgr::instance().openCommandSocket(sock_cfg);
+        }
+    }
+}
+
 isc::data::ConstElementPtr
 configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
     if (!config_set) {
@@ -646,25 +687,8 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
             subnet_parser->build(subnet_config->second);
         }
 
-        // Get command socket configuration from the config file.
-        // This code expects the following structure:
-        // {
-        //     "socket-type": "unix",
-        //     "socket-name": "/tmp/kea4.sock"
-        // }
-        ConstElementPtr sock_cfg =
-            CfgMgr::instance().getStagingCfg()->getControlSocketInfo();
-
-        // Close existing socket (if any).
-        isc::config::CommandMgr::instance().closeCommandSocket();
-        if (sock_cfg) {
-            // This will create a control socket and will install external socket
-            // in IfaceMgr. That socket will be monitored when Dhcp4Srv::receivePacket()
-            // calls IfaceMgr::receive4() and callback in CommandMgr will be called,
-            // if necessary. If there were previously open command socket, it will
-            // be closed.
-            isc::config::CommandMgr::instance().openCommandSocket(sock_cfg);
-        }
+        // Setup the command channel.
+        configureCommandChannel();
 
         // the leases database parser is the last to be run.
         std::map<std::string, ConstElementPtr>::const_iterator leases_config =

+ 4 - 32
src/bin/dhcp4/kea_controller.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-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
@@ -34,11 +34,6 @@ void configure(const std::string& file_name) {
     // This is a configuration backend implementation that reads the
     // configuration from a JSON file.
 
-    // We are starting the configuration process so we should remove any
-    // staging configuration that has been created during previous
-    // configuration attempts.
-    CfgMgr::instance().rollback();
-
     isc::data::ConstElementPtr json;
     isc::data::ConstElementPtr dhcp4;
     isc::data::ConstElementPtr logger;
@@ -70,26 +65,14 @@ void configure(const std::string& file_name) {
                       " Did you forget to add { } around your configuration?");
         }
 
-        // If there's no logging element, we'll just pass NULL pointer,
-        // which will be handled by configureLogger().
-        Daemon::configureLogger(json->get("Logging"),
-                                CfgMgr::instance().getStagingCfg());
-
-        // Get Dhcp4 component from the config
-        dhcp4 = json->get("Dhcp4");
-        if (!dhcp4) {
-            isc_throw(isc::BadValue, "no mandatory 'Dhcp4' entry in"
-                      " the configuration");
-        }
-
         // Use parsed JSON structures to configure the server
-        result = ControlledDhcpv4Srv::processCommand("config-reload", dhcp4);
+        result = ControlledDhcpv4Srv::processCommand("set-config", json);
         if (!result) {
             // Undetermined status of the configuration. This should never
             // happen, but as the configureDhcp4Server returns a pointer, it is
             // theoretically possible that it will return NULL.
             isc_throw(isc::BadValue, "undefined result of "
-                      "processCommand(\"config-reload\", dhcp4)");
+                      "processCommand(\"set-config\", json)");
         }
 
         // Now check is the returned result is successful (rcode=0) or not
@@ -102,17 +85,6 @@ void configure(const std::string& file_name) {
                 "no details available";
             isc_throw(isc::BadValue, reason);
         }
-
-        // If configuration was parsed successfully, apply the new logger
-        // configuration to log4cplus. It is done before commit in case
-        // something goes wrong.
-        CfgMgr::instance().getStagingCfg()->applyLoggingCfg();
-
-        // Use new configuration.
-        /// @todo: This commit should be moved to
-        /// CtrlDhcp4Srv::commandConfigReloadHandler.
-        CfgMgr::instance().commit();
-
     }  catch (const std::exception& ex) {
         // If configuration failed at any stage, we drop the staging
         // configuration and continue to use the previous one.
@@ -167,7 +139,7 @@ ControlledDhcpv4Srv::init(const std::string& file_name) {
 
     // We don't need to call openActiveSockets() or startD2() as these
     // methods are called in processConfig() which is called by
-    // processCommand("reload-config", ...)
+    // processCommand("set-config", ...)
 
     // Set signal handlers. When the SIGHUP is received by the process
     // the server reconfiguration will be triggered. When SIGTERM or

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

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -15,7 +15,9 @@
 #include <dhcpsrv/lease.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <hooks/hooks_manager.h>
+#include <log/logger_support.h>
 #include <stats/stats_mgr.h>
+#include <testutils/io_utils.h>
 #include <testutils/unix_control_client.h>
 
 #include "marker_file.h"
@@ -121,6 +123,13 @@ public:
         ConstElementPtr config;
         ASSERT_NO_THROW(config = parseDHCP4(config_txt));
         ConstElementPtr answer = server_->processConfig(config);
+
+        // Commit the configuration so any subsequent reconfigurations
+        // will only close the command channel if its configuration has
+        // changed.
+        CfgMgr::instance().commit();
+
+
         ASSERT_TRUE(answer);
 
         int status = 0;
@@ -472,4 +481,147 @@ TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelStats) {
               response);
 }
 
+// Check that the "set-config" command will replace current configuration
+TEST_F(CtrlChannelDhcpv4SrvTest, set_config) {
+    createUnixChannelServer();
+
+    // Define strings to permutate the config arguments
+    // (Note the line feeds makes errors easy to find)
+    string set_config_txt = "{ \"command\": \"set-config\" \n";
+    string args_txt = " \"arguments\": { \n";
+    string dhcp4_cfg_txt =
+        "    \"Dhcp4\": { \n"
+        "        \"interfaces-config\": { \n"
+        "            \"interfaces\": [\"*\"] \n"
+        "        },   \n"
+        "        \"valid-lifetime\": 4000, \n"
+        "        \"renew-timer\": 1000, \n"
+        "        \"rebind-timer\": 2000, \n"
+        "        \"lease-database\": { \n"
+        "           \"type\": \"memfile\", \n"
+        "           \"persist\":false, \n"
+        "           \"lfc-interval\": 0  \n"
+        "        }, \n"
+        "       \"expired-leases-processing\": { \n"
+        "            \"reclaim-timer-wait-time\": 0, \n"
+        "            \"hold-reclaimed-time\": 0, \n"
+        "            \"flush-reclaimed-timer-wait-time\": 0 \n"
+        "        },"
+        "        \"subnet4\": [ \n";
+    string subnet1 =
+        "               {\"subnet\": \"192.2.0.0/24\", \n"
+        "                \"pools\": [{ \"pool\": \"192.2.0.1-192.2.0.50\" }]}\n";
+    string subnet2 =
+        "               {\"subnet\": \"192.2.1.0/24\", \n"
+        "                \"pools\": [{ \"pool\": \"192.2.1.1-192.2.1.50\" }]}\n";
+    string bad_subnet =
+        "               {\"BOGUS\": \"192.2.2.0/24\", \n"
+        "                \"pools\": [{ \"pool\": \"192.2.2.1-192.2.2.50\" }]}\n";
+    string subnet_footer =
+        "          ] \n";
+    string control_socket_header =
+        "       ,\"control-socket\": { \n"
+        "       \"socket-type\": \"unix\", \n"
+        "       \"socket-name\": \"";
+    string control_socket_footer =
+        "\"   \n} \n";
+    string logger_txt =
+        "    \"Logging\": { \n"
+        "        \"loggers\": [ { \n"
+        "            \"name\": \"kea\", \n"
+        "            \"severity\": \"FATAL\", \n"
+        "            \"output_options\": [{ \n"
+        "                \"output\": \"/dev/null\" \n"
+        "            }] \n"
+        "        }] \n"
+        "    } \n";
+
+    std::ostringstream os;
+
+    // Create a valid config with all the parts should parse
+    os << set_config_txt << ","
+        << args_txt
+        << dhcp4_cfg_txt
+        << subnet1
+        << subnet_footer
+        << control_socket_header
+        << socket_path_
+        << control_socket_footer
+        << "}\n"                      // close dhcp4
+        << ","
+        << logger_txt
+        << "}}";
+
+    // Send the set-config command
+    std::string response;
+    sendUnixCommand(os.str(), response);
+
+    // Verify the configuration was successful.
+    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }",
+              response);
+
+    // Check that the config was indeed applied.
+    const Subnet4Collection* subnets =
+        CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+    EXPECT_EQ(1, subnets->size());
+
+    // Create a config with malformed subnet that should fail to parse.
+    os.str("");
+    os << set_config_txt << ","
+        << args_txt
+        << dhcp4_cfg_txt
+        << bad_subnet
+        << subnet_footer
+        << control_socket_header
+        << socket_path_
+        << control_socket_footer
+        << "}\n"                      // close dhcp4
+        "}}";
+
+    // Send the set-config command
+    sendUnixCommand(os.str(), response);
+
+    // Should fail with a syntax error
+    EXPECT_EQ("{ \"result\": 1, "
+              "\"text\": \"unsupported parameter: BOGUS (<string>:20:26)\" }",
+              response);
+
+    // Check that the config was not lost
+    subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+    EXPECT_EQ(1, subnets->size());
+
+    // Create a valid config with two subnets and no command channel.
+    // It should succeed, client should still receive the response
+    os.str("");
+    os << set_config_txt << ","
+        << args_txt
+        << dhcp4_cfg_txt
+        << subnet1
+        << ",\n"
+        << subnet2
+        << subnet_footer
+        << "}\n"                      // close dhcp4
+        << "}}";
+
+    /* Verify the control channel socket exists */
+    ASSERT_TRUE(fileExists(socket_path_));
+
+    // Send the set-config command
+    sendUnixCommand(os.str(), response);
+
+    /* Verify the control channel socket no longer exists */
+    EXPECT_FALSE(fileExists(socket_path_));
+
+    // With no command channel, should still receive the response.
+    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }",
+              response);
+
+    // Check that the config was not lost
+    subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+    EXPECT_EQ(2, subnets->size());
+
+    // Clean up after the test.
+    CfgMgr::instance().clear();
+}
+
 } // End of anonymous namespace

+ 5 - 1
src/bin/dhcp4/tests/dhcp4_test_utils.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -24,6 +24,7 @@
 #include <dhcpsrv/lease.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
+#include <log/logger_support.h>
 #include <stats/stats_mgr.h>
 
 using namespace std;
@@ -47,6 +48,9 @@ BaseServerTest::~BaseServerTest() {
 
     // Revert to original data directory.
     CfgMgr::instance().setDataDir(original_datadir_);
+
+    // Revert to unit test logging, in case the test reconfigured it.
+    isc::log::initLogger();
 }
 
 Dhcpv4SrvTest::Dhcpv4SrvTest()

+ 1 - 2
src/bin/dhcp4/tests/kea_controller_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-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
@@ -63,7 +63,6 @@ public:
 
     ~JSONFileBackendTest() {
         LeaseMgrFactory::destroy();
-        isc::log::setDefaultLoggingOutput();
         static_cast<void>(remove(TEST_FILE));
         static_cast<void>(remove(TEST_INCLUDE));
     };

+ 73 - 4
src/bin/dhcp6/ctrl_dhcp6_srv.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-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
@@ -67,10 +67,72 @@ ControlledDhcpv6Srv::commandLibReloadHandler(const string&, ConstElementPtr) {
 
 ConstElementPtr
 ControlledDhcpv6Srv::commandConfigReloadHandler(const string&, ConstElementPtr args) {
+    // Use set-config as it handles logging and server config
+    return (commandSetConfigHandler("set-config", args));
+}
+
+ConstElementPtr
+ControlledDhcpv6Srv::commandSetConfigHandler(const string&,
+                                             ConstElementPtr args) {
+    const int status_code = 1; // 1 indicates an error
+    ConstElementPtr dhcp6;
+    string message;
+
+    // Command arguments are expected to be:
+    // { "Dhcp6": { ... }, "Logging": { ... } }
+    // The Logging component is technically optional. If it's not supplied
+    // logging will revert to default logging.
+    if (!args) {
+        message = "Missing mandatory 'arguments' parameter.";
+    } else {
+        dhcp6 = args->get("Dhcp6");
+        if (!dhcp6) {
+            message = "Missing mandatory 'Dhcp6' parameter.";
+        } else if (dhcp6->getType() != Element::map) {
+            message = "'Dhcp6' parameter expected to be a map.";
+        }
+    }
 
-    return (processConfig(args));
+    if (!message.empty()) {
+        // Something is amiss with arguments, return a failure response.
+        ConstElementPtr result = isc::config::createAnswer(status_code,
+                                                           message);
+        return (result);
+    }
+
+    // We are starting the configuration process so we should remove any
+    // staging configuration that has been created during previous
+    // configuration attempts.
+    CfgMgr::instance().rollback();
+
+    // Logging is a sibling element and must be parsed explicitly.
+    // The call to configureLogger parses the given Logging element if
+    // not null, into the staging config.  Note this does not alter the
+    // current loggers, they remain in effect until we apply the
+    // logging config below.  If no logging is supplied logging will
+    // revert to default logging.
+    Daemon::configureLogger(args->get("Logging"),
+                            CfgMgr::instance().getStagingCfg());
+
+    // Now we configure the server proper.
+    ConstElementPtr result = processConfig(dhcp6);
+
+    // If the configuration parsed successfully, apply the new logger
+    // configuration and the commit the new configuration.  We apply
+    // the logging first in case there's a configuration failure.
+    int rcode = 0;
+    isc::config::parseAnswer(rcode, result);
+    if (rcode == 0) {
+        CfgMgr::instance().getStagingCfg()->applyLoggingCfg();
+
+        // Use new configuration.
+        CfgMgr::instance().commit();
+    }
+
+    return (result);
 }
 
+
 ConstElementPtr
 ControlledDhcpv6Srv::commandLeasesReclaimHandler(const string&,
                                                  ConstElementPtr args) {
@@ -116,12 +178,17 @@ ControlledDhcpv6Srv::processCommand(const std::string& command,
         if (command == "shutdown") {
             return (srv->commandShutdownHandler(command, args));
 
+        /// @todo: register config-reload (see CtrlDhcpv6Srv::commandConfigReloadHandler)
+
         } else if (command == "libreload") {
             return (srv->commandLibReloadHandler(command, args));
 
         } else if (command == "config-reload") {
             return (srv->commandConfigReloadHandler(command, args));
 
+        } else if (command == "set-config") {
+            return (srv->commandSetConfigHandler(command, args));
+
         } else if (command == "leases-reclaim") {
             return (srv->commandLeasesReclaimHandler(command, args));
         }
@@ -279,11 +346,12 @@ ControlledDhcpv6Srv::ControlledDhcpv6Srv(uint16_t port)
     CommandMgr::instance().registerCommand("shutdown",
         boost::bind(&ControlledDhcpv6Srv::commandShutdownHandler, this, _1, _2));
 
-    /// @todo: register config-reload (see CtrlDhcpv4Srv::commandConfigReloadHandler)
-
     CommandMgr::instance().registerCommand("libreload",
         boost::bind(&ControlledDhcpv6Srv::commandLibReloadHandler, this, _1, _2));
 
+    CommandMgr::instance().registerCommand("set-config",
+        boost::bind(&ControlledDhcpv6Srv::commandSetConfigHandler, this, _1, _2));
+
     CommandMgr::instance().registerCommand("leases-reclaim",
         boost::bind(&ControlledDhcpv6Srv::commandLeasesReclaimHandler, this, _1, _2));
 
@@ -327,6 +395,7 @@ ControlledDhcpv6Srv::~ControlledDhcpv6Srv() {
         // Deregister any registered commands
         CommandMgr::instance().deregisterCommand("shutdown");
         CommandMgr::instance().deregisterCommand("libreload");
+        CommandMgr::instance().deregisterCommand("set-config");
         CommandMgr::instance().deregisterCommand("leases-reclaim");
         CommandMgr::instance().deregisterCommand("statistic-get");
         CommandMgr::instance().deregisterCommand("statistic-reset");

+ 15 - 1
src/bin/dhcp6/ctrl_dhcp6_srv.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -143,6 +143,20 @@ private:
     commandConfigReloadHandler(const std::string& command,
                                isc::data::ConstElementPtr args);
 
+    /// @brief handler for processing 'set-config' command
+    ///
+    /// This handler processes set-config command, which processes
+    /// configuration specified in args parameter.
+    /// @param command (parameter ignored)
+    /// @param args configuration to be processed. Expected format:
+    /// map containing Dhcp6 map that contains DHCPv6 server configuration.
+    /// May also contain Logging map that specifies logging configuration.
+    ///
+    /// @return status of the command
+    isc::data::ConstElementPtr
+    commandSetConfigHandler(const std::string& command,
+                            isc::data::ConstElementPtr args);
+
     /// @brief Handler for processing 'leases-reclaim' command
     ///
     /// This handler processes leases-reclaim command, which triggers

+ 44 - 20
src/bin/dhcp6/json_config_parser.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -770,6 +770,47 @@ void setGlobalParameters6() {
     }
 }
 
+/// @brief Initialize the command channel based on the staging configuration
+///
+/// Only close the current channel, if the new channel configuration is
+/// different.  This avoids disconnecting a client and hence not sending them
+/// a command result, unless they specifically alter the channel configuration.
+/// In that case the user simply has to accept they'll be disconnected.
+///
+void configureCommandChannel() {
+    // Get new socket configuration.
+    ConstElementPtr sock_cfg =
+        CfgMgr::instance().getStagingCfg()->getControlSocketInfo();
+
+    // Get current socket configuration.
+    ConstElementPtr current_sock_cfg =
+            CfgMgr::instance().getCurrentCfg()->getControlSocketInfo();
+
+    // Determine if the socket configuration has changed. It has if
+    // both old and new configuration is specified but respective
+    // data elements are't equal.
+    bool sock_changed = (sock_cfg && current_sock_cfg &&
+                         !sock_cfg->equals(*current_sock_cfg));
+
+    // If the previous or new socket configuration doesn't exist or
+    // the new configuration differs from the old configuration we
+    // close the exisitng socket and open a new socket as appropriate.
+    // Note that closing an existing socket means the clien will not
+    // receive the configuration result.
+    if (!sock_cfg || !current_sock_cfg || sock_changed) {
+        // Close the existing socket (if any).
+        isc::config::CommandMgr::instance().closeCommandSocket();
+
+        if (sock_cfg) {
+            // This will create a control socket and install the external
+            // socket in IfaceMgr. That socket will be monitored when
+            // Dhcp4Srv::receivePacket() calls IfaceMgr::receive4() and
+            // callback in CommandMgr will be called, if necessary.
+            isc::config::CommandMgr::instance().openCommandSocket(sock_cfg);
+        }
+    }
+}
+
 isc::data::ConstElementPtr
 configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
     if (!config_set) {
@@ -921,25 +962,8 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
             subnet_parser->build(subnet_config->second);
         }
 
-        // Get command socket configuration from the config file.
-        // This code expects the following structure:
-        // {
-        //     "socket-type": "unix",
-        //     "socket-name": "/tmp/kea6.sock"
-        // }
-        ConstElementPtr sock_cfg =
-            CfgMgr::instance().getStagingCfg()->getControlSocketInfo();
-
-        // Close existing socket (if any).
-        isc::config::CommandMgr::instance().closeCommandSocket();
-        if (sock_cfg) {
-            // This will create a control socket and will install external socket
-            // in IfaceMgr. That socket will be monitored when Dhcp4Srv::receivePacket()
-            // calls IfaceMgr::receive4() and callback in CommandMgr will be called,
-            // if necessary. If there were previously open command socket, it will
-            // be closed.
-            isc::config::CommandMgr::instance().openCommandSocket(sock_cfg);
-        }
+        // Setup the command channel.
+        configureCommandChannel();
 
         // The lease database parser is the last to be run.
         std::map<std::string, ConstElementPtr>::const_iterator leases_config =

+ 3 - 33
src/bin/dhcp6/kea_controller.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-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
@@ -38,11 +38,6 @@ void configure(const std::string& file_name) {
     // This is a configuration backend implementation that reads the
     // configuration from a JSON file.
 
-    // We are starting the configuration process so we should remove any
-    // staging configuration that has been created during previous
-    // configuration attempts.
-    CfgMgr::instance().rollback();
-
     isc::data::ConstElementPtr json;
     isc::data::ConstElementPtr dhcp6;
     isc::data::ConstElementPtr logger;
@@ -74,29 +69,14 @@ void configure(const std::string& file_name) {
                       " Did you forget to add { } around your configuration?");
         }
 
-        // Let's configure logging before applying the configuration,
-        // so we can log things during configuration process.
-        // If there's no logging element, we'll just pass NULL pointer,
-        // which will be handled by configureLogger().
-        Daemon::configureLogger(json->get("Logging"),
-                                CfgMgr::instance().getStagingCfg());
-
-        // Get Dhcp6 component from the config
-        dhcp6 = json->get("Dhcp6");
-
-        if (!dhcp6) {
-            isc_throw(isc::BadValue, "no mandatory 'Dhcp6' entry in"
-                      " the configuration");
-        }
-
         // Use parsed JSON structures to configure the server
-        result = ControlledDhcpv6Srv::processCommand("config-reload", dhcp6);
+        result = ControlledDhcpv6Srv::processCommand("set-config", json);
         if (!result) {
             // Undetermined status of the configuration. This should never
             // happen, but as the configureDhcp6Server returns a pointer, it is
             // theoretically possible that it will return NULL.
             isc_throw(isc::BadValue, "undefined result of "
-                      "processCommand(\"config-reload\", dhcp6)");
+                      "processCommand(\"set-config\", json)");
         }
 
         // Now check is the returned result is successful (rcode=0) or not
@@ -109,15 +89,6 @@ void configure(const std::string& file_name) {
                 "no details available";
             isc_throw(isc::BadValue, reason);
         }
-
-        // If configuration was parsed successfully, apply the new logger
-        // configuration to log4cplus. It is done before commit in case
-        // something goes wrong.
-        CfgMgr::instance().getStagingCfg()->applyLoggingCfg();
-
-        // Use new configuration.
-        CfgMgr::instance().commit();
-
     }  catch (const std::exception& ex) {
         // If configuration failed at any stage, we drop the staging
         // configuration and continue to use the previous one.
@@ -128,7 +99,6 @@ void configure(const std::string& file_name) {
         isc_throw(isc::BadValue, "configuration error using file '"
                   << file_name << "': " << ex.what());
     }
-
 }
 
 /// @brief Signals handler for DHCPv6 server.

+ 159 - 5
src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -15,8 +15,10 @@
 #include <dhcp6/ctrl_dhcp6_srv.h>
 #include <dhcp6/tests/dhcp6_test_utils.h>
 #include <hooks/hooks_manager.h>
+#include <log/logger_support.h>
 #include <stats/stats_mgr.h>
 #include <testutils/unix_control_client.h>
+#include <testutils/io_utils.h>
 
 #include "marker_file.h"
 #include "test_libraries.h"
@@ -25,6 +27,7 @@
 #include <gtest/gtest.h>
 
 #include <sys/select.h>
+#include <sys/stat.h>
 #include <sys/ioctl.h>
 #include <cstdlib>
 
@@ -148,6 +151,12 @@ public:
         ConstElementPtr config;
         ASSERT_NO_THROW(config = parseDHCP6(config_txt));
         ConstElementPtr answer = server_->processConfig(config);
+
+        // Commit the configuration so any subsequent reconfigurations
+        // will only close the command channel if its configuration has
+        // changed.
+        CfgMgr::instance().commit();
+
         ASSERT_TRUE(answer);
 
         int status = 0;
@@ -291,7 +300,7 @@ TEST_F(CtrlDhcpv6SrvTest, configReload) {
 
     // Use empty parameters list
     // Prepare configuration file.
-    string config_txt = "{ \"interfaces-config\": {"
+    string config_txt = "{ \"Dhcp6\": { \"interfaces-config\": {"
         "  \"interfaces\": [ \"*\" ]"
         "},"
         "\"preferred-lifetime\": 3000,"
@@ -310,10 +319,10 @@ TEST_F(CtrlDhcpv6SrvTest, configReload) {
         "    \"pools\": [ { \"pool\": \"2001:db8:3::/80\" } ],"
         "    \"subnet\": \"2001:db8:3::/64\" "
         " } ],"
-        "\"valid-lifetime\": 4000 }";
+        "\"valid-lifetime\": 4000 }}";
 
     ConstElementPtr config;
-    ASSERT_NO_THROW(config = parseDHCP6(config_txt));
+    ASSERT_NO_THROW(config = parseJSON(config_txt));
 
     // Make sure there are no subnets configured.
     CfgMgr::instance().clear();
@@ -327,13 +336,158 @@ TEST_F(CtrlDhcpv6SrvTest, configReload) {
 
     // Check that the config was indeed applied.
     const Subnet6Collection* subnets =
-        CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getAll();
+        CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
     EXPECT_EQ(3, subnets->size());
 
     // Clean up after the test.
     CfgMgr::instance().clear();
 }
 
+// Check that the "set-config" command will replace current configuration
+TEST_F(CtrlChannelDhcpv6SrvTest, set_config) {
+    createUnixChannelServer();
+
+    // Define strings to permutate the config arguments
+    // (Note the line feeds makes errors easy to find)
+    string set_config_txt = "{ \"command\": \"set-config\" \n";
+    string args_txt = " \"arguments\": { \n";
+    string dhcp6_cfg_txt =
+        "    \"Dhcp6\": { \n"
+        "        \"interfaces-config\": { \n"
+        "            \"interfaces\": [\"*\"] \n"
+        "        },   \n"
+        "        \"preferred-lifetime\": 3000, \n"
+        "        \"valid-lifetime\": 4000, \n"
+        "        \"renew-timer\": 1000, \n"
+        "        \"rebind-timer\": 2000, \n"
+        "        \"lease-database\": { \n"
+        "           \"type\": \"memfile\", \n"
+        "           \"persist\":false, \n"
+        "           \"lfc-interval\": 0  \n"
+        "        }, \n"
+        "        \"expired-leases-processing\": { \n"
+        "            \"reclaim-timer-wait-time\": 0, \n"
+        "            \"hold-reclaimed-time\": 0, \n"
+        "            \"flush-reclaimed-timer-wait-time\": 0 \n"
+        "        },"
+        "        \"subnet6\": [ \n";
+    string subnet1 =
+        "               {\"subnet\": \"3002::/64\", \n"
+        "                \"pools\": [{ \"pool\": \"3002::100-3002::200\" }]}\n";
+    string subnet2 =
+        "               {\"subnet\": \"3003::/64\", \n"
+        "                \"pools\": [{ \"pool\": \"3003::100-3003::200\" }]}\n";
+    string bad_subnet =
+        "               {\"BOGUS\": \"3005::/64\", \n"
+        "                \"pools\": [{ \"pool\": \"3005::100-3005::200\" }]}\n";
+    string subnet_footer =
+        "          ] \n";
+    string control_socket_header =
+        "       ,\"control-socket\": { \n"
+        "       \"socket-type\": \"unix\", \n"
+        "       \"socket-name\": \"";
+    string control_socket_footer =
+        "\"   \n} \n";
+    string logger_txt =
+        "    \"Logging\": { \n"
+        "        \"loggers\": [ { \n"
+        "            \"name\": \"kea\", \n"
+        "            \"severity\": \"FATAL\", \n"
+        "            \"output_options\": [{ \n"
+        "                \"output\": \"/dev/null\" \n"
+        "            }] \n"
+        "        }] \n"
+        "    } \n";
+
+    std::ostringstream os;
+
+    // Create a valid config with all the parts should parse
+    os << set_config_txt << ","
+        << args_txt
+        << dhcp6_cfg_txt
+        << subnet1
+        << subnet_footer
+        << control_socket_header
+        << socket_path_
+        << control_socket_footer
+        << "}\n"                      // close dhcp6
+        << ","
+        << logger_txt
+        << "}}";
+
+    // Send the set-config command
+    std::string response;
+    sendUnixCommand(os.str(), response);
+
+    // Verify the configuration was successful.
+    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }",
+              response);
+
+    // Check that the config was indeed applied.
+    const Subnet6Collection* subnets =
+        CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
+    EXPECT_EQ(1, subnets->size());
+
+    // Create a config with malformed subnet that should fail to parse.
+    os.str("");
+    os << set_config_txt << ","
+        << args_txt
+        << dhcp6_cfg_txt
+        << bad_subnet
+        << subnet_footer
+        << control_socket_header
+        << socket_path_
+        << control_socket_footer
+        << "}\n"                      // close dhcp6
+        "}}";
+
+    // Send the set-config command
+    sendUnixCommand(os.str(), response);
+
+    // Should fail with a syntax error
+    EXPECT_EQ("{ \"result\": 1, "
+              "\"text\": \"unsupported parameter: BOGUS (<string>:21:26)\" }",
+              response);
+
+    // Check that the config was not lost
+    subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
+    EXPECT_EQ(1, subnets->size());
+
+    // Create a valid config with two subnets and no command channel.
+    // It should succeed but client will not receive a the response
+    os.str("");
+    os << set_config_txt << ","
+        << args_txt
+        << dhcp6_cfg_txt
+        << subnet1
+        << ",\n"
+        << subnet2
+        << subnet_footer
+        << "}\n"                      // close dhcp6
+        << "}}";
+
+    /* Verify the control channel socket exists */
+    ASSERT_TRUE(fileExists(socket_path_));
+
+    // Send the set-config command
+    sendUnixCommand(os.str(), response);
+
+    /* Verify the control channel socket no longer exists */
+    EXPECT_FALSE(fileExists(socket_path_));
+
+    // With no command channel, should still receive the response.
+    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }",
+              response);
+
+    // Check that the config was not lost
+    subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
+    EXPECT_EQ(2, subnets->size());
+
+    // Clean up after the test.
+    CfgMgr::instance().clear();
+}
+
+
 typedef std::map<std::string, isc::data::ConstElementPtr> ElementMap;
 
 // This test checks which commands are registered by the DHCPv4 server.

+ 5 - 1
src/bin/dhcp6/tests/dhcp6_test_utils.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -10,6 +10,7 @@
 #include <dhcp6/tests/dhcp6_test_utils.h>
 #include <dhcp6/json_config_parser.h>
 #include <dhcp/tests/pkt_captures.h>
+#include <log/logger_support.h>
 #include <util/pointer_util.h>
 #include <cc/command_interpreter.h>
 #include <stats/stats_mgr.h>
@@ -46,6 +47,9 @@ BaseServerTest::~BaseServerTest() {
 
     // Revert to original data directory.
     CfgMgr::instance().setDataDir(original_datadir_);
+
+    // Revert to unit test logging in case the test reconfigured logging.
+    isc::log::initLogger();
 }
 
 Dhcpv6SrvTest::Dhcpv6SrvTest()

+ 29 - 3
src/lib/config/command_mgr.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -11,6 +11,7 @@
 #include <dhcp/iface_mgr.h>
 #include <config/config_log.h>
 #include <boost/bind.hpp>
+#include <unistd.h>
 
 using namespace isc::data;
 
@@ -145,6 +146,19 @@ CommandMgr::commandReader(int sockfd) {
         return;
     }
 
+    // Duplicate the connection's socket in the event, the command causes the
+    // channel to close (like a reconfig).  This permits us to always have
+    // a socket on which to respond. If for some reason  we can't fall back
+    // to the connection socket.
+    int rsp_fd = dup(sockfd);
+    if (rsp_fd < 0 ) {
+        // Highly unlikely
+        const char* errmsg = strerror(errno);
+        LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_DUP_WARN)
+                  .arg(errmsg);
+        rsp_fd = sockfd;
+    }
+
     LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_READ).arg(rval).arg(sockfd);
 
     // Ok, we received something. Let's see if we can make any sense of it.
@@ -163,6 +177,11 @@ CommandMgr::commandReader(int sockfd) {
 
     if (!rsp) {
         LOG_WARN(command_logger, COMMAND_RESPONSE_ERROR);
+        // Only close the duped socket if it's different (should be)
+        if (rsp_fd != sockfd) {
+            close(rsp_fd);
+        }
+
         return;
     }
 
@@ -179,7 +198,8 @@ CommandMgr::commandReader(int sockfd) {
     }
 
     // Send the data back over socket.
-    rval = write(sockfd, txt.c_str(), len);
+    rval = write(rsp_fd, txt.c_str(), len);
+    int saverr = errno;
 
     LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_WRITE).arg(len).arg(sockfd);
 
@@ -187,7 +207,13 @@ CommandMgr::commandReader(int sockfd) {
         // Response transmission failed. Since the response failed, it doesn't
         // make sense to send any status codes. Let's log it and be done with
         // it.
-        LOG_ERROR(command_logger, COMMAND_SOCKET_WRITE_FAIL).arg(len).arg(sockfd);
+        LOG_ERROR(command_logger, COMMAND_SOCKET_WRITE_FAIL)
+                  .arg(len).arg(sockfd).arg(strerror(saverr));
+    }
+
+    // Only close the duped socket if it's different (should be)
+    if (rsp_fd != sockfd) {
+        close(rsp_fd);
     }
 }
 

+ 9 - 2
src/lib/config/config_messages.mes

@@ -1,4 +1,4 @@
-# Copyright (C) 2011-2015 Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2011-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
@@ -57,6 +57,13 @@ This error message indicates that the server failed to set non-blocking mode
 on just created socket. That socket was created for accepting specific
 incoming connection. Additional information may be provided as third parameter.
 
+% COMMAND_SOCKET_DUP_WARN Failed to duplicate socket for response: %1
+This debug message indicates that the commandReader was unable to duplicate
+the connection socket prior to executing the command. This is most likely a
+system resource issue.  The command should still be processed and the response
+sent, unless the command caused the command channel to be closed (e.g. a
+reconfiguration command).
+
 % COMMAND_SOCKET_READ Received %1 bytes over command socket %2
 This debug message indicates that specified number of bytes was received
 over command socket identified by specified file descriptor.
@@ -86,6 +93,6 @@ descriptor and path specified.
 This debug message indicates that the specified number of bytes was sent
 over command socket identifier by the specified file descriptor.
 
-% COMMAND_SOCKET_WRITE_FAIL Error while writing %1 bytes to command socket %2
+% COMMAND_SOCKET_WRITE_FAIL Error while writing %1 bytes to command socket %2 : %3
 This error message indicates that an error was encountered while
 attempting to send a response to the command socket.

+ 3 - 5
src/lib/testutils/io_utils.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -15,10 +15,8 @@ namespace dhcp {
 namespace test {
 
 bool fileExists(const std::string& file_path) {
-    std::ifstream fs(file_path.c_str());
-    const bool file_exists = fs.good();
-    fs.close();
-    return (file_exists);
+    struct stat statbuf;
+    return(stat(file_path.c_str(), &statbuf) == 0);
 }
 
 std::string readFile(const std::string& file_path) {

+ 2 - 1
src/lib/testutils/io_utils.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -8,6 +8,7 @@
 #define TEST_IO_UTILS_H
 
 #include <string>
+#include <sys/stat.h>
 
 namespace isc {
 namespace dhcp {