diff options
Diffstat (limited to 'src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc')
-rw-r--r-- | src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc | 981 |
1 files changed, 952 insertions, 29 deletions
diff --git a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc index 77bcb09035..8efddd0d32 100644 --- a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -6,15 +6,20 @@ #include <config.h> +#include <asiolink/interval_timer.h> +#include <asiolink/io_service.h> #include <cc/command_interpreter.h> #include <config/command_mgr.h> #include <dhcp/dhcp4.h> #include <dhcp4/ctrl_dhcp4_srv.h> +#include <dhcp4/tests/dhcp4_test_utils.h> #include <dhcpsrv/cfgmgr.h> #include <dhcpsrv/lease.h> #include <dhcpsrv/lease_mgr_factory.h> #include <hooks/hooks_manager.h> +#include <log/logger_support.h> #include <stats/stats_mgr.h> +#include <testutils/io_utils.h> #include <testutils/unix_control_client.h> #include "marker_file.h" @@ -23,9 +28,12 @@ #include <boost/scoped_ptr.hpp> #include <gtest/gtest.h> +#include <cstdlib> #include <fstream> +#include <iomanip> #include <iostream> #include <sstream> +#include <thread> #include <arpa/inet.h> #include <unistd.h> @@ -39,18 +47,50 @@ using namespace isc::dhcp; using namespace isc::dhcp::test; using namespace isc::hooks; using namespace isc::stats; +using namespace isc::test; namespace { +/// @brief Simple RAII class which stops IO service upon destruction +/// of the object. +class IOServiceWork { +public: + + /// @brief Constructor. + /// + /// @param io_service Pointer to the IO service to be stopped. + explicit IOServiceWork(const IOServicePtr& io_service) + : io_service_(io_service) { + } + + /// @brief Destructor. + /// + /// Stops IO service. + ~IOServiceWork() { + io_service_->stop(); + } + +private: + + /// @brief Pointer to the IO service to be stopped upon destruction. + IOServicePtr io_service_; + +}; + class NakedControlledDhcpv4Srv: public ControlledDhcpv4Srv { // "Naked" DHCPv4 server, exposes internal fields public: - NakedControlledDhcpv4Srv():ControlledDhcpv4Srv(0) { } + NakedControlledDhcpv4Srv():ControlledDhcpv4Srv(0) { + CfgMgr::instance().setFamily(AF_INET); + } /// Expose internal methods for the sake of testing using Dhcpv4Srv::receivePacket; }; +/// @brief Default control connection timeout. +const size_t DEFAULT_CONNECTION_TIMEOUT = 10; + /// @brief Fixture class intended for testin control channel in the DHCPv4Srv class CtrlChannelDhcpv4SrvTest : public ::testing::Test { public: @@ -78,10 +118,22 @@ public: ~CtrlChannelDhcpv4SrvTest() { LeaseMgrFactory::destroy(); StatsMgr::instance().removeAll(); + + CommandMgr::instance().closeCommandSocket(); + CommandMgr::instance().deregisterAll(); + CommandMgr::instance().setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT); + server_.reset(); - reset(); }; + /// @brief Returns pointer to the server's IO service. + /// + /// @return Pointer to the server's IO service or null pointer if the server + /// hasn't been created. + IOServicePtr getIOService() { + return (server_ ? server_->getIOService() : IOServicePtr()); + } + void createUnixChannelServer() { ::remove(socket_path_.c_str()); @@ -117,8 +169,16 @@ public: ASSERT_NO_THROW(server_.reset(new NakedControlledDhcpv4Srv())); - ConstElementPtr config = Element::fromJSON(config_txt); + ConstElementPtr config; + ASSERT_NO_THROW(config = parseDHCP4(config_txt)); ConstElementPtr answer = server_->processConfig(config); + + // Commit the configuration so any subsequent reconfigurations + // will only close the command channel if its configuration has + // changed. + CfgMgr::instance().commit(); + + ASSERT_TRUE(answer); int status = 0; @@ -167,15 +227,16 @@ public: client.reset(new UnixControlClient()); ASSERT_TRUE(client); - // Connect and then call server's receivePacket() so it can - // detect the control socket connect and call the accept handler + // 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(server_->receivePacket(0)); + ASSERT_NO_THROW(getIOService()->poll()); - // Send the command and then call server's receivePacket() so it can - // detect the inbound data and call the read handler + // 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(server_->receivePacket(0)); + 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. @@ -184,7 +245,141 @@ public: // Now disconnect and process the close event client->disconnectFromServer(); - ASSERT_NO_THROW(server_->receivePacket(0)); + + 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 std::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 write-config 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 std::string& response_txt, int exp_status, + const std::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; + } + } + + /// @brief Handler for long command. + /// + /// It checks whether the received command is equal to the one specified + /// as an argument. + /// + /// @param expected_command String representing an expected command. + /// @param command_name Command name received by the handler. + /// @param arguments Command arguments received by the handler. + /// + /// @returns Success answer. + static ConstElementPtr + longCommandHandler(const std::string& expected_command, + const std::string& command_name, + const ConstElementPtr& arguments) { + // The handler is called with a command name and the structure holding + // command arguments. We have to rebuild the command from those + // two arguments so as it can be compared against expected_command. + ElementPtr entire_command = Element::createMap(); + entire_command->set("command", Element::create(command_name)); + entire_command->set("arguments", (arguments)); + + // The rebuilt command will have a different order of parameters so + // let's parse expected_command back to JSON to guarantee that + // both structures are built using the same order. + EXPECT_EQ(Element::fromJSON(expected_command)->str(), + entire_command->str()); + return (createAnswer(0, "long command received ok")); + } + + /// @brief Command handler which generates long response + /// + /// This handler generates a large response (over 400kB). It includes + /// a list of randomly generated strings to make sure that the test + /// can catch out of order delivery. + static ConstElementPtr longResponseHandler(const std::string&, + const ConstElementPtr&) { + // By seeding the generator with the constant value we will always + // get the same sequence of generated strings. + std::srand(1); + ElementPtr arguments = Element::createList(); + for (unsigned i = 0; i < 40000; ++i) { + std::ostringstream s; + s << std::setw(10) << std::rand(); + arguments->add(Element::create(s.str())); + } + return (createAnswer(0, arguments)); } }; @@ -221,12 +416,7 @@ TEST_F(CtrlChannelDhcpv4SrvTest, commands) { // Check that the "libreload" command will reload libraries TEST_F(CtrlChannelDhcpv4SrvTest, libreload) { - - // Sending commands for processing now requires a server that can process - // them. - ASSERT_NO_THROW( - server_.reset(new NakedControlledDhcpv4Srv()); - ); + createUnixChannelServer(); // Ensure no marker files to start with. ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE)); @@ -252,15 +442,11 @@ TEST_F(CtrlChannelDhcpv4SrvTest, libreload) { // Now execute the "libreload" command. This should cause the libraries // to unload and to reload. - - // Use empty parameters list - ElementPtr params(new isc::data::MapElement()); - int rcode = -1; - - ConstElementPtr result = - ControlledDhcpv4Srv::processCommand("libreload", params); - ConstElementPtr comment = parseAnswer(rcode, result); - EXPECT_EQ(0, rcode); // Expect success + std::string response; + sendUnixCommand("{ \"command\": \"libreload\" }", response); + EXPECT_EQ("{ \"result\": 0, " + "\"text\": \"Hooks libraries successfully reloaded.\" }" + , response); // Check that the libraries have unloaded and reloaded. The libraries are // unloaded in the reverse order to which they are loaded. When they load, @@ -293,12 +479,20 @@ TEST_F(CtrlChannelDhcpv4SrvTest, commandsRegistration) { std::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-set\"") != string::npos); + EXPECT_TRUE(command_list.find("\"config-write\"") != string::npos); + EXPECT_TRUE(command_list.find("\"leases-reclaim\"") != string::npos); + EXPECT_TRUE(command_list.find("\"libreload\"") != string::npos); + EXPECT_TRUE(command_list.find("\"shutdown\"") != string::npos); EXPECT_TRUE(command_list.find("\"statistic-get\"") != string::npos); EXPECT_TRUE(command_list.find("\"statistic-get-all\"") != string::npos); EXPECT_TRUE(command_list.find("\"statistic-remove\"") != string::npos); EXPECT_TRUE(command_list.find("\"statistic-remove-all\"") != string::npos); EXPECT_TRUE(command_list.find("\"statistic-reset\"") != string::npos); EXPECT_TRUE(command_list.find("\"statistic-reset-all\"") != string::npos); + EXPECT_TRUE(command_list.find("\"version-get\"") != string::npos); // Ok, and now delete the server. It should deregister its commands. server_.reset(); @@ -317,12 +511,12 @@ TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelNegative) { std::string response; sendUnixCommand("{ \"command\": \"bogus\" }", response); - EXPECT_EQ("{ \"result\": 1," + EXPECT_EQ("{ \"result\": 2," " \"text\": \"'bogus' command not supported.\" }", response); sendUnixCommand("utter nonsense", response); EXPECT_EQ("{ \"result\": 1, " - "\"text\": \"error: unexpected character u in <string>:1:2\" }", + "\"text\": \"invalid first character u\" }", response); } @@ -394,7 +588,26 @@ TEST_F(CtrlChannelDhcpv4SrvTest, controlLeasesReclaim) { EXPECT_TRUE(lease1->stateExpiredReclaimed()); } -// Thist test verifies that the DHCP server immediately removed expired +// This test verifies that the DHCP server handles version-get commands +TEST_F(CtrlChannelDhcpv4SrvTest, getversion) { + createUnixChannelServer(); + + std::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); +} + +// This test verifies that the DHCP server immediately removed expired +// This test verifies that the DHCP server immediately removed expired // leases on leases-reclaim command with remove = true TEST_F(CtrlChannelDhcpv4SrvTest, controlLeasesReclaimRemove) { createUnixChannelServer(); @@ -479,4 +692,714 @@ TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelStats) { response); } +// Check that the "config-set" command will replace current configuration +TEST_F(CtrlChannelDhcpv4SrvTest, configSet) { + createUnixChannelServer(); + + // Define strings to permutate the config arguments + // (Note the line feeds makes errors easy to find) + string set_config_txt = "{ \"command\": \"config-set\" \n"; + string args_txt = " \"arguments\": { \n"; + string dhcp4_cfg_txt = + " \"Dhcp4\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet4\": [ \n"; + string subnet1 = + " {\"subnet\": \"192.2.0.0/24\", \n" + " \"pools\": [{ \"pool\": \"192.2.0.1-192.2.0.50\" }]}\n"; + string subnet2 = + " {\"subnet\": \"192.2.1.0/24\", \n" + " \"pools\": [{ \"pool\": \"192.2.1.1-192.2.1.50\" }]}\n"; + string bad_subnet = + " {\"BOGUS\": \"192.2.2.0/24\", \n" + " \"pools\": [{ \"pool\": \"192.2.2.1-192.2.2.50\" }]}\n"; + string subnet_footer = + " ] \n"; + string control_socket_header = + " ,\"control-socket\": { \n" + " \"socket-type\": \"unix\", \n" + " \"socket-name\": \""; + string control_socket_footer = + "\" \n} \n"; + string logger_txt = + " \"Logging\": { \n" + " \"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output_options\": [{ \n" + " \"output\": \"/dev/null\" \n" + " }] \n" + " }] \n" + " } \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << set_config_txt << "," + << args_txt + << dhcp4_cfg_txt + << subnet1 + << subnet_footer + << control_socket_header + << socket_path_ + << control_socket_footer + << "}\n" // close dhcp4 + << "," + << logger_txt + << "}}"; + + // Send the config-set command + std::string response; + sendUnixCommand(os.str(), response); + + // Verify the configuration was successful. + EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }", + response); + + // Check that the config was indeed applied. + const Subnet4Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + // Create a config with malformed subnet that should fail to parse. + os.str(""); + os << set_config_txt << "," + << args_txt + << dhcp4_cfg_txt + << bad_subnet + << subnet_footer + << control_socket_header + << socket_path_ + << control_socket_footer + << "}\n" // close dhcp4 + "}}"; + + // Send the config-set command + sendUnixCommand(os.str(), response); + + // Should fail with a syntax error + EXPECT_EQ("{ \"result\": 1, " + "\"text\": \"subnet configuration failed: mandatory 'subnet' " + "parameter is missing for a subnet being configured (<wire>:19:17)\" }", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + // Create a valid config with two subnets and no command channel. + // It should succeed, client should still receive the response + os.str(""); + os << set_config_txt << "," + << args_txt + << dhcp4_cfg_txt + << subnet1 + << ",\n" + << subnet2 + << subnet_footer + << "}\n" // close dhcp4 + << "}}"; + + // Verify the control channel socket exists. + ASSERT_TRUE(fileExists(socket_path_)); + + // Send the config-set command. + sendUnixCommand(os.str(), response); + + // Verify the control channel socket no longer exists. + EXPECT_FALSE(fileExists(socket_path_)); + + // With no command channel, should still receive the response. + EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(2, subnets->size()); + + // Clean up after the test. + CfgMgr::instance().clear(); +} + +// Tests that the server properly responds to shtudown command sent +// via ControlChannel +TEST_F(CtrlChannelDhcpv4SrvTest, listCommands) { + createUnixChannelServer(); + std::string response; + + sendUnixCommand("{ \"command\": \"list-commands\" }", response); + + ConstElementPtr rsp; + EXPECT_NO_THROW(rsp = Element::fromJSON(response)); + + // We expect the server to report at least the following commands: + checkListCommands(rsp, "build-report"); + checkListCommands(rsp, "config-get"); + checkListCommands(rsp, "config-reload"); + checkListCommands(rsp, "config-set"); + checkListCommands(rsp, "config-write"); + checkListCommands(rsp, "list-commands"); + checkListCommands(rsp, "leases-reclaim"); + checkListCommands(rsp, "libreload"); + checkListCommands(rsp, "shutdown"); + checkListCommands(rsp, "statistic-get"); + checkListCommands(rsp, "statistic-get-all"); + checkListCommands(rsp, "statistic-remove"); + checkListCommands(rsp, "statistic-remove-all"); + checkListCommands(rsp, "statistic-reset"); + checkListCommands(rsp, "statistic-reset-all"); + checkListCommands(rsp, "version-get"); +} + +// 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(CtrlChannelDhcpv4SrvTest, configGet) { + createUnixChannelServer(); + std::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("Dhcp4")); +} + +// Verify that the "config-test" command will do what we expect. +TEST_F(CtrlChannelDhcpv4SrvTest, configTest) { + createUnixChannelServer(); + + // Define strings to permutate the config arguments + // (Note the line feeds makes errors easy to find) + string set_config_txt = "{ \"command\": \"config-set\" \n"; + string config_test_txt = "{ \"command\": \"config-test\" \n"; + string args_txt = " \"arguments\": { \n"; + string dhcp4_cfg_txt = + " \"Dhcp4\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet4\": [ \n"; + string subnet1 = + " {\"subnet\": \"192.2.0.0/24\", \n" + " \"pools\": [{ \"pool\": \"192.2.0.1-192.2.0.50\" }]}\n"; + string subnet2 = + " {\"subnet\": \"192.2.1.0/24\", \n" + " \"pools\": [{ \"pool\": \"192.2.1.1-192.2.1.50\" }]}\n"; + string bad_subnet = + " {\"BOGUS\": \"192.2.2.0/24\", \n" + " \"pools\": [{ \"pool\": \"192.2.2.1-192.2.2.50\" }]}\n"; + string subnet_footer = + " ] \n"; + string control_socket_header = + " ,\"control-socket\": { \n" + " \"socket-type\": \"unix\", \n" + " \"socket-name\": \""; + string control_socket_footer = + "\" \n} \n"; + string logger_txt = + " \"Logging\": { \n" + " \"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output_options\": [{ \n" + " \"output\": \"/dev/null\" \n" + " }] \n" + " }] \n" + " } \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << set_config_txt << "," + << args_txt + << dhcp4_cfg_txt + << subnet1 + << subnet_footer + << control_socket_header + << socket_path_ + << control_socket_footer + << "}\n" // close dhcp4 + << "," + << logger_txt + << "}}"; + + // Send the config-set command + std::string response; + sendUnixCommand(os.str(), response); + + // Verify the configuration was successful. + EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }", + response); + + // Check that the config was indeed applied. + const Subnet4Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + // Create a config with malformed subnet that should fail to parse. + os.str(""); + os << config_test_txt << "," + << args_txt + << dhcp4_cfg_txt + << bad_subnet + << subnet_footer + << control_socket_header + << socket_path_ + << control_socket_footer + << "}\n" // close dhcp4 + "}}"; + + // Send the config-test command + sendUnixCommand(os.str(), response); + + // Should fail with a syntax error + EXPECT_EQ("{ \"result\": 1, " + "\"text\": \"subnet configuration failed: mandatory 'subnet' " + "parameter is missing for a subnet being configured (<wire>:19:17)\" }", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + // Create a valid config with two subnets and no command channel. + os.str(""); + os << config_test_txt << "," + << args_txt + << dhcp4_cfg_txt + << subnet1 + << ",\n" + << subnet2 + << subnet_footer + << "}\n" // close dhcp4 + << "}}"; + + // Verify the control channel socket exists. + ASSERT_TRUE(fileExists(socket_path_)); + + // Send the config-test command + sendUnixCommand(os.str(), response); + + // Verify the control channel socket still exists. + EXPECT_TRUE(fileExists(socket_path_)); + + // Verify the configuration was successful. + EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration seems sane. " + "Control-socket, hook-libraries, and D2 configuration were " + "sanity checked, but not applied.\" }", + response); + + // Check that the config was not applied + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + // Clean up after the test. + CfgMgr::instance().clear(); +} + +// Tests if config-write can be called without any parameters. +TEST_F(CtrlChannelDhcpv4SrvTest, writeConfigNoFilename) { + createUnixChannelServer(); + std::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(CtrlChannelDhcpv4SrvTest, writeConfigFilename) { + createUnixChannelServer(); + std::string response; + + sendUnixCommand("{ \"command\": \"config-write\", " + "\"arguments\": { \"filename\": \"test2.json\" } }", response); + checkConfigWrite(response, CONTROL_RESULT_SUCCESS, "test2.json"); + ::remove("test2.json"); +} + +// Tests if config-reload attempts to reload a file and reports that the +// file is missing. +TEST_F(CtrlChannelDhcpv4SrvTest, configReloadMissingFile) { + createUnixChannelServer(); + std::string response; + + // This is normally set to whatever value is passed to -c when the server is + // started, but we're not starting it that way, so need to set it by hand. + server_->setConfigFile("test6.json"); + + // Tell the server to reload its configuration. It should attempt to load + // test6.json (and fail, because the file is not there). + sendUnixCommand("{ \"command\": \"config-reload\" }", response); + + // Verify the reload was rejected. + EXPECT_EQ("{ \"result\": 1, \"text\": \"Config reload failed:" + "configuration error using file 'test6.json': Unable to open file " + "test6.json\" }", + response); +} + +// Tests if config-reload attempts to reload a file and reports that the +// file is not a valid JSON. +TEST_F(CtrlChannelDhcpv4SrvTest, configReloadBrokenFile) { + createUnixChannelServer(); + std::string response; + + // This is normally set to whatever value is passed to -c when the server is + // started, but we're not starting it that way, so need to set it by hand. + server_->setConfigFile("test7.json"); + + // Although Kea is smart, its AI routines are not smart enough to handle + // this one... at least not yet. + ofstream f("test7.json", ios::trunc); + f << "gimme some addrs, bro!"; + f.close(); + + // Now tell Kea to reload its config. + sendUnixCommand("{ \"command\": \"config-reload\" }", response); + + // Verify the reload will fail. + EXPECT_EQ("{ \"result\": 1, \"text\": \"Config reload failed:" + "configuration error using file 'test7.json': " + "test7.json:1.1: Invalid character: g\" }", + response); + + ::remove("test7.json"); +} + +// Tests if config-reload attempts to reload a file and reports that the +// file is loaded correctly. +TEST_F(CtrlChannelDhcpv4SrvTest, configReloadValid) { + createUnixChannelServer(); + std::string response; + + // This is normally set to whatever value is passed to -c when the server is + // started, but we're not starting it that way, so need to set it by hand. + server_->setConfigFile("test8.json"); + + // Ok, enough fooling around. Let's create a valid config. + const std::string cfg_txt = + "{ \"Dhcp4\": {" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"subnet4\": [" + " { \"subnet\": \"192.0.2.0/24\" }," + " { \"subnet\": \"192.0.3.0/24\" }" + " ]," + " \"valid-lifetime\": 4000," + " \"lease-database\": {" + " \"type\": \"memfile\", \"persist\": false }" + "} }"; + ofstream f("test8.json", ios::trunc); + f << cfg_txt; + f.close(); + + // This command should reload test8.json config. + sendUnixCommand("{ \"command\": \"config-reload\" }", response); + // Verify the configuration was successful. + EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }", + response); + + // Check that the config was indeed applied. + const Subnet4Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(2, subnets->size()); + + ::remove("test8.json"); +} + +/// Verify that concurrent connections over the control channel can be +/// established. +/// @todo Future Kea 1.3 tickets will modify the behavior of the CommandMgr +/// such that the server will be able to send response in multiple chunks. +/// This test will need to be extended. For now, the receive and write +/// operations are atomic and there is no conflict between concurrent +/// connections. +TEST_F(CtrlChannelDhcpv4SrvTest, concurrentConnections) { + createUnixChannelServer(); + + boost::scoped_ptr<UnixControlClient> client1(new UnixControlClient()); + ASSERT_TRUE(client1); + + boost::scoped_ptr<UnixControlClient> client2(new UnixControlClient()); + ASSERT_TRUE(client2); + + // Client 1 connects. + ASSERT_TRUE(client1->connectToServer(socket_path_)); + ASSERT_NO_THROW(getIOService()->poll()); + + // Client 2 connects. + ASSERT_TRUE(client2->connectToServer(socket_path_)); + ASSERT_NO_THROW(getIOService()->poll()); + + // Send the command while another client is connected. + ASSERT_TRUE(client2->sendCommand("{ \"command\": \"list-commands\" }")); + ASSERT_NO_THROW(getIOService()->poll()); + + std::string response; + // The server should respond ok. + ASSERT_TRUE(client2->getResponse(response)); + EXPECT_TRUE(response.find("\"result\": 0") != std::string::npos); + + // Disconnect the servers. + client1->disconnectFromServer(); + client2->disconnectFromServer(); + ASSERT_NO_THROW(getIOService()->poll()); +} + +// This test verifies that the server can receive and process a large command. +TEST_F(CtrlChannelDhcpv4SrvTest, longCommand) { + + std::ostringstream command; + + // This is the desired size of the command sent to the server (1MB). The + // actual size sent will be slightly greater than that. + const size_t command_size = 1024 * 1000; + + while (command.tellp() < command_size) { + + // We're sending command 'foo' with arguments being a list of + // strings. If this is the first transmission, send command name + // and open the arguments list. Also insert the first argument + // so as all subsequent arguments can be prefixed with a comma. + if (command.tellp() == 0) { + command << "{ \"command\": \"foo\", \"arguments\": [ \"begin\""; + + } else { + // Generate a random number and insert it into the stream as + // 10 digits long string. + std::ostringstream arg; + arg << setw(10) << std::rand(); + // Append the argument in the command. + command << ", \"" << arg.str() << "\"\n"; + + // If we have hit the limit of the command size, close braces to + // get appropriate JSON. + if (command.tellp() > command_size) { + command << "] }"; + } + } + } + + ASSERT_NO_THROW( + CommandMgr::instance().registerCommand("foo", + boost::bind(&CtrlChannelDhcpv4SrvTest::longCommandHandler, + command.str(), _1, _2)); + ); + + createUnixChannelServer(); + + std::string response; + std::thread th([this, &response, &command]() { + + // IO service will be stopped automatically when this object goes + // out of scope and is destroyed. This is useful because we use + // asserts which may break the thread in various exit points. + IOServiceWork work(getIOService()); + + // Create client which we will use to send command to the server. + boost::scoped_ptr<UnixControlClient> client(new UnixControlClient()); + ASSERT_TRUE(client); + + // Connect to the server. This will trigger acceptor handler on the + // server side and create a new connection. + ASSERT_TRUE(client->connectToServer(socket_path_)); + + // Initially the remaining_string holds the entire command and we + // will be erasing the portions that we have sent. + std::string remaining_data = command.str(); + while (!remaining_data.empty()) { + // Send the command in chunks of 1024 bytes. + const size_t l = remaining_data.size() < 1024 ? remaining_data.size() : 1024; + ASSERT_TRUE(client->sendCommand(remaining_data.substr(0, l))); + remaining_data.erase(0, l); + } + + // Set timeout to 5 seconds to allow the time for the server to send + // a response. + const unsigned int timeout = 5; + ASSERT_TRUE(client->getResponse(response, timeout)); + + // We're done. Close the connection to the server. + client->disconnectFromServer(); + }); + + // Run the server until the command has been processed and response + // received. + getIOService()->run(); + + // Wait for the thread to complete. + th.join(); + + EXPECT_EQ("{ \"result\": 0, \"text\": \"long command received ok\" }", + response); +} + +// This test verifies that the server can send long response to the client. +TEST_F(CtrlChannelDhcpv4SrvTest, longResponse) { + // We need to generate large response. The simplest way is to create + // a command and a handler which will generate some static response + // of a desired size. + ASSERT_NO_THROW( + CommandMgr::instance().registerCommand("foo", + boost::bind(&CtrlChannelDhcpv4SrvTest::longResponseHandler, _1, _2)); + ); + + createUnixChannelServer(); + + // The UnixControlClient doesn't have any means to check that the entire + // response has been received. What we want to do is to generate a + // reference response using our command handler and then compare + // what we have received over the unix domain socket with this reference + // response to figure out when to stop receiving. + std::string reference_response = longResponseHandler("foo", ConstElementPtr())->str(); + + // In this stream we're going to collect out partial responses. + std::ostringstream response; + + // The client is synchronous so it is useful to run it in a thread. + std::thread th([this, &response, reference_response]() { + + // IO service will be stopped automatically when this object goes + // out of scope and is destroyed. This is useful because we use + // asserts which may break the thread in various exit points. + IOServiceWork work(getIOService()); + + // Remember the response size so as we know when we should stop + // receiving. + const size_t long_response_size = reference_response.size(); + + // Create the client and connect it to the server. + boost::scoped_ptr<UnixControlClient> client(new UnixControlClient()); + ASSERT_TRUE(client); + ASSERT_TRUE(client->connectToServer(socket_path_)); + + // Send the stub command. + std::string command = "{ \"command\": \"foo\", \"arguments\": { } }"; + ASSERT_TRUE(client->sendCommand(command)); + + // Keep receiving response data until we have received the full answer. + while (response.tellp() < long_response_size) { + std::string partial; + const unsigned int timeout = 5; + ASSERT_TRUE(client->getResponse(partial, 5)); + response << partial; + } + + // We have received the entire response, so close the connection and + // stop the IO service. + client->disconnectFromServer(); + }); + + // Run the server until the entire response has been received. + getIOService()->run(); + + // Wait for the thread to complete. + th.join(); + + // Make sure we have received correct response. + EXPECT_EQ(reference_response, response.str()); +} + +// This test verifies that the server signals timeout if the transmission +// takes too long. +TEST_F(CtrlChannelDhcpv4SrvTest, connectionTimeout) { + createUnixChannelServer(); + + // Set connection timeout to 2s to prevent long waiting time for the + // timeout during this test. + const unsigned short timeout = 2; + CommandMgr::instance().setConnectionTimeout(timeout); + + // Server's response will be assigned to this variable. + std::string response; + + // It is useful to create a thread and run the server and the client + // at the same time and independently. + std::thread th([this, &response]() { + + // IO service will be stopped automatically when this object goes + // out of scope and is destroyed. This is useful because we use + // asserts which may break the thread in various exit points. + IOServiceWork work(getIOService()); + + // Create the client and connect it to the server. + boost::scoped_ptr<UnixControlClient> client(new UnixControlClient()); + ASSERT_TRUE(client); + ASSERT_TRUE(client->connectToServer(socket_path_)); + + // Send partial command. The server will be waiting for the remaining + // part to be sent and will eventually signal a timeout. + std::string command = "{ \"command\": \"foo\" "; + ASSERT_TRUE(client->sendCommand(command)); + + // Let's wait up to 15s for the server's response. The response + // should arrive sooner assuming that the timeout mechanism for + // the server is working properly. + const unsigned int timeout = 15; + ASSERT_TRUE(client->getResponse(response, timeout)); + + // Explicitly close the client's connection. + client->disconnectFromServer(); + }); + + // Run the server until stopped. + getIOService()->run(); + + // Wait for the thread to return. + th.join(); + + // Check that the server has signalled a timeout. + EXPECT_EQ("{ \"result\": 1, \"text\": \"Connection over control channel" + " timed out\" }", response); +} + + + } // End of anonymous namespace |