Browse Source

[master] Merge branch 'trac3880' (control channel for DHCPv4)

Conflicts:
	doc/guide/dhcp4-srv.xml
	src/bin/dhcp4/tests/Makefile.am
	src/lib/stats/stats_mgr.h
	src/lib/stats/tests/stats_mgr_unittest.cc
Tomek Mrugalski 10 years ago
parent
commit
6886583955
36 changed files with 2358 additions and 15 deletions
  1. 7 0
      ChangeLog
  2. 5 0
      doc/devel/mainpage.dox
  3. 1 1
      doc/guide/Makefile.am
  4. 15 0
      doc/guide/ctrl-channel.xml
  5. 30 0
      doc/guide/dhcp4-srv.xml
  6. 2 0
      doc/guide/kea-guide.xml
  7. 42 1
      src/bin/dhcp4/ctrl_dhcp4_srv.cc
  8. 23 0
      src/bin/dhcp4/json_config_parser.cc
  9. 2 1
      src/bin/dhcp4/kea_controller.cc
  10. 1 0
      src/bin/dhcp4/tests/Makefile.am
  11. 191 0
      src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
  12. 2 0
      src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
  13. 2 2
      src/lib/Makefile.am
  14. 5 1
      src/lib/config/Makefile.am
  15. 209 0
      src/lib/config/command-socket.dox
  16. 245 0
      src/lib/config/command_mgr.cc
  17. 203 0
      src/lib/config/command_mgr.h
  18. 51 0
      src/lib/config/command_socket.cc
  19. 113 0
      src/lib/config/command_socket.h
  20. 211 0
      src/lib/config/command_socket_factory.cc
  21. 47 0
      src/lib/config/command_socket_factory.h
  22. 3 1
      src/lib/config/config_log.cc
  23. 10 6
      src/lib/config/config_log.h
  24. 85 1
      src/lib/config/config_messages.mes
  25. 4 0
      src/lib/config/tests/Makefile.am
  26. 223 0
      src/lib/config/tests/command_mgr_unittests.cc
  27. 77 0
      src/lib/config/tests/command_socket_factory_unittests.cc
  28. 9 0
      src/lib/dhcp/iface_mgr.cc
  29. 3 1
      src/lib/dhcp/iface_mgr.h
  30. 21 0
      src/lib/dhcpsrv/parsers/dhcp_parsers.cc
  31. 17 0
      src/lib/dhcpsrv/parsers/dhcp_parsers.h
  32. 16 0
      src/lib/dhcpsrv/srv_config.h
  33. 4 0
      src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
  34. 92 0
      src/lib/stats/stats_mgr.cc
  35. 114 0
      src/lib/stats/stats_mgr.h
  36. 273 0
      src/lib/stats/tests/stats_mgr_unittest.cc

+ 7 - 0
ChangeLog

@@ -1,3 +1,10 @@
+958.	[func]		tomek
+	DHCPv4 server now supports control channel, implemented over UNIX
+	socket. Currently supported commands are: statistic-get,
+	statistic-reset, statistic-remove, statistic-get-all,
+	statistic-reset-all, statistic-remove-all, shutdown.
+	(Trac #3880, git tbd)
+
 957.	[func]		tomek
 	Per IPv4 subnet statistics (subnet[id].assigned-addresses and
 	subnet[id].total-addresses) has been implemented.

+ 5 - 0
doc/devel/mainpage.dox

@@ -79,6 +79,11 @@
  * - @subpage lfc
  *   - @subpage lfcProcessing
  *   - @subpage lfcFiles
+ * - @subpage ctrlSocket
+ *   - @subpage ctrlSocketOverview
+ *   - @subpage ctrlSocketClient
+ *   - @subpage ctrlSocketImpl
+ *   - @subpage ctrlSocketConnections
  * - @subpage libdhcp
  *   - @subpage libdhcpIntro
  *   - @subpage libdhcpRelay

+ 1 - 1
doc/guide/Makefile.am

@@ -7,7 +7,7 @@ dist_html_DATA = $(HTMLDOCS) kea-guide.css
 
 DOCBOOK = kea-guide.xml intro.xml quickstart.xml install.xml admin.xml config.xml
 DOCBOOK += keactrl.xml dhcp4-srv.xml dhcp6-srv.xml logging.xml ddns.xml hooks.xml
-DOCBOOK += libdhcp.xml lfc.xml stats.xml
+DOCBOOK += libdhcp.xml lfc.xml stats.xml ctrl-channel.xml
 
 EXTRA_DIST = $(DOCBOOK)
 DISTCLEANFILES = $(HTMLDOCS) $(DOCS) kea-messages.xml

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

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" [
+<!ENTITY mdash  "&#x2014;" >
+<!ENTITY % version SYSTEM "version.ent">
+%version;
+]>
+
+  <chapter id="ctrl-channel">
+    <title>Management API</title>
+    <para>
+      @todo: To be implemented in ticket 3880.
+    </para>
+
+  </chapter>

+ 30 - 0
doc/guide/dhcp4-srv.xml

@@ -2860,6 +2860,36 @@ appropiately -->
     </para>
 </section>
 
+    <section id="dhcp4-ctrl-channel">
+      <title>Management API for the DHCPv4 server</title>
+      <para>
+        Management API has been introduced in Kea 0.9.2. It allows issuing specific
+        management commands, like statistics retrieval, reconfiguration or shutdown.
+        For more details, see <xref linkend="ctrl-channel" />. Currently the only
+        supported communication channel type is UNIX stream socket. By default there
+        are no sockets open. To instruct Kea to open a socket, the following entry
+        in the configuration file can be used:
+<screen>
+"Dhcp4": {
+    "control-socket": {
+        "socket-type": "unix",
+        "socket-name": <userinput>"/path/to/the/unix/socket"</userinput>
+    },
+
+    "subnet4": [
+        ...
+    ],
+    ...
+}
+</screen>
+      </para>
+
+      <para>
+        Communication over control channel is conducted using JSON structures.
+        See the Control Channel section in the Kea Developer's Guide for more details.
+      </para>
+    </section>
+
     <section id="dhcp4-std">
       <title>Supported DHCP Standards</title>
       <para>The following standards are currently supported:</para>

+ 2 - 0
doc/guide/kea-guide.xml

@@ -75,6 +75,8 @@
 
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="stats.xml" />
 
+  <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="ctrl-channel.xml" />
+
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="libdhcp.xml" />
 
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="logging.xml" />

+ 42 - 1
src/bin/dhcp4/ctrl_dhcp4_srv.cc

@@ -19,9 +19,13 @@
 #include <hooks/hooks_manager.h>
 #include <dhcp4/json_config_parser.h>
 #include <dhcpsrv/cfgmgr.h>
+#include <config/command_mgr.h>
+#include <stats/stats_mgr.h>
 
 using namespace isc::data;
 using namespace isc::hooks;
+using namespace isc::config;
+using namespace isc::stats;
 using namespace std;
 
 namespace isc {
@@ -121,7 +125,6 @@ ControlledDhcpv4Srv::processConfig(isc::data::ConstElementPtr config) {
 
     ConstElementPtr answer = configureDhcp4Server(*srv, config);
 
-
     // Check that configuration was successful. If not, do not reopen sockets
     // and don't bother with DDNS stuff.
     try {
@@ -164,6 +167,32 @@ ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t port /*= DHCP4_SERVER_PORT*/)
                   "There is another Dhcpv4Srv instance already.");
     }
     server_ = this; // remember this instance for later use in handlers
+
+    // Register supported commands in CommandMgr
+    CommandMgr::instance().registerCommand("shutdown",
+        boost::bind(&ControlledDhcpv4Srv::commandShutdownHandler, this, _1, _2));
+
+    /// @todo: register config-reload (see CtrlDhcpv4Srv::commandConfigReloadHandler)
+    /// @todo: register libreload (see CtrlDhcpv4Srv::commandLibReloadHandler)
+
+    // Register statistic related commands
+    CommandMgr::instance().registerCommand("statistic-get",
+        boost::bind(&StatsMgr::statisticGetHandler, _1, _2));
+
+    CommandMgr::instance().registerCommand("statistic-reset",
+        boost::bind(&StatsMgr::statisticResetHandler, _1, _2));
+
+    CommandMgr::instance().registerCommand("statistic-remove",
+        boost::bind(&StatsMgr::statisticRemoveHandler, _1, _2));
+
+    CommandMgr::instance().registerCommand("statistic-get-all",
+        boost::bind(&StatsMgr::statisticGetAllHandler, _1, _2));
+
+    CommandMgr::instance().registerCommand("statistic-reset-all",
+        boost::bind(&StatsMgr::statisticResetAllHandler, _1, _2));
+
+    CommandMgr::instance().registerCommand("statistic-remove-all",
+        boost::bind(&StatsMgr::statisticRemoveAllHandler, _1, _2));
 }
 
 void ControlledDhcpv4Srv::shutdown() {
@@ -174,6 +203,18 @@ void ControlledDhcpv4Srv::shutdown() {
 ControlledDhcpv4Srv::~ControlledDhcpv4Srv() {
     cleanup();
 
+    // Close the command socket (if it exists).
+    CommandMgr::instance().closeCommandSocket();
+
+    // Deregister any registered commands
+    CommandMgr::instance().deregisterCommand("shutdown");
+    CommandMgr::instance().deregisterCommand("statistic-get");
+    CommandMgr::instance().deregisterCommand("statistic-reset");
+    CommandMgr::instance().deregisterCommand("statistic-remove");
+    CommandMgr::instance().deregisterCommand("statistic-get-all");
+    CommandMgr::instance().deregisterCommand("statistic-reset-all");
+    CommandMgr::instance().deregisterCommand("statistic-remove-all");
+
     server_ = NULL; // forget this instance. Noone should call any handlers at
                     // this stage.
 }

+ 23 - 0
src/bin/dhcp4/json_config_parser.cc

@@ -27,6 +27,7 @@
 #include <dhcpsrv/parsers/host_reservation_parser.h>
 #include <dhcpsrv/parsers/host_reservations_list_parser.h>
 #include <dhcpsrv/parsers/ifaces_config_parser.h>
+#include <config/command_mgr.h>
 #include <util/encode/hex.h>
 #include <util/strutil.h>
 
@@ -399,6 +400,8 @@ namespace dhcp {
         parser = new D2ClientConfigParser(config_id);
     } else if (config_id.compare("match-client-id") == 0) {
         parser = new BooleanParser(config_id, globalContext()->boolean_values_);
+    } else if (config_id.compare("control-socket") == 0) {
+        parser = new ControlSocketParser(config_id);
     } else {
         isc_throw(DhcpConfigError,
                 "unsupported global configuration parameter: "
@@ -532,6 +535,26 @@ 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);
+        }
+
         // the leases database parser is the last to be run.
         std::map<std::string, ConstElementPtr>::const_iterator leases_config =
             values_map.find("lease-database");

+ 2 - 1
src/bin/dhcp4/kea_controller.cc

@@ -104,6 +104,8 @@ void configure(const std::string& file_name) {
         CfgMgr::instance().getStagingCfg()->applyLoggingCfg();
 
         // Use new configuration.
+        /// @todo: This commit should be moved to
+        /// CtrlDhcp4Srv::commandConfigReloadHandler.
         CfgMgr::instance().commit();
 
     }  catch (const std::exception& ex) {
@@ -171,7 +173,6 @@ ControlledDhcpv4Srv::init(const std::string& file_name) {
     signal_set_.reset(new isc::util::SignalSet(SIGINT, SIGHUP, SIGTERM));
     // Set the pointer to the handler function.
     signal_handler_ = signalHandler;
-
 }
 
 void ControlledDhcpv4Srv::cleanup() {

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

@@ -109,6 +109,7 @@ dhcp4_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/io/libkea-util-io.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/bin/cfgrpt/libcfgrpt.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
 endif
 
 noinst_EXTRA_DIST = configs-list.txt

+ 191 - 0
src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc

@@ -15,6 +15,7 @@
 #include <config.h>
 
 #include <cc/command_interpreter.h>
+#include <config/command_mgr.h>
 #include <dhcp/dhcp4.h>
 #include <dhcp4/ctrl_dhcp4_srv.h>
 #include <hooks/hooks_manager.h>
@@ -71,6 +72,71 @@ public:
         static_cast<void>(unlink(LOAD_MARKER_FILE));
         static_cast<void>(unlink(UNLOAD_MARKER_FILE));
     }
+
+    /// @brief sends commands over specified UNIX socket
+    ///
+    /// @param command command to be sent (should be valid JSON)
+    /// @param response response received (expected to be a valid JSON)
+    /// @param socket_path UNIX socket path
+    ///
+    /// @return true if send/response exchange was successful, false otherwise
+    bool sendCommandUnixSocket(const std::string& command,
+                               std::string& response,
+                               const std::string& socket_path) {
+
+        // Create UNIX socket
+        int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+        if (socket_fd < 0) {
+            ADD_FAILURE() << "Failed to open unix stream socket.";
+            return (false);
+        }
+
+        // Prepare socket address
+        struct sockaddr_un srv_addr;
+        memset(&srv_addr, 0, sizeof(struct sockaddr_un));
+        srv_addr.sun_family = AF_UNIX;
+        strncpy(srv_addr.sun_path, socket_path.c_str(), sizeof(srv_addr.sun_path));
+        socklen_t len = sizeof(srv_addr);
+
+        // Connect to the specified UNIX socket
+        int status = connect(socket_fd, (struct sockaddr*)&srv_addr, len);
+        if (status == -1) {
+            ADD_FAILURE() << "Failed to connect unix socket: fd=" << socket_fd
+                          << ", path=" << socket_path;
+            close(socket_fd);
+            return (false);
+        }
+
+
+        // Send command
+        cout << "Sending command: " << command << endl;
+        int bytes_sent = send(socket_fd, command.c_str(), command.length(), 0);
+        if (bytes_sent < command.length()) {
+            ADD_FAILURE() << "Failed to send " << command.length()
+                      << " bytes, send() returned " << bytes_sent;
+            close(socket_fd);
+            return (false);
+        }
+
+        // Receive response
+        /// @todo: this may block if server fails to respond. Some sort of
+        /// of a timer is needed.
+        char buf[65536];
+        memset(buf, 0, sizeof(buf));
+        int bytes_rcvd = recv(socket_fd, buf, sizeof(buf), 0);
+        if (bytes_rcvd < 0) {
+            ADD_FAILURE() << "Failed to receive a response. recv() returned "
+                      << bytes_rcvd;
+            close(socket_fd);
+            return (false);
+        }
+
+        // Convert the response to a string, close the socket and return
+        response = string(buf, bytes_rcvd);
+        cout << "Received response: " << response << endl;
+        close(socket_fd);
+        return (true);
+    }
 };
 
 TEST_F(CtrlDhcpv4SrvTest, commands) {
@@ -156,4 +222,129 @@ TEST_F(CtrlDhcpv4SrvTest, libreload) {
     EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "1212"));
 }
 
+// This test checks which commands are registered by the DHCPv4 server.
+TEST_F(CtrlDhcpv4SrvTest, commandsRegistration) {
+
+    ConstElementPtr list_cmds = createCommand("list-commands");
+    ConstElementPtr answer;
+
+    // By default the list should be empty (except the standard list-commands
+    // supported by the CommandMgr itself)
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
+    ASSERT_TRUE(answer);
+    ASSERT_TRUE(answer->get("arguments"));
+    EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str());
+
+    // Created server should register several additional commands.
+    boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+    ASSERT_NO_THROW(
+        srv.reset(new ControlledDhcpv4Srv(0));
+    );
+
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
+    ASSERT_TRUE(answer);
+    ASSERT_TRUE(answer->get("arguments"));
+    EXPECT_EQ("[ \"list-commands\", \"shutdown\", "
+              "\"statistic-get\", \"statistic-get-all\", "
+              "\"statistic-remove\", \"statistic-remove-all\", "
+              "\"statistic-reset\", \"statistic-reset-all\" ]",
+              answer->get("arguments")->str());
+
+    // Ok, and now delete the server. It should deregister its commands.
+    srv.reset();
+
+    // The list should be (almost) empty again.
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
+    ASSERT_TRUE(answer);
+    ASSERT_TRUE(answer->get("arguments"));
+    EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str());
+}
+
+// Checks if the server is able to parse control socket configuration and
+// configures the command socket properly.
+
+/// @todo: This unit-test is disabled, because it causes weird issues, when
+/// IfaceMgr::receive4() is called in a separate process. That's a side effect
+/// of how we run the test. We should either investigate why IfaceMgr doesn't
+/// work correctly after fork or develop a small tool that will send data
+/// from stdin to specified UNIX socket, print out the responses on stdout
+/// and develop shell tests for this.
+///
+/// Note that the test passes when run on its own, but not when run along with
+/// other unit-tests.
+TEST_F(CtrlDhcpv4SrvTest, DISABLED_commandSocketBasic) {
+
+    string socket_path = string(TEST_DATA_DIR) + "/kea4.sock";
+    ::remove(socket_path.c_str());
+
+    // Just a simple config. The important part here is the socket
+    // location information.
+    std::string config_txt =
+        "{"
+        "    \"interfaces-config\": {"
+        "        \"interfaces\": [ \"*\" ]"
+        "    },"
+        "    \"rebind-timer\": 2000, "
+        "    \"renew-timer\": 1000, "
+        "    \"subnet4\": [ ],"
+        "    \"valid-lifetime\": 4000,"
+        "    \"control-socket\": {"
+        "        \"socket-type\": \"unix\","
+        "        \"socket-name\": \"" + socket_path + "\""
+        "    },"
+        "    \"lease-database\": { \"type\": \"memfile\", \"persist\": false }"
+        "}";
+
+    pid_t pid = fork();
+    if (pid > 0) {
+        cout << "Created child process: " << pid << endl;
+
+        string command("{ \"command\": \"shutdown\" }");
+        string response;
+
+        sleep(3);
+
+        EXPECT_TRUE(sendCommandUnixSocket(command, response, socket_path));
+
+        kill(pid, SIGTERM);
+        int status;
+        waitpid(pid, &status, 0);
+
+        ASSERT_NE(0, response.length());
+
+        ConstElementPtr rsp;
+        EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+        ASSERT_TRUE(rsp);
+
+        int status_code;
+        ConstElementPtr comment = parseAnswer(status_code, rsp);
+        EXPECT_EQ(0, status_code);
+    } else {
+
+        IfaceMgr::instance().deleteAllExternalSockets();
+
+        boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+        ASSERT_NO_THROW(
+            srv.reset(new ControlledDhcpv4Srv(0));
+            );
+
+        ConstElementPtr config = Element::fromJSON(config_txt);
+
+        ConstElementPtr answer = srv->processConfig(config);
+        ASSERT_TRUE(answer);
+
+        int status = 0;
+        isc::config::parseAnswer(status, answer);
+        EXPECT_EQ(0, status);
+
+        // Now check that the socket was indeed open.
+        ASSERT_TRUE(isc::config::CommandMgr::instance().getControlSocketFD() > -1);
+
+        cout << "Child process: pid=" << pid << ", running server." << endl;
+        srv->run();
+
+        exit(EXIT_SUCCESS);
+    }
+}
+
 } // End of anonymous namespace

+ 2 - 0
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -17,6 +17,7 @@
 
 #include <asiolink/io_address.h>
 #include <cc/command_interpreter.h>
+#include <config/command_mgr.h>
 #include <dhcp4/tests/dhcp4_test_utils.h>
 #include <dhcp/tests/pkt_captures.h>
 #include <dhcp/dhcp4.h>
@@ -56,6 +57,7 @@ using namespace isc::dhcp;
 using namespace isc::data;
 using namespace isc::asiolink;
 using namespace isc::hooks;
+using namespace isc::config;
 using namespace isc::dhcp::test;
 using namespace isc::test;
 

+ 2 - 2
src/lib/Makefile.am

@@ -1,3 +1,3 @@
 # The following build order must be maintained.
-SUBDIRS = exceptions util log hooks cryptolink dns cc stats config \
-          asiolink asiodns testutils dhcp dhcp_ddns dhcpsrv
+SUBDIRS = exceptions util log hooks cryptolink dns cc asiolink dhcp config stats \
+          asiodns testutils dhcp_ddns dhcpsrv

+ 5 - 1
src/lib/config/Makefile.am

@@ -17,17 +17,21 @@ BUILT_SOURCES = config_messages.h config_messages.cc
 lib_LTLIBRARIES = libkea-cfgclient.la
 libkea_cfgclient_la_SOURCES = config_data.h config_data.cc
 libkea_cfgclient_la_SOURCES += module_spec.h module_spec.cc
+libkea_cfgclient_la_SOURCES += command_mgr.cc command_mgr.h
+libkea_cfgclient_la_SOURCES += command_socket.cc command_socket.h
+libkea_cfgclient_la_SOURCES += command_socket_factory.cc command_socket_factory.h
 libkea_cfgclient_la_SOURCES += config_log.h config_log.cc
 
 libkea_cfgclient_la_LIBADD = $(top_builddir)/src/lib/cc/libkea-cc.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
 libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
+libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
 
 libkea_cfgclient_la_LDFLAGS = -no-undefined -version-info 1:0:1
 
 nodist_libkea_cfgclient_la_SOURCES  = config_messages.h config_messages.cc
 
 # The message file should be in the distribution.
-EXTRA_DIST = config_messages.mes
+EXTRA_DIST = config_messages.mes command-socket.dox
 
 CLEANFILES = *.gcno *.gcda config_messages.h config_messages.cc s-messages

+ 209 - 0
src/lib/config/command-socket.dox

@@ -0,0 +1,209 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ @page ctrlSocket Control Channel
+
+@section ctrlSocketOverview Control Channel Overview
+
+In many cases it is useful to manage certain aspects of the DHCP servers
+while they are running. In Kea, this may be done via the Control Channel.
+Control Channel allows an external entity (e.g. a tool run by a sysadmin
+or a script) to issue commands to the server which can influence the its
+behavior or retreive information from it. Several notable examples
+envisioned are: reconfiguration, statistics retrival and manipulation,
+and shutdown.
+@note Currently, only the DHCPv4 component supports Control Channel.
+@todo: Update this text once Control Channel support in DHCPv6 is added.
+
+Communication over Control Channel is conducted using JSON structures.
+Currently (Kea 0.9.2) the only supported communication channel is UNIX stream
+sockets, but additional types may be added in the future.
+
+If configured, Kea will open a socket and will listen for any incoming
+connections. A process connecting to this socket is expected to send JSON
+commands structured as follows:
+
+@code
+{
+    "command": "foo",
+    "arguments": {
+        "param_foo": "value1",
+        "param_bar": "value2",
+        ...
+    }
+}
+@endcode
+
+- command - is the name of command to execute and is mandatory.
+- arguments - it may be absent, contain a single parameter or a map or parameters
+required to carry out the given command.  The exact content and format is command specific.
+
+The server will process the incoming command and then send a response of the form:
+
+@code
+{
+    "result": 0|1,
+    "text": "textual description",
+    "arguments": {
+        "argument1": "value1",
+        "argument2": "value2",
+        ...
+    }
+}
+@endcode
+
+- result - indicates the outcome of the command. A value of 0 means a success while
+any non-zero value designates an error. Currently 1 is used as a generic error, but additional
+error codes may be added in the future.
+- text field - typically appears when result is non-zero and contains description of the error
+encountered.
+- arguments - is a map of additional data values returned by the server, specific to the
+command issue. The map is always present, even if it contains no data values.
+
+@section ctrlSocketClient Using Control Channel
+
+Here are two examples of how to access the Control Channel:
+
+1. Use socat tool, which is available in many Linux and BSD distributions.
+See http://www.dest-unreach.org/socat/ for details. To use it:
+@code
+socat UNIX:/var/run/kea/kea4.sock -
+@endcode
+You then can type JSON commands and get responses (also in JSON format).
+
+2. Here's an example C code that connects and gets a list of supported commands:
+@code
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+int main(int argc, const char* argv[]) {
+
+    if (argc != 2) {
+        printf("Usage: %s socket_path\n", argv[0]);
+        return (1);
+    }
+
+    // Create UNIX stream socket.
+    int socket_fd;
+    if ((socket_fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
+    {
+        perror("Failed to create UNIX stream");
+        return (1);
+    }
+
+    // Specify the address to connect to (unix path)
+    struct sockaddr_un srv_addr;
+    memset(&srv_addr, 0, sizeof(struct sockaddr_un));
+    srv_addr.sun_family = AF_UNIX;
+    strcpy(srv_addr.sun_path, argv[1]);
+    socklen_t len = sizeof(srv_addr);
+
+    // Try to connect.
+    if (connect(socket_fd, (struct sockaddr*) &srv_addr, len) == -1) {
+        perror("Failed to connect");
+        return (1);
+    }
+
+    // Send a command to list all available commands.
+    char buf[1024];
+    sprintf(buf, "{ \"command\": \"list-commands\" }");
+    int bytes_sent = send(socket_fd, buf, strlen(buf), 0);
+    printf("%d bytes sent\n", bytes_sent);
+
+    // Receive a response (should be JSON formatted list of commands)
+    int bytes_rcvd = recv(socket_fd, buf, sizeof(buf), 0);
+    printf("%d bytes received: [%s]\n", bytes_rcvd, buf);
+
+    // Close the socket
+    close(socket_fd);
+
+    return 0;
+}
+@endcode
+
+@section ctrlSocketImpl Control Channel Implementation
+
+Control Channel is implemented in @ref isc::config::CommandMgr. It is a signleton
+class that allows registration of callbacks that handle specific commands.
+It internally supports a single command: @c list-commands that returns a list
+of supported commands. This component is expected to be shared among all daemons.
+
+There are 3 main methods that are expected to be used by developers:
+- @ref isc::config::CommandMgr::registerCommand, which allows registration of
+  additional commands.
+- @ref isc::config::CommandMgr::deregisterCommand, which allows removing previously
+  registered command.
+- @ref isc::config::CommandMgr::processCommand, which allows handling specified
+  command.
+
+There are also two methods for managing control sockets. They are not expected
+to be used directly, unless someone implements a new Control Channel (e.g. TCP
+or HTTPS connection):
+
+- @ref isc::config::CommandMgr::openCommandSocket that passes structure defined
+  in the configuration file. Currently only two parameters are supported: socket-type
+  (which must contain value 'unix') and socket-name (which contains unix path for
+  the named socket to be created). This method calls @ref
+  isc::config::CommandSocketFactory::create method, which parses the parameters
+  and instantiates one object from a class derived from @ref isc::config::CommandSocket.
+  Again, currently only UNIX type is supported, but the factory
+  class is expected to be extended to cover additional types.
+- @ref isc::config::CommandMgr::closeCommandSocket() - it is used to close the
+  socket. It calls close method on the @ref isc::config::CommandSocket object, if
+  one exists. In particular, for UNIX socket, it also deletes the file after socket
+  was closed.
+
+@section ctrlSocketConnections Accepting connections
+
+Control Channel is connection oriented communication. In that sense it is
+different than all other communications supported so far in Kea. To facilitate
+connections, several mechanisms were implemented. Intially a single UNIX socket
+it opened (see isc::config::UnixCommandSocket). Its @ref
+isc::config::UnixCommandSocket::receiveHandler callback method is
+installed in @ref isc::dhcp::IfaceMgr to process incoming connections. When the
+select call in @ref isc::dhcp::IfaceMgr::receive4 indicates that there is some data to be
+processed, this callback calls accept, which creates a new socket for handling
+this particular incoming connection. Once the socket descriptor is known, a new
+instance of @ref isc::config::ConnectionSocket is created to represent that
+socket (and the whole ongoing connection). It installs another callback
+(@ref isc::config::ConnectionSocket::receiveHandler that calls
+(@ref isc::config::CommandMgr::commandReader) that will process incoming
+data or will close the socket when necessary. CommandReader reads data from
+incoming socket and attempts to parse it as JSON structures. If successful,
+it calls isc::config::CommandMgr::processCommand(), serializes the structure
+returned and attempts to send it back.
+
+@todo Currently commands and responses up to 64KB are supported. It was deemed
+sufficient for the current needs, but in the future we may need to extend
+it to handle bigger structures.
+
+*/

+ 245 - 0
src/lib/config/command_mgr.cc

@@ -0,0 +1,245 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/command_mgr.h>
+#include <config/command_socket_factory.h>
+#include <cc/data.h>
+#include <cc/command_interpreter.h>
+#include <dhcp/iface_mgr.h>
+#include <config/config_log.h>
+#include <boost/bind.hpp>
+
+using namespace isc::data;
+
+namespace isc {
+namespace config {
+
+CommandMgr::CommandMgr() {
+    registerCommand("list-commands",
+        boost::bind(&CommandMgr::listCommandsHandler, this, _1, _2));
+}
+
+CommandSocketPtr
+CommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
+    if (socket_) {
+        isc_throw(SocketError, "There is already a control socket open");
+    }
+
+    socket_ = CommandSocketFactory::create(socket_info);
+
+    return (socket_);
+}
+
+void CommandMgr::closeCommandSocket() {
+    // First, let's close the socket for incoming new connections.
+    if (socket_) {
+        socket_->close();
+        socket_.reset();
+    }
+
+    // Now let's close all existing connections that we may have.
+    for (std::list<CommandSocketPtr>::iterator conn = connections_.begin();
+         conn != connections_.end(); ++conn) {
+        (*conn)->close();
+    }
+    connections_.clear();
+}
+
+
+void CommandMgr::addConnection(const CommandSocketPtr& conn) {
+    connections_.push_back(conn);
+}
+
+bool CommandMgr::closeConnection(int fd) {
+
+    // Let's iterate over all currently registered connections.
+    for (std::list<CommandSocketPtr>::iterator conn = connections_.begin();
+         conn != connections_.end(); ++conn) {
+
+        // If found, close it.
+        if ((*conn)->getFD() == fd) {
+            (*conn)->close();
+            connections_.erase(conn);
+            return (true);
+        }
+    }
+
+    return (false);
+}
+
+CommandMgr&
+CommandMgr::instance() {
+    static CommandMgr cmd_mgr;
+    return (cmd_mgr);
+}
+
+void CommandMgr::registerCommand(const std::string& cmd, CommandHandler handler) {
+
+    if (!handler) {
+        isc_throw(InvalidCommandHandler, "Specified command handler is NULL");
+    }
+
+    HandlerContainer::const_iterator it = handlers_.find(cmd);
+    if (it != handlers_.end()) {
+        isc_throw(InvalidCommandName, "Handler for command '" << cmd
+                  << "' is already installed.");
+    }
+
+    handlers_.insert(make_pair(cmd, handler));
+
+    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_REGISTERED).arg(cmd);
+}
+
+void CommandMgr::deregisterCommand(const std::string& cmd) {
+    if (cmd == "list-commands") {
+        isc_throw(InvalidCommandName,
+                  "Can't uninstall internal command 'list-commands'");
+    }
+
+    HandlerContainer::iterator it = handlers_.find(cmd);
+    if (it == handlers_.end()) {
+        isc_throw(InvalidCommandName, "Handler for command '" << cmd
+                  << "' not found.");
+    }
+    handlers_.erase(it);
+
+    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_DEREGISTERED).arg(cmd);
+}
+
+void CommandMgr::deregisterAll() {
+
+    // No need to log anything here. deregisterAll is not used in production
+    // code, just in tests.
+    handlers_.clear();
+    registerCommand("list-commands",
+        boost::bind(&CommandMgr::listCommandsHandler, this, _1, _2));
+}
+
+void
+CommandMgr::commandReader(int sockfd) {
+
+    /// @todo: We do not handle commands that are larger than 64K.
+
+    // We should not expect commands bigger than 64K.
+    char buf[65536];
+    memset(buf, 0, sizeof(buf));
+    ConstElementPtr cmd, rsp;
+
+    // Read incoming data.
+    int rval = read(sockfd, buf, sizeof(buf));
+    if (rval < 0) {
+        // Read failed
+        LOG_ERROR(command_logger, COMMAND_SOCKET_READ_FAIL).arg(rval).arg(sockfd);
+
+        /// @todo: Should we close the connection, similar to what is already
+        /// being done for rval == 0?
+        return;
+    } else if (rval == 0) {
+
+        // Remove it from the active connections list.
+        instance().closeConnection(sockfd);
+
+        return;
+    }
+
+    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.
+    try {
+
+        // Try to interpret it as JSON.
+        cmd = Element::fromJSON(std::string(buf), true);
+
+        // If successful, then process it as a command.
+        rsp = CommandMgr::instance().processCommand(cmd);
+    } catch (const Exception& ex) {
+        LOG_WARN(command_logger, COMMAND_PROCESS_ERROR1).arg(ex.what());
+        rsp = createAnswer(CONTROL_RESULT_ERROR, std::string(ex.what()));
+    }
+
+    if (!rsp) {
+        LOG_WARN(command_logger, COMMAND_RESPONSE_ERROR);
+        return;
+    }
+
+    // Let's convert JSON response to text. Note that at this stage
+    // the rsp pointer is always set.
+    std::string txt = rsp->str();
+    size_t len = txt.length();
+    if (len > 65535) {
+        // Hmm, our response is too large. Let's send the first
+        // 64KB and hope for the best.
+        LOG_ERROR(command_logger, COMMAND_SOCKET_RESPONSE_TOOLARGE).arg(len);
+
+        len = 65535;
+    }
+
+    // Send the data back over socket.
+    rval = write(sockfd, txt.c_str(), len);
+
+    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_WRITE).arg(len).arg(sockfd);
+
+    if (rval < 0) {
+        // 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);
+    }
+}
+
+isc::data::ConstElementPtr
+CommandMgr::processCommand(const isc::data::ConstElementPtr& cmd) {
+    if (!cmd) {
+        return (createAnswer(CONTROL_RESULT_ERROR,
+                             "Command processing failed: NULL command parameter"));
+    }
+
+    try {
+        ConstElementPtr arg;
+        std::string name = parseCommand(arg, cmd);
+
+        LOG_INFO(command_logger, COMMAND_RECEIVED).arg(name);
+
+        HandlerContainer::const_iterator it = handlers_.find(name);
+        if (it == handlers_.end()) {
+            // Ok, there's no such command.
+            return (createAnswer(CONTROL_RESULT_ERROR,
+                                 "'" + name + "' command not supported."));
+        }
+
+        // Call the actual handler and return whatever it returned
+        return (it->second(name, arg));
+
+    } catch (const Exception& e) {
+        LOG_WARN(command_logger, COMMAND_PROCESS_ERROR2).arg(e.what());
+        return (createAnswer(CONTROL_RESULT_ERROR,
+                             std::string("Error during command processing:")
+                             + e.what()));
+    }
+}
+
+isc::data::ConstElementPtr
+CommandMgr::listCommandsHandler(const std::string& name,
+                                const isc::data::ConstElementPtr& params) {
+    using namespace isc::data;
+    ElementPtr commands = Element::createList();
+    for (HandlerContainer::const_iterator it = handlers_.begin();
+         it != handlers_.end(); ++it) {
+        commands->add(Element::create(it->first));
+    }
+    return (createAnswer(CONTROL_RESULT_SUCCESS, commands));
+}
+
+}; // end of isc::config
+}; // end of isc

+ 203 - 0
src/lib/config/command_mgr.h

@@ -0,0 +1,203 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef COMMAND_MGR_H
+#define COMMAND_MGR_H
+
+#include <cc/data.h>
+#include <config/command_socket.h>
+#include <boost/noncopyable.hpp>
+#include <boost/function.hpp>
+#include <string>
+#include <list>
+#include <map>
+
+namespace isc {
+namespace config {
+
+/// @brief CommandMgr exception indicating that the handler specified is not valid
+class InvalidCommandHandler : public Exception {
+public:
+    InvalidCommandHandler(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief CommandMgr exception indicating that the command name is not valid
+class InvalidCommandName : public Exception {
+public:
+    InvalidCommandName(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Commands Manager, responsible for processing external commands
+///
+/// Commands Manager is a generic interface for handling external commands.
+/// Commands can be received over control sockets. Currently unix socket is
+/// supported, but additional type (udp, tcp, https etc.) may be added later.
+/// The commands and responses are sent in JSON format.
+/// See http://kea.isc.org/wiki/StatsDesign for details.
+///
+/// In general, the command has the following format:
+/// {
+///     "command": "statistic-get",
+///     "arguments": {
+///         "name": "received-packets"
+///     }
+/// }
+///
+/// And the response is:
+///
+/// {
+///     "result": 0,
+///     "observations": {
+///         "received-packets": [ [ 1234, "2015-04-15 12:34:45.123" ] ]
+///     }
+/// }
+///
+/// CommandsMgr does not implement the commands (except one, "commands-list")
+/// itself, but rather provides an interface (see @ref registerCommand,
+/// @ref deregisterCommand, @ref processCommand) for other components to use
+/// it. The @ref CommandHandler type is specified in a way to easily use
+/// existing command handlers in DHCPv4 and DHCPv6 components.
+class CommandMgr : public boost::noncopyable {
+public:
+
+    /// @brief Defines command handler type
+    ///
+    /// Command handlers are expected to use this format.
+    /// @param name name of the commands
+    /// @param params parameters specific to the command
+    /// @return response (created with createAnswer())
+    typedef boost::function<isc::data::ConstElementPtr (const std::string& name,
+        const isc::data::ConstElementPtr& params)> CommandHandler;
+
+    /// @brief CommandMgr is a singleton class. This method returns reference
+    /// to its sole instance.
+    ///
+    /// @return the only existing instance of the manager
+    static CommandMgr& instance();
+
+    /// @brief Opens control socket with paramters specified in socket_info
+    ///
+    /// Currently supported types are:
+    /// - unix (required parameters: socket-type: unix, socket-name:/unix/path)
+    ///
+    /// This method will close previously open command socket (if exists).
+    ///
+    /// @throw CommandSocketError if socket creation fails.
+    /// @throw SocketError if command socket is already open.
+    ///
+    /// @param socket_info describes control socket parameters
+    /// @return object representing a socket
+    CommandSocketPtr
+    openCommandSocket(const isc::data::ConstElementPtr& socket_info);
+
+    /// @brief Shuts down any open control sockets
+    void closeCommandSocket();
+
+    /// @brief Registers specified command handler for a given command
+    ///
+    /// @param cmd name of the command to be handled
+    /// @param handler pointer to the method that will handle the command
+    void registerCommand(const std::string& cmd, CommandHandler handler);
+
+    /// @brief Deregisters specified command handler
+    ///
+    /// @param cmd name of the command that's no longer handled
+    void deregisterCommand(const std::string& cmd);
+
+    /// @brief Triggers command processing
+    ///
+    /// This method processes specified command. The command is specified using
+    /// a single Element. See @ref CommandMgr for description of its syntax.
+    /// Typically, this method is called internally, when there's a new data
+    /// received over control socket. However, in some cases (e.g. signal received)
+    /// it may be called by external code explicitly. Hence this method is public.
+    isc::data::ConstElementPtr processCommand(const isc::data::ConstElementPtr& cmd);
+
+    /// @brief Reads data from a socket, parses as JSON command and processes it
+    ///
+    /// This method is used to handle traffic on connected socket. This callback
+    /// is installed by the @ref isc::config::UnixCommandSocket::receiveHandler
+    /// once the incoming connection is accepted. If end-of-file is detected, this
+    /// method will close the socket and will uninstall itself from
+    /// @ref isc::dhcp::IfaceMgr.
+    ///
+    /// @param sockfd socket descriptor of a connected socket
+    static void commandReader(int sockfd);
+
+    /// @brief Auxiliary method that removes all installed commands.
+    ///
+    /// The only unwipeable method is list-commands, which is internally
+    /// handled at all times.
+    void deregisterAll();
+
+    /// @brief Adds an information about opened connection socket
+    ///
+    /// @param conn Connection socket to be stored
+    void addConnection(const CommandSocketPtr& conn);
+
+    /// @brief Closes connection with a specific socket descriptor
+    ///
+    /// @param fd socket descriptor
+    /// @return true if closed successfully, false if not found
+    bool closeConnection(int fd);
+
+    /// @brief Returns control socket descriptor
+    ///
+    /// This method should be used only in tests.
+    int getControlSocketFD() const {
+        return (socket_->getFD());
+    }
+
+private:
+
+    /// @brief Private constructor
+    ///
+    /// Registers internal 'list-commands' command.
+    CommandMgr();
+
+    /// @brief 'list-commands' command handler
+    ///
+    /// This method implements command 'list-commands'. It returns a list of all
+    /// currently supported commands.
+    /// @param name name of the command (should always be 'list-commands')
+    /// @param params additional parameters (ignored)
+    /// @return structure that includes all currently supported commands
+    isc::data::ConstElementPtr
+    listCommandsHandler(const std::string& name,
+                        const isc::data::ConstElementPtr& params);
+
+    typedef std::map<std::string, CommandHandler> HandlerContainer;
+
+    /// @brief Container for command handlers
+    HandlerContainer handlers_;
+
+    /// @brief Control socket structure
+    ///
+    /// This is the socket that accepts incoming connections. There can be at
+    /// most one (if command channel is configured).
+    CommandSocketPtr socket_;
+
+    /// @brief Sockets for open connections
+    ///
+    /// These are the sockets that are dedicated to handle a specific connection.
+    /// Their number is equal to number of current control connections.
+    std::list<CommandSocketPtr> connections_;
+};
+
+}; // end of isc::config namespace
+}; // end of isc namespace
+
+#endif

+ 51 - 0
src/lib/config/command_socket.cc

@@ -0,0 +1,51 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/command_socket.h>
+#include <config/command_mgr.h>
+#include <config/config_log.h>
+#include <dhcp/iface_mgr.h>
+#include <boost/bind.hpp>
+#include <unistd.h>
+
+namespace isc {
+namespace config {
+
+ConnectionSocket::ConnectionSocket(int sockfd) {
+    sockfd_ = sockfd;
+
+    // Install commandReader callback. When there's any data incoming on this
+    // socket, commandReader will be called and process it. It may also
+    // eventually close this socket.
+    isc::dhcp::IfaceMgr::instance().addExternalSocket(sockfd,
+        boost::bind(&ConnectionSocket::receiveHandler, this));
+    }
+
+void ConnectionSocket::close() {
+    LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_CLOSED).arg(sockfd_);
+
+    // Unregister this callback
+    isc::dhcp::IfaceMgr::instance().deleteExternalSocket(sockfd_);
+
+    // We're closing a connection, not the whole socket. It's ok to just
+    // close the connection and don't delete anything.
+    ::close(sockfd_);
+}
+
+void ConnectionSocket::receiveHandler() {
+    CommandMgr::instance().commandReader(sockfd_);
+}
+
+};
+};

+ 113 - 0
src/lib/config/command_socket.h

@@ -0,0 +1,113 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef COMMAND_SOCKET_H
+#define COMMAND_SOCKET_H
+
+#include <cc/data.h>
+#include <unistd.h>
+
+namespace isc {
+namespace config {
+
+/// @brief An exception indicating that specified socket parameters are invalid
+class BadSocketInfo : public Exception {
+public:
+    BadSocketInfo(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief An exception indicating a problem with socket operation
+class SocketError : public Exception {
+public:
+    SocketError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Abstract base class that represents an open command socket
+///
+/// Derived classes are expected to handle specific socket types (e.g. UNIX
+/// or https).
+///
+/// For derived classes, see @ref UnixCommandSocket for a socket that
+/// accepts connections over UNIX socket and @ref ConnectionSocket that
+/// handles established connections (currently over UNIX sockets, but
+/// should be generic).
+class CommandSocket {
+public:
+    /// @brief Method used to handle incoming data
+    ///
+    /// This may be registered in @ref isc::dhcp::IfaceMgr
+    virtual void receiveHandler() = 0;
+
+    /// @brief General method for closing socket.
+    ///
+    /// This is the default implementation that simply closes
+    /// the socket. Derived classes may do additional steps
+    /// to terminate the connection.
+    virtual void close() {
+        ::close(sockfd_);
+    }
+
+    /// @brief Virtual destructor.
+    virtual ~CommandSocket() {
+        close();
+    }
+
+    /// @brief Returns socket descriptor.
+    int getFD() const {
+        return (sockfd_);
+    }
+
+protected:
+    /// Stores socket descriptor.
+    int sockfd_;
+};
+
+/// Pointer to a command socket object
+typedef boost::shared_ptr<CommandSocket> CommandSocketPtr;
+
+/// @brief This class represents a streaming socket for handling connections
+///
+/// Initially a socket (e.g. UNIX) is opened (represented by other classes, e.g.
+/// @ref UnixCommandSocket). Once incoming connection is detected, that class
+/// calls accept(), which returns a new socket dedicated to handling that
+/// specific connection. That socket is represented by this class.
+class ConnectionSocket : public CommandSocket {
+public:
+    /// @brief Default constructor
+    ///
+    /// This constructor is used in methods that call accept on existing
+    /// sockets. accept() returns a socket descriptor. Hence only one
+    /// parameter here.
+    ///
+    /// @param sockfd socket descriptor
+    ConnectionSocket(int sockfd);
+
+    /// @brief Method used to handle incoming data
+    ///
+    /// This method calls isc::config::CommandMgr::commandReader method.
+    virtual void receiveHandler();
+
+    /// @brief Closes socket.
+    ///
+    /// This method closes the socket, prints appropriate log message and
+    /// unregisters callback from @ref isc::dhcp::IfaceMgr.
+    virtual void close();
+};
+
+};
+};
+
+#endif

+ 211 - 0
src/lib/config/command_socket_factory.cc

@@ -0,0 +1,211 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/command_socket_factory.h>
+#include <config/config_log.h>
+#include <config/command_mgr.h>
+#include <dhcp/iface_mgr.h>
+#include <boost/bind.hpp>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <string.h>
+#include <errno.h>
+#include <cstdio>
+#include <fcntl.h>
+
+using namespace isc::data;
+
+namespace isc {
+namespace config {
+
+/// @brief Wrapper for UNIX stream sockets
+///
+/// There are two UNIX socket types: datagram-based (equivalent of UDP) and
+/// stream-based (equivalent of TCP). This class represents stream-based
+/// sockets. It opens up a unix-socket and waits for incoming connections.
+/// Once incoming connection is detected, accept() system call is called
+/// and a new socket for that particular connection is returned. A new
+/// object of @ref ConnectionSocket is created.
+class UnixCommandSocket : public CommandSocket {
+public:
+    /// @brief Default constructor
+    ///
+    /// Opens specified UNIX socket.
+    ///
+    /// @param filename socket filename
+    UnixCommandSocket(const std::string& filename)
+        : filename_(filename) {
+
+        // Create the socket and set it up.
+        sockfd_ = createUnixSocket(filename_);
+
+        // Install this socket in Interface Manager.
+        isc::dhcp::IfaceMgr::instance().addExternalSocket(sockfd_,
+            boost::bind(&UnixCommandSocket::receiveHandler, this));
+    }
+
+private:
+
+    /// @brief Auxiliary method for creating a UNIX socket
+    ///
+    /// @param file_name specifies socket file path
+    /// @return socket file descriptor
+    int createUnixSocket(const std::string& file_name) {
+
+        int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+        if (fd == -1) {
+            isc_throw(isc::config::SocketError, "Failed to create AF_UNIX socket:"
+                      << strerror(errno));
+        }
+
+        // Let's remove the old file. We don't care about any possible
+        // errors here. The file should not be there if the file was
+        // shut down properly.
+        remove(file_name.c_str());
+
+        // Set this socket to be non-blocking one.
+        if (fcntl(fd, F_SETFL, O_NONBLOCK) !=0 ) {
+            const char* errmsg = strerror(errno);
+            ::close(fd);
+            isc_throw(SocketError, "Failed to set non-block mode on unix socket "
+                      << fd << ": " << errmsg);
+        }
+
+        // Now bind the socket to the specified path.
+        struct sockaddr_un addr;
+        memset(&addr, 0, sizeof(addr));
+        addr.sun_family = AF_UNIX;
+        strncpy(addr.sun_path, file_name.c_str(), sizeof(addr.sun_path)-1);
+        if (bind(fd, (struct sockaddr*)&addr, sizeof(addr))) {
+            const char* errmsg = strerror(errno);
+            ::close(fd);
+            remove(file_name.c_str());
+            isc_throw(isc::config::SocketError, "Failed to bind socket " << fd
+                      << " to " << file_name << ": " << errmsg);
+        }
+
+        // One means that we allow at most 1 awaiting connections.
+        // Any additional attempts will get ECONNREFUSED error.
+        // That means that at any given time, there may be at most one controlling
+        // connection.
+        /// @todo: Make the number of parallel connections configurable.
+        int status = listen(fd, 1);
+        if (status < 0) {
+            const char* errmsg = strerror(errno);
+            ::close(fd);
+            remove(file_name.c_str());
+            isc_throw(isc::config::SocketError, "Failed to listen on socket fd="
+                      << fd << ", filename=" << file_name << ": " << errmsg);
+        }
+
+        // Woohoo! Socket opened, let's log it!
+        LOG_INFO(command_logger, COMMAND_SOCKET_UNIX_OPEN).arg(fd).arg(file_name);
+
+        return (fd);
+    }
+
+    /// @brief Connection acceptor, a callback used to accept incoming connections.
+    ///
+    /// This callback is used on a control socket. Once called, it will accept
+    /// incoming connection, create a new socket for it and create an instance
+    /// of ConnectionSocket, which will take care of the rest (i.e. install
+    /// appropriate callback for that new socket in @ref isc::dhcp::IfaceMgr).
+    void receiveHandler() {
+
+        // This method is specific to receiving data over UNIX socket, so using
+        // sockaddr_un instead of sockaddr_storage here is ok.
+        struct sockaddr_un client_addr;
+        socklen_t client_addr_len;
+        client_addr_len = sizeof(client_addr);
+
+        // Accept incoming connection. This will create a separate socket for
+        // handling this specific connection.
+        int fd2 = accept(sockfd_, reinterpret_cast<struct sockaddr*>(&client_addr),
+                         &client_addr_len);
+        if (fd2 == -1) {
+            LOG_ERROR(command_logger, COMMAND_SOCKET_ACCEPT_FAIL)
+                .arg(sockfd_).arg(strerror(errno));
+            return;
+        }
+
+        // And now create an object that represents that new connection.
+        CommandSocketPtr conn(new ConnectionSocket(fd2));
+
+        // Not sure if this is really needed, but let's set it to non-blocking
+        // mode.
+        if (fcntl(fd2, F_SETFL, O_NONBLOCK) != 0) {
+            // Failed to set socket to non-blocking mode.
+            LOG_ERROR(command_logger, COMMAND_SOCKET_FAIL_NONBLOCK)
+                .arg(fd2).arg(sockfd_).arg(strerror(errno));
+
+            conn.reset();
+            return;
+        }
+
+        // Remember this socket descriptor. It will be needed when we shut down
+        // the server.
+        CommandMgr::instance().addConnection(conn);
+
+        LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_OPENED).arg(fd2)
+            .arg(sockfd_);
+    }
+
+    // This method is called when we shutdown the connection.
+    void close() {
+        LOG_INFO(command_logger, COMMAND_SOCKET_UNIX_CLOSE).arg(sockfd_)
+            .arg(filename_);
+
+        isc::dhcp::IfaceMgr::instance().deleteExternalSocket(sockfd_);
+
+        // Close should always succeed. We don't care if we're able to delete
+        // the socket or not.
+        ::close(sockfd_);
+        remove(filename_.c_str());
+    }
+
+    /// @brief UNIX filename representing this socket
+    std::string filename_;
+};
+
+CommandSocketPtr
+CommandSocketFactory::create(const isc::data::ConstElementPtr& socket_info) {
+    if(!socket_info) {
+        isc_throw(BadSocketInfo, "Missing socket_info parameters, can't create socket.");
+    }
+
+    ConstElementPtr type = socket_info->get("socket-type");
+    if (!type) {
+        isc_throw(BadSocketInfo, "Mandatory 'socket-type' parameter missing");
+    }
+
+    if (type->stringValue() == "unix") {
+        // UNIX socket is requested. It takes one parameter: socket-name that
+        // specifies UNIX path of the socket.
+        ConstElementPtr name = socket_info->get("socket-name");
+        if (!name) {
+            isc_throw(BadSocketInfo, "Mandatory 'socket-name' parameter missing");
+        }
+        if (name->getType() != Element::string) {
+            isc_throw(BadSocketInfo, "'socket-name' parameter expected to be a string");
+        }
+
+        return (CommandSocketPtr(new UnixCommandSocket(name->stringValue())));
+    } else {
+        isc_throw(BadSocketInfo, "Specified socket type ('" + type->stringValue()
+                  + "') is not supported.");
+    }
+}
+
+};
+};

+ 47 - 0
src/lib/config/command_socket_factory.h

@@ -0,0 +1,47 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef COMMAND_SOCKET_FACTORY_H
+#define COMMAND_SOCKET_FACTORY_H
+
+#include <cc/data.h>
+#include <config/command_socket.h>
+
+namespace isc {
+namespace config {
+
+/// A factory class for opening command socket
+///
+/// This class provides an interface for opening command socket.
+class CommandSocketFactory {
+public:
+
+    /// @brief Creates a socket specified by socket_info structure
+    ///
+    ///
+    /// Currently supported types are:
+    /// - unix
+    ///
+    /// See @ref CommandMgr::openCommandSocket for detailed description.
+    /// @throw CommandSocketError
+    ///
+    /// @param socket_info structure that describes the socket
+    /// @return socket descriptor
+    static CommandSocketPtr create(const isc::data::ConstElementPtr& socket_info);
+};
+
+};
+};
+
+#endif

+ 3 - 1
src/lib/config/config_log.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011,2015  Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -21,6 +21,8 @@ namespace config {
 
 isc::log::Logger config_logger("config");
 
+isc::log::Logger command_logger("commands");
+
 } // namespace nsas
 } // namespace isc
 

+ 10 - 6
src/lib/config/config_log.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2011  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011,2015  Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -21,20 +21,24 @@
 namespace isc {
 namespace config {
 
-/// \brief Config Logging
-///
-/// Defines logger object for config log messages
-
-/// \brief Config Logger
+/// @brief Command processing Logger
 ///
 /// Define the logger used to log messages.  We could define it in multiple
 /// modules, but defining in a single module and linking to it saves time and
 /// space.
 extern isc::log::Logger config_logger;
 
+/// @brief Command processing Logger
+///
+/// Define the logger used to log messages related to command processing.
+extern isc::log::Logger command_logger;
+
 // Enumerate configuration elements as they are processed.
 const int DBG_CONFIG_PROCESS = DBGLVL_TRACE_BASIC;
 
+// Enumerate configuration elements as they are processed.
+const int DBG_COMMAND = DBGLVL_COMMAND;
+
 } // namespace config
 } // namespace isc
 

+ 85 - 1
src/lib/config/config_messages.mes

@@ -1,4 +1,4 @@
-# Copyright (C) 2011, 2014  Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2011, 2014-2015  Internet Systems Consortium, Inc. ("ISC")
 #
 # Permission to use, copy, modify, and/or distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -14,6 +14,90 @@
 
 $NAMESPACE isc::config
 
+% COMMAND_PROCESS_ERROR1 Error while processing command: %1
+This warning message indicates that the server encountered an error while
+processing received command. Additional information will be provided, if
+available. Additional log messages may provide more details.
+
+% COMMAND_PROCESS_ERROR2 Error while processing command: %1
+This warning message indicates that the server encountered an error while
+processing received command. The difference, compared to COMMAND_PROCESS_ERROR1
+is that the initial command was well formed and the error occurred during
+logic processing, not the command parsing. Additional information will be
+provided, if available. Additional log messages may provide more details.
+
+% COMMAND_RECEIVED Received command '%1'
+This informational message indicates that a command was received over command
+socket. The nature of this command and its possible results will be logged
+with separate messages.
+
+% COMMAND_RESPONSE_ERROR Server failed to generate response for command: %1
+This error message indicates that the server failed to generate response for
+specified command. This likely indicates a server logic error, as the server
+is expected to generate valid responses for all commands, even malformed
+ones.
+
+% COMMAND_SOCKET_FAIL_NONBLOCK Failed to set non-blocking mode for socket %1 created for incoming connection on socket %2: %3
+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_ACCEPT_FAIL Failed to accept incoming connection on command socket %1: %2
+This error indicates that the server detected incoming connection and executed
+accept system call on said socket, but this call returned an error. Additional
+information may be provided by the system as second parameter.
+
+% 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.
+
+% COMMAND_SOCKET_READ_FAIL Encountered error %1 while reading from command socket %2
+This error message indicates that an error was encountered while
+reading from command socket.
+
+% COMMAND_SOCKET_WRITE Sent response of %1 bytes over command socket %2
+This debug message indicates that the specified number of bytes was sent
+over command socket identifier by the specified file descriptor.
+
+% COMMAND_SOCKET_RESPONSE_TOOLARGE Server's response was larger (%1) than supported 64KB
+This error message indicates that the server received a command and generated
+an answer for it, but that response was larger than supported 64KB. Server
+will attempt to send the first 64KB of the response. Depending on the nature
+of this response, this may indicate a software or configuration error. Future
+Kea versions are expected to have better support for large responses.
+
+% COMMAND_SOCKET_WRITE_FAIL Error while writing %1 bytes to command socket %2
+This error message indicates that an error was encountered while
+attempting to send a response to the command socket.
+
+% COMMAND_SOCKET_UNIX_OPEN Command socket opened: UNIX, fd=%1, path=%2
+This informational message indicates that the daemon opened a command
+processing socket. This is a UNIX socket. It was opened with the file
+descriptor and path specified.
+
+% COMMAND_SOCKET_UNIX_CLOSE Command socket closed: UNIX, fd=%1, path=%2
+This informational message indicates that the daemon closed a command
+processing socket. This was a UNIX socket. It was opened with the file
+descriptor and path specified.
+
+% COMMAND_SOCKET_CONNECTION_OPENED Opened socket %1 for incoming command connection on socket %2
+This is an informational message that a new incoming command connection was
+detected and a dedicated socket was opened for that connection.
+
+% COMMAND_SOCKET_CONNECTION_CLOSED Closed socket %1 for existing command connection
+This is an informational message that the socket created for handling
+client's connection is closed. This usually means that the client disconnected,
+but may also mean a timeout.
+
+% COMMAND_REGISTERED Command %1 registered
+This debug message indicates that the daemon started supporting specified
+command. If the command socket is open, this command can now be issued.
+
+% 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
+open and this command is issued, the daemon will not be able to process it.
+
 % CONFIG_CCSESSION_MSG error in CC session message: %1
 There was a problem with an incoming message on the command and control
 channel. The message does not appear to be a valid command, and is

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

@@ -2,6 +2,7 @@ SUBDIRS = testdata .
 
 AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
 AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_top_srcdir)/src/lib/testutils/testdata\"
 
 AM_CXXFLAGS = $(KEA_CXXFLAGS)
 
@@ -18,7 +19,9 @@ TESTS =
 if HAVE_GTEST
 TESTS += run_unittests
 run_unittests_SOURCES = module_spec_unittests.cc
+run_unittests_SOURCES += command_socket_factory_unittests.cc
 run_unittests_SOURCES += config_data_unittests.cc run_unittests.cc
+run_unittests_SOURCES += command_mgr_unittests.cc
 
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
@@ -28,6 +31,7 @@ run_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
 run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
 run_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
 run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
+run_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
 
 endif
 

+ 223 - 0
src/lib/config/tests/command_mgr_unittests.cc

@@ -0,0 +1,223 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <gtest/gtest.h>
+
+#include <config/command_mgr.h>
+#include <cc/command_interpreter.h>
+
+using namespace isc::data;
+using namespace isc::config;
+using namespace std;
+
+// Test class for Command Manager
+class CommandMgrTest : public ::testing::Test {
+public:
+
+    /// Default constructor
+    CommandMgrTest() {
+        handler_name = "";
+        handler_params = ElementPtr();
+        handler_called = false;
+
+        CommandMgr::instance().deregisterAll();
+        CommandMgr::instance().closeCommandSocket();
+    }
+
+    /// Default destructor
+    ~CommandMgrTest() {
+        CommandMgr::instance().deregisterAll();
+        CommandMgr::instance().closeCommandSocket();
+    }
+
+    /// @brief A simple command handler that always returns an eror
+    static ConstElementPtr my_handler(const std::string& name,
+                                      const ConstElementPtr& params) {
+
+        handler_name = name;
+        handler_params = params;
+        handler_called = true;
+
+        return (createAnswer(123, "test error message"));
+    }
+
+    /// @brief Name of the command (used in my_handler)
+    static std::string handler_name;
+
+    /// @brief Parameters passed to the handler (used in my_handler)
+    static ConstElementPtr handler_params;
+
+    /// @brief Indicates whether my_handler was called
+    static bool handler_called;
+};
+
+/// Name passed to the handler (used in my_handler)
+std::string CommandMgrTest::handler_name("");
+
+/// Parameters passed to the handler (used in my_handler)
+ConstElementPtr CommandMgrTest::handler_params;
+
+/// Indicates whether my_handler was called
+bool CommandMgrTest::handler_called(false);
+
+// Test checks whether the internal command 'list-commands'
+// is working properly.
+TEST_F(CommandMgrTest, listCommandsEmpty) {
+
+    ConstElementPtr command = createCommand("list-commands");
+
+    ConstElementPtr answer;
+
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(command));
+
+    ASSERT_TRUE(answer);
+
+    EXPECT_EQ("{ \"arguments\": [ \"list-commands\" ], \"result\": 0 }",
+              answer->str());
+}
+
+// Test checks whether calling a bogus command is handled properly.
+TEST_F(CommandMgrTest, bogusCommand) {
+
+    ConstElementPtr command = createCommand("no-such-command");
+
+    ConstElementPtr answer;
+
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(command));
+
+    // Make sure the status code is non-zero
+    ASSERT_TRUE(answer);
+    int status_code;
+    parseAnswer(status_code, answer);
+    EXPECT_EQ(CONTROL_RESULT_ERROR, status_code);
+}
+
+// Test checks whether handlers installation is sanitized. In particular,
+// whether NULL handler and attempt to install handlers for the same
+// command twice are rejected.
+TEST_F(CommandMgrTest, handlerInstall) {
+
+    // Check that it's not allowed to install NULL pointer instead of a real
+    // command.
+    EXPECT_THROW(CommandMgr::instance().registerCommand("my-command",
+                 NULL), InvalidCommandHandler);
+
+    // This registration should succeed.
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command",
+                                                           my_handler));
+
+    // Check that it's not possible to install handlers for the same
+    // command twice.
+    EXPECT_THROW(CommandMgr::instance().registerCommand("my-command",
+                 my_handler), InvalidCommandName);
+}
+
+// Test checks whether the internal list-commands command is working
+// correctly. Also, checks installation and deinstallation of other
+// command handlers.
+TEST_F(CommandMgrTest, listCommands) {
+
+    // Let's install two custom commands.
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("make-a-coffee",
+                                                           my_handler));
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("do-the-dishes",
+                                                           my_handler));
+
+    // And then run 'list-commands'
+    ConstElementPtr list_all = createCommand("list-commands");
+    ConstElementPtr answer;
+
+    // Now check that the command is returned by list-commands
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_all));
+    ASSERT_TRUE(answer);
+    EXPECT_EQ("{ \"arguments\": [ \"do-the-dishes\", \"list-commands\", "
+              "\"make-a-coffee\" ], \"result\": 0 }", answer->str());
+
+    // Now unregister one command
+    EXPECT_NO_THROW(CommandMgr::instance().deregisterCommand("do-the-dishes"));
+
+    // Now check that the command is returned by list-commands
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_all));
+    ASSERT_TRUE(answer);
+    EXPECT_EQ("{ \"arguments\": [ \"list-commands\", "
+              "\"make-a-coffee\" ], \"result\": 0 }", answer->str());
+
+    // Now test deregistration. It should work the first time.
+    EXPECT_NO_THROW(CommandMgr::instance().deregisterCommand("make-a-coffee"));
+
+    // Second time should throw an exception as the handler is no longer there.
+    EXPECT_THROW(CommandMgr::instance().deregisterCommand("make-a-coffee"),
+                 InvalidCommandName);
+
+    // You can't unistall list-commands as it's the internal handler.
+    // It always must be there.
+    EXPECT_THROW(CommandMgr::instance().deregisterCommand("list-commands"),
+                 InvalidCommandName);
+
+    // Attempt to register a handler for existing command should fail.
+    EXPECT_THROW(CommandMgr::instance().registerCommand("list-commands",
+                 my_handler), InvalidCommandName);
+}
+
+// Test checks whether deregisterAll method uninstalls all handlers,
+// except list-commands.
+TEST_F(CommandMgrTest, deregisterAll) {
+
+    // Install a couple handlers.
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command1",
+                                                           my_handler));
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command2",
+                                                           my_handler));
+
+    EXPECT_NO_THROW(CommandMgr::instance().deregisterAll());
+
+    ConstElementPtr answer;
+    EXPECT_NO_THROW(answer = CommandMgr::instance()
+                    .processCommand(createCommand("list-commands")));
+    ASSERT_TRUE(answer);
+    EXPECT_EQ("{ \"arguments\": [ \"list-commands\" ], \"result\": 0 }",
+              answer->str());
+}
+
+// Test checks whether a command handler can be installed and then
+// runs through processCommand to check that it's indeed called.
+TEST_F(CommandMgrTest, processCommand) {
+
+    // Install my handler
+    EXPECT_NO_THROW(CommandMgr::instance().registerCommand("my-command",
+                                                           my_handler));
+
+    // Now tell CommandMgr to process a command 'my-command' with the
+    // specified parameter.
+    ElementPtr my_params = Element::fromJSON("[ \"just\", \"some\", \"data\" ]");
+    ConstElementPtr command = createCommand("my-command", my_params);
+    ConstElementPtr answer;
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(command));
+
+    // There should be an answer.
+    ASSERT_TRUE(answer);
+
+    // my_handler remembers all passed parameters and returns status code of 123.
+    ConstElementPtr answer_arg;
+    int status_code;
+    // Check that the returned status code is correct.
+    EXPECT_NO_THROW(answer_arg = parseAnswer(status_code, answer));
+    EXPECT_EQ(123, status_code);
+
+    // Check that the parameters passed are correct.
+    EXPECT_EQ(true, handler_called);
+    EXPECT_EQ("my-command", handler_name);
+    ASSERT_TRUE(handler_params);
+    EXPECT_EQ("[ \"just\", \"some\", \"data\" ]", handler_params->str());
+}

+ 77 - 0
src/lib/config/tests/command_socket_factory_unittests.cc

@@ -0,0 +1,77 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <gtest/gtest.h>
+
+#include <cc/data.h>
+#include <config/command_mgr.h>
+#include <config/command_socket.h>
+#include <config/command_socket_factory.h>
+#include <cstdio>
+
+using namespace isc::config;
+using namespace isc::data;
+
+// Test class for Command Manager
+class CommandSocketFactoryTest : public ::testing::Test {
+public:
+
+    /// Default constructor
+    CommandSocketFactoryTest() {
+        // Remove any stale socket files
+        remove(SOCKET_NAME.c_str());
+    }
+
+    /// Default destructor
+    ~CommandSocketFactoryTest() {
+
+        // Remove any stale socket files
+        remove(SOCKET_NAME.c_str());
+    }
+
+    static const std::string SOCKET_NAME;
+};
+
+const std::string CommandSocketFactoryTest::SOCKET_NAME =
+    std::string(TEST_DATA_DIR) + "/test-socket";
+
+TEST_F(CommandSocketFactoryTest, unixCreate) {
+    // Null pointer is obviously a bad idea.
+    EXPECT_THROW(CommandSocketFactory::create(ConstElementPtr()),
+                 isc::config::BadSocketInfo);
+
+    // So is passing no parameters.
+    ElementPtr socket_info = Element::createMap();
+    EXPECT_THROW(CommandSocketFactory::create(socket_info),
+                 isc::config::BadSocketInfo);
+
+    // We don't support ipx sockets
+    socket_info->set("socket-type", Element::create("ipx"));
+    EXPECT_THROW(CommandSocketFactory::create(socket_info),
+                 isc::config::BadSocketInfo);
+
+    socket_info->set("socket-type", Element::create("unix"));
+    EXPECT_THROW(CommandSocketFactory::create(socket_info),
+                 isc::config::BadSocketInfo);
+
+    socket_info->set("socket-name", Element::create(SOCKET_NAME));
+    CommandSocketPtr sock;
+    EXPECT_NO_THROW(sock = CommandSocketFactory::create(socket_info));
+    ASSERT_TRUE(sock);
+    EXPECT_NE(-1, sock->getFD());
+
+    // It should be possible to close the socket.
+    EXPECT_NO_THROW(sock->close());
+}
+

+ 9 - 0
src/lib/dhcp/iface_mgr.cc

@@ -292,6 +292,10 @@ IfaceMgr::isDirectResponseSupported() const {
 
 void
 IfaceMgr::addExternalSocket(int socketfd, SocketCallback callback) {
+    if (socketfd < 0) {
+        isc_throw(BadValue, "Attempted to install callback for invalid socket "
+                  << socketfd);
+    }
     BOOST_FOREACH(SocketCallbackInfo s, callbacks_) {
         // There's such a socket description there already.
         // Update the callback and we're done
@@ -320,6 +324,11 @@ IfaceMgr::deleteExternalSocket(int socketfd) {
 }
 
 void
+IfaceMgr::deleteAllExternalSockets() {
+    callbacks_.clear();
+}
+
+void
 IfaceMgr::setPacketFilter(const PktFilterPtr& packet_filter) {
     // Do not allow NULL pointer.
     if (!packet_filter) {

+ 3 - 1
src/lib/dhcp/iface_mgr.h

@@ -973,9 +973,11 @@ public:
     void addExternalSocket(int socketfd, SocketCallback callback);
 
     /// @brief Deletes external socket
-
     void deleteExternalSocket(int socketfd);
 
+    /// @brief Deletes all external sockets.
+    void deleteAllExternalSockets();
+
     /// @brief Set packet filter object to handle sending and receiving DHCPv4
     /// messages.
     ///

+ 21 - 0
src/lib/dhcpsrv/parsers/dhcp_parsers.cc

@@ -214,6 +214,27 @@ MACSourcesListConfigParser::commit() {
     // Nothing to do.
 }
 
+// ******************** ControlSocketParser *************************
+ControlSocketParser::ControlSocketParser(const std::string& param_name) {
+    if (param_name != "control-socket") {
+        isc_throw(BadValue, "Internal error. Control socket parser called "
+                  " for wrong parameter:" << param_name);
+    }
+}
+
+void ControlSocketParser::build(isc::data::ConstElementPtr value) {
+    if (value->getType() != Element::map) {
+        isc_throw(BadValue, "Specified control-socket is expected to be a map"
+                  ", i.e. a structure defined within { }");
+    }
+    CfgMgr::instance().getStagingCfg()->setControlSocketInfo(value);
+}
+
+/// @brief Does nothing.
+void ControlSocketParser::commit() {
+    // Nothing to do.
+}
+
 // ******************** HooksLibrariesParser *************************
 
 HooksLibrariesParser::HooksLibrariesParser(const std::string& param_name)

+ 17 - 0
src/lib/dhcpsrv/parsers/dhcp_parsers.h

@@ -420,6 +420,23 @@ private:
     ParserContextPtr global_context_;
 };
 
+/// @brief Parser for the control-socket structure
+///
+/// It does not parse anything, simply stores the element in
+/// the staging config.
+class ControlSocketParser : public DhcpConfigParser {
+public:
+
+    ControlSocketParser(const std::string& param_name);
+
+    /// @brief Stores contents of the control-socket map in the staging config.
+    ///
+    /// @param value pointer to the content of parsed values
+    virtual void build(isc::data::ConstElementPtr value);
+
+    /// @brief Does nothing.
+    virtual void commit();
+};
 
 /// @brief Parser for hooks library list
 ///

+ 16 - 0
src/lib/dhcpsrv/srv_config.h

@@ -24,6 +24,7 @@
 #include <dhcpsrv/cfg_subnets6.h>
 #include <dhcpsrv/cfg_mac_source.h>
 #include <dhcpsrv/logging_info.h>
+#include <cc/data.h>
 #include <boost/shared_ptr.hpp>
 #include <vector>
 #include <stdint.h>
@@ -276,6 +277,18 @@ public:
         return (cfg_mac_source_);
     }
 
+    /// @brief Returns information about control socket
+    /// @return pointer to the Element that holds control-socket map
+    const isc::data::ConstElementPtr getControlSocketInfo() const {
+        return (control_socket_);
+    }
+
+    /// @brief Sets information about the control socket
+    /// @param control_socket Element that holds control-socket map
+    void setControlSocketInfo(const isc::data::ConstElementPtr& control_socket) {
+        control_socket_ = control_socket;
+    }
+
     /// @brief Copies the currnet configuration to a new configuration.
     ///
     /// This method copies the parameters stored in the configuration to
@@ -407,6 +420,9 @@ private:
     /// This object holds a set of RSOO-enabled options. See
     /// RFC 6422 for the definition of the RSOO-enabled option.
     CfgRSOOPtr cfg_rsoo_;
+
+    /// @brief Pointer to the control-socket information
+    isc::data::ConstElementPtr control_socket_;
 };
 
 /// @name Pointers to the @c SrvConfig object.

+ 4 - 0
src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc

@@ -1757,3 +1757,7 @@ TEST_F(ParseConfigTest, validRelayInfo6) {
     // Unparseable text that looks like IPv6 address, but has too many colons
     EXPECT_THROW(parser->build(json_bogus2), DhcpConfigError);
 }
+
+// There's no test for ControlSocketParser, as it is tested in the DHCPv4 code
+// (see CtrlDhcpv4SrvTest.commandSocketBasic in
+// src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc).

+ 92 - 0
src/lib/stats/stats_mgr.cc

@@ -14,8 +14,12 @@
 
 #include <exceptions/exceptions.h>
 #include <stats/stats_mgr.h>
+#include <cc/data.h>
+#include <cc/command_interpreter.h>
 
 using namespace std;
+using namespace isc::data;
+using namespace isc::config;
 
 namespace isc {
 namespace stats {
@@ -142,7 +146,95 @@ size_t StatsMgr::count() const {
     return (global_->stats_.size());
 }
 
+isc::data::ConstElementPtr
+StatsMgr::statisticGetHandler(const std::string& /*name*/,
+                              const isc::data::ConstElementPtr& params) {
+    std::string name, error;
+    if (!getStatName(params, name, error)) {
+        return (createAnswer(CONTROL_RESULT_ERROR, error));
+    }
+    return (createAnswer(CONTROL_RESULT_SUCCESS,
+                         instance().get(name)));
+}
+
+isc::data::ConstElementPtr
+StatsMgr::statisticResetHandler(const std::string& /*name*/,
+                                const isc::data::ConstElementPtr& params) {
+    std::string name, error;
+    if (!getStatName(params, name, error)) {
+        return (createAnswer(CONTROL_RESULT_ERROR, error));
+    }
+
+    if (instance().reset(name)) {
+        return (createAnswer(CONTROL_RESULT_SUCCESS,
+                             "Statistic '" + name + "' reset."));
+    } else {
+        return (createAnswer(CONTROL_RESULT_ERROR,
+                             "No '" + name + "' statistic found"));
+    }
+}
+
+isc::data::ConstElementPtr
+StatsMgr::statisticRemoveHandler(const std::string& /*name*/,
+                                 const isc::data::ConstElementPtr& params) {
+    std::string name, error;
+    if (!getStatName(params, name, error)) {
+        return (createAnswer(CONTROL_RESULT_ERROR, error));
+    }
+    if (instance().del(name)) {
+        return (createAnswer(CONTROL_RESULT_SUCCESS,
+                             "Statistic '" + name + "' removed."));
+    } else {
+        return (createAnswer(CONTROL_RESULT_ERROR,
+                             "No '" + name + "' statistic found"));
+    }
+
+}
 
+isc::data::ConstElementPtr
+StatsMgr::statisticRemoveAllHandler(const std::string& /*name*/,
+                                    const isc::data::ConstElementPtr& /*params*/) {
+    instance().removeAll();
+    return (createAnswer(CONTROL_RESULT_SUCCESS,
+                         "All statistics removed."));
+}
+
+isc::data::ConstElementPtr
+StatsMgr::statisticGetAllHandler(const std::string& /*name*/,
+                                 const isc::data::ConstElementPtr& /*params*/) {
+    ConstElementPtr all_stats = instance().getAll();
+    return (createAnswer(CONTROL_RESULT_SUCCESS, all_stats));
+}
+
+isc::data::ConstElementPtr
+StatsMgr::statisticResetAllHandler(const std::string& /*name*/,
+                                   const isc::data::ConstElementPtr& /*params*/) {
+    instance().resetAll();
+    return (createAnswer(CONTROL_RESULT_SUCCESS,
+                         "All statistics reset to neutral values."));
+}
+
+bool
+StatsMgr::getStatName(const isc::data::ConstElementPtr& params,
+                      std::string& name,
+                      std::string& reason) {
+    if (!params) {
+        reason = "Missing mandatory 'name' parameter.";
+        return (false);
+    }
+    ConstElementPtr stat_name = params->get("name");
+    if (!stat_name) {
+        reason = "Missing mandatory 'name' parameter.";
+        return (false);
+    }
+    if (stat_name->getType() != Element::string) {
+        reason = "'name' parameter expected to be a string.";
+        return (false);
+    }
+
+    name = stat_name->stringValue();
+    return (true);
+}
 
 };
 };

+ 114 - 0
src/lib/stats/stats_mgr.h

@@ -237,6 +237,104 @@ class StatsMgr : public boost::noncopyable {
         return (name.str());
     }
 
+    /// @defgroup command_methods Methods are used to handle commands.
+    ///
+    /// @brief The following methods are used to handle commands:
+    ///
+    /// @{
+
+    /// @brief Handles statistic-get command
+    ///
+    /// This method handles statistic-get command, which returns value
+    /// of a given statistic). It expects one parameter stored in params map:
+    /// name: name-of-the-statistic
+    ///
+    /// Example params structure:
+    /// {
+    ///     "name": "packets-received"
+    /// }
+    ///
+    /// @param name name of the command (ignored, should be "statistic-get")
+    /// @param params structure containing a map that contains "name"
+    /// @param return answer containing details of specified statistic
+    static isc::data::ConstElementPtr
+    statisticGetHandler(const std::string& name,
+                        const isc::data::ConstElementPtr& params);
+
+    /// @param Handles statistic-reset command
+    ///
+    /// This method handles statistic-reset command, which resets value
+    /// of a given statistic. It expects one parameter stored in params map:
+    /// name: name-of-the-statistic
+    ///
+    /// Example params structure:
+    /// {
+    ///     "name": "packets-received"
+    /// }
+    ///
+    /// @param name name of the command (ignored, should be "statistic-reset")
+    /// @param params structure containing a map that contains "name"
+    /// @param return answer containing confirmation
+    static isc::data::ConstElementPtr
+    statisticResetHandler(const std::string& name,
+                          const isc::data::ConstElementPtr& params);
+
+    /// @param Handles statistic-remove command
+    ///
+    /// This method handles statistic-reset command, which removes a given
+    /// statistic completely. It expects one parameter stored in params map:
+    /// name: name-of-the-statistic
+    ///
+    /// Example params structure:
+    /// {
+    ///     "name": "packets-received"
+    /// }
+    ///
+    /// @param name name of the command (ignored, should be "statistic-remove")
+    /// @param params structure containing a map that contains "name" element
+    /// @param return answer containing confirmation
+    static isc::data::ConstElementPtr
+    statisticRemoveHandler(const std::string& name,
+                           const isc::data::ConstElementPtr& params);
+
+    /// @brief Handles statistic-get-all command
+    ///
+    /// This method handles statistic-get-all command, which returns values
+    /// of all statistics. Params parameter is ignored.
+    ///
+    /// @param name name of the command (ignored, should be "statistic-get-all")
+    /// @param params ignored
+    /// @param return answer containing values of all statistic
+    static isc::data::ConstElementPtr
+    statisticGetAllHandler(const std::string& name,
+                           const isc::data::ConstElementPtr& params);
+
+    /// @brief Handles statistic-reset-all command
+    ///
+    /// This method handles statistic-reset-all command, which sets values of
+    /// all statistics back to zero. Params parameter is ignored.
+    ///
+    /// @param name name of the command (ignored, should be "statistic-reset-all")
+    /// @param params ignored
+    /// @param return answer confirming success of this operation
+    static isc::data::ConstElementPtr
+    statisticResetAllHandler(const std::string& name,
+                             const isc::data::ConstElementPtr& params);
+
+    /// @brief Handles statistic-remove-all command
+    ///
+    /// This method handles statistic-remove-all command, which removes all
+    /// statistics. Params parameter is ignored.
+    ///
+    /// @param name name of the command (ignored, should be "statistic-remove-all")
+    /// @param params ignored
+    /// @param return answer confirming success of this operation
+    static isc::data::ConstElementPtr
+    statisticRemoveAllHandler(const std::string& name,
+                              const isc::data::ConstElementPtr& params);
+
+    /// @}
+
  private:
 
     /// @brief Sets a given statistic to specified value (internal version).
@@ -308,6 +406,22 @@ class StatsMgr : public boost::noncopyable {
     /// @return true if deleted, false if not found
     bool deleteObservation(const std::string& name);
 
+    /// @brief Utility method that attempts to extract statistic name
+    ///
+    /// This method attempts to extract statistic name from the params
+    /// structure. It is expected to be a map that contains 'name' element,
+    /// that is of type string. If present as expected, statistic name
+    /// set and true is returned. If missing or is of incorrect type, the reason
+    /// is specified in reason parameter and false is returned.
+    ///
+    /// @param params parameters structure received in command
+    /// @param name [out] name of the statistic (if no error detected)
+    /// @param reason [out] failure reason (if error is detected)
+    /// @return true (if everything is ok), false otherwise
+    static bool getStatName(const isc::data::ConstElementPtr& params,
+                            std::string& name,
+                            std::string& reason);
+
     // This is a global context. All statistics will initially be stored here.
     StatContextPtr global_;
 };

+ 273 - 0
src/lib/stats/tests/stats_mgr_unittest.cc

@@ -17,6 +17,7 @@
 #include <stats/stats_mgr.h>
 #include <exceptions/exceptions.h>
 #include <cc/data.h>
+#include <cc/command_interpreter.h>
 #include <util/boost_time_utils.h>
 #include <boost/date_time/posix_time/posix_time_types.hpp>
 #include <boost/shared_ptr.hpp>
@@ -28,6 +29,7 @@
 using namespace isc;
 using namespace isc::data;
 using namespace isc::stats;
+using namespace isc::config;
 using namespace boost::posix_time;
 
 namespace {
@@ -42,6 +44,7 @@ public:
     /// Makes sure that the Statistics Manager is instantiated.
     StatsMgrTest() {
         StatsMgr::instance();
+        StatsMgr::instance().removeAll();
     }
 
     /// @brief Destructor
@@ -413,4 +416,274 @@ TEST_F(StatsMgrTest, generateName) {
               StatsMgr::generateName("subnet", "foo", "pkt4-received"));
 }
 
+// Test checks if statistic-get handler is able to return specified statistic.
+TEST_F(StatsMgrTest, commandStatisticGet) {
+    StatsMgr::instance().setValue("alpha", static_cast<int64_t>(1234));
+
+    ElementPtr params = Element::createMap();
+    params->set("name", Element::create("alpha"));
+
+    ConstElementPtr rsp = StatsMgr::instance().statisticGetHandler("statistic-get",
+                                                                   params);
+
+    ObservationPtr alpha;
+    EXPECT_NO_THROW(alpha = StatsMgr::instance().getObservation("alpha"));
+    ASSERT_TRUE(alpha);
+
+    std::string exp = "{ \"alpha\": [ [ 1234, \""
+        + isc::util::ptimeToText(alpha->getInteger().second) + "\" ] ] }";
+
+    EXPECT_EQ("{ \"arguments\": " + exp + ", \"result\": 0 }", rsp->str());
+}
+
+// Test checks if statistic-get is able to handle:
+// - a request without parameters
+// - a request with missing statistic name
+// - a request for non-existing statistic.
+TEST_F(StatsMgrTest, commandStatisticGetNegative) {
+
+    // Case 1: a request without parameters
+    ConstElementPtr rsp = StatsMgr::instance().statisticGetHandler("statistic-get",
+                                                                   ElementPtr());
+    int status_code;
+    ASSERT_NO_THROW(parseAnswer(status_code, rsp));
+    EXPECT_EQ(status_code, CONTROL_RESULT_ERROR);
+
+    // Case 2: a request with missing statistic name
+    ElementPtr params = Element::createMap();
+    rsp = StatsMgr::instance().statisticGetHandler("statistic-get", params);
+    ASSERT_NO_THROW(parseAnswer(status_code, rsp));
+    EXPECT_EQ(status_code, CONTROL_RESULT_ERROR);
+
+    // Case 3: a request for non-existing statistic
+    params->set("name", Element::create("alpha"));
+    rsp = StatsMgr::instance().statisticGetHandler("statistic-get", params);
+    EXPECT_EQ("{ \"arguments\": {  }, \"result\": 0 }", rsp->str());
+}
+
+// This test checks whether statistc-get-all command returns all statistics
+// correctly.
+TEST_F(StatsMgrTest, commandGetAll) {
+
+    // Set a couple of statistics
+    StatsMgr::instance().setValue("alpha", static_cast<int64_t>(1234));
+    StatsMgr::instance().setValue("beta", 12.34);
+    StatsMgr::instance().setValue("gamma", time_duration(1,2,3,4));
+    StatsMgr::instance().setValue("delta", "Lorem ipsum");
+
+    // Now get them. They're used to generate expected output
+    ConstElementPtr rep_alpha = StatsMgr::instance().get("alpha");
+    ConstElementPtr rep_beta = StatsMgr::instance().get("beta");
+    ConstElementPtr rep_gamma = StatsMgr::instance().get("gamma");
+    ConstElementPtr rep_delta = StatsMgr::instance().get("delta");
+
+    ASSERT_TRUE(rep_alpha);
+    ASSERT_TRUE(rep_beta);
+    ASSERT_TRUE(rep_gamma);
+    ASSERT_TRUE(rep_delta);
+
+    std::string exp_str_alpha = "[ [ 1234, \""
+        + isc::util::ptimeToText(StatsMgr::instance().getObservation("alpha")
+                                   ->getInteger().second) + "\" ] ]";
+    std::string exp_str_beta = "[ [ 12.34, \""
+        + isc::util::ptimeToText(StatsMgr::instance().getObservation("beta")
+                                   ->getFloat().second) + "\" ] ]";
+    std::string exp_str_gamma = "[ [ \"01:02:03.000004\", \""
+        + isc::util::ptimeToText(StatsMgr::instance().getObservation("gamma")
+                                   ->getDuration().second) + "\" ] ]";
+    std::string exp_str_delta = "[ [ \"Lorem ipsum\", \""
+        + isc::util::ptimeToText(StatsMgr::instance().getObservation("delta")
+                                   ->getString().second) + "\" ] ]";
+
+    // Check that all of them can be reported at once
+    ConstElementPtr rsp = StatsMgr::instance().statisticGetAllHandler(
+        "statistic-get-all", ElementPtr());
+    ASSERT_TRUE(rsp);
+    int status_code;
+    ConstElementPtr rep_all = parseAnswer(status_code, rsp);
+    ASSERT_EQ(0, status_code);
+    ASSERT_TRUE(rep_all);
+
+    // Verifying this is a bit more involved, as we don't know whether the
+    // order would be preserved or not.
+    EXPECT_EQ(4, rep_all->size());
+    ASSERT_TRUE(rep_all->get("alpha"));
+    ASSERT_TRUE(rep_all->get("beta"));
+    ASSERT_TRUE(rep_all->get("delta"));
+    ASSERT_TRUE(rep_all->get("gamma"));
+    EXPECT_FALSE(rep_all->get("epsilon"));
+
+    EXPECT_EQ(exp_str_alpha, rep_all->get("alpha")->str());
+    EXPECT_EQ(exp_str_beta, rep_all->get("beta")->str());
+    EXPECT_EQ(exp_str_gamma, rep_all->get("gamma")->str());
+    EXPECT_EQ(exp_str_delta, rep_all->get("delta")->str());
+}
+
+// Test checks if statistic-reset handler is able to reset specified statistic.
+TEST_F(StatsMgrTest, commandStatisticReset) {
+    StatsMgr::instance().setValue("alpha", static_cast<int64_t>(1234));
+
+    ElementPtr params = Element::createMap();
+    params->set("name", Element::create("alpha"));
+
+    ConstElementPtr rsp =
+        StatsMgr::instance().statisticResetHandler("statistic-reset", params);
+    int status_code;
+    ASSERT_NO_THROW(parseAnswer(status_code, rsp));
+    EXPECT_EQ(CONTROL_RESULT_SUCCESS, status_code);
+
+    ObservationPtr alpha;
+    EXPECT_NO_THROW(alpha = StatsMgr::instance().getObservation("alpha"));
+    ASSERT_TRUE(alpha);
+
+    // Check that it was indeed reset
+    EXPECT_EQ(0, alpha->getInteger().first);
+}
+
+// Test checks if statistic-reset is able to handle:
+// - a request without parameters
+// - a request with missing statistic name
+// - a request for non-existing statistic.
+TEST_F(StatsMgrTest, commandStatisticResetNegative) {
+
+    // Case 1: a request without parameters
+    ConstElementPtr rsp =
+        StatsMgr::instance().statisticResetHandler("statistic-reset", ElementPtr());
+    int status_code;
+    ASSERT_NO_THROW(parseAnswer(status_code, rsp));
+    EXPECT_EQ(status_code, CONTROL_RESULT_ERROR);
+
+    // Case 2: a request with missing statistic name
+    ElementPtr params = Element::createMap();
+    rsp = StatsMgr::instance().statisticResetHandler("statistic-reset", params);
+    ASSERT_NO_THROW(parseAnswer(status_code, rsp));
+    EXPECT_EQ(status_code, CONTROL_RESULT_ERROR);
+
+    // Case 3: a request for non-existing statistic
+    params->set("name", Element::create("alpha"));
+    rsp = StatsMgr::instance().statisticResetHandler("statistic-reset", params);
+    EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'alpha' statistic found\" }",
+              rsp->str());
+}
+
+// This test checks whether statistic-reset-all command really resets all
+// statistics correctly.
+TEST_F(StatsMgrTest, commandResetAll) {
+
+    // Set a couple of statistics
+    StatsMgr::instance().setValue("alpha", static_cast<int64_t>(1234));
+    StatsMgr::instance().setValue("beta", 12.34);
+    StatsMgr::instance().setValue("gamma", time_duration(1,2,3,4));
+    StatsMgr::instance().setValue("delta", "Lorem ipsum");
+
+    // Now get them. They're used to generate expected output
+    ConstElementPtr rep_alpha = StatsMgr::instance().get("alpha");
+    ConstElementPtr rep_beta = StatsMgr::instance().get("beta");
+    ConstElementPtr rep_gamma = StatsMgr::instance().get("gamma");
+    ConstElementPtr rep_delta = StatsMgr::instance().get("delta");
+
+    ASSERT_TRUE(rep_alpha);
+    ASSERT_TRUE(rep_beta);
+    ASSERT_TRUE(rep_gamma);
+    ASSERT_TRUE(rep_delta);
+
+    std::string exp_str_alpha = "[ [ 1234, \""
+        + isc::util::ptimeToText(StatsMgr::instance().getObservation("alpha")
+                                   ->getInteger().second) + "\" ] ]";
+    std::string exp_str_beta = "[ [ 12.34, \""
+        + isc::util::ptimeToText(StatsMgr::instance().getObservation("beta")
+                                   ->getFloat().second) + "\" ] ]";
+    std::string exp_str_gamma = "[ [ \"01:02:03.000004\", \""
+        + isc::util::ptimeToText(StatsMgr::instance().getObservation("gamma")
+                                   ->getDuration().second) + "\" ] ]";
+    std::string exp_str_delta = "[ [ \"Lorem ipsum\", \""
+        + isc::util::ptimeToText(StatsMgr::instance().getObservation("delta")
+                                   ->getString().second) + "\" ] ]";
+
+    // Check that all of them can be reset at once
+    ConstElementPtr rsp = StatsMgr::instance().statisticResetAllHandler(
+        "statistic-reset-all", ElementPtr());
+    ASSERT_TRUE(rsp);
+    int status_code;
+    ConstElementPtr rep_all = parseAnswer(status_code, rsp);
+    ASSERT_EQ(0, status_code);
+    ASSERT_TRUE(rep_all);
+
+    // Check that they're indeed reset
+    EXPECT_EQ(0, StatsMgr::instance().getObservation("alpha")->getInteger().first);
+    EXPECT_EQ(0.0f,
+              StatsMgr::instance().getObservation("beta")->getFloat().first);
+    EXPECT_EQ(time_duration(0,0,0,0),
+              StatsMgr::instance().getObservation("gamma")->getDuration().first);
+    EXPECT_EQ("",
+              StatsMgr::instance().getObservation("delta")->getString().first);
+}
+
+// Test checks if statistic-remove handler is able to remove a statistic.
+TEST_F(StatsMgrTest, commandStatisticRemove) {
+    StatsMgr::instance().setValue("alpha", static_cast<int64_t>(1234));
+
+    ElementPtr params = Element::createMap();
+    params->set("name", Element::create("alpha"));
+
+    ConstElementPtr rsp =
+        StatsMgr::instance().statisticRemoveHandler("statistic-remove", params);
+    int status_code;
+    ASSERT_NO_THROW(parseAnswer(status_code, rsp));
+    EXPECT_EQ(CONTROL_RESULT_SUCCESS, status_code);
+
+    // It should be gone.
+    EXPECT_FALSE(StatsMgr::instance().getObservation("alpha"));
+}
+
+// Test checks if statistic-remove is able to handle:
+// - a request without parameters
+// - a request with missing statistic name
+// - a request for non-existing statistic.
+TEST_F(StatsMgrTest, commandStatisticRemoveNegative) {
+
+    // Case 1: a request without parameters
+    ConstElementPtr rsp =
+        StatsMgr::instance().statisticRemoveHandler("statistic-remove", ElementPtr());
+    int status_code;
+    ASSERT_NO_THROW(parseAnswer(status_code, rsp));
+    EXPECT_EQ(status_code, CONTROL_RESULT_ERROR);
+
+    // Case 2: a request with missing statistic name
+    ElementPtr params = Element::createMap();
+    rsp = StatsMgr::instance().statisticRemoveHandler("statistic-remove", params);
+    ASSERT_NO_THROW(parseAnswer(status_code, rsp));
+    EXPECT_EQ(status_code, CONTROL_RESULT_ERROR);
+
+    // Case 3: a request for non-existing statistic
+    params->set("name", Element::create("alpha"));
+    rsp = StatsMgr::instance().statisticRemoveHandler("statistic-remove", params);
+    EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'alpha' statistic found\" }",
+              rsp->str());
+}
+
+// This test checks whether statistic-remove-all command really resets all
+// statistics correctly.
+TEST_F(StatsMgrTest, commandRemoveAll) {
+
+    // Set a couple of statistics
+    StatsMgr::instance().setValue("alpha", static_cast<int64_t>(1234));
+    StatsMgr::instance().setValue("beta", 12.34);
+    StatsMgr::instance().setValue("gamma", time_duration(1,2,3,4));
+    StatsMgr::instance().setValue("delta", "Lorem ipsum");
+
+    // Check that all of them can be reset at once
+    ConstElementPtr rsp = StatsMgr::instance().statisticRemoveAllHandler(
+        "statistic-remove-all", ElementPtr());
+    ASSERT_TRUE(rsp);
+    int status_code;
+    ConstElementPtr rep_all = parseAnswer(status_code, rsp);
+    ASSERT_EQ(0, status_code);
+
+    EXPECT_FALSE(StatsMgr::instance().getObservation("alpha"));
+    EXPECT_FALSE(StatsMgr::instance().getObservation("beta"));
+    EXPECT_FALSE(StatsMgr::instance().getObservation("gamma"));
+    EXPECT_FALSE(StatsMgr::instance().getObservation("delta"));
+}
+
 };