diff options
author | Andrei Pavel <andrei.pavel@qualitance.com> | 2017-08-17 20:04:29 +0200 |
---|---|---|
committer | Andrei Pavel <andrei.pavel@qualitance.com> | 2017-08-17 20:04:29 +0200 |
commit | 529d15326887b3513413567e497118b3db2c24f3 (patch) | |
tree | 8b66b262349433802bd52e920bb4783baac57cb3 /src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc | |
parent | Added mysql_execute_script (diff) | |
parent | [master] Added ChangeLog 1288 for trac 5315. (diff) | |
download | kea-529d15326887b3513413567e497118b3db2c24f3.tar.xz kea-529d15326887b3513413567e497118b3db2c24f3.zip |
Merge branch 'isc-master' into minor-changes
Diffstat (limited to 'src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc')
-rw-r--r-- | src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc | 1002 |
1 files changed, 929 insertions, 73 deletions
diff --git a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc index 1b4ade9daf..a34a4a8572 100644 --- a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -15,8 +15,10 @@ #include <dhcp6/ctrl_dhcp6_srv.h> #include <dhcp6/tests/dhcp6_test_utils.h> #include <hooks/hooks_manager.h> +#include <log/logger_support.h> #include <stats/stats_mgr.h> #include <testutils/unix_control_client.h> +#include <testutils/io_utils.h> #include "marker_file.h" #include "test_libraries.h" @@ -24,10 +26,16 @@ #include <boost/scoped_ptr.hpp> #include <gtest/gtest.h> +#include <iomanip> +#include <sstream> + #include <sys/select.h> +#include <sys/stat.h> #include <sys/ioctl.h> #include <cstdlib> +#include <thread> + using namespace std; using namespace isc::asiolink; using namespace isc::config; @@ -36,21 +44,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 NakedControlledDhcpv6Srv: public ControlledDhcpv6Srv { // "Naked" DHCPv6 server, exposes internal fields public: NakedControlledDhcpv6Srv():ControlledDhcpv6Srv(DHCP6_SERVER_PORT + 10000) { + CfgMgr::instance().setFamily(AF_INET6); } /// Expose internal methods for the sake of testing using Dhcpv6Srv::receivePacket; }; +/// @brief Default control connection timeout. +const size_t DEFAULT_CONNECTION_TIMEOUT = 10; + class CtrlDhcpv6SrvTest : public BaseServerTest { public: CtrlDhcpv6SrvTest() @@ -61,6 +98,9 @@ public: virtual ~CtrlDhcpv6SrvTest() { LeaseMgrFactory::destroy(); StatsMgr::instance().removeAll(); + CommandMgr::instance().deregisterAll(); + CommandMgr::instance().setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT); + reset(); }; @@ -110,6 +150,14 @@ public: 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() { static_cast<void>(::remove(socket_path_.c_str())); @@ -148,6 +196,12 @@ public: ConstElementPtr config; ASSERT_NO_THROW(config = parseDHCP6(config_txt)); ConstElementPtr answer = server_->processConfig(config); + + // Commit the configuration so any subsequent reconfigurations + // will only close the command channel if its configuration has + // changed. + CfgMgr::instance().commit(); + ASSERT_TRUE(answer); int status = 0; @@ -182,15 +236,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. @@ -199,7 +254,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)); } }; @@ -236,14 +425,8 @@ TEST_F(CtrlDhcpv6SrvTest, commands) { } // Check that the "libreload" command will reload libraries -TEST_F(CtrlDhcpv6SrvTest, libreload) { - - // Sending commands for processing now requires a server that can process - // them. - boost::scoped_ptr<ControlledDhcpv6Srv> srv; - ASSERT_NO_THROW( - srv.reset(new ControlledDhcpv6Srv(0)) - ); +TEST_F(CtrlChannelDhcpv6SrvTest, libreload) { + createUnixChannelServer(); // Ensure no marker files to start with. ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE)); @@ -269,15 +452,11 @@ TEST_F(CtrlDhcpv6SrvTest, 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 = - ControlledDhcpv6Srv::processCommand("libreload", params); - ConstElementPtr comment = isc::config::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, @@ -286,59 +465,292 @@ TEST_F(CtrlDhcpv6SrvTest, libreload) { EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "1212")); } -// Check that the "configReload" command will reload libraries -TEST_F(CtrlDhcpv6SrvTest, configReload) { +// Check that the "config-set" command will replace current configuration +TEST_F(CtrlChannelDhcpv6SrvTest, configSet) { + createUnixChannelServer(); - // Sending commands for processing now requires a server that can process - // them. - boost::scoped_ptr<ControlledDhcpv6Srv> srv; - ASSERT_NO_THROW( - srv.reset(new ControlledDhcpv6Srv(0)) - ); + // 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 dhcp6_cfg_txt = + " \"Dhcp6\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"preferred-lifetime\": 3000, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet6\": [ \n"; + string subnet1 = + " {\"subnet\": \"3002::/64\", \n" + " \"pools\": [{ \"pool\": \"3002::100-3002::200\" }]}\n"; + string subnet2 = + " {\"subnet\": \"3003::/64\", \n" + " \"pools\": [{ \"pool\": \"3003::100-3003::200\" }]}\n"; + string bad_subnet = + " {\"BOGUS\": \"3005::/64\", \n" + " \"pools\": [{ \"pool\": \"3005::100-3005::200\" }]}\n"; + string subnet_footer = + " ] \n"; + string control_socket_header = + " ,\"control-socket\": { \n" + " \"socket-type\": \"unix\", \n" + " \"socket-name\": \""; + string control_socket_footer = + "\" \n} \n"; + string logger_txt = + " \"Logging\": { \n" + " \"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output_options\": [{ \n" + " \"output\": \"/dev/null\" \n" + " }] \n" + " }] \n" + " } \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << set_config_txt << "," + << args_txt + << dhcp6_cfg_txt + << subnet1 + << subnet_footer + << control_socket_header + << socket_path_ + << control_socket_footer + << "}\n" // close dhcp6 + << "," + << logger_txt + << "}}"; + + // Send the config-set command + std::string response; + sendUnixCommand(os.str(), response); - // Now execute the "libreload" command. This should cause the libraries - // to unload and to reload. + // Verify the configuration was successful. + EXPECT_EQ("{ \"result\": 0, \"text\": \"Configuration successful.\" }", + response); - // Use empty parameters list - // Prepare configuration file. - string config_txt = "{ \"interfaces-config\": {" - " \"interfaces\": [ \"*\" ]" - "}," - "\"preferred-lifetime\": 3000," - "\"rebind-timer\": 2000, " - "\"renew-timer\": 1000, " - "\"subnet6\": [ { " - " \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ]," - " \"subnet\": \"2001:db8:1::/64\" " - " }," - " {" - " \"pools\": [ { \"pool\": \"2001:db8:2::/80\" } ]," - " \"subnet\": \"2001:db8:2::/64\", " - " \"id\": 0" - " }," - " {" - " \"pools\": [ { \"pool\": \"2001:db8:3::/80\" } ]," - " \"subnet\": \"2001:db8:3::/64\" " - " } ]," - "\"valid-lifetime\": 4000 }"; - - ConstElementPtr config; - ASSERT_NO_THROW(config = parseDHCP6(config_txt)); - - // Make sure there are no subnets configured. + // Check that the config was indeed applied. + const Subnet6Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + // Create a config with malformed subnet that should fail to parse. + os.str(""); + os << set_config_txt << "," + << args_txt + << dhcp6_cfg_txt + << bad_subnet + << subnet_footer + << control_socket_header + << socket_path_ + << control_socket_footer + << "}\n" // close dhcp6 + "}}"; + + // Send the 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>:20:17)\" }", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + // Create a valid config with two subnets and no command channel. + // It should succeed but client will not receive a the response + os.str(""); + os << set_config_txt << "," + << args_txt + << dhcp6_cfg_txt + << subnet1 + << ",\n" + << subnet2 + << subnet_footer + << "}\n" // close dhcp6 + << "}}"; + + // Verify the control channel socket exists. + ASSERT_TRUE(fileExists(socket_path_)); + + // Send the 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()->getCfgSubnets6()->getAll(); + EXPECT_EQ(2, subnets->size()); + + // Clean up after the test. CfgMgr::instance().clear(); +} - // Now send the command - int rcode = -1; - ConstElementPtr result = - ControlledDhcpv6Srv::processCommand("config-reload", config); - ConstElementPtr comment = isc::config::parseAnswer(rcode, result); - EXPECT_EQ(0, rcode); // Expect success + // Verify that the "config-test" command will do what we expect. +TEST_F(CtrlChannelDhcpv6SrvTest, 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 dhcp6_cfg_txt = + " \"Dhcp6\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"preferred-lifetime\": 3000, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet6\": [ \n"; + string subnet1 = + " {\"subnet\": \"3002::/64\", \n" + " \"pools\": [{ \"pool\": \"3002::100-3002::200\" }]}\n"; + string subnet2 = + " {\"subnet\": \"3003::/64\", \n" + " \"pools\": [{ \"pool\": \"3003::100-3003::200\" }]}\n"; + string bad_subnet = + " {\"BOGUS\": \"3005::/64\", \n" + " \"pools\": [{ \"pool\": \"3005::100-3005::200\" }]}\n"; + string subnet_footer = + " ] \n"; + string control_socket_header = + " ,\"control-socket\": { \n" + " \"socket-type\": \"unix\", \n" + " \"socket-name\": \""; + string control_socket_footer = + "\" \n} \n"; + string logger_txt = + " \"Logging\": { \n" + " \"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output_options\": [{ \n" + " \"output\": \"/dev/null\" \n" + " }] \n" + " }] \n" + " } \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << set_config_txt << "," + << args_txt + << dhcp6_cfg_txt + << subnet1 + << subnet_footer + << control_socket_header + << socket_path_ + << control_socket_footer + << "}\n" // close dhcp6 + << "," + << logger_txt + << "}}"; + + // Send the 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 Subnet6Collection* subnets = - CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getAll(); - EXPECT_EQ(3, subnets->size()); + CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->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 + << dhcp6_cfg_txt + << bad_subnet + << subnet_footer + << control_socket_header + << socket_path_ + << control_socket_footer + << "}\n" // close dhcp6 + "}}"; + + // 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>:20:17)\" }", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + // Create a valid config with two subnets and no command channel. + os.str(""); + os << config_test_txt << "," + << args_txt + << dhcp6_cfg_txt + << subnet1 + << ",\n" + << subnet2 + << subnet_footer + << "}\n" // close dhcp6 + << "}}"; + + // Verify the control channel socket exists. + ASSERT_TRUE(fileExists(socket_path_)); + + // Send the 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()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); // Clean up after the test. CfgMgr::instance().clear(); @@ -346,7 +758,7 @@ TEST_F(CtrlDhcpv6SrvTest, configReload) { typedef std::map<std::string, isc::data::ConstElementPtr> ElementMap; -// This test checks which commands are registered by the DHCPv4 server. +// This test checks which commands are registered by the DHCPv6 server. TEST_F(CtrlDhcpv6SrvTest, commandsRegistration) { ConstElementPtr list_cmds = createCommand("list-commands"); @@ -372,12 +784,20 @@ TEST_F(CtrlDhcpv6SrvTest, 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-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("\"config-set\"") != 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. srv.reset(); @@ -396,12 +816,12 @@ TEST_F(CtrlChannelDhcpv6SrvTest, 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); } @@ -415,6 +835,24 @@ TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelShutdown) { EXPECT_EQ("{ \"result\": 0, \"text\": \"Shutting down.\" }",response); } +// This test verifies that the DHCP server handles version-get commands +TEST_F(CtrlChannelDhcpv6SrvTest, 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 reclaims expired // leases on leases-reclaim command TEST_F(CtrlChannelDhcpv6SrvTest, controlLeasesReclaim) { @@ -566,4 +1004,422 @@ TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelStats) { response); } +// Tests that the server properly responds to shtudown command sent +// via ControlChannel +TEST_F(CtrlChannelDhcpv6SrvTest, commandsList) { + 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-set"); + checkListCommands(rsp, "config-test"); + checkListCommands(rsp, "config-write"); + checkListCommands(rsp, "list-commands"); + checkListCommands(rsp, "leases-reclaim"); + checkListCommands(rsp, "libreload"); + checkListCommands(rsp, "version-get"); + 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"); +} + +// Tests if the server returns its configuration using config-get. +// Note there are separate tests that verify if toElement() called by the +// get-config handler are actually converting the configuration correctly. +TEST_F(CtrlChannelDhcpv6SrvTest, 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("Dhcp6")); +} + +// Tests if config-write can be called without any parameters. +TEST_F(CtrlChannelDhcpv6SrvTest, configWriteNoFilename) { + 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(CtrlChannelDhcpv6SrvTest, configWriteFilename) { + 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(CtrlChannelDhcpv6SrvTest, 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(CtrlChannelDhcpv6SrvTest, 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 addr, 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(CtrlChannelDhcpv6SrvTest, 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 = + "{ \"Dhcp6\": {" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"subnet6\": [" + " { \"subnet\": \"2001:db8:1::/64\" }," + " { \"subnet\": \"2001:db8:2::/64\" }" + " ]," + " \"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 Subnet6Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->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(CtrlChannelDhcpv6SrvTest, 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(CtrlChannelDhcpv6SrvTest, 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(&CtrlChannelDhcpv6SrvTest::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(CtrlChannelDhcpv6SrvTest, 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(&CtrlChannelDhcpv6SrvTest::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(CtrlChannelDhcpv6SrvTest, 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 |