Commit f9db8f64 authored by Francis Dupont's avatar Francis Dupont

[30-implement-control-socket-for-ddns-2] Added channel management - todo finish new unit tests

parent 96414103
......@@ -55,22 +55,22 @@ D2Controller::registerCommands() {
// These are the commands always supported by the D2 server.
// Please keep the list in alphabetic order.
CommandMgr::instance().registerCommand("build-report",
CommandMgr::instance().registerCommand(BUILD_REPORT_COMMAND,
boost::bind(&D2Controller::buildReportHandler, this, _1, _2));
CommandMgr::instance().registerCommand("config-get",
CommandMgr::instance().registerCommand(CONFIG_GET_COMMAND,
boost::bind(&D2Controller::configGetHandler, this, _1, _2));
CommandMgr::instance().registerCommand("config-test",
CommandMgr::instance().registerCommand(CONFIG_TEST_COMMAND,
boost::bind(&D2Controller::configTestHandler, this, _1, _2));
CommandMgr::instance().registerCommand("config-write",
CommandMgr::instance().registerCommand(CONFIG_WRITE_COMMAND,
boost::bind(&D2Controller::configWriteHandler, this, _1, _2));
CommandMgr::instance().registerCommand("shutdown",
CommandMgr::instance().registerCommand(SHUT_DOWN_COMMAND,
boost::bind(&D2Controller::shutdownHandler, this, _1, _2));
CommandMgr::instance().registerCommand("version-get",
CommandMgr::instance().registerCommand(VERSION_GET_COMMAND,
boost::bind(&D2Controller::versionGetHandler, this, _1, _2));
}
......@@ -81,12 +81,12 @@ D2Controller::deregisterCommands() {
CommandMgr::instance().closeCommandSocket();
// Deregister any registered commands (please keep in alphabetic order)
CommandMgr::instance().deregisterCommand("build-report");
CommandMgr::instance().deregisterCommand("config-get");
CommandMgr::instance().deregisterCommand("config-test");
CommandMgr::instance().deregisterCommand("config-write");
CommandMgr::instance().deregisterCommand("shutdown");
CommandMgr::instance().deregisterCommand("version-get");
CommandMgr::instance().deregisterCommand(BUILD_REPORT_COMMAND);
CommandMgr::instance().deregisterCommand(CONFIG_GET_COMMAND);
CommandMgr::instance().deregisterCommand(CONFIG_TEST_COMMAND);
CommandMgr::instance().deregisterCommand(CONFIG_WRITE_COMMAND);
CommandMgr::instance().deregisterCommand(SHUT_DOWN_COMMAND);
CommandMgr::instance().deregisterCommand(VERSION_GET_COMMAND);
} catch (...) {
// What to do? Simply ignore...
......@@ -110,7 +110,6 @@ D2Controller::parseFile(const std::string& file_name) {
}
D2Controller::~D2Controller() {
deregisterCommands();
}
std::string
......
......@@ -12,6 +12,10 @@
namespace isc {
namespace d2 {
class D2Controller;
/// @brief Pointer to a process controller.
typedef boost::shared_ptr<D2Controller> D2ControllerPtr;
/// @brief Process Controller for D2 Process
/// This class is the DHCP-DDNS specific derivation of DControllerBase. It
/// creates and manages an instance of the DHCP-DDNS application process,
......@@ -46,6 +50,7 @@ public:
void registerCommands();
/// @brief Deregister commands.
/// @note Does not throw.
void deregisterCommands();
protected:
......@@ -77,6 +82,9 @@ private:
/// @brief Constructor is declared private to maintain the integrity of
/// the singleton instance.
D2Controller();
/// To facilitate unit testing.
friend class NakedD2Controller;
};
}; // namespace isc::d2
......
......@@ -7,6 +7,7 @@
#include <config.h>
#include <asiolink/asio_wrapper.h>
#include <cc/command_interpreter.h>
#include <config/command_mgr.h>
#include <d2/d2_log.h>
#include <d2/d2_cfg_mgr.h>
#include <d2/d2_controller.h>
......@@ -23,7 +24,8 @@ const unsigned int D2Process::QUEUE_RESTART_PERCENT = 80;
D2Process::D2Process(const char* name, const asiolink::IOServicePtr& io_service)
: DProcessBase(name, io_service, DCfgMgrBasePtr(new D2CfgMgr())),
reconf_queue_flag_(false), shutdown_type_(SD_NORMAL) {
reconf_queue_flag_(false), reconf_control_socket_flag_(false),
shutdown_type_(SD_NORMAL) {
// Instantiate queue manager. Note that queue manager does not start
// listening at this point. That can only occur after configuration has
......@@ -46,12 +48,19 @@ D2Process::init() {
void
D2Process::run() {
LOG_INFO(d2_logger, DHCP_DDNS_STARTED).arg(VERSION);
D2ControllerPtr controller =
boost::dynamic_pointer_cast<D2Controller>(D2Controller::instance());
try {
// Now logging was initialized so commands can be registered.
boost::dynamic_pointer_cast<D2Controller>(D2Controller::instance())->registerCommands();
controller->registerCommands();
// Loop forever until we are allowed to shutdown.
while (!canShutdown()) {
// Check if the command channel should be (re-)configured.
if (getReconfControlSocketFlag()) {
reconfigureCommandChannel();
}
// Check on the state of the request queue. Take any
// actions necessary regarding it.
checkQueueStatus();
......@@ -65,7 +74,8 @@ D2Process::run() {
// a. NCR message has been received
// b. Transaction IO has completed
// c. Interval timer expired
// d. Something stopped IO service (runIO returns 0)
// d. Control channel event
// e. Something stopped IO service (runIO returns 0)
if (runIO() == 0) {
// Pretty sure this amounts to an unexpected stop and we
// should bail out now. Normal shutdowns do not utilize
......@@ -76,7 +86,7 @@ D2Process::run() {
}
} catch (const std::exception& ex) {
LOG_FATAL(d2_logger, DHCP_DDNS_FAILED).arg(ex.what());
boost::dynamic_pointer_cast<D2Controller>(D2Controller::instance())->deregisterCommands();
controller->deregisterCommands();
isc_throw (DProcessBaseError,
"Process run method failed: " << ex.what());
}
......@@ -85,7 +95,7 @@ D2Process::run() {
// this might be the place to do it, once there is a persistence mgr.
// This may also be better in checkQueueStatus.
boost::dynamic_pointer_cast<D2Controller>(D2Controller::instance())->deregisterCommands();
controller->deregisterCommands();
LOG_DEBUG(d2_logger, isc::log::DBGLVL_START_SHUT, DHCP_DDNS_RUN_EXIT);
......@@ -219,6 +229,7 @@ D2Process::configure(isc::data::ConstElementPtr config_set, bool check_only) {
// action. In integrated mode, this will send a failed response back
// to the configuration backend.
reconf_queue_flag_ = false;
reconf_control_socket_flag_ = false;
return (answer);
}
......@@ -234,6 +245,7 @@ D2Process::configure(isc::data::ConstElementPtr config_set, bool check_only) {
// things that need reconfiguration. It might also be useful if we
// did some analysis to decide what if anything we need to do.)
reconf_queue_flag_ = true;
reconf_control_socket_flag_ = true;
// If we are here, configuration was valid, at least it parsed correctly
// and therefore contained no invalid values.
......@@ -399,5 +411,39 @@ const char* D2Process::getShutdownTypeStr(const ShutdownType& type) {
return (str);
}
void
D2Process::reconfigureCommandChannel() {
reconf_control_socket_flag_ = false;
// Current socket configuration.
static isc::data::ConstElementPtr current_sock_cfg;
// Get new socket configuration.
isc::data::ConstElementPtr sock_cfg = getD2CfgMgr()->getControlSocketInfo();
// Determine if the socket configuration has changed. It has if
// both old and new configuration is specified but respective
// data elements aren't equal.
bool sock_changed = (sock_cfg && current_sock_cfg &&
!sock_cfg->equals(*current_sock_cfg));
// If the previous or new socket configuration doesn't exist or
// the new configuration differs from the old configuration we
// close the existing socket and open a new socket as appropriate.
// Note that closing an existing socket means the client will not
// receive the configuration result.
if (!sock_cfg || !current_sock_cfg || sock_changed) {
// Close the existing socket (if any).
isc::config::CommandMgr::instance().closeCommandSocket();
if (sock_cfg) {
isc::config::CommandMgr::instance().openCommandSocket(sock_cfg);
}
}
// Commit the new socket configuration.
current_sock_cfg = sock_cfg;
}
}; // namespace isc::d2
}; // namespace isc
// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2013-2018 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
......@@ -254,6 +254,15 @@ protected:
shutdown_type_ = value;
}
/// @brief (Re-)Configure the command channel.
///
/// Only close the current channel, if the new channel configuration is
/// different. This avoids disconnecting a client and hence not sending
/// them a command result, unless they specifically alter the channel
/// configuration. In that case the user simply has to accept they'll
/// be disconnected.
void reconfigureCommandChannel();
public:
/// @brief Returns a pointer to the configuration manager.
/// Note, this method cannot return a reference as it uses dynamic
......@@ -275,6 +284,11 @@ public:
return (reconf_queue_flag_);
}
/// @brief Returns true if the control socket should be reconfigured.
bool getReconfControlSocketFlag() const {
return (reconf_control_socket_flag_);
}
/// @brief Returns the type of shutdown requested.
///
/// Note, this value is meaningless unless shouldShutdown() returns true.
......@@ -300,6 +314,9 @@ private:
/// @brief Indicates if the queue manager should be reconfigured.
bool reconf_queue_flag_;
/// @brief Indicates if the control socket should be reconfigured.
bool reconf_control_socket_flag_;
/// @brief Indicates the type of shutdown requested.
ShutdownType shutdown_type_;
};
......
......@@ -58,6 +58,7 @@ d2_unittests_SOURCES += d2_controller_unittests.cc
d2_unittests_SOURCES += d2_simple_parser_unittest.cc
d2_unittests_SOURCES += parser_unittest.cc parser_unittest.h
d2_unittests_SOURCES += get_config_unittest.cc
d2_unittests_SOURCES += d2_command_unittest.cc
d2_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
d2_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS)
......
// Copyright (C) 2018 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 <asiolink/interval_timer.h>
#include <asiolink/io_service.h>
#include <cc/command_interpreter.h>
#include <config/command_mgr.h>
#include <config/timeouts.h>
#include <testutils/io_utils.h>
#include <testutils/unix_control_client.h>
#include <d2/d2_controller.h>
#include <d2/d2_process.h>
#include <d2/parser_context.h>
#include <gtest/gtest.h>
#include <boost/pointer_cast.hpp>
#include <fstream>
#include <iostream>
#include <sstream>
#include <thread>
using namespace std;
using namespace isc;
using namespace isc::asiolink;
using namespace isc::config;
using namespace isc::d2;
using namespace isc::data;
using namespace isc::dhcp::test;
using namespace isc::process;
using namespace boost::asio;
namespace isc {
namespace d2 {
class NakedD2Controller;
typedef boost::shared_ptr<NakedD2Controller> NakedD2ControllerPtr;
class NakedD2Controller : public D2Controller {
// "Naked" D2 controller, exposes internal methods.
public:
static DControllerBasePtr& instance() {
if (!getController()) {
DControllerBasePtr controller_ptr(new NakedD2Controller());
setController(controller_ptr);
}
return (getController());
}
virtual ~NakedD2Controller() { }
using DControllerBase::getIOService;
using DControllerBase::initProcess;
D2ProcessPtr getProcess() {
return (boost::dynamic_pointer_cast<D2Process>(DControllerBase::getProcess()));
}
private:
NakedD2Controller() { }
};
}; // namespace isc::d2
}; // namespace isc
namespace {
/// @brief Simple RAII class which stops IO service upon destruction
/// of the object.
class IOServiceWork {
public:
/// @brief Constructor.
///
/// @param io_service Pointer to the IO service to be stopped.
explicit IOServiceWork(const IOServicePtr& io_service)
: io_service_(io_service) {
}
/// @brief Destructor.
///
/// Stops IO service.
~IOServiceWork() {
io_service_->stop();
}
private:
/// @brief Pointer to the IO service to be stopped upon destruction.
IOServicePtr io_service_;
};
/// @brief Fixture class intended for testin control channel in D2.
class CtrlChannelD2Test : public ::testing::Test {
public:
/// @brief Path to the UNIX socket being used to communicate with the server.
string socket_path_;
/// @brief Reference to the base controller object.
DControllerBasePtr& server_;
/// @brief Cast controller object.
NakedD2Controller* d2Controller() {
return (dynamic_cast<NakedD2Controller*>(server_.get()));
}
/// @brief Configuration file.
static const char* CFG_TEST_FILE;
/// @brief Default constructor.
///
/// Sets socket path to its default value.
CtrlChannelD2Test()
: server_(NakedD2Controller::instance()) {
const char* env = getenv("KEA_SOCKET_TEST_DIR");
if (env) {
socket_path_ = string(env) + "/d2.sock";
} else {
socket_path_ = string(TEST_DATA_BUILDDIR) + "/d2.sock";
}
::remove(socket_path_.c_str());
}
/// @brief Destructor.
~CtrlChannelD2Test() {
// Include deregister & co.
server_.reset();
// Remove files.
::remove(CFG_TEST_FILE);
::remove(socket_path_.c_str());
// Reset command manager.
CommandMgr::instance().deregisterAll();
CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND);
}
/// @brief Returns pointer to the server's IO service.
///
/// @return Pointer to the server's IO service or null pointer if the
/// hasn't been created server.
IOServicePtr getIOService() {
return (server_ ? d2Controller()->getIOService() : IOServicePtr());
}
/// @brief Runs parser in DHCPDDNS mode
///
/// @param config input configuration
/// @param verbose display errors
/// @return element pointer representing the configuration
ElementPtr parseDHCPDDNS(const string& config, bool verbose = false) {
try {
D2ParserContext ctx;
return (ctx.parseString(config,
D2ParserContext::PARSER_SUB_DHCPDDNS));
} catch (const std::exception& ex) {
if (verbose) {
cout << "EXCEPTION: " << ex.what() << endl;
}
throw;
}
}
/// @brief Create a server with a command channel.
void createUnixChannelServer() {
::remove(socket_path_.c_str());
// Just a simple config. The important part here is the socket
// location information.
string header =
"{"
" \"ip-address\": \"192.168.77.1\","
" \"port\": 777,"
" \"control-socket\": {"
" \"socket-type\": \"unix\","
" \"socket-name\": \"";
string footer =
"\""
" },"
" \"tsig-keys\": [],"
" \"forward-ddns\" : {},"
" \"reverse-ddns\" : {}"
"}";
// Fill in the socket-name value with socket_path_ to make
// the actual configuration text.
string config_txt = header + socket_path_ + footer;
ASSERT_TRUE(server_);
ConstElementPtr config;
ASSERT_NO_THROW(config = parseDHCPDDNS(config_txt, true));
ASSERT_NO_THROW(d2Controller()->initProcess());
D2ProcessPtr proc = d2Controller()->getProcess();
ASSERT_TRUE(proc);
ConstElementPtr answer = proc->configure(config, false);
ASSERT_TRUE(answer);
int status = 0;
ConstElementPtr txt = 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(CommandMgr::instance().getControlSocketFD(), -1);
}
/// @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 polls the server's I/O service
/// where needed to cause the server to process IO events on
/// 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 string& command, string& response) {
response = "";
boost::scoped_ptr<UnixControlClient> client;
client.reset(new UnixControlClient());
ASSERT_TRUE(client);
// Connect to the server. This is expected to trigger server's acceptor
// handler when IOService::poll() is run.
ASSERT_TRUE(client->connectToServer(socket_path_));
ASSERT_NO_THROW(getIOService()->poll());
// Send the command. This will trigger server's handler which receives
// data over the unix domain socket. The server will start sending
// response to the client.
ASSERT_TRUE(client->sendCommand(command));
ASSERT_NO_THROW(getIOService()->poll());
// 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(getIOService()->poll());
}
/// @brief Checks response for list-commands.
///
/// This method checks if the list-commands response is generally sane
/// and whether specified command is mentioned in the response.
///
/// @param rsp response sent back by the server.
/// @param command command expected to be on the list.
void checkListCommands(const ConstElementPtr& rsp, const string command) {
ConstElementPtr params;
int status_code = -1;
EXPECT_NO_THROW(params = parseAnswer(status_code, rsp));
EXPECT_EQ(CONTROL_RESULT_SUCCESS, status_code);
ASSERT_TRUE(params);
ASSERT_EQ(Element::list, params->getType());
int cnt = 0;
for (size_t i = 0; i < params->size(); ++i) {
string tmp = params->get(i)->stringValue();
if (tmp == command) {
// Command found, but that's not enough.
// Need to continue working through the list to see
// if there are no duplicates.
cnt++;
}
}
// Exactly one command on the list is expected.
EXPECT_EQ(1, cnt) << "Command " << command << " not found";
}
/// @brief Check if the answer for config-write command is correct.
///
/// @param response_txt response in text form.
/// (as read from the control socket)
/// @param exp_status expected status.
/// (0 success, 1 failure)
/// @param exp_txt for success cases this defines the expected filename,
/// for failure cases this defines the expected error message.
void checkConfigWrite(const string& response_txt, int exp_status,
const string& exp_txt = "") {
ConstElementPtr rsp;
EXPECT_NO_THROW(rsp = Element::fromJSON(response_txt));
ASSERT_TRUE(rsp);
int status;
ConstElementPtr params = parseAnswer(status, rsp);
EXPECT_EQ(exp_status, status);
if (exp_status == CONTROL_RESULT_SUCCESS) {
// Let's check couple things...
// The parameters must include filename.
ASSERT_TRUE(params);
ASSERT_TRUE(params->get("filename"));
ASSERT_EQ(Element::string, params->get("filename")->getType());
EXPECT_EQ(exp_txt, params->get("filename")->stringValue());
// The parameters must include size. And the size
// must indicate some content.
ASSERT_TRUE(params->get("size"));
ASSERT_EQ(Element::integer, params->get("size")->getType());
int64_t size = params->get("size")->intValue();
EXPECT_LE(1, size);
// Now check if the file is really there and suitable for
// opening.
ifstream f(exp_txt, ios::binary | ios::ate);
ASSERT_TRUE(f.good());
// Now check that it is the correct size as reported.
EXPECT_EQ(size, static_cast<int64_t>(f.tellg()));
// Finally, check that it's really a JSON.
ElementPtr from_file = Element::fromJSONFile(exp_txt);
ASSERT_TRUE(from_file);
} else if (exp_status == CONTROL_RESULT_ERROR) {
// Let's check if the reason for failure was given.
ConstElementPtr text = rsp->get("text");
ASSERT_TRUE(text);
ASSERT_EQ(Element::string, text->getType());
EXPECT_EQ(exp_txt, text->stringValue());
} else {
ADD_FAILURE() << "Invalid expected status: " << exp_status;
}
}
/// @brief Handler for long command.
///
/// It checks whether the received command is equal to the one specified
/// as an argument.
///
/// @param expected_command String representing an expected command.
/// @param command_name Command name received by the handler.
/// @param arguments Command arguments received by the handler.
///
/// @returns Success answer.
static ConstElementPtr
longCommandHandler(const string& expected_command,
const string& command_name,
const ConstElementPtr& arguments) {
// The handler is called with a command name and the structure holding
// command arguments. We have to rebuild the command from those
// two arguments so as it can be compared against expected_command.
ElementPtr entire_command = Element::createMap();
entire_command->set("command", Element::create(command_name));
entire_command->set("arguments", (arguments));
// The rebuilt command will have a different order of parameters so
// let's parse expected_command back to JSON to guarantee that
// both structures are built using the same order.
EXPECT_EQ(Element::fromJSON(expected_command)->str(),
entire_command->str());
return (createAnswer(0, "long command received ok"));
}
/// @brief Command handler which generates long response.
///
/// This handler generates a large response (over 400kB). It includes
/// a list of randomly generated strings to make sure that the test
/// can catch out of order delivery.
static ConstElementPtr
longResponseHandler(const string&, const ConstElementPtr&) {
ElementPtr arguments = Element::createList();
for (unsigned i = 0; i < 80000; ++i) {
std::ostringstream s;
s << std::setw(5) << i;
arguments->add(Element::create(s.str()));
}
return (createAnswer(0, arguments));
}
};
const char* CtrlChannelD2Test::CFG_TEST_FILE = "d2-test-config.json";
// Test bad syntax rejected by the parser.
TEST_F(CtrlChannelD2Test, parser) {
// no empty map.
string bad1 =
"{"
" \"ip-address\": \"192.168.77.1\","
" \"port\": 777,"
" \"control-socket\": { },"
" \"tsig-keys\": [],"
" \"forward-ddns\" : {},"
" \"reverse-ddns\" : {}"
"}";
ASSERT_THROW(parseDHCPDDNS(bad1), D2ParseError);
// unknown keyword.
string bad2 =
"{"
" \"ip-address\": \"192.168.77.1\","
" \"port\": 777,"
" \"control-socket\": {"
" \"socket-type\": \"unix\","
" \"socket-name\": \"/tmp/d2.sock\","
" \"bogus\": \"unknown...\""
" },"
" \"tsig-keys\": [],"
" \"forward-ddns\" : {},"
" \"reverse-ddns\" : {}"
"}";
ASSERT_THROW(parseDHCPDDNS(bad2), D2ParseError);
}
// Test bad syntax rejected by the process.
TEST_F(CtrlChannelD2Test, configure) {
ASSERT_TRUE(server_);
ASSERT_NO_THROW(d2Controller()->initProcess());
D2ProcessPtr proc = d2Controller()->getProcess();
ASSERT_TRUE(proc);
// no type.
string bad1 =
"{"
" \"ip-address\": \"192.168.77.1\","
" \"port\": 777,"
" \"control-socket\": {"
" \"socket-name\": \"/tmp/d2.sock\""
" },"
" \"tsig-keys\": [],"
" \"forward-ddns\" : {},"
" \"reverse-ddns\" : {}"
"}";
ConstElementPtr config;
ASSERT_NO_THROW(config = parseDHCPDDNS(bad1, true));
ConstElementPtr answer = proc->configure(config, false);
ASSERT_TRUE(answer);
int status = 0;
ConstElementPtr txt = parseAnswer(status, answer);
EXPECT_EQ(1, status);
ASSERT_TRUE(txt);
ASSERT_EQ(Element::string, txt->getType());
EXPECT_EQ("Mandatory 'socket-type' parameter missing", txt->stringValue());
EXPECT_EQ(-1, CommandMgr::instance().getControlSocketFD());
// bad type.
string bad2 =
"{"
" \"ip-address\": \"192.168.77.1\","
" \"port\": 777,"
" \"control-socket\": {"
" \"socket-type\": \"bogus\","
" \"socket-name\": \"/tmp/d2.sock\""
" },"
" \"tsig-keys\": [],"
" \"forward-ddns\" : {},"
" \"reverse-ddns\" : {}"
"}";
ASSERT_NO_THROW(config = parseDHCPDDNS(bad2, true));
answer = proc->configure(config, false);
ASSERT_TRUE(answer);
status = 0;
txt = parseAnswer(status, answer);
EXPECT_EQ(1, status);
ASSERT_TRUE(txt);