diff --git a/src/bin/dhcp4/tests/Makefile.am b/src/bin/dhcp4/tests/Makefile.am index cc7d534da6..f4a025c687 100644 --- a/src/bin/dhcp4/tests/Makefile.am +++ b/src/bin/dhcp4/tests/Makefile.am @@ -111,6 +111,7 @@ dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtes dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/io/libkea-util-io.la dhcp4_unittests_LDADD += $(top_builddir)/src/lib/cfgrpt/libcfgrpt.la dhcp4_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la +dhcp4_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la endif noinst_EXTRA_DIST = configs-list.txt diff --git a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc index 8c56930d95..b286a5a4d6 100644 --- a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc @@ -18,7 +18,9 @@ #include #include #include +#include #include +#include #include "marker_file.h" #include "test_libraries.h" @@ -47,19 +49,99 @@ namespace { class NakedControlledDhcpv4Srv: public ControlledDhcpv4Srv { // "Naked" DHCPv4 server, exposes internal fields public: - NakedControlledDhcpv4Srv():ControlledDhcpv4Srv(DHCP4_SERVER_PORT + 10000) { } + NakedControlledDhcpv4Srv():ControlledDhcpv4Srv(0) { } + + /// Expose internal methods for the sake of testing + using Dhcpv4Srv::receivePacket; }; -class CtrlDhcpv4SrvTest : public ::testing::Test { +/// @brief Fixture class intended for testin control channel in the DHCPv4Srv +class CtrlChannelDhcpv4SrvTest : public ::testing::Test { public: - CtrlDhcpv4SrvTest() { + + /// @brief Path to the UNIX socket being used to communicate with the server + std::string socket_path_; + + /// @brief Pointer to the tested server object + boost::shared_ptr server_; + + /// @brief Default constructor + /// + /// Sets socket path to its default value. + CtrlChannelDhcpv4SrvTest() { + const char* env = getenv("KEA_SOCKET_TEST_DIR"); + if (env) { + socket_path_ = string(env) + "/kea4.sock"; + } else { + socket_path_ = string(TEST_DATA_BUILDDIR) + "/kea4.sock"; + } reset(); + + // This is a workaround for odd problems with gtest. gtest does + // shady with socket decriptors. In particular, sometimes we + // get 0 as descriptor for socket() call. Technically it is valid, + // but then gtest closes descriptor 0 and the socket becomes + // unusable. This workaround opens up one file decriptor. In case + // 0 is available, it will be consumed here. + dummy_fd_ = socket(AF_INET, SOCK_DGRAM, 0); + if (dummy_fd_ == 0) { + std::cout << "Socket descriptor 0 workaround is useful." << std::endl; + } } - ~CtrlDhcpv4SrvTest() { + /// @brief Destructor + ~CtrlChannelDhcpv4SrvTest() { + server_.reset(); reset(); + + // close dummy descriptor + close(dummy_fd_); }; + void createUnixChannelServer() { + ::remove(socket_path_.c_str()); + + // Just a simple config. The important part here is the socket + // location information. + std::string header = + "{" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"rebind-timer\": 2000, " + " \"renew-timer\": 1000, " + " \"subnet4\": [ ]," + " \"valid-lifetime\": 4000," + " \"control-socket\": {" + " \"socket-type\": \"unix\"," + " \"socket-name\": \""; + + std::string footer = + "\" }," + " \"lease-database\": {" + " \"type\": \"memfile\", \"persist\": false }" + "}"; + + // Fill in the socket-name value with socket_path_ to + // make the actual configuration text. + std::string config_txt = header + socket_path_ + footer; + + ASSERT_NO_THROW(server_.reset(new NakedControlledDhcpv4Srv())); + + ConstElementPtr config = Element::fromJSON(config_txt); + ConstElementPtr answer = server_->processConfig(config); + ASSERT_TRUE(answer); + + int status = 0; + ConstElementPtr txt = isc::config::parseAnswer(status, answer); + // This should succeed. If not, print the error message. + ASSERT_EQ(0, status) << txt->str(); + + // Now check that the socket was indeed open. + ASSERT_GT(isc::config::CommandMgr::instance().getControlSocketFD(), -1); + } + + /// @brief Reset hooks data /// /// Resets the data for the hooks-related portion of the test by ensuring @@ -71,79 +153,61 @@ public: // Get rid of any marker files. static_cast(remove(LOAD_MARKER_FILE)); static_cast(remove(UNLOAD_MARKER_FILE)); + + IfaceMgr::instance().deleteAllExternalSockets(); + CfgMgr::instance().clear(); + + // Remove unix socket file + ::remove(socket_path_.c_str()); } - /// @brief sends commands over specified UNIX socket + /// @brief Conducts a command/response exchange via UnixCommandSocket /// - /// @param command command to be sent (should be valid JSON) - /// @param response response received (expected to be a valid JSON) - /// @param socket_path UNIX socket path + /// This method connects to the given server over the given socket path. + /// If successful, it then sends the given command and retrieves the + /// server's response. Note that it calls the server's receivePacket() + /// method where needed to cause the server to process IO events on + /// control channel the control channel sockets. /// - /// @return true if send/response exchange was successful, false otherwise - bool sendCommandUnixSocket(const std::string& command, - std::string& response, - const std::string& socket_path) { + /// @param command the command text to execute in JSON form + /// @param response variable into which the received response should be + /// placed. + void sendUnixCommand(const std::string& command, std::string& response) { + response = ""; + boost::scoped_ptr client; + client.reset(new UnixControlClient()); + ASSERT_TRUE(client); - // Create UNIX socket - int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (socket_fd < 0) { - ADD_FAILURE() << "Failed to open unix stream socket."; - return (false); - } + // Connect and then call server's receivePacket() so it can + // detect the control socket connect and call the accept handler + ASSERT_TRUE(client->connectToServer(socket_path_)); + ASSERT_NO_THROW(server_->receivePacket(0)); - // Prepare socket address - struct sockaddr_un srv_addr; - memset(&srv_addr, 0, sizeof(struct sockaddr_un)); - srv_addr.sun_family = AF_UNIX; - strncpy(srv_addr.sun_path, socket_path.c_str(), sizeof(srv_addr.sun_path)); - socklen_t len = sizeof(srv_addr); + // Send the command and then call server's receivePacket() so it can + // detect the inbound data and call the read handler + ASSERT_TRUE(client->sendCommand(command)); + ASSERT_NO_THROW(server_->receivePacket(0)); - // Connect to the specified UNIX socket - int status = connect(socket_fd, (struct sockaddr*)&srv_addr, len); - if (status == -1) { - ADD_FAILURE() << "Failed to connect unix socket: fd=" << socket_fd - << ", path=" << socket_path; - close(socket_fd); - return (false); - } + // Read the response generated by the server. Note that getResponse + // only fails if there an IO error or no response data was present. + // It is not based on the response content. + ASSERT_TRUE(client->getResponse(response)); - - // Send command - cout << "Sending command: " << command << endl; - int bytes_sent = send(socket_fd, command.c_str(), command.length(), 0); - if (bytes_sent < command.length()) { - ADD_FAILURE() << "Failed to send " << command.length() - << " bytes, send() returned " << bytes_sent; - close(socket_fd); - return (false); - } - - // Receive response - /// @todo: this may block if server fails to respond. Some sort of - /// of a timer is needed. - char buf[65536]; - memset(buf, 0, sizeof(buf)); - int bytes_rcvd = recv(socket_fd, buf, sizeof(buf), 0); - if (bytes_rcvd < 0) { - ADD_FAILURE() << "Failed to receive a response. recv() returned " - << bytes_rcvd; - close(socket_fd); - return (false); - } - - // Convert the response to a string, close the socket and return - response = string(buf, bytes_rcvd); - cout << "Received response: " << response << endl; - close(socket_fd); - return (true); + // Now disconnect and process the close event + client->disconnectFromServer(); + ASSERT_NO_THROW(server_->receivePacket(0)); } + + /// @brief dummy file descriptor + /// + /// See ctor for details. + int dummy_fd_; }; -TEST_F(CtrlDhcpv4SrvTest, commands) { +TEST_F(CtrlChannelDhcpv4SrvTest, commands) { - boost::scoped_ptr srv; ASSERT_NO_THROW( - srv.reset(new ControlledDhcpv4Srv(DHCP4_SERVER_PORT + 10000)) + server_.reset(new NakedControlledDhcpv4Srv()); ); // Use empty parameters list @@ -172,13 +236,12 @@ TEST_F(CtrlDhcpv4SrvTest, commands) { // Check that the "libreload" command will reload libraries -TEST_F(CtrlDhcpv4SrvTest, libreload) { +TEST_F(CtrlChannelDhcpv4SrvTest, libreload) { // Sending commands for processing now requires a server that can process // them. - boost::scoped_ptr srv; ASSERT_NO_THROW( - srv.reset(new ControlledDhcpv4Srv(0)) + server_.reset(new NakedControlledDhcpv4Srv()); ); // Ensure no marker files to start with. @@ -223,7 +286,7 @@ TEST_F(CtrlDhcpv4SrvTest, libreload) { } // This test checks which commands are registered by the DHCPv4 server. -TEST_F(CtrlDhcpv4SrvTest, commandsRegistration) { +TEST_F(CtrlChannelDhcpv4SrvTest, commandsRegistration) { ConstElementPtr list_cmds = createCommand("list-commands"); ConstElementPtr answer; @@ -236,22 +299,25 @@ TEST_F(CtrlDhcpv4SrvTest, commandsRegistration) { EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str()); // Created server should register several additional commands. - boost::scoped_ptr srv; ASSERT_NO_THROW( - srv.reset(new ControlledDhcpv4Srv(0)); + server_.reset(new NakedControlledDhcpv4Srv()); ); EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds)); ASSERT_TRUE(answer); ASSERT_TRUE(answer->get("arguments")); - EXPECT_EQ("[ \"list-commands\", \"shutdown\", " - "\"statistic-get\", \"statistic-get-all\", " - "\"statistic-remove\", \"statistic-remove-all\", " - "\"statistic-reset\", \"statistic-reset-all\" ]", - answer->get("arguments")->str()); + std::string command_list = answer->get("arguments")->str(); + + EXPECT_TRUE(command_list.find("\"list-commands\"") != 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); // Ok, and now delete the server. It should deregister its commands. - srv.reset(); + server_.reset(); // The list should be (almost) empty again. EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds)); @@ -260,4 +326,77 @@ TEST_F(CtrlDhcpv4SrvTest, commandsRegistration) { EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str()); } +// Tests that the server properly responds to invalid commands sent +// via ControlChannel +TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelNegative) { + createUnixChannelServer(); + std::string response; + + sendUnixCommand("{ \"command\": \"bogus\" }", response); + EXPECT_EQ("{ \"result\": 1," + " \"text\": \"'bogus' command not supported.\" }", response); + + sendUnixCommand("utter nonsense", response); + EXPECT_EQ("{ \"result\": 1, " + "\"text\": \"error: unexpected character u in :1:2\" }", + response); +} + +// Tests that the server properly responds to shtudown command sent +// via ControlChannel +TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelShutdown) { + createUnixChannelServer(); + std::string response; + + sendUnixCommand("{ \"command\": \"shutdown\" }", response); + EXPECT_EQ("{ \"result\": 0, \"text\": \"Shutting down.\" }",response); +} + +// Tests that the server properly responds to statistics commands. Note this +// is really only intended to verify that the appropriate Statistics handler +// is called based on the command. It is not intended to be an exhaustive +// test of Dhcpv4 statistics. +TEST_F(CtrlChannelDhcpv4SrvTest, controlChannelStats) { + createUnixChannelServer(); + std::string response; + + // Check statistic-get + sendUnixCommand("{ \"command\" : \"statistic-get\", " + " \"arguments\": {" + " \"name\":\"bogus\" }}", response); + EXPECT_EQ("{ \"arguments\": { }, \"result\": 0 }", response); + + // Check statistic-get-all + sendUnixCommand("{ \"command\" : \"statistic-get-all\", " + " \"arguments\": {}}", response); + EXPECT_EQ("{ \"arguments\": { }, \"result\": 0 }", response); + + // Check statistic-reset + sendUnixCommand("{ \"command\" : \"statistic-reset\", " + " \"arguments\": {" + " \"name\":\"bogus\" }}", response); + EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'bogus' statistic found\" }", + response); + + // Check statistic-reset-all + sendUnixCommand("{ \"command\" : \"statistic-reset-all\", " + " \"arguments\": {}}", response); + EXPECT_EQ("{ \"result\": 0, \"text\": " + "\"All statistics reset to neutral values.\" }", response); + + // Check statistic-remove + sendUnixCommand("{ \"command\" : \"statistic-remove\", " + " \"arguments\": {" + " \"name\":\"bogus\" }}", response); + EXPECT_EQ("{ \"result\": 1, \"text\": \"No 'bogus' statistic found\" }", + response); + + // Check statistic-remove-all + sendUnixCommand("{ \"command\" : \"statistic-remove-all\", " + " \"arguments\": {}}", response); + EXPECT_EQ("{ \"result\": 0, \"text\": \"All statistics removed.\" }", + response); +} + + } // End of anonymous namespace diff --git a/src/bin/dhcp6/tests/Makefile.am b/src/bin/dhcp6/tests/Makefile.am index f00c7520dd..335fd075ae 100644 --- a/src/bin/dhcp6/tests/Makefile.am +++ b/src/bin/dhcp6/tests/Makefile.am @@ -113,6 +113,7 @@ dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/io/libkea-util-io.la dhcp6_unittests_LDADD += $(top_builddir)/src/lib/cfgrpt/libcfgrpt.la dhcp6_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la +dhcp6_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la endif diff --git a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc index 86fc34c437..cab9f19408 100644 --- a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include "marker_file.h" #include "test_libraries.h" @@ -39,150 +40,6 @@ using namespace isc::hooks; namespace { -/// Class that acts as a UnixCommandSocket client -/// It can connect to an open UnixCommandSocket and exchange ControlChannel -/// commands and responses. -class UnixControlClient { -public: - UnixControlClient() { - socket_fd_ = -1; - } - - ~UnixControlClient() { - disconnectFromServer(); - } - - /// @brief Closes the Control Channel socket - void disconnectFromServer() { - if (socket_fd_ >= 0) { - static_cast(close(socket_fd_)); - socket_fd_ = -1; - } - } - - /// @brief Connects to a Unix socket at the given path - /// @param socket_path pathname of the socket to open - /// @return true if the connect was successful, false otherwise - bool connectToServer(const std::string& socket_path) { - // Create UNIX socket - socket_fd_ = socket(AF_UNIX, SOCK_STREAM, 0); - if (socket_fd_ < 0) { - const char* errmsg = strerror(errno); - ADD_FAILURE() << "Failed to open unix stream socket: " << errmsg; - return (false); - } - - struct sockaddr_un srv_addr; - if (socket_path.size() > sizeof(srv_addr.sun_path) - 1) { - ADD_FAILURE() << "Socket path specified (" << socket_path - << ") is larger than " << (sizeof(srv_addr.sun_path) - 1) - << " allowed."; - disconnectFromServer(); - return (false); - } - - // Prepare socket address - memset(&srv_addr, 0, sizeof(srv_addr)); - srv_addr.sun_family = AF_UNIX; - strncpy(srv_addr.sun_path, socket_path.c_str(), - sizeof(srv_addr.sun_path)); - socklen_t len = sizeof(srv_addr); - - // Connect to the specified UNIX socket - int status = connect(socket_fd_, (struct sockaddr*)&srv_addr, len); - if (status == -1) { - const char* errmsg = strerror(errno); - ADD_FAILURE() << "Failed to connect unix socket: fd=" << socket_fd_ - << ", path=" << socket_path << " : " << errmsg; - disconnectFromServer(); - return (false); - } - - return (true); - } - - /// @brief Sends the given command across the open Control Channel - /// @param command the command text to execute in JSON form - /// @return true if the send succeeds, false otherwise - bool sendCommand(const std::string& command) { - // Send command - int bytes_sent = send(socket_fd_, command.c_str(), command.length(), 0); - if (bytes_sent < command.length()) { - const char* errmsg = strerror(errno); - ADD_FAILURE() << "Failed to send " << command.length() - << " bytes, send() returned " << bytes_sent - << " : " << errmsg; - return (false); - } - - return (true); - } - - /// @brief Reads the response text from the open Control Channel - /// @param response variable into which the received response should be - /// placed. - /// @return true if data was successfully read from the socket, - /// false otherwise - bool getResponse(std::string& response) { - // Receive response - // @todo implement select check to see if data is waiting - char buf[65536]; - memset(buf, 0, sizeof(buf)); - switch (selectCheck()) { - case -1: { - const char* errmsg = strerror(errno); - ADD_FAILURE() << "getResponse - select failed: " << errmsg; - return (false); - } - case 0: - ADD_FAILURE() << "No response data sent"; - return (false); - - default: - break; - } - - int bytes_rcvd = recv(socket_fd_, buf, sizeof(buf), 0); - if (bytes_rcvd < 0) { - const char* errmsg = strerror(errno); - ADD_FAILURE() << "Failed to receive a response. recv() returned " - << bytes_rcvd << " : " << errmsg; - return (false); - } - - if (bytes_rcvd >= sizeof(buf)) { - ADD_FAILURE() << "Response size too large: " << bytes_rcvd; - return (false); - } - - // Convert the response to a string - response = string(buf, bytes_rcvd); - return (true); - } - - - /// @brief Uses select to poll the Control Channel for data waiting - /// @return -1 on error, 0 if no data is available, 1 if data is ready - int selectCheck() { - int maxfd = 0; - - fd_set read_fds; - FD_ZERO(&read_fds); - - // Add this socket to listening set - FD_SET(socket_fd_, &read_fds); - maxfd = socket_fd_; - - struct timeval select_timeout; - select_timeout.tv_sec = 0; - select_timeout.tv_usec = 0; - - return (select(maxfd + 1, &read_fds, NULL, NULL, &select_timeout)); - } - - /// @brief Retains the fd of the open socket - int socket_fd_; -}; class NakedControlledDhcpv6Srv: public ControlledDhcpv6Srv { @@ -191,11 +48,8 @@ public: NakedControlledDhcpv6Srv():ControlledDhcpv6Srv(DHCP6_SERVER_PORT + 10000) { } - /// @brief Exposes server's receivePacket method - virtual Pkt6Ptr receivePacket(int timeout) { - return(Dhcpv6Srv::receivePacket(timeout)); - } - + /// Expose internal methods for the sake of testing + using Dhcpv6Srv::receivePacket; }; class CtrlDhcpv6SrvTest : public ::testing::Test { @@ -228,9 +82,16 @@ public: class CtrlChannelDhcpv6SrvTest : public CtrlDhcpv6SrvTest { public: + + /// @brief Path to the UNIX socket being used to communicate with the server std::string socket_path_; + + /// @brief Pointer to the tested server object boost::shared_ptr server_; + /// @brief Default constructor + /// + /// Sets socket path to its default value. CtrlChannelDhcpv6SrvTest() { const char* env = getenv("KEA_SOCKET_TEST_DIR"); if (env) { @@ -241,6 +102,7 @@ public: reset(); } + /// @brief Destructor ~CtrlChannelDhcpv6SrvTest() { server_.reset(); reset(); diff --git a/src/lib/testutils/Makefile.am b/src/lib/testutils/Makefile.am index 90f4419027..769b5cff3f 100644 --- a/src/lib/testutils/Makefile.am +++ b/src/lib/testutils/Makefile.am @@ -11,6 +11,7 @@ noinst_LTLIBRARIES = libkea-testutils.la libkea_testutils_la_SOURCES = srv_test.h srv_test.cc libkea_testutils_la_SOURCES += dnsmessage_test.h dnsmessage_test.cc +libkea_testutils_la_SOURCES += unix_control_client.h unix_control_client.cc libkea_testutils_la_SOURCES += mockups.h libkea_testutils_la_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) libkea_testutils_la_LIBADD = $(top_builddir)/src/lib/asiolink/libkea-asiolink.la diff --git a/src/lib/testutils/unix_control_client.cc b/src/lib/testutils/unix_control_client.cc new file mode 100644 index 0000000000..d22f3e471f --- /dev/null +++ b/src/lib/testutils/unix_control_client.cc @@ -0,0 +1,150 @@ +// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include +#include + +namespace isc { +namespace dhcp { +namespace test { + +UnixControlClient::UnixControlClient() { + socket_fd_ = -1; +} + +UnixControlClient::~UnixControlClient() { + disconnectFromServer(); +} + + /// @brief Closes the Control Channel socket +void UnixControlClient::disconnectFromServer() { + if (socket_fd_ >= 0) { + static_cast(close(socket_fd_)); + socket_fd_ = -1; + } +} + +bool UnixControlClient::connectToServer(const std::string& socket_path) { + // Create UNIX socket + socket_fd_ = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd_ < 0) { + const char* errmsg = strerror(errno); + ADD_FAILURE() << "Failed to open unix stream socket: " << errmsg; + return (false); + } + + struct sockaddr_un srv_addr; + if (socket_path.size() > sizeof(srv_addr.sun_path) - 1) { + ADD_FAILURE() << "Socket path specified (" << socket_path + << ") is larger than " << (sizeof(srv_addr.sun_path) - 1) + << " allowed."; + disconnectFromServer(); + return (false); + } + + // Prepare socket address + memset(&srv_addr, 0, sizeof(srv_addr)); + srv_addr.sun_family = AF_UNIX; + strncpy(srv_addr.sun_path, socket_path.c_str(), + sizeof(srv_addr.sun_path)); + socklen_t len = sizeof(srv_addr); + + // Connect to the specified UNIX socket + int status = connect(socket_fd_, (struct sockaddr*)&srv_addr, len); + if (status == -1) { + const char* errmsg = strerror(errno); + ADD_FAILURE() << "Failed to connect unix socket: fd=" << socket_fd_ + << ", path=" << socket_path << " : " << errmsg; + disconnectFromServer(); + return (false); + } + + return (true); +} + +bool UnixControlClient::sendCommand(const std::string& command) { + // Send command + int bytes_sent = send(socket_fd_, command.c_str(), command.length(), 0); + if (bytes_sent < command.length()) { + const char* errmsg = strerror(errno); + ADD_FAILURE() << "Failed to send " << command.length() + << " bytes, send() returned " << bytes_sent + << " : " << errmsg; + return (false); + } + + return (true); +} + +bool UnixControlClient::getResponse(std::string& response) { + // Receive response + char buf[65536]; + memset(buf, 0, sizeof(buf)); + switch (selectCheck()) { + case -1: { + const char* errmsg = strerror(errno); + ADD_FAILURE() << "getResponse - select failed: " << errmsg; + return (false); + } + case 0: + ADD_FAILURE() << "No response data sent"; + return (false); + + default: + break; + } + + int bytes_rcvd = recv(socket_fd_, buf, sizeof(buf), 0); + if (bytes_rcvd < 0) { + const char* errmsg = strerror(errno); + ADD_FAILURE() << "Failed to receive a response. recv() returned " + << bytes_rcvd << " : " << errmsg; + return (false); + } + + if (bytes_rcvd >= sizeof(buf)) { + ADD_FAILURE() << "Response size too large: " << bytes_rcvd; + return (false); + } + + // Convert the response to a string + response = std::string(buf, bytes_rcvd); + return (true); +} + +int UnixControlClient::selectCheck() { + int maxfd = 0; + + fd_set read_fds; + FD_ZERO(&read_fds); + + // Add this socket to listening set + FD_SET(socket_fd_, &read_fds); + maxfd = socket_fd_; + + struct timeval select_timeout; + select_timeout.tv_sec = 0; + select_timeout.tv_usec = 0; + + return (select(maxfd + 1, &read_fds, NULL, NULL, &select_timeout)); +} + +}; +}; +}; diff --git a/src/lib/testutils/unix_control_client.h b/src/lib/testutils/unix_control_client.h new file mode 100644 index 0000000000..c08e51d6e5 --- /dev/null +++ b/src/lib/testutils/unix_control_client.h @@ -0,0 +1,71 @@ +// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef UNIX_CONTROL_CLIENT_H +#define UNIX_CONTROL_CLIENT_H + +#include + +namespace isc { +namespace dhcp { +namespace test { + +/// @brief Class that acts as a UnixCommandSocket client +/// +/// This class is expected to be used unit-tests that attempt to communicate +/// with the servers that use control channel (see src/lib/config/command_mgr.h) +/// It can connect to an open UnixCommandSocket and exchange ControlChannel +/// commands and responses. +class UnixControlClient { +public: + + /// @brief Default constructor + UnixControlClient(); + + /// @brief Destructor + ~UnixControlClient(); + + /// @brief Closes the Control Channel socket + void disconnectFromServer(); + + /// @brief Connects to a Unix socket at the given path + /// @param socket_path pathname of the socket to open + /// @return true if the connect was successful, false otherwise + bool connectToServer(const std::string& socket_path); + + /// @brief Sends the given command across the open Control Channel + /// @param command the command text to execute in JSON form + /// @return true if the send succeeds, false otherwise + bool sendCommand(const std::string& command); + + /// @brief Reads the response text from the open Control Channel + /// @param response variable into which the received response should be + /// placed. + /// @return true if data was successfully read from the socket, + /// false otherwise + bool getResponse(std::string& response); + + /// @brief Uses select to poll the Control Channel for data waiting + /// @return -1 on error, 0 if no data is available, 1 if data is ready + int selectCheck(); + + /// @brief Retains the fd of the open socket + int socket_fd_; +}; + +}; // end of isc::dhcp::test namespace +}; // end of isc::dhcp namespace +}; // end of isc namespace + +#endif // UNIX_CONTROL_CLIENT_H