Commit e103da11 authored by Thomas Markwalder's avatar Thomas Markwalder
Browse files

[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)
parent c3346812
// 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 <config.h>
#include <cc/data.h>
#include <config/command_mgr.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcp6/ctrl_dhcp6_srv.h>
#include <dhcp6/dhcp6_log.h>
#include <hooks/hooks_manager.h>
#include <dhcp6/json_config_parser.h>
#include <hooks/hooks_manager.h>
#include <stats/stats_mgr.h>
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.
}
......
......@@ -17,6 +17,7 @@
#include <asiolink/io_address.h>
#include <cc/data.h>
#include <cc/command_interpreter.h>
#include <config/command_mgr.h>
#include <dhcp/libdhcp++.h>
#include <dhcp6/json_config_parser.h>
#include <dhcp6/dhcp6_log.h>
......@@ -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<std::string, ConstElementPtr>::const_iterator leases_config =
values_map.find("lease-database");
......
......@@ -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
......
......@@ -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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment