From: Francis Dupont Date: Thu, 28 Jun 2018 00:15:25 +0000 (+0200) Subject: [3543] Finished X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=72cc154a69da1699c2ce26b8e268dff8ffb1abb9;p=thirdparty%2Fkea.git [3543] Finished --- diff --git a/doc/examples/agent/simple.json b/doc/examples/agent/simple.json index 15be619f69..b64a647a62 100644 --- a/doc/examples/agent/simple.json +++ b/doc/examples/agent/simple.json @@ -42,9 +42,7 @@ "socket-name": "/path/to/the/unix/socket-v6" }, - // Currently DHCP-DDNS (nicknamed D2) does not support - // command channel yet, but we hope this will change in the - // future. + // Location of the D2 command channel socket. "d2": { "socket-type": "unix", diff --git a/doc/guide/agent.xml b/doc/guide/agent.xml index 6a45c84353..e11b8a42ec 100644 --- a/doc/guide/agent.xml +++ b/doc/guide/agent.xml @@ -77,7 +77,11 @@ "socket-type": "unix", "socket-name": "/path/to/the/unix/socket-v6", "user-context": { "version": 3 } - } + }, + "d2": { + "socket-type": "unix", + "socket-name": "/path/to/the/unix/socket-d2" + }, }, "hooks-libraries": [ @@ -130,9 +134,10 @@ commands to it. Obviously, the DHCPv4 server must be configured to listen to connections via this same socket. In other words, the command socket configuration for the DHCPv4 server and CA (for this server) - must match. Consult the and the - to learn how the socket - configuration is specified for the DHCPv4 and DHCPv6 services. + must match. Consult the , the + and + to learn how the socket + configuration is specified for the DHCPv4, DHCPv6 and D2 services. diff --git a/src/bin/agent/ca_command_mgr.cc b/src/bin/agent/ca_command_mgr.cc index 6386527b4c..3371bf53e6 100644 --- a/src/bin/agent/ca_command_mgr.cc +++ b/src/bin/agent/ca_command_mgr.cc @@ -118,12 +118,12 @@ CtrlAgentCommandMgr::handleCommandInternal(std::string cmd_name, s << text->stringValue(); s << " You did not include \"service\" parameter in the command," " which indicates that Kea Control Agent should process this" - " command rather than forward it to one or more DHCP servers. If you" + " command rather than forward it to one or more DHCP servers. If you" " aimed to send this command to one of the DHCP servers you" " should include the \"service\" parameter in your request, e.g." " \"service\": [ \"dhcp4\" ] to forward the command to the DHCPv4" - " server, or \"service\": [ \"dhcp4\", \"dhcp6\" ] to forward it to" - " both DHCPv4 and DHCPv6 servers etc."; + " server, or \"service\": [ \"dhcp4\", \"dhcp6\", \"d2\" ] to forward it to" + " DHCPv4, DHCPv6 and D2 servers etc."; answer->set(CONTROL_TEXT, Element::create(s.str())); } diff --git a/src/bin/agent/tests/ca_command_mgr_unittests.cc b/src/bin/agent/tests/ca_command_mgr_unittests.cc index ba692c4573..f8a7833a71 100644 --- a/src/bin/agent/tests/ca_command_mgr_unittests.cc +++ b/src/bin/agent/tests/ca_command_mgr_unittests.cc @@ -296,6 +296,11 @@ TEST_F(CtrlAgentCommandMgrTest, forwardToDHCPv6Server) { testForward("dhcp6", "dhcp6", isc::config::CONTROL_RESULT_SUCCESS); } +/// Check that control command is successfully forwarded to the D2 server. +TEST_F(CtrlAgentCommandMgrTest, forwardToD2Server) { + testForward("d2", "d2", isc::config::CONTROL_RESULT_SUCCESS); +} + /// Check that the same command is forwarded to multiple servers. TEST_F(CtrlAgentCommandMgrTest, forwardToBothDHCPServers) { configureControlSocket("dhcp6"); @@ -304,6 +309,16 @@ TEST_F(CtrlAgentCommandMgrTest, forwardToBothDHCPServers) { isc::config::CONTROL_RESULT_SUCCESS, -1, 2); } +/// Check that the same command is forwarded to all servers. +TEST_F(CtrlAgentCommandMgrTest, forwardToAllServers) { + configureControlSocket("dhcp6"); + configureControlSocket("d2"); + + testForward("dhcp4", "dhcp4,dhcp6,d2", isc::config::CONTROL_RESULT_SUCCESS, + isc::config::CONTROL_RESULT_SUCCESS, + isc::config::CONTROL_RESULT_SUCCESS, 3); +} + /// Check that the command may forwarded to the second server even if /// forwarding to a first server fails. TEST_F(CtrlAgentCommandMgrTest, failForwardToServer) { diff --git a/src/bin/d2/d2_controller.h b/src/bin/d2/d2_controller.h index 3ec84b03fb..f53cf6eb51 100644 --- a/src/bin/d2/d2_controller.h +++ b/src/bin/d2/d2_controller.h @@ -53,6 +53,14 @@ public: void deregisterCommands(); protected: + /// @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 Creates an instance of the DHCP-DDNS specific application /// process. This method is invoked during the process initialization /// step of the controller launch. @@ -63,14 +71,6 @@ protected: /// 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 diff --git a/src/bin/d2/tests/d2_command_unittest.cc b/src/bin/d2/tests/d2_command_unittest.cc index 42374496d6..9f9328819a 100644 --- a/src/bin/d2/tests/d2_command_unittest.cc +++ b/src/bin/d2/tests/d2_command_unittest.cc @@ -6,6 +6,8 @@ #include +#include +#include #include #include #include @@ -19,6 +21,7 @@ #include #include #include +#include using namespace std; using namespace isc; @@ -32,6 +35,32 @@ using namespace boost::asio; 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 NakedD2Controller; typedef boost::shared_ptr NakedD2ControllerPtr; @@ -97,6 +126,10 @@ public: // Remove files. ::remove(CFG_TEST_FILE); ::remove(socket_path_.c_str()); + + // Reset command manager. + CommandMgr::instance().deregisterAll(); + CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND); } /// @brief Returns pointer to the server's IO service. @@ -125,42 +158,6 @@ public: } } - /// @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()); @@ -332,6 +329,51 @@ public: 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 string& expected_command, + const 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 string&, const ConstElementPtr&) { + ElementPtr arguments = Element::createList(); + for (unsigned i = 0; i < 80000; ++i) { + std::ostringstream s; + s << std::setw(5) << i; + arguments->add(Element::create(s.str())); + } + return (createAnswer(0, arguments)); + } }; const char* CtrlChannelD2Test::CFG_TEST_FILE = "d2-test-config.json"; @@ -533,6 +575,25 @@ TEST_F(CtrlChannelD2Test, getversion) { EXPECT_TRUE(response.find("GTEST_VERSION") != string::npos); } +// Tests that the server properly responds to list-commands command. +TEST_F(CtrlChannelD2Test, listCommands) { + EXPECT_NO_THROW(createUnixChannelServer()); + 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-write"); + checkListCommands(rsp, "list-commands"); + checkListCommands(rsp, "shutdown"); + 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. @@ -647,7 +708,6 @@ TEST_F(CtrlChannelD2Test, configTest) { 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); @@ -716,7 +776,293 @@ TEST_F(CtrlChannelD2Test, writeConfigFilename) { ::remove("test2.json"); } -// TODO: concurrentConnections, longCommand, longResponse, -// connectionTimeoutPartialCommand, connectionTimeoutNoData +/// Verify that concurrent connections over the control channel can be +/// established. (@todo change when response will be sent in multiple chunks) +TEST_F(CtrlChannelD2Test, concurrentConnections) { + EXPECT_NO_THROW(createUnixChannelServer()); + + boost::scoped_ptr client1(new UnixControlClient()); + ASSERT_TRUE(client1); + + boost::scoped_ptr 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()); + + 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(CtrlChannelD2Test, longCommand) { + + 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. + 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(&CtrlChannelD2Test::longCommandHandler, + command.str(), _1, _2)); + ); + + createUnixChannelServer(); + + 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 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. + 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(CtrlChannelD2Test, 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(&CtrlChannelD2Test::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. + string reference_response = longResponseHandler("foo", ConstElementPtr())->str(); + + // In this stream we're going to collect out partial responses. + 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 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, timeout)); + 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, after receiving a partial command +TEST_F(CtrlChannelD2Test, connectionTimeoutPartialCommand) { + createUnixChannelServer(); + + // Set connection timeout to 2s to prevent long waiting time for the + // timeout during this test. + const unsigned short timeout = 2000; + CommandMgr::instance().setConnectionTimeout(timeout); + + // Server's response will be assigned to this variable. + 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 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. + 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, discarded partial command of 19 bytes\" }" , + response); +} + +// This test verifies that the server signals timeout if the transmission +// takes too long, having received no data from the client. +TEST_F(CtrlChannelD2Test, connectionTimeoutNoData) { + createUnixChannelServer(); + + // Set connection timeout to 2s to prevent long waiting time for the + // timeout during this test. + const unsigned short timeout = 2000; + CommandMgr::instance().setConnectionTimeout(timeout); + + // Server's response will be assigned to this variable. + 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 client(new UnixControlClient()); + ASSERT_TRUE(client); + ASSERT_TRUE(client->connectToServer(socket_path_)); + + // 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 diff --git a/src/bin/keactrl/kea-ctrl-agent.conf.pre b/src/bin/keactrl/kea-ctrl-agent.conf.pre index 0619408b96..94177a7068 100644 --- a/src/bin/keactrl/kea-ctrl-agent.conf.pre +++ b/src/bin/keactrl/kea-ctrl-agent.conf.pre @@ -21,8 +21,8 @@ "http-port": 8080, // Specify location of the files to which the Control Agent - // should connect to forward commands to the DHCPv4 and DHCPv6 - // server via unix domain socket. + // should connect to forward commands to the DHCPv4, DHCPv6 + // and D2 servers via unix domain sockets. "control-sockets": { "dhcp4": { "socket-type": "unix", @@ -31,6 +31,10 @@ "dhcp6": { "socket-type": "unix", "socket-name": "/tmp/kea-dhcp6-ctrl.sock" + }, + "d2": { + "socket-type": "unix", + "socket-name": "/tmp/kea-dhcp-ddns-ctrl.sock" } }, diff --git a/src/bin/keactrl/kea-dhcp-ddns.conf.pre b/src/bin/keactrl/kea-dhcp-ddns.conf.pre index b1910974d9..cec180d9ba 100644 --- a/src/bin/keactrl/kea-dhcp-ddns.conf.pre +++ b/src/bin/keactrl/kea-dhcp-ddns.conf.pre @@ -21,9 +21,13 @@ { "ip-address": "127.0.0.1", "port": 53001, + "control-socket": { + "socket-type": "unix", + "socket-name": "/tmp/kea-dhcp-ddns-ctrl.sock" + }, "tsig-keys": [], "forward-ddns" : {}, - "reverse-ddns" : {} + "reverse-ddns" : {}, }, // Logging configuration starts here. Kea uses different loggers to log various