]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[3543] Checkpoint: almost done, some unit tests to port
authorFrancis Dupont <fdupont@isc.org>
Wed, 27 Jun 2018 21:26:47 +0000 (23:26 +0200)
committerFrancis Dupont <fdupont@isc.org>
Thu, 27 Dec 2018 20:00:15 +0000 (21:00 +0100)
src/bin/d2/d2_cfg_mgr.cc
src/bin/d2/d2_cfg_mgr.h
src/bin/d2/d2_controller.cc
src/bin/d2/d2_controller.h
src/bin/d2/d2_process.cc
src/bin/d2/tests/Makefile.am
src/bin/d2/tests/d2_command_unittest.cc [new file with mode: 0644]
src/lib/config/command_mgr.cc
src/lib/config/command_mgr.h

index c8cad8068404f6b58e41abe7b79c2331d7a7547c..35a16f0b0faab353e2f68456ef5b8fca83e2f5b7 100644 (file)
@@ -35,7 +35,8 @@ D2CfgContext::D2CfgContext()
       forward_mgr_(new DdnsDomainListMgr("forward-ddns")),
       reverse_mgr_(new DdnsDomainListMgr("reverse-ddns")),
       keys_(new TSIGKeyInfoMap()),
-      control_socket_(ConstElementPtr()) {
+      control_socket_(ConstElementPtr()),
+      old_control_socket_(ConstElementPtr()) {
 }
 
 D2CfgContext::D2CfgContext(const D2CfgContext& rhs) : ConfigBase(rhs) {
@@ -53,6 +54,7 @@ D2CfgContext::D2CfgContext(const D2CfgContext& rhs) : ConfigBase(rhs) {
     keys_ = rhs.keys_;
 
     control_socket_ = rhs.control_socket_;
+    old_control_socket_ = rhs.old_control_socket_;
 }
 
 D2CfgContext::~D2CfgContext() {
index d8b51763d1be0b1b624f9c642cbf106f1656979a..888653d80e622d507f2feb37fb374c25d7f8eeef 100644 (file)
@@ -91,15 +91,27 @@ public:
     }
 
     /// @brief Returns information about control socket
+    ///
+    /// @param old When true returns the "old" value
     /// @return pointer to the Element that holds control-socket map
-    const isc::data::ConstElementPtr getControlSocketInfo() const {
-        return (control_socket_);
+    const isc::data::ConstElementPtr getControlSocketInfo(bool old = false) const {
+        if (!old) {
+            return (control_socket_);
+        } else {
+            return (old_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;
+    /// @param old When true sets the "old" value.
+    void setControlSocketInfo(const isc::data::ConstElementPtr& control_socket,
+                              bool old = false) {
+        if (!old) {
+            control_socket_ = control_socket;
+        } else {
+            old_control_socket_ = control_socket;
+        }
     }
 
     /// @brief Unparse a configuration object
@@ -129,6 +141,9 @@ private:
 
     /// @brief Pointer to the control-socket information
     isc::data::ConstElementPtr control_socket_;
+
+    /// @brief Pointer to the old control-socket information
+    isc::data::ConstElementPtr old_control_socket_;
 };
 
 /// @brief Defines a pointer for DdnsDomain instances.
index 947a2baea2f862e944a94e9e7ebe4f96ac68df3c..b04693d4df0580bed2070fa0a779564b73c27e66 100644 (file)
@@ -64,10 +64,7 @@ D2Controller::parseFile(const std::string& file_name) {
 }
 
 D2Controller::~D2Controller() {
-    if (has_command_channel_) {
-        has_command_channel_ = false;
-        deregisterCommands();
-    }
+    deregisterCommands();
 }
 
 std::string
@@ -81,6 +78,10 @@ D2Controller::getVersionAddendum() {
 
 void
 D2Controller::registerCommands() {
+    // Call once.
+    if (has_command_channel_) {
+        return;
+    }
     has_command_channel_ = true;
 
     // CommandMgr uses IO service to run asynchronous socket operations.
@@ -109,6 +110,12 @@ D2Controller::registerCommands() {
 
 void
 D2Controller::deregisterCommands() {
+    // Call once.
+    if (!has_command_channel_) {
+        return;
+    }
+    has_command_channel_ = false;
+
     // Close the command socket (if it exists).
     CommandMgr::instance().closeCommandSocket();
 
index 1673533929552edc7667fb090f6a0e84b51e82e4..3ec84b03fbcb187b4b405c638ddf9b9432e111fe 100644 (file)
@@ -53,10 +53,6 @@ public:
     void deregisterCommands();
 
 protected:
-    /// @brief Returns version info specific to D2
-    virtual std::string getVersionAddendum();
-
-private:
     /// @brief Creates an instance of the DHCP-DDNS specific application
     /// process.  This method is invoked during the process initialization
     /// step of the controller launch.
@@ -67,6 +63,14 @@ private:
     /// pointer.
     virtual process::DProcessBase* createProcess();
 
+    /// @brief Returns version info specific to D2
+    virtual std::string getVersionAddendum();
+
+    /// @brief Constructor is declared protected to maintain the integrity of
+    /// the singleton instance.
+    D2Controller();
+
+private:
     ///@brief Parse a given file into Elements
     ///
     /// Uses bison parsing to parse a JSON configuration file into an
@@ -78,10 +82,6 @@ private:
     /// @throw BadValue if the file is empty
     virtual isc::data::ConstElementPtr parseFile(const std::string& file_name);
 
-    /// @brief Constructor is declared private to maintain the integrity of
-    /// the singleton instance.
-    D2Controller();
-
     /// @brief Flag set to true when command channel is enabled.
     bool has_command_channel_;
 };
index 5848f7f85b45180671d836940beddcf45b0aa9bb..dbf8ee4ab35fb88a3de57f7e90d3cc38f2705396 100644 (file)
@@ -218,7 +218,12 @@ D2Process::configure(isc::data::ConstElementPtr config_set, bool check_only) {
     }
 
     // Set the command channel.
-    configureCommandChannel();
+    try {
+        configureCommandChannel();
+    } catch (const isc::Exception& ex) {
+        answer = isc::config::createAnswer(2, ex.what());
+        return (answer);
+    }
 
     // Set the reconf_queue_flag to indicate that we need to reconfigure
     // the queue manager.  Reconfiguring the queue manager may be asynchronous
@@ -254,12 +259,31 @@ D2Process::configureCommandChannel() {
     if (!ctrl) {
         return;
     }
+
+    // Get the previous config.
+    isc::data::ConstElementPtr old_sock_cfg = ctx->getControlSocketInfo(true);
+
+    // Get the new config.
     isc::data::ConstElementPtr sock_cfg = ctx->getControlSocketInfo();
-    if (sock_cfg && (sock_cfg->size() > 0)) {
+
+    // Determine if the socket configuration has changed. It has if
+    // both old and new configuration is specified but respective
+    // data elements aren't equal.
+    bool sock_changed = (old_sock_cfg && (old_sock_cfg->size() > 0) &&
+                         sock_cfg && (sock_cfg->size() > 0) &&
+                         !sock_cfg->equals(*old_sock_cfg));
+    if (old_sock_cfg && (old_sock_cfg->size() > 0) &&
+        (!sock_cfg || (sock_cfg->size() == 0) || sock_changed)) {
+        // Close the existing socket.
+        isc::config::CommandMgr::instance().closeCommandSocket();
+        ctx->setControlSocketInfo(isc::data::ConstElementPtr(), true);
+    }
+    if (sock_cfg && (sock_cfg->size() > 0) &&
+        (!old_sock_cfg || (old_sock_cfg->size() == 0) || sock_changed)) {
         // Assume that CommandMgr works with D2 I/O.
-        isc::config::CommandMgr::instance().setIOService(getIoService());
-        isc::config::CommandMgr::instance().openCommandSocket(sock_cfg);
         ctrl->registerCommands();
+        isc::config::CommandMgr::instance().openCommandSocket(sock_cfg, true);
+        ctx->setControlSocketInfo(sock_cfg, true);
     }
 }
 
index 44dd2e9493c2880525ddb5f22f624d38cf0e458f..168f50c7b8ef65a5f8837fcef632a82ee00a9525 100644 (file)
@@ -58,6 +58,7 @@ d2_unittests_SOURCES += d2_controller_unittests.cc
 d2_unittests_SOURCES += d2_simple_parser_unittest.cc
 d2_unittests_SOURCES += parser_unittest.cc parser_unittest.h
 d2_unittests_SOURCES += get_config_unittest.cc
+d2_unittests_SOURCES += d2_command_unittest.cc
 
 d2_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 d2_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS)
diff --git a/src/bin/d2/tests/d2_command_unittest.cc b/src/bin/d2/tests/d2_command_unittest.cc
new file mode 100644 (file)
index 0000000..4237449
--- /dev/null
@@ -0,0 +1,722 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <cc/command_interpreter.h>
+#include <config/command_mgr.h>
+#include <config/timeouts.h>
+#include <testutils/io_utils.h>
+#include <testutils/unix_control_client.h>
+#include <d2/d2_controller.h>
+#include <d2/d2_process.h>
+#include <d2/parser_context.h>
+#include <gtest/gtest.h>
+#include <boost/pointer_cast.hpp>
+#include <fstream>
+#include <iostream>
+#include <sstream>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::d2;
+using namespace isc::data;
+using namespace isc::dhcp::test;
+using namespace isc::process;
+using namespace boost::asio;
+
+namespace {
+
+class NakedD2Controller;
+typedef boost::shared_ptr<NakedD2Controller> NakedD2ControllerPtr;
+
+class NakedD2Controller : public D2Controller {
+    // "Naked" D2 controller, exposes internal methods.
+public:
+    static DControllerBasePtr& instance() {
+        if (!getController()) {
+            DControllerBasePtr controller_ptr(new NakedD2Controller());
+            setController(controller_ptr);
+        }
+
+        return (getController());
+    }
+
+    virtual ~NakedD2Controller() { }
+
+    using DControllerBase::getIOService;
+    using DControllerBase::initProcess;
+    using DControllerBase::getProcess;
+
+private:
+    NakedD2Controller() { }
+};
+
+/// @brief Fixture class intended for testin control channel in D2.
+class CtrlChannelD2Test : public ::testing::Test {
+public:
+
+    /// @brief Path to the UNIX socket being used to communicate with the server.
+    string socket_path_;
+
+    /// @brief Reference to the base controller object.
+    DControllerBasePtr& server_;
+
+    /// @brief Cast controller object.
+    NakedD2Controller* get() {
+        return (dynamic_cast<NakedD2Controller*>(server_.get()));
+    }
+
+    /// @brief Configuration file.
+    static const char* CFG_TEST_FILE;
+
+    /// @brief Default constructor.
+    ///
+    /// Sets socket path to its default value.
+    CtrlChannelD2Test()
+        : server_(NakedD2Controller::instance()) {
+        const char* env = getenv("KEA_SOCKET_TEST_DIR");
+        if (env) {
+            socket_path_ = string(env) + "/d2.sock";
+        } else {
+            socket_path_ = string(TEST_DATA_BUILDDIR) + "/d2.sock";
+        }
+        ::remove(socket_path_.c_str());
+    }
+
+    /// @brief Destructor.
+    ~CtrlChannelD2Test() {
+        // Include deregister & co.
+        server_.reset();
+
+        // Remove files.
+        ::remove(CFG_TEST_FILE);
+        ::remove(socket_path_.c_str());
+    }
+
+    /// @brief Returns pointer to the server's IO service.
+    ///
+    /// @return Pointer to the server's IO service or null pointer if the
+    /// hasn't been created server.
+    IOServicePtr getIOService() {
+        return (server_ ? get()->getIOService() : IOServicePtr());
+    }
+
+    /// @brief Runs parser in DHCPDDNS mode
+    ///
+    /// @param config input configuration
+    /// @param verbose display errors
+    /// @return element pointer representing the configuration
+    ElementPtr parseDHCPDDNS(const string& config, bool verbose = false) {
+        try {
+            D2ParserContext ctx;
+            return (ctx.parseString(config,
+                                    D2ParserContext::PARSER_SUB_DHCPDDNS));
+        } catch (const std::exception& ex) {
+            if (verbose) {
+                cout << "EXCEPTION: " << ex.what() << endl;
+            }
+            throw;
+        }
+    }
+
+    /// @brief Convenience method for invoking standard, valid launch.
+    ///
+    /// This method sets up a timed run of the D2Controller::launch.
+    /// It does the following:
+    ///  - It creates command line argument variables argc/argv
+    ///  - Creates the config file with the given content.
+    ///  - Schedules a shutdown time timer to call D2ontroller::executeShutdown
+    ///    after the interval
+    ///  - Invokes D2Controller::launch() with the command line arguments
+    ///
+    /// @param config configuration file content to write before calling launch
+
+    /// @param run_time_ms  maximum amount of time to allow runProcess()
+    ///        to continue.
+    void runWithConfig(const string& config, int run_time_ms) {
+        /// write config file.
+        ofstream out(CFG_TEST_FILE, ios::trunc);
+        ASSERT_TRUE(out.is_open());
+        out << "{ \"DhcpDdns\":\n" << config << "\n}\n";
+        out.close();
+
+        // Shutdown (without error) after runtime.
+        IntervalTimer timer(*getIOService());
+        auto genShutdownCallback = [this]() {
+            ElementPtr arg_set;
+            server_->shutdownHandler(SHUT_DOWN_COMMAND, arg_set);
+        };
+        timer.setup(genShutdownCallback, run_time_ms);
+        
+        char* argv[] = { const_cast<char*>("progName"),
+                         const_cast<char*>("-c"),
+                         const_cast<char*>(CFG_TEST_FILE),
+                         const_cast<char*>("-d") };
+        server_->launch(4, argv, false);
+    }
+
+    /// @brief Create a server with a command channel.
+    void createUnixChannelServer() {
+        ::remove(socket_path_.c_str());
+
+        // Just a simple config. The important part here is the socket
+        // location information.
+        string header =
+            "{"
+            "    \"ip-address\": \"192.168.77.1\","
+            "    \"port\": 777,"
+            "    \"control-socket\": {"
+            "        \"socket-type\": \"unix\","
+            "        \"socket-name\": \"";
+
+        string footer =
+            "\""
+            "    },"
+            "    \"tsig-keys\": [],"
+            "    \"forward-ddns\" : {},"
+            "    \"reverse-ddns\" : {}"
+            "}";
+
+        // Fill in the socket-name value with socket_path_ to make
+        // the actual configuration text.
+        string config_txt = header + socket_path_  + footer;
+
+        ASSERT_TRUE(server_);
+
+        ConstElementPtr config;
+        ASSERT_NO_THROW(config = parseDHCPDDNS(config_txt, true));
+        ASSERT_NO_THROW(get()->initProcess());
+        D2ProcessPtr proc = boost::dynamic_pointer_cast<D2Process>(get()->getProcess());
+        ASSERT_TRUE(proc);
+        ConstElementPtr answer = proc->configure(config, false);
+        ASSERT_TRUE(answer);
+
+        int status = 0;
+        ConstElementPtr txt = parseAnswer(status, answer);
+        // This should succeed. If not, print the error message.
+        ASSERT_EQ(0, status) << txt->str();
+
+        // Now check that the socket was indeed open.
+        ASSERT_GT(CommandMgr::instance().getControlSocketFD(), -1);
+    }
+
+    /// @brief Conducts a command/response exchange via UnixCommandSocket.
+    ///
+    /// This method connects to the given server over the given socket path.
+    /// If successful, it then sends the given command and retrieves the
+    /// server's response.  Note that it polls the server's I/O service
+    /// where needed to cause the server to process IO events on
+    /// the control channel sockets
+    ///
+    /// @param command the command text to execute in JSON form
+    /// @param response variable into which the received response should be
+    ///        placed.
+    void sendUnixCommand(const string& command, string& response) {
+        response = "";
+        boost::scoped_ptr<UnixControlClient> client;
+        client.reset(new UnixControlClient());
+        ASSERT_TRUE(client);
+
+        // Connect to the server. This is expected to trigger server's acceptor
+        // handler when IOService::poll() is run.
+        ASSERT_TRUE(client->connectToServer(socket_path_));
+        ASSERT_NO_THROW(getIOService()->poll());
+
+        // Send the command. This will trigger server's handler which receives
+        // data over the unix domain socket. The server will start sending
+        // response to the client.
+        ASSERT_TRUE(client->sendCommand(command));
+        ASSERT_NO_THROW(getIOService()->poll());
+
+        // Read the response generated by the server. Note that getResponse
+        // only fails if there an IO error or no response data was present.
+        // It is not based on the response content.
+        ASSERT_TRUE(client->getResponse(response));
+
+        // Now disconnect and process the close event.
+        client->disconnectFromServer();
+
+        ASSERT_NO_THROW(getIOService()->poll());
+    }
+
+    /// @brief Checks response for list-commands.
+    ///
+    /// This method checks if the list-commands response is generally sane
+    /// and whether specified command is mentioned in the response.
+    ///
+    /// @param rsp response sent back by the server.
+    /// @param command command expected to be on the list.
+    void checkListCommands(const ConstElementPtr& rsp, const string command) {
+        ConstElementPtr params;
+        int status_code = -1;
+        EXPECT_NO_THROW(params = parseAnswer(status_code, rsp));
+        EXPECT_EQ(CONTROL_RESULT_SUCCESS, status_code);
+        ASSERT_TRUE(params);
+        ASSERT_EQ(Element::list, params->getType());
+
+        int cnt = 0;
+        for (size_t i = 0; i < params->size(); ++i) {
+            string tmp = params->get(i)->stringValue();
+            if (tmp == command) {
+                // Command found, but that's not enough.
+                // Need to continue working through the list to see
+                // if there are no duplicates.
+                cnt++;
+            }
+        }
+
+        // Exactly one command on the list is expected.
+        EXPECT_EQ(1, cnt) << "Command " << command << " not found";
+    }
+
+    /// @brief Check if the answer for config-write command is correct.
+    ///
+    /// @param response_txt response in text form.
+    ///        (as read from the control socket)
+    /// @param exp_status expected status.
+    ///        (0 success, 1 failure)
+    /// @param exp_txt for success cases this defines the expected filename,
+    ///        for failure cases this defines the expected error message.
+    void checkConfigWrite(const string& response_txt, int exp_status,
+                          const string& exp_txt = "") {
+
+        ConstElementPtr rsp;
+        EXPECT_NO_THROW(rsp = Element::fromJSON(response_txt));
+        ASSERT_TRUE(rsp);
+
+        int status;
+        ConstElementPtr params = parseAnswer(status, rsp);
+        EXPECT_EQ(exp_status, status);
+
+        if (exp_status == CONTROL_RESULT_SUCCESS) {
+            // Let's check couple things...
+
+            // The parameters must include filename.
+            ASSERT_TRUE(params);
+            ASSERT_TRUE(params->get("filename"));
+            ASSERT_EQ(Element::string, params->get("filename")->getType());
+            EXPECT_EQ(exp_txt, params->get("filename")->stringValue());
+
+            // The parameters must include size. And the size
+            // must indicate some content.
+            ASSERT_TRUE(params->get("size"));
+            ASSERT_EQ(Element::integer, params->get("size")->getType());
+            int64_t size = params->get("size")->intValue();
+            EXPECT_LE(1, size);
+
+            // Now check if the file is really there and suitable for
+            // opening.
+            ifstream f(exp_txt, ios::binary | ios::ate);
+            ASSERT_TRUE(f.good());
+
+            // Now check that it is the correct size as reported.
+            EXPECT_EQ(size, static_cast<int64_t>(f.tellg()));
+
+            // Finally, check that it's really a JSON.
+            ElementPtr from_file = Element::fromJSONFile(exp_txt);
+            ASSERT_TRUE(from_file);
+        } else if (exp_status == CONTROL_RESULT_ERROR) {
+
+            // Let's check if the reason for failure was given.
+            ConstElementPtr text = rsp->get("text");
+            ASSERT_TRUE(text);
+            ASSERT_EQ(Element::string, text->getType());
+            EXPECT_EQ(exp_txt, text->stringValue());
+        } else {
+            ADD_FAILURE() << "Invalid expected status: " << exp_status;
+        }
+    }
+};
+
+const char* CtrlChannelD2Test::CFG_TEST_FILE = "d2-test-config.json";
+
+// Test bad syntax rejected by the parser.
+TEST_F(CtrlChannelD2Test, parser) {
+    // no empty map.
+    string bad1 =
+        "{"
+        "    \"ip-address\": \"192.168.77.1\","
+        "    \"port\": 777,"
+        "    \"control-socket\": { },"
+        "    \"tsig-keys\": [],"
+        "    \"forward-ddns\" : {},"
+        "    \"reverse-ddns\" : {}"
+        "}";
+    ASSERT_THROW(parseDHCPDDNS(bad1), D2ParseError);
+
+    // unknown keyword.
+    string bad2 =
+        "{"
+        "    \"ip-address\": \"192.168.77.1\","
+        "    \"port\": 777,"
+        "    \"control-socket\": {"
+        "        \"socket-type\": \"unix\","
+        "        \"socket-name\": \"/tmp/d2.sock\","
+        "        \"bogus\": \"unknown...\""
+        "    },"
+        "    \"tsig-keys\": [],"
+        "    \"forward-ddns\" : {},"
+        "    \"reverse-ddns\" : {}"
+        "}";
+    ASSERT_THROW(parseDHCPDDNS(bad2), D2ParseError);
+}
+
+// Test bad syntax rejected by the process.
+TEST_F(CtrlChannelD2Test, configure) {
+    ASSERT_TRUE(server_);
+    ASSERT_NO_THROW(get()->initProcess());
+    D2ProcessPtr proc = boost::dynamic_pointer_cast<D2Process>(get()->getProcess());
+    ASSERT_TRUE(proc);
+
+    // no type.
+    string bad1 =
+        "{"
+        "    \"ip-address\": \"192.168.77.1\","
+        "    \"port\": 777,"
+        "    \"control-socket\": {"
+        "        \"socket-name\": \"/tmp/d2.sock\""
+        "    },"
+        "    \"tsig-keys\": [],"
+        "    \"forward-ddns\" : {},"
+        "    \"reverse-ddns\" : {}"
+        "}";
+    ConstElementPtr config;
+    ASSERT_NO_THROW(config = parseDHCPDDNS(bad1, true));
+
+    ConstElementPtr answer = proc->configure(config, false);
+    ASSERT_TRUE(answer);
+
+    int status = 0;
+    ConstElementPtr txt = parseAnswer(status, answer);
+    EXPECT_EQ(2, status);
+    ASSERT_TRUE(txt);
+    ASSERT_EQ(Element::string, txt->getType());
+    EXPECT_EQ("Mandatory 'socket-type' parameter missing", txt->stringValue());
+    EXPECT_EQ(-1, CommandMgr::instance().getControlSocketFD());
+
+    // bad type.
+    string bad2 =
+        "{"
+        "    \"ip-address\": \"192.168.77.1\","
+        "    \"port\": 777,"
+        "    \"control-socket\": {"
+        "        \"socket-type\": \"bogus\","
+        "        \"socket-name\": \"/tmp/d2.sock\""
+        "    },"
+        "    \"tsig-keys\": [],"
+        "    \"forward-ddns\" : {},"
+        "    \"reverse-ddns\" : {}"
+        "}";
+    ASSERT_NO_THROW(config = parseDHCPDDNS(bad2, true));
+
+    answer = proc->configure(config, false);
+    ASSERT_TRUE(answer);
+
+    status = 0;
+    txt = parseAnswer(status, answer);
+    EXPECT_EQ(2, status);
+    ASSERT_TRUE(txt);
+    ASSERT_EQ(Element::string, txt->getType());
+    EXPECT_EQ("Invalid 'socket-type' parameter value bogus",
+              txt->stringValue());
+    EXPECT_EQ(-1, CommandMgr::instance().getControlSocketFD());
+
+    // no name.
+    string bad3 =
+        "{"
+        "    \"ip-address\": \"192.168.77.1\","
+        "    \"port\": 777,"
+        "    \"control-socket\": {"
+        "        \"socket-type\": \"unix\""
+        "    },"
+        "    \"tsig-keys\": [],"
+        "    \"forward-ddns\" : {},"
+        "    \"reverse-ddns\" : {}"
+        "}";
+    ASSERT_NO_THROW(config = parseDHCPDDNS(bad3, true));
+
+    answer = proc->configure(config, false);
+    ASSERT_TRUE(answer);
+
+    status = 0;
+    txt = parseAnswer(status, answer);
+    EXPECT_EQ(2, status);
+    ASSERT_TRUE(txt);
+    ASSERT_EQ(Element::string, txt->getType());
+    EXPECT_EQ("Mandatory 'socket-name' parameter missing",
+              txt->stringValue());
+    EXPECT_EQ(-1, CommandMgr::instance().getControlSocketFD());
+}
+
+// This test checks which commands are registered by the D2 server.
+TEST_F(CtrlChannelD2Test, 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.
+    EXPECT_NO_THROW(createUnixChannelServer());
+
+    EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
+    ASSERT_TRUE(answer);
+    ASSERT_TRUE(answer->get("arguments"));
+    string command_list = answer->get("arguments")->str();
+
+    EXPECT_TRUE(command_list.find("\"list-commands\"") != string::npos);
+    EXPECT_TRUE(command_list.find("\"build-report\"") != string::npos);
+    EXPECT_TRUE(command_list.find("\"config-get\"") != string::npos);
+    EXPECT_TRUE(command_list.find("\"config-write\"") != string::npos);
+    EXPECT_TRUE(command_list.find("\"shutdown\"") != string::npos);
+    EXPECT_TRUE(command_list.find("\"version-get\"") != string::npos);
+
+    // Ok, and now delete the server. It should deregister its commands.
+    server_.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());
+}
+
+// Tests that the server properly responds to invalid commands.
+TEST_F(CtrlChannelD2Test, invalid) {
+    EXPECT_NO_THROW(createUnixChannelServer());
+    string response;
+
+    sendUnixCommand("{ \"command\": \"bogus\" }", response);
+    EXPECT_EQ("{ \"result\": 2, \"text\": \"'bogus' command not supported.\" }",
+              response);
+
+    sendUnixCommand("utter nonsense", response);
+    EXPECT_EQ("{ \"result\": 1, \"text\": \"invalid first character u\" }",
+              response);
+}
+
+// Tests that the server properly responds to shtudown command.
+TEST_F(CtrlChannelD2Test, shutdown) {
+    EXPECT_NO_THROW(createUnixChannelServer());
+    string response;
+
+    sendUnixCommand("{ \"command\": \"shutdown\" }", response);
+    EXPECT_EQ("{ \"result\": 0, \"text\": \"Shutdown initiated, type is: normal\" }",
+              response);
+}
+
+// This test verifies that the DHCP server handles version-get commands.
+TEST_F(CtrlChannelD2Test, getversion) {
+    EXPECT_NO_THROW(createUnixChannelServer());
+    string response;
+
+    // Send the version-get command.
+    sendUnixCommand("{ \"command\": \"version-get\" }", response);
+    EXPECT_TRUE(response.find("\"result\": 0") != string::npos);
+    EXPECT_TRUE(response.find("log4cplus") != string::npos);
+    EXPECT_FALSE(response.find("GTEST_VERSION") != string::npos);
+
+    // Send the build-report command.
+    sendUnixCommand("{ \"command\": \"build-report\" }", response);
+    EXPECT_TRUE(response.find("\"result\": 0") != string::npos);
+    EXPECT_TRUE(response.find("GTEST_VERSION") != string::npos);
+}
+
+// Tests if the server returns its configuration using config-get.
+// Note there are separate tests that verify if toElement() called by the
+// config-get handler are actually converting the configuration correctly.
+TEST_F(CtrlChannelD2Test, configGet) {
+    EXPECT_NO_THROW(createUnixChannelServer());
+    string response;
+
+    sendUnixCommand("{ \"command\": \"config-get\" }", response);
+    ConstElementPtr rsp;
+
+    // The response should be a valid JSON.
+    EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+    ASSERT_TRUE(rsp);
+
+    int status;
+    ConstElementPtr cfg = parseAnswer(status, rsp);
+    EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+    // Ok, now roughly check if the response seems legit.
+    ASSERT_TRUE(cfg);
+    ASSERT_EQ(Element::map, cfg->getType());
+    EXPECT_TRUE(cfg->get("DhcpDdns"));
+}
+
+// Verify that the "config-test" command will do what we expect.
+TEST_F(CtrlChannelD2Test, configTest) {
+
+    // Define strings to permutate the config arguments.
+    // (Note the line feeds makes errors easy to find)
+    string config_test_txt = "{ \"command\": \"config-test\" \n";
+    string args_txt = " \"arguments\": { \n";
+    string d2_header =
+        "    \"DhcpDdns\": \n";
+    string d2_cfg_txt =
+        "    { \n"
+        "        \"ip-address\": \"192.168.77.1\", \n"
+        "        \"port\": 777, \n"
+        "        \"forward-ddns\" : {}, \n"
+        "        \"reverse-ddns\" : {}, \n"
+        "        \"tsig-keys\": [ \n";
+    string key1 =
+        "            {\"name\": \"d2_key.example.com\", \n"
+        "             \"algorithm\": \"hmac-md5\", \n"
+        "             \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\"} \n";
+    string key2 =
+        "           {\"name\": \"d2_key.billcat.net\", \n"
+        "            \"algorithm\": \"hmac-md5\", \n"
+        "            \"digest-bits\": 120, \n"
+        "            \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\"} \n";
+    string bad_key =
+        "            {\"BOGUS\": \"d2_key.example.com\", \n"
+        "             \"algorithm\": \"hmac-md5\", \n"
+        "             \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\"} \n";
+    string key_footer =
+        "          ] \n";
+    string control_socket_header =
+        "       ,\"control-socket\": { \n"
+        "           \"socket-type\": \"unix\", \n"
+        "           \"socket-name\": \"";
+    string control_socket_footer =
+        "\"   \n} \n";
+
+    ostringstream os;
+    // Create a valid config with all the parts should parse.
+    os << d2_cfg_txt
+       << key1
+       << key_footer
+       << control_socket_header
+       << socket_path_
+       << control_socket_footer
+       << "}\n";
+    
+    ASSERT_TRUE(server_);
+
+    ConstElementPtr config;
+    ASSERT_NO_THROW(config = parseDHCPDDNS(os.str(), true));
+    ASSERT_NO_THROW(get()->initProcess());
+    D2ProcessPtr proc = boost::dynamic_pointer_cast<D2Process>(get()->getProcess());
+    ASSERT_TRUE(proc);
+    ConstElementPtr answer = proc->configure(config, false);
+    ASSERT_TRUE(answer);
+    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration committed.\" }",
+              answer->str());
+    
+    // Check that the config was indeed applied.
+    D2CfgMgrPtr cfg_mgr = proc->getD2CfgMgr();
+    ASSERT_TRUE(cfg_mgr);
+    D2CfgContextPtr d2_context = cfg_mgr->getD2CfgContext();
+    ASSERT_TRUE(d2_context);
+    TSIGKeyInfoMapPtr keys = d2_context->getKeys();
+    ASSERT_TRUE(keys);
+    EXPECT_EQ(1, keys->size());
+
+    ASSERT_GT(CommandMgr::instance().getControlSocketFD(), -1);
+
+    // Create a config with malformed subnet that should fail to parse.
+    os.str("");
+    os << config_test_txt << ","
+       << args_txt
+       << d2_header
+       << d2_cfg_txt
+       << bad_key
+       << key_footer
+       << control_socket_header
+       << socket_path_
+       << control_socket_footer
+       << "}\n"                        // close DhcpDdns.
+       << "}}";
+
+    // Send the config-test command.
+    string response;
+    sendUnixCommand(os.str(), response);
+
+    // Should fail with a syntax error.
+    cerr << os.str();
+    EXPECT_EQ("{ \"result\": 1, \"text\": \"element: tsig-keys : missing parameter 'name' (<wire>:9:14)<wire>:8:23\" }",
+              response);
+
+    // Check that the config was not lost.
+    keys = d2_context->getKeys();
+    ASSERT_TRUE(keys);
+    EXPECT_EQ(1, keys->size());
+
+    // Create a valid config with two keys and no command channel.
+    os.str("");
+    os << config_test_txt << ","
+       << args_txt
+       << d2_header
+       << d2_cfg_txt
+       << key1
+       << ",\n"
+       << key2
+       << key_footer
+       << "}\n"                        // close DhcpDdns.
+       << "}}";
+
+    // Verify the control channel socket exists.
+    ASSERT_TRUE(test::fileExists(socket_path_));
+
+    // Send the config-test command.
+    sendUnixCommand(os.str(), response);
+
+    // Verify the control channel socket still exists.
+    EXPECT_TRUE(test::fileExists(socket_path_));
+
+    // Verify the configuration was successful.
+    EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration seems sane.\" }",
+              response);
+
+    // Check that the config was not applied.
+    keys = d2_context->getKeys();
+    ASSERT_TRUE(keys);
+    EXPECT_EQ(1, keys->size());
+}
+// Tests if config-write can be called without any parameters.
+TEST_F(CtrlChannelD2Test, writeConfigNoFilename) {
+    EXPECT_NO_THROW(createUnixChannelServer());
+    string response;
+
+    // This is normally set by the command line -c parameter.
+    server_->setConfigFile("test1.json");
+
+    // If the filename is not explicitly specified, the name used
+    // in -c command line switch is used.
+    sendUnixCommand("{ \"command\": \"config-write\" }", response);
+
+    checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "test1.json");
+    ::remove("test1.json");
+}
+
+// Tests if config-write can be called with a valid filename as parameter.
+TEST_F(CtrlChannelD2Test, writeConfigFilename) {
+    EXPECT_NO_THROW(createUnixChannelServer());
+    string response;
+
+    sendUnixCommand("{ \"command\": \"config-write\", "
+                    "\"arguments\": { \"filename\": \"test2.json\" } }",
+                    response);
+    checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "test2.json");
+    ::remove("test2.json");
+}
+
+// TODO: concurrentConnections, longCommand, longResponse,
+//       connectionTimeoutPartialCommand, connectionTimeoutNoData
+
+} // End of anonymous namespace
index 8b491926cc3d5fd6f1ff5cd60cdc95abfacf2cdb..8d7de18e184594d73bc44bf933dcf623e2618d5f 100644 (file)
@@ -61,21 +61,26 @@ public:
     /// @param connection_pool Reference to the connection pool to which this
     /// connection belongs.
     /// @param timeout Connection timeout (in seconds).
+    /// @param direct Use I/O service (vs. interface manager).
     Connection(const IOServicePtr& io_service,
                const boost::shared_ptr<UnixDomainSocket>& socket,
                ConnectionPool& connection_pool,
-               const long timeout)
+               const long timeout,
+               bool direct = false)
         : socket_(socket), timeout_timer_(*io_service), timeout_(timeout),
           buf_(), response_(), connection_pool_(connection_pool), feed_(),
-          response_in_progress_(false), watch_socket_(new util::WatchSocket()) {
+          response_in_progress_(false), watch_socket_(new util::WatchSocket()),
+          direct_(direct) {
 
         LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_OPENED)
             .arg(socket_->getNative());
 
         // Callback value of 0 is used to indicate that callback function is
         // not installed.
-        isc::dhcp::IfaceMgr::instance().addExternalSocket(watch_socket_->getSelectFd(), 0);
-        isc::dhcp::IfaceMgr::instance().addExternalSocket(socket_->getNative(), 0);
+        if (!direct_) {
+            isc::dhcp::IfaceMgr::instance().addExternalSocket(watch_socket_->getSelectFd(), 0);
+            isc::dhcp::IfaceMgr::instance().addExternalSocket(socket_->getNative(), 0);
+        }
 
         // Initialize state model for receiving and preparsing commands.
         feed_.initModel();
@@ -108,8 +113,10 @@ public:
             LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_CLOSED)
                 .arg(socket_->getNative());
 
-            isc::dhcp::IfaceMgr::instance().deleteExternalSocket(watch_socket_->getSelectFd());
-            isc::dhcp::IfaceMgr::instance().deleteExternalSocket(socket_->getNative());
+            if (!direct_) {
+                isc::dhcp::IfaceMgr::instance().deleteExternalSocket(watch_socket_->getSelectFd());
+                isc::dhcp::IfaceMgr::instance().deleteExternalSocket(socket_->getNative());
+            }
 
             // Close watch socket and log errors if occur.
             std::string watch_error;
@@ -228,6 +235,9 @@ private:
     /// @brief Pointer to watch socket instance used to signal that the socket
     /// is ready for read or write.
     util::WatchSocketPtr watch_socket_;
+
+    /// @brief Direct Use I/O service (vs. interface manager).
+    bool direct_;
 };
 
 /// @brief Pointer to the @c Connection.
@@ -474,15 +484,18 @@ public:
     /// @brief Constructor.
     CommandMgrImpl()
         : io_service_(), acceptor_(), socket_(), socket_name_(),
-          connection_pool_(), timeout_(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND) {
+          connection_pool_(), timeout_(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND),
+          direct_(false) {
     }
 
     /// @brief Opens acceptor service allowing the control clients to connect.
     ///
     /// @param socket_info Configuration information for the control socket.
+    /// @param direct Use I/O service (vs. interface manager).
     /// @throw BadSocketInfo When socket configuration is invalid.
     /// @throw SocketError When socket operation fails.
-    void openCommandSocket(const isc::data::ConstElementPtr& socket_info);
+    void openCommandSocket(const isc::data::ConstElementPtr& socket_info,
+                           bool direct = false);
 
     /// @brief Asynchronously accepts next connection.
     void doAccept();
@@ -507,13 +520,19 @@ public:
 
     /// @brief Connection timeout
     long timeout_;
+
+    /// @brief Direct Use I/O service (vs. interface manager).
+    bool direct_;
 };
 
 void
-CommandMgrImpl::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
+CommandMgrImpl::openCommandSocket(const isc::data::ConstElementPtr& socket_info,
+                                  bool direct) {
+    direct_ = direct;
+
     socket_name_.clear();
 
-    if(!socket_info) {
+    if (!socket_info) {
         isc_throw(BadSocketInfo, "Missing socket_info parameters, can't create socket.");
     }
 
@@ -553,7 +572,9 @@ CommandMgrImpl::openCommandSocket(const isc::data::ConstElementPtr& socket_info)
         acceptor_->listen();
 
         // Install this socket in Interface Manager.
-        isc::dhcp::IfaceMgr::instance().addExternalSocket(acceptor_->getNative(), 0);
+        if (!direct_) {
+            isc::dhcp::IfaceMgr::instance().addExternalSocket(acceptor_->getNative(), 0);
+        }
 
         doAccept();
 
@@ -571,7 +592,7 @@ CommandMgrImpl::doAccept() {
             // New connection is arriving. Start asynchronous transmission.
             ConnectionPtr connection(new Connection(io_service_, socket_,
                                                     connection_pool_,
-                                                    timeout_));
+                                                    timeout_, direct_));
             connection_pool_.start(connection);
 
         } else if (ec.value() != boost::asio::error::operation_aborted) {
@@ -591,14 +612,17 @@ CommandMgr::CommandMgr()
 }
 
 void
-CommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
-    impl_->openCommandSocket(socket_info);
+    CommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info,
+                                  bool direct) {
+    impl_->openCommandSocket(socket_info, direct);
 }
 
 void CommandMgr::closeCommandSocket() {
     // Close acceptor if the acceptor is open.
     if (impl_->acceptor_ && impl_->acceptor_->isOpen()) {
-        isc::dhcp::IfaceMgr::instance().deleteExternalSocket(impl_->acceptor_->getNative());
+        if (!impl_->direct_) {
+            isc::dhcp::IfaceMgr::instance().deleteExternalSocket(impl_->acceptor_->getNative());
+        }
         impl_->acceptor_->close();
         static_cast<void>(::remove(impl_->socket_name_.c_str()));
     }
index f4ba6c1c05fb584844abfdce6d85e44b38619fc9..ac624887cf700f15d0af1457d186dcdf14e78679 100644 (file)
@@ -68,8 +68,9 @@ public:
     /// @throw SocketError When socket operation fails.
     ///
     /// @param socket_info Configuration information for the control socket.
+    /// @param direct Use I/O service (vs. interface manager).
     void
-    openCommandSocket(const isc::data::ConstElementPtr& socket_info);
+    openCommandSocket(const isc::data::ConstElementPtr& socket_info, bool direct = false);
 
     /// @brief Shuts down any open control sockets
     void closeCommandSocket();