diff options
Diffstat (limited to 'src/bin/agent/tests/ca_command_mgr_unittests.cc')
-rw-r--r-- | src/bin/agent/tests/ca_command_mgr_unittests.cc | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/src/bin/agent/tests/ca_command_mgr_unittests.cc b/src/bin/agent/tests/ca_command_mgr_unittests.cc new file mode 100644 index 0000000..6353916 --- /dev/null +++ b/src/bin/agent/tests/ca_command_mgr_unittests.cc @@ -0,0 +1,425 @@ +// Copyright (C) 2017-2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <agent/ca_cfg_mgr.h> +#include <agent/ca_command_mgr.h> +#include <agent/ca_controller.h> +#include <agent/ca_process.h> +#include <asiolink/asio_wrapper.h> +#include <asiolink/interval_timer.h> +#include <asiolink/io_service.h> +#include <asiolink/testutils/test_server_unix_socket.h> +#include <cc/command_interpreter.h> +#include <cc/data.h> +#include <process/testutils/d_test_stubs.h> +#include <boost/pointer_cast.hpp> +#include <gtest/gtest.h> +#include <testutils/sandbox.h> +#include <cstdlib> +#include <functional> +#include <vector> +#include <thread> + +using namespace isc::agent; +using namespace isc::asiolink; +using namespace isc::data; +using namespace isc::process; + +namespace { + +/// @brief Test timeout in ms. +const long TEST_TIMEOUT = 10000; + +/// @brief Test fixture class for @ref CtrlAgentCommandMgr. +/// +/// @todo Add tests for various commands, including the cases when the +/// commands are forwarded to other servers via unix sockets. +/// Meanwhile, this is just a placeholder for the tests. +class CtrlAgentCommandMgrTest : public DControllerTest { +public: + isc::test::Sandbox sandbox; + + /// @brief Constructor. + /// + /// Deregisters all commands except 'list-commands'. + CtrlAgentCommandMgrTest() + : DControllerTest(CtrlAgentController::instance), + mgr_(CtrlAgentCommandMgr::instance()) { + mgr_.deregisterAll(); + removeUnixSocketFile(); + initProcess(); + } + + /// @brief Destructor. + /// + /// Deregisters all commands except 'list-commands'. + virtual ~CtrlAgentCommandMgrTest() { + mgr_.deregisterAll(); + removeUnixSocketFile(); + } + + /// @brief Verifies received answer + /// + /// @todo Add better checks for failure cases and for + /// verification of the response parameters. + /// + /// @param answer answer to be verified + /// @param expected_code0 code expected to be returned in first result within + /// the answer. + /// @param expected_code1 code expected to be returned in second result within + /// the answer. + /// @param expected_code2 code expected to be returned in third result within + /// the answer. + void checkAnswer(const ConstElementPtr& answer, const int expected_code0 = 0, + const int expected_code1 = -1, const int expected_code2 = -1) { + std::vector<int> expected_codes; + if (expected_code0 >= 0) { + expected_codes.push_back(expected_code0); + } + + if (expected_code1 >= 0) { + expected_codes.push_back(expected_code1); + } + + if (expected_code2 >= 0) { + expected_codes.push_back(expected_code2); + } + + int status_code; + // There may be multiple answers returned within a list. + std::vector<ElementPtr> answer_list = answer->listValue(); + + ASSERT_EQ(expected_codes.size(), answer_list.size()); + // Check all answers. + for (auto ans = answer_list.cbegin(); ans != answer_list.cend(); + ++ans) { + ConstElementPtr text; + ASSERT_NO_THROW(text = isc::config::parseAnswer(status_code, *ans)); + EXPECT_EQ(expected_codes[std::distance(answer_list.cbegin(), ans)], + status_code) + << "answer contains text: " << text->stringValue(); + } + } + + /// @brief Returns socket file path. + /// + /// If the KEA_SOCKET_TEST_DIR environment variable is specified, the + /// socket file is created in the location pointed to by this variable. + /// Otherwise, it is created in the build directory. + std::string unixSocketFilePath() { + std::string socket_path; + const char* env = getenv("KEA_SOCKET_TEST_DIR"); + if (env) { + socket_path = std::string(env) + "/test-socket"; + } else { + socket_path = sandbox.join("test-socket"); + } + return (socket_path); + } + + /// @brief Removes unix socket descriptor. + void removeUnixSocketFile() { + static_cast<void>(remove(unixSocketFilePath().c_str())); + } + + /// @brief Returns pointer to CtrlAgentProcess instance. + CtrlAgentProcessPtr getCtrlAgentProcess() { + return (boost::dynamic_pointer_cast<CtrlAgentProcess>(getProcess())); + } + + /// @brief Returns pointer to CtrlAgentCfgMgr instance for a process. + CtrlAgentCfgMgrPtr getCtrlAgentCfgMgr() { + CtrlAgentCfgMgrPtr p; + if (getCtrlAgentProcess()) { + p = getCtrlAgentProcess()->getCtrlAgentCfgMgr(); + } + return (p); + } + + /// @brief Returns a pointer to the configuration context. + CtrlAgentCfgContextPtr getCtrlAgentCfgContext() { + CtrlAgentCfgContextPtr p; + if (getCtrlAgentCfgMgr()) { + p = getCtrlAgentCfgMgr()->getCtrlAgentCfgContext(); + } + return (p); + } + + /// @brief Adds configuration of the control socket. + /// + /// @param service Service for which socket configuration is to be added. + void + configureControlSocket(const std::string& service) { + CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); + ASSERT_TRUE(ctx); + + ElementPtr control_socket = Element::createMap(); + control_socket->set("socket-name", + Element::create(unixSocketFilePath())); + ctx->setControlSocketInfo(control_socket, service); + } + + /// @brief Create and bind server side socket. + /// + /// @param response Stub response to be sent from the server socket to the + /// client. + /// @param use_thread Indicates if the IO service will be ran in thread. + void bindServerSocket(const std::string& response, + const bool use_thread = false) { + server_socket_.reset(new test::TestServerUnixSocket(*getIOService(), + unixSocketFilePath(), + response)); + server_socket_->startTimer(TEST_TIMEOUT); + server_socket_->bindServerSocket(use_thread); + } + + /// @brief Creates command with no arguments. + /// + /// @param command_name Command name. + /// @param service Service value to be added to the command. This value is + /// specified as a list of comma separated values, e.g. "dhcp4, dhcp6". + /// + /// @return Pointer to the instance of the created command. + ConstElementPtr createCommand(const std::string& command_name, + const std::string& service) { + ElementPtr command = Element::createMap(); + command->set("command", Element::create(command_name)); + + // Only add the 'service' parameter if non-empty. + if (!service.empty()) { + std::string s = boost::replace_all_copy(service, ",", "\",\""); + s = std::string("[ \"") + s + std::string("\" ]"); + command->set("service", Element::fromJSON(s)); + } + + command->set("arguments", Element::createMap()); + + return (command); + } + + /// @brief Test forwarding the command. + /// + /// @param server_type Server for which the client socket should be + /// configured. + /// @param service Service to be included in the command. + /// @param expected_result0 Expected first result in response from the server. + /// @param expected_result1 Expected second result in response from the server. + /// @param expected_result2 Expected third result in response from the server. + /// server socket after which the IO service should be stopped. + /// @param expected_responses Number of responses after which the test finishes. + /// @param server_response Stub response to be sent by the server. + void testForward(const std::string& configured_service, + const std::string& service, + const int expected_result0, + const int expected_result1 = -1, + const int expected_result2 = -1, + const size_t expected_responses = 1, + const std::string& server_response = "{ \"result\": 0 }") { + // Configure client side socket. + configureControlSocket(configured_service); + // Create server side socket. + bindServerSocket(server_response, true); + + // The client side communication is synchronous. To be able to respond + // to this we need to run the server side socket at the same time as the + // client. Running IO service in a thread guarantees that the server + //responds as soon as it receives the control command. + std::thread th(std::bind(&IOService::run, getIOService().get())); + + + // Wait for the IO service in thread to actually run. + server_socket_->waitForRunning(); + + ConstElementPtr command = createCommand("foo", service); + ConstElementPtr answer = mgr_.processCommand(command); + + // Stop IO service immediately and let the thread die. + getIOService()->stop(); + + // Wait for the thread to finish. + th.join(); + + // Cancel all asynchronous operations on the server. + server_socket_->stopServer(); + + // We have some cancelled operations for which we need to invoke the + // handlers with the operation_aborted error code. + getIOService()->get_io_service().reset(); + getIOService()->poll(); + + EXPECT_EQ(expected_responses, server_socket_->getResponseNum()); + checkAnswer(answer, expected_result0, expected_result1, expected_result2); + } + + /// @brief a convenience reference to control agent command manager + CtrlAgentCommandMgr& mgr_; + + /// @brief Pointer to the test server unix socket. + test::TestServerUnixSocketPtr server_socket_; +}; + +/// Just a basic test checking that non-existent command is handled +/// properly. +TEST_F(CtrlAgentCommandMgrTest, bogus) { + ConstElementPtr answer; + EXPECT_NO_THROW(answer = mgr_.processCommand(createCommand("fish-and-chips-please", ""))); + checkAnswer(answer, isc::config::CONTROL_RESULT_COMMAND_UNSUPPORTED); +}; + +// Test verifying that parameter other than command, arguments and service is +// rejected and that the correct error is returned. +TEST_F(CtrlAgentCommandMgrTest, extraParameter) { + ElementPtr command = Element::createMap(); + command->set("command", Element::create("list-commands")); + command->set("arguments", Element::createMap()); + command->set("extra-arg", Element::createMap()); + + ConstElementPtr answer; + EXPECT_NO_THROW(answer = mgr_.processCommand(command)); + checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR); +} + +/// Just a basic test checking that 'list-commands' is supported. +TEST_F(CtrlAgentCommandMgrTest, listCommands) { + ConstElementPtr answer; + EXPECT_NO_THROW(answer = mgr_.processCommand(createCommand("list-commands", ""))); + + checkAnswer(answer, isc::config::CONTROL_RESULT_SUCCESS); +}; + +/// Check that control command is successfully forwarded to the DHCPv4 server. +TEST_F(CtrlAgentCommandMgrTest, forwardToDHCPv4Server) { + testForward("dhcp4", "dhcp4", isc::config::CONTROL_RESULT_SUCCESS); +} + +/// Check that control command is successfully forwarded to the DHCPv6 server. +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"); + + testForward("dhcp4", "dhcp4,dhcp6", isc::config::CONTROL_RESULT_SUCCESS, + 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) { + testForward("dhcp6", "dhcp4,dhcp6", + isc::config::CONTROL_RESULT_ERROR, + isc::config::CONTROL_RESULT_SUCCESS); +} + +/// Check that control command is not forwarded if the service is not specified. +TEST_F(CtrlAgentCommandMgrTest, noService) { + testForward("dhcp6", "", + isc::config::CONTROL_RESULT_COMMAND_UNSUPPORTED, + -1, -1, 0); +} + +/// Check that error is returned to the client when the server to which the +/// command was forwarded sent an invalid message. +TEST_F(CtrlAgentCommandMgrTest, invalidAnswer) { + testForward("dhcp6", "dhcp6", + isc::config::CONTROL_RESULT_ERROR, -1, -1, 1, + "{ \"result\": }"); +} + +/// Check that connection is dropped if it takes too long. The test checks +/// client's behavior when partial JSON is returned. Client will be waiting +/// for the '}' and will timeout because it is never received. +/// @todo Currently this test is disabled because we don't have configurable +/// timeout value. It is hardcoded to 5 sec, which is too long for the +/// unit test to run. +TEST_F(CtrlAgentCommandMgrTest, DISABLED_connectionTimeout) { + testForward("dhcp6", "dhcp6", + isc::config::CONTROL_RESULT_ERROR, -1, -1, 1, + "{ \"result\": 0"); +} + +/// Check that error is returned to the client if the forwarding socket is +/// not configured for the given service. +TEST_F(CtrlAgentCommandMgrTest, noClientSocket) { + ConstElementPtr command = createCommand("foo", "dhcp4"); + ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(), + command); + + checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR); +} + +/// Check that error is returned to the client if the remote server to +/// which the control command is to be forwarded is not available. +TEST_F(CtrlAgentCommandMgrTest, noServerSocket) { + configureControlSocket("dhcp6"); + + ConstElementPtr command = createCommand("foo", "dhcp6"); + ConstElementPtr answer = mgr_.handleCommand("foo", ConstElementPtr(), + command); + + checkAnswer(answer, isc::config::CONTROL_RESULT_ERROR); +} + +// Check that list-commands command is forwarded when the service +// value is specified. +TEST_F(CtrlAgentCommandMgrTest, forwardListCommands) { + // Configure client side socket. + configureControlSocket("dhcp4"); + // Create server side socket. + bindServerSocket("{ \"result\" : 3 }", true); + + // The client side communication is synchronous. To be able to respond + // to this we need to run the server side socket at the same time. + // Running IO service in a thread guarantees that the server responds + // as soon as it receives the control command. + std::thread th(std::bind(&IOService::run, getIOService().get())); + + // Wait for the IO service in thread to actually run. + server_socket_->waitForRunning(); + + ConstElementPtr command = createCommand("list-commands", "dhcp4"); + ConstElementPtr answer = mgr_.handleCommand("list-commands", ConstElementPtr(), + command); + + // Stop IO service immediately and let the thread die. + getIOService()->stop(); + + // Wait for the thread to finish. + th.join(); + + // Cancel all asynchronous operations on the server. + server_socket_->stopServer(); + + // We have some cancelled operations for which we need to invoke the + // handlers with the operation_aborted error code. + getIOService()->get_io_service().reset(); + getIOService()->poll(); + + // Answer of 3 is specific to the stub response we send when the + // command is forwarded. So having this value returned means that + // the command was forwarded as expected. + checkAnswer(answer, 3); +} + +} |