From: Francis Dupont Date: Wed, 27 Jun 2018 21:26:47 +0000 (+0200) Subject: [3543] Checkpoint: almost done, some unit tests to port X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=64c9352d54d163000196bf73a9726f56a51d2491;p=thirdparty%2Fkea.git [3543] Checkpoint: almost done, some unit tests to port --- diff --git a/src/bin/d2/d2_cfg_mgr.cc b/src/bin/d2/d2_cfg_mgr.cc index c8cad80684..35a16f0b0f 100644 --- a/src/bin/d2/d2_cfg_mgr.cc +++ b/src/bin/d2/d2_cfg_mgr.cc @@ -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() { diff --git a/src/bin/d2/d2_cfg_mgr.h b/src/bin/d2/d2_cfg_mgr.h index d8b51763d1..888653d80e 100644 --- a/src/bin/d2/d2_cfg_mgr.h +++ b/src/bin/d2/d2_cfg_mgr.h @@ -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. diff --git a/src/bin/d2/d2_controller.cc b/src/bin/d2/d2_controller.cc index 947a2baea2..b04693d4df 100644 --- a/src/bin/d2/d2_controller.cc +++ b/src/bin/d2/d2_controller.cc @@ -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(); diff --git a/src/bin/d2/d2_controller.h b/src/bin/d2/d2_controller.h index 1673533929..3ec84b03fb 100644 --- a/src/bin/d2/d2_controller.h +++ b/src/bin/d2/d2_controller.h @@ -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_; }; diff --git a/src/bin/d2/d2_process.cc b/src/bin/d2/d2_process.cc index 5848f7f85b..dbf8ee4ab3 100644 --- a/src/bin/d2/d2_process.cc +++ b/src/bin/d2/d2_process.cc @@ -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); } } diff --git a/src/bin/d2/tests/Makefile.am b/src/bin/d2/tests/Makefile.am index 44dd2e9493..168f50c7b8 100644 --- a/src/bin/d2/tests/Makefile.am +++ b/src/bin/d2/tests/Makefile.am @@ -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 index 0000000000..42374496d6 --- /dev/null +++ b/src/bin/d2/tests/d2_command_unittest.cc @@ -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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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(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("progName"), + const_cast("-c"), + const_cast(CFG_TEST_FILE), + const_cast("-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(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 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(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(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(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' (:9:14):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 diff --git a/src/lib/config/command_mgr.cc b/src/lib/config/command_mgr.cc index 8b491926cc..8d7de18e18 100644 --- a/src/lib/config/command_mgr.cc +++ b/src/lib/config/command_mgr.cc @@ -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& 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(::remove(impl_->socket_name_.c_str())); } diff --git a/src/lib/config/command_mgr.h b/src/lib/config/command_mgr.h index f4ba6c1c05..ac624887cf 100644 --- a/src/lib/config/command_mgr.h +++ b/src/lib/config/command_mgr.h @@ -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();