From e103da116202f52aa5e24415e494ed25cb97441c Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Thu, 18 Jun 2015 15:11:22 -0400 Subject: [PATCH] [3797] Added support for Control Channel to DHCPv6 src/bin/dhcp6/ctrl_dhcp6_srv.cc ControlledDhcpv6Srv::ControlledDhcpv6Srv() added CommandMgr init and handler registration ControlledDhcpv6Srv::~ControlledDhcpv6Srv() { added CommandMgr shutdown and handler deregistration src/bin/dhcp6/json_config_parser.cc - createGlobal6DhcpConfigParser() added support for "control-socket" element - configureDhcp6Server() added logic to configure CommandMgr based on control-socket configuration element src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc - UnixControlClient - new class that acts as UnixCommandSocket client - CtrlChannelDhcpv6SrvTest - new test fixture for testing a DHCPv6 server with a Control Channel - Added the following tests: TEST_F(CtrlDhcpv6SrvTest, commandsRegistration) TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelNegative) TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelShutdown) TEST_F(CtrlChannelDhcpv6SrvTest, controlChannelStats) --- src/bin/dhcp6/ctrl_dhcp6_srv.cc | 46 ++- src/bin/dhcp6/json_config_parser.cc | 23 ++ src/bin/dhcp6/tests/Makefile.am | 2 + .../dhcp6/tests/ctrl_dhcp6_srv_unittest.cc | 361 +++++++++++++++++- 4 files changed, 427 insertions(+), 5 deletions(-) diff --git a/src/bin/dhcp6/ctrl_dhcp6_srv.cc b/src/bin/dhcp6/ctrl_dhcp6_srv.cc index 844efac4c5..7a1e84b230 100644 --- a/src/bin/dhcp6/ctrl_dhcp6_srv.cc +++ b/src/bin/dhcp6/ctrl_dhcp6_srv.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2014-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 @@ -14,14 +14,18 @@ #include #include +#include #include #include #include -#include #include +#include +#include +using namespace isc::config; using namespace isc::data; using namespace isc::hooks; +using namespace isc::stats; using namespace std; namespace isc { @@ -158,6 +162,32 @@ ControlledDhcpv6Srv::ControlledDhcpv6Srv(uint16_t port) "There is another Dhcpv6Srv instance already."); } server_ = this; // remember this instance for use in callback + + // Register supported commands in CommandMgr + CommandMgr::instance().registerCommand("shutdown", + boost::bind(&ControlledDhcpv6Srv::commandShutdownHandler, this, _1, _2)); + + /// @todo: register config-reload (see CtrlDhcpv4Srv::commandConfigReloadHandler) + /// @todo: register libreload (see CtrlDhcpv4Srv::commandLibReloadHandler) + + // Register statistic related commands + CommandMgr::instance().registerCommand("statistic-get", + boost::bind(&StatsMgr::statisticGetHandler, _1, _2)); + + CommandMgr::instance().registerCommand("statistic-reset", + boost::bind(&StatsMgr::statisticResetHandler, _1, _2)); + + CommandMgr::instance().registerCommand("statistic-remove", + boost::bind(&StatsMgr::statisticRemoveHandler, _1, _2)); + + CommandMgr::instance().registerCommand("statistic-get-all", + boost::bind(&StatsMgr::statisticGetAllHandler, _1, _2)); + + CommandMgr::instance().registerCommand("statistic-reset-all", + boost::bind(&StatsMgr::statisticResetAllHandler, _1, _2)); + + CommandMgr::instance().registerCommand("statistic-remove-all", + boost::bind(&StatsMgr::statisticRemoveAllHandler, _1, _2)); } void ControlledDhcpv6Srv::shutdown() { @@ -168,6 +198,18 @@ void ControlledDhcpv6Srv::shutdown() { ControlledDhcpv6Srv::~ControlledDhcpv6Srv() { cleanup(); + // Close the command socket (if it exists). + CommandMgr::instance().closeCommandSocket(); + + // Deregister any registered commands + CommandMgr::instance().deregisterCommand("shutdown"); + CommandMgr::instance().deregisterCommand("statistic-get"); + CommandMgr::instance().deregisterCommand("statistic-reset"); + CommandMgr::instance().deregisterCommand("statistic-remove"); + CommandMgr::instance().deregisterCommand("statistic-get-all"); + CommandMgr::instance().deregisterCommand("statistic-reset-all"); + CommandMgr::instance().deregisterCommand("statistic-remove-all"); + server_ = NULL; // forget this instance. There should be no callback anymore // at this stage anyway. } diff --git a/src/bin/dhcp6/json_config_parser.cc b/src/bin/dhcp6/json_config_parser.cc index 4d273ba812..9c5143fea2 100644 --- a/src/bin/dhcp6/json_config_parser.cc +++ b/src/bin/dhcp6/json_config_parser.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -693,6 +694,8 @@ namespace dhcp { globalContext()); } else if (config_id.compare("relay-supplied-options") == 0) { parser = new RSOOListConfigParser(config_id); + } else if (config_id.compare("control-socket") == 0) { + parser = new ControlSocketParser(config_id); } else { isc_throw(DhcpConfigError, "unsupported global configuration parameter: " @@ -815,6 +818,26 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) { subnet_parser->build(subnet_config->second); } + // Get command socket configuration from the config file. + // This code expects the following structure: + // { + // "socket-type": "unix", + // "socket-name": "/tmp/kea6.sock" + // } + ConstElementPtr sock_cfg = + CfgMgr::instance().getStagingCfg()->getControlSocketInfo(); + + // Close existing socket (if any). + isc::config::CommandMgr::instance().closeCommandSocket(); + if (sock_cfg) { + // This will create a control socket and will install external socket + // in IfaceMgr. That socket will be monitored when Dhcp4Srv::receivePacket() + // calls IfaceMgr::receive4() and callback in CommandMgr will be called, + // if necessary. If there were previously open command socket, it will + // be closed. + isc::config::CommandMgr::instance().openCommandSocket(sock_cfg); + } + // The lease database parser is the last to be run. std::map::const_iterator leases_config = values_map.find("lease-database"); diff --git a/src/bin/dhcp6/tests/Makefile.am b/src/bin/dhcp6/tests/Makefile.am index ed16e00a60..07b94d8117 100644 --- a/src/bin/dhcp6/tests/Makefile.am +++ b/src/bin/dhcp6/tests/Makefile.am @@ -21,6 +21,8 @@ AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header AM_CPPFLAGS += -I$(top_srcdir)/src/bin AM_CPPFLAGS += -DTOP_BUILDDIR="\"$(top_builddir)\"" AM_CPPFLAGS += $(BOOST_INCLUDES) +AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_top_srcdir)/src/lib/testutils/testdata\" +AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/bin/dhcp6/tests\" AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" CLEANFILES = $(builddir)/logger_lockfile diff --git a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc index 0aff170acd..fe4473d492 100644 --- a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -25,7 +26,11 @@ #include #include +#include +#include + 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(unlink(LOAD_MARKER_FILE)); static_cast(unlink(UNLOAD_MARKER_FILE)); + IfaceMgr::instance().deleteAllExternalSockets(); + CfgMgr::instance().clear(); + } + +}; + +class CtrlChannelDhcpv6SrvTest : public CtrlDhcpv6SrvTest { +public: + std::string socket_path_; + boost::shared_ptr 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 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 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 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 :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 -- 2.47.2