|
@@ -15,6 +15,7 @@
|
|
|
#include <config.h>
|
|
|
|
|
|
#include <cc/command_interpreter.h>
|
|
|
+#include <config/command_mgr.h>
|
|
|
#include <dhcpsrv/cfgmgr.h>
|
|
|
#include <dhcp6/ctrl_dhcp6_srv.h>
|
|
|
#include <hooks/hooks_manager.h>
|
|
@@ -25,7 +26,11 @@
|
|
|
#include <boost/scoped_ptr.hpp>
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
+#include <sys/select.h>
|
|
|
+#include <sys/ioctl.h>
|
|
|
+
|
|
|
using namespace std;
|
|
|
+using namespace isc::config;
|
|
|
using namespace isc::data;
|
|
|
using namespace isc::dhcp;
|
|
|
using namespace isc::dhcp::test;
|
|
@@ -33,10 +38,150 @@ 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) {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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);
|
|
|
+
|
|
|
+ // 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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() {
|
|
|
+ fd_set read_fds;
|
|
|
+ int maxfd = 0;
|
|
|
+
|
|
|
+ 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 {
|
|
|
// "Naked" DHCPv6 server, exposes internal fields
|
|
|
public:
|
|
|
- NakedControlledDhcpv6Srv():ControlledDhcpv6Srv(DHCP6_SERVER_PORT + 10000) { }
|
|
|
+ NakedControlledDhcpv6Srv():ControlledDhcpv6Srv(DHCP6_SERVER_PORT + 10000) {
|
|
|
+ }
|
|
|
+
|
|
|
+ /// @brief Exposes server's receivePacket method
|
|
|
+ virtual Pkt6Ptr receivePacket(int timeout) {
|
|
|
+ return(Dhcpv6Srv::receivePacket(timeout));
|
|
|
+ }
|
|
|
+
|
|
|
};
|
|
|
|
|
|
class CtrlDhcpv6SrvTest : public ::testing::Test {
|
|
@@ -45,24 +190,124 @@ public:
|
|
|
reset();
|
|
|
}
|
|
|
|
|
|
- ~CtrlDhcpv6SrvTest() {
|
|
|
+ virtual ~CtrlDhcpv6SrvTest() {
|
|
|
reset();
|
|
|
};
|
|
|
|
|
|
+
|
|
|
/// @brief Reset hooks data
|
|
|
///
|
|
|
/// Resets the data for the hooks-related portion of the test by ensuring
|
|
|
/// that no libraries are loaded and that any marker files are deleted.
|
|
|
- void reset() {
|
|
|
+ virtual void reset() {
|
|
|
// Unload any previously-loaded libraries.
|
|
|
HooksManager::unloadLibraries();
|
|
|
|
|
|
// Get rid of any marker files.
|
|
|
static_cast<void>(unlink(LOAD_MARKER_FILE));
|
|
|
static_cast<void>(unlink(UNLOAD_MARKER_FILE));
|
|
|
+ IfaceMgr::instance().deleteAllExternalSockets();
|
|
|
+ CfgMgr::instance().clear();
|
|
|
+ }
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+class CtrlChannelDhcpv6SrvTest : public CtrlDhcpv6SrvTest {
|
|
|
+public:
|
|
|
+ std::string socket_path_;
|
|
|
+ boost::shared_ptr<NakedControlledDhcpv6Srv> server_;
|
|
|
+
|
|
|
+ CtrlChannelDhcpv6SrvTest() {
|
|
|
+ socket_path_ = string(TEST_DATA_DIR) + "/kea6.sock";
|
|
|
+ reset();
|
|
|
+ }
|
|
|
+
|
|
|
+ ~CtrlChannelDhcpv6SrvTest() {
|
|
|
+ server_.reset();
|
|
|
+ reset();
|
|
|
+ };
|
|
|
+
|
|
|
+ void createUnixChannelServer() {
|
|
|
+ ::remove(socket_path_.c_str());
|
|
|
+
|
|
|
+ // Just a simple config. The important part here is the socket
|
|
|
+ // location information.
|
|
|
+ std::string config_txt =
|
|
|
+ "{"
|
|
|
+ " \"interfaces-config\": {"
|
|
|
+ " \"interfaces\": [ \"*\" ]"
|
|
|
+ " },"
|
|
|
+ " \"rebind-timer\": 2000, "
|
|
|
+ " \"renew-timer\": 1000, "
|
|
|
+ " \"subnet6\": [ ],"
|
|
|
+ " \"valid-lifetime\": 4000,"
|
|
|
+ " \"control-socket\": {"
|
|
|
+ " \"socket-type\": \"unix\","
|
|
|
+ " \"socket-name\": \"" + socket_path_ + "\""
|
|
|
+ " },"
|
|
|
+ " \"lease-database\": {"
|
|
|
+ " \"type\": \"memfile\", \"persist\": false }"
|
|
|
+ "}";
|
|
|
+
|
|
|
+ ASSERT_NO_THROW(server_.reset(new NakedControlledDhcpv6Srv()));
|
|
|
+
|
|
|
+ ConstElementPtr config = Element::fromJSON(config_txt);
|
|
|
+ ConstElementPtr answer = server_->processConfig(config);
|
|
|
+ ASSERT_TRUE(answer);
|
|
|
+
|
|
|
+ int status = 0;
|
|
|
+ isc::config::parseAnswer(status, answer);
|
|
|
+ ASSERT_EQ(0, status);
|
|
|
+
|
|
|
+ // Now check that the socket was indeed open.
|
|
|
+ ASSERT_GT(isc::config::CommandMgr::instance().getControlSocketFD(), -1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// @brief Reset
|
|
|
+ void reset() {
|
|
|
+ CtrlDhcpv6SrvTest::reset();
|
|
|
+ ::remove(socket_path_.c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ /// @brief Conducts a command/response exchange via UnixCommandSocket
|
|
|
+ ///
|
|
|
+ /// 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.
|
|
|
+ ///
|
|
|
+ /// @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<UnixControlClient> client;
|
|
|
+ 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
|
|
|
+ ASSERT_TRUE(client->connectToServer(socket_path_));
|
|
|
+ ASSERT_NO_THROW(server_->receivePacket(0));
|
|
|
+
|
|
|
+ // 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));
|
|
|
+
|
|
|
+ // 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));
|
|
|
+
|
|
|
+ // Now disconnect and process the close event
|
|
|
+ client->disconnectFromServer();
|
|
|
+ ASSERT_NO_THROW(server_->receivePacket(0));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+
|
|
|
TEST_F(CtrlDhcpv6SrvTest, commands) {
|
|
|
|
|
|
boost::scoped_ptr<ControlledDhcpv6Srv> srv;
|
|
@@ -202,4 +447,114 @@ TEST_F(CtrlDhcpv6SrvTest, configReload) {
|
|
|
CfgMgr::instance().clear();
|
|
|
}
|
|
|
|
|
|
+// This test checks which commands are registered by the DHCPv4 server.
|
|
|
+TEST_F(CtrlDhcpv6SrvTest, commandsRegistration) {
|
|
|
+
|
|
|
+ ConstElementPtr list_cmds = createCommand("list-commands");
|
|
|
+ ConstElementPtr answer;
|
|
|
+
|
|
|
+ // By default the list should be empty (except the standard list-commands
|
|
|
+ // supported by the CommandMgr itself)
|
|
|
+ EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
|
|
|
+ ASSERT_TRUE(answer);
|
|
|
+ ASSERT_TRUE(answer->get("arguments"));
|
|
|
+ EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str());
|
|
|
+
|
|
|
+ // Created server should register several additional commands.
|
|
|
+ boost::scoped_ptr<ControlledDhcpv6Srv> srv;
|
|
|
+ ASSERT_NO_THROW(
|
|
|
+ srv.reset(new ControlledDhcpv6Srv(0));
|
|
|
+ );
|
|
|
+
|
|
|
+ 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());
|
|
|
+
|
|
|
+ // Ok, and now delete the server. It should deregister its commands.
|
|
|
+ srv.reset();
|
|
|
+
|
|
|
+ // The list should be (almost) empty again.
|
|
|
+ EXPECT_NO_THROW(answer = CommandMgr::instance().processCommand(list_cmds));
|
|
|
+ ASSERT_TRUE(answer);
|
|
|
+ ASSERT_TRUE(answer->get("arguments"));
|
|
|
+ EXPECT_EQ("[ \"list-commands\" ]", answer->get("arguments")->str());
|
|
|
+}
|
|
|
+
|
|
|
+// Tests that the server properly responds to invalid commands sent
|
|
|
+// via ControlChannel
|
|
|
+TEST_F(CtrlChannelDhcpv6SrvTest, 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 <string>:1:2\" }",
|
|
|
+ response);
|
|
|
+}
|
|
|
+
|
|
|
+// Tests that the server properly responds to shtudown command sent
|
|
|
+// via ControlChannel
|
|
|
+TEST_F(CtrlChannelDhcpv6SrvTest, 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 Dhcpv6 statistics.
|
|
|
+TEST_F(CtrlChannelDhcpv6SrvTest, 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
|