diff options
author | Francis Dupont <fdupont@isc.org> | 2024-09-01 20:55:56 +0200 |
---|---|---|
committer | Francis Dupont <fdupont@isc.org> | 2024-09-20 14:55:54 +0200 |
commit | 57430023310e59178e5f19cd8c0dba36ee4545d8 (patch) | |
tree | c6222f6c6e5c20c2001714a4811938309e67c772 /src | |
parent | [#3477] Typo (diff) | |
download | kea-57430023310e59178e5f19cd8c0dba36ee4545d8.tar.xz kea-57430023310e59178e5f19cd8c0dba36ee4545d8.zip |
[#3506] Checkpoint: split UnixCommandMgr
Diffstat (limited to 'src')
25 files changed, 955 insertions, 859 deletions
diff --git a/src/bin/agent/ca_cfg_mgr.h b/src/bin/agent/ca_cfg_mgr.h index 6234bd44f6..1cbfa82837 100644 --- a/src/bin/agent/ca_cfg_mgr.h +++ b/src/bin/agent/ca_cfg_mgr.h @@ -50,7 +50,7 @@ public: /// This method returns Element tree structure that describes the control /// socket (or null pointer if the socket is not defined for a particular /// server type). This information is expected to be compatible with - /// data passed to @ref isc::config::CommandMgr::openCommandSocket. + /// data passed to @ref isc::config::UnixCommandMgr::openCommandSocket. /// /// @param service server being controlled /// @return pointer to the Element that holds control-socket map (or NULL) @@ -61,7 +61,7 @@ public: /// /// This method stores Element tree structure that describes the control /// socket. This information is expected to be compatible with - /// data passed to @ref isc::config::CommandMgr::openCommandSocket. + /// data passed to @ref isc::config::UnixCommandMgr::openCommandSocket. /// /// @param control_socket Element that holds control-socket map /// @param service server being controlled diff --git a/src/bin/d2/d2_controller.cc b/src/bin/d2/d2_controller.cc index 9534d0b9bf..0dfd021d2d 100644 --- a/src/bin/d2/d2_controller.cc +++ b/src/bin/d2/d2_controller.cc @@ -8,6 +8,7 @@ #include <config/command_mgr.h> #include <config/http_command_mgr.h> +#include <config/unix_command_mgr.h> #include <d2/d2_controller.h> #include <d2/d2_process.h> #include <d2/parser_context.h> @@ -105,7 +106,7 @@ void D2Controller::deregisterCommands() { try { // Close command sockets. - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); HttpCommandMgr::instance().close(); // Deregister any registered commands (please keep in alphabetic order) diff --git a/src/bin/d2/d2_process.cc b/src/bin/d2/d2_process.cc index 2974e8fb2a..8b0da4cf2d 100644 --- a/src/bin/d2/d2_process.cc +++ b/src/bin/d2/d2_process.cc @@ -10,6 +10,7 @@ #include <cc/command_interpreter.h> #include <config/command_mgr.h> #include <config/http_command_mgr.h> +#include <config/unix_command_mgr.h> #include <d2/d2_controller.h> #include <d2/d2_process.h> #include <d2srv/d2_cfg_mgr.h> @@ -77,7 +78,7 @@ void D2Process::init() { using namespace isc::config; // Command managers use IO service to run asynchronous socket operations. - CommandMgr::instance().setIOService(getIOService()); + UnixCommandMgr::instance().setIOService(getIOService()); HttpCommandMgr::instance().setIOService(getIOService()); // Set the HTTP authentication default realm. @@ -161,7 +162,7 @@ D2Process::runIO() { // service is stopped it will return immediately with a cnt of zero. cnt = getIOService()->runOne(); } - config::HttpCommandMgr::instance().garbageCollectListeners(); + HttpCommandMgr::instance().garbageCollectListeners(); return (cnt); } @@ -515,13 +516,13 @@ D2Process::reconfigureCommandChannel() { if (!sock_cfg || !current_control_socket_ || sock_changed) { // Close the existing socket. if (current_control_socket_) { - isc::config::CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); current_control_socket_.reset(); } // Open the new socket. if (sock_cfg) { - isc::config::CommandMgr::instance().openCommandSocket(sock_cfg); + UnixCommandMgr::instance().openCommandSocket(sock_cfg); } } diff --git a/src/bin/d2/tests/d2_command_unittest.cc b/src/bin/d2/tests/d2_command_unittest.cc index f9f7604927..a63a505ce7 100644 --- a/src/bin/d2/tests/d2_command_unittest.cc +++ b/src/bin/d2/tests/d2_command_unittest.cc @@ -11,6 +11,7 @@ #include <cc/command_interpreter.h> #include <config/command_mgr.h> #include <config/timeouts.h> +#include <config/unix_command_mgr.h> #include <testutils/io_utils.h> #include <testutils/unix_control_client.h> #include <d2/d2_controller.h> @@ -144,7 +145,7 @@ public: // Reset command manager. CommandMgr::instance().deregisterAll(); - CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND); + UnixCommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND); } /// @brief Returns pointer to the server's IO service. @@ -216,7 +217,7 @@ public: ASSERT_EQ(0, status) << txt->str(); // Now check that the socket was indeed open. - ASSERT_GT(CommandMgr::instance().getControlSocketFD(), -1); + ASSERT_GT(UnixCommandMgr::instance().getControlSocketFD(), -1); } /// @brief Conducts a command/response exchange via UnixCommandSocket. @@ -456,7 +457,7 @@ TEST_F(CtrlChannelD2Test, configure) { ASSERT_EQ(Element::string, txt->getType()); EXPECT_EQ("'socket-type' parameter is mandatory in control-sockets items", txt->stringValue()); - EXPECT_EQ(-1, CommandMgr::instance().getControlSocketFD()); + EXPECT_EQ(-1, UnixCommandMgr::instance().getControlSocketFD()); // no name. string bad3 = @@ -482,7 +483,7 @@ TEST_F(CtrlChannelD2Test, configure) { ASSERT_EQ(Element::string, txt->getType()); EXPECT_EQ("Mandatory 'socket-name' parameter missing", txt->stringValue()); - EXPECT_EQ(-1, CommandMgr::instance().getControlSocketFD()); + EXPECT_EQ(-1, UnixCommandMgr::instance().getControlSocketFD()); } // This test checks which commands are registered by the D2 server. @@ -787,7 +788,7 @@ TEST_F(CtrlChannelD2Test, configTest) { ASSERT_TRUE(keys); EXPECT_EQ(1, keys->size()); - ASSERT_GT(CommandMgr::instance().getControlSocketFD(), -1); + ASSERT_GT(UnixCommandMgr::instance().getControlSocketFD(), -1); // Create a config with invalid content that should fail to parse. os.str(""); @@ -926,7 +927,7 @@ TEST_F(CtrlChannelD2Test, configSet) { ASSERT_TRUE(keys); EXPECT_EQ(1, keys->size()); - ASSERT_GT(CommandMgr::instance().getControlSocketFD(), -1); + ASSERT_GT(UnixCommandMgr::instance().getControlSocketFD(), -1); // Create a config with invalid content that should fail to parse. os.str(""); @@ -1316,7 +1317,7 @@ TEST_F(CtrlChannelD2Test, connectionTimeoutPartialCommand) { // Set connection timeout to 2s to prevent long waiting time for the // timeout during this test. const unsigned short timeout = 2000; - CommandMgr::instance().setConnectionTimeout(timeout); + UnixCommandMgr::instance().setConnectionTimeout(timeout); // Server's response will be assigned to this variable. string response; @@ -1369,7 +1370,7 @@ TEST_F(CtrlChannelD2Test, connectionTimeoutNoData) { // Set connection timeout to 2s to prevent long waiting time for the // timeout during this test. const unsigned short timeout = 2000; - CommandMgr::instance().setConnectionTimeout(timeout); + UnixCommandMgr::instance().setConnectionTimeout(timeout); // Server's response will be assigned to this variable. string response; diff --git a/src/bin/dhcp4/ctrl_dhcp4_srv.cc b/src/bin/dhcp4/ctrl_dhcp4_srv.cc index 4201b60cb7..f464e0a94c 100644 --- a/src/bin/dhcp4/ctrl_dhcp4_srv.cc +++ b/src/bin/dhcp4/ctrl_dhcp4_srv.cc @@ -12,6 +12,7 @@ #include <cc/data.h> #include <config/command_mgr.h> #include <config/http_command_mgr.h> +#include <config/unix_command_mgr.h> #include <cryptolink/crypto_hash.h> #include <dhcp/libdhcp++.h> #include <dhcp4/ctrl_dhcp4_srv.h> @@ -1082,7 +1083,7 @@ ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t server_port /*= DHCP4_SERVER_P TimerMgr::instance()->setIOService(getIOService()); // Command managers use IO service to run asynchronous socket operations. - CommandMgr::instance().setIOService(getIOService()); + UnixCommandMgr::instance().setIOService(getIOService()); HttpCommandMgr::instance().setIOService(getIOService()); // Set the HTTP authentication default realm. @@ -1193,7 +1194,7 @@ ControlledDhcpv4Srv::~ControlledDhcpv4Srv() { cleanup(); // Close command sockets. - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); HttpCommandMgr::instance().close(); // Deregister any registered commands (please keep in alphabetic order) diff --git a/src/bin/dhcp4/json_config_parser.cc b/src/bin/dhcp4/json_config_parser.cc index 79dec5174a..a244083413 100644 --- a/src/bin/dhcp4/json_config_parser.cc +++ b/src/bin/dhcp4/json_config_parser.cc @@ -10,6 +10,7 @@ #include <cc/command_interpreter.h> #include <config/command_mgr.h> #include <config/http_command_mgr.h> +#include <config/unix_command_mgr.h> #include <database/database_connection.h> #include <database/dbaccess_parser.h> #include <database/backend_selector.h> @@ -340,14 +341,14 @@ void configureCommandChannel() { // receive the configuration result. if (!sock_cfg || !current_sock_cfg || sock_changed) { // Close the existing socket (if any). - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); if (sock_cfg) { // This will create a control socket and install the external // socket in IfaceMgr. That socket will be monitored when // Dhcp4Srv::receivePacket() calls IfaceMgr::receive4() and // callback in CommandMgr will be called, if necessary. - CommandMgr::instance().openCommandSocket(sock_cfg); + UnixCommandMgr::instance().openCommandSocket(sock_cfg); } } diff --git a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc index 169e6aa2e4..632e1df759 100644 --- a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc @@ -10,6 +10,7 @@ #include <asiolink/io_service.h> #include <cc/command_interpreter.h> #include <config/command_mgr.h> +#include <config/unix_command_mgr.h> #include <config/timeouts.h> #include <dhcp/dhcp4.h> #include <dhcp/libdhcp++.h> @@ -131,9 +132,9 @@ public: LeaseMgrFactory::destroy(); StatsMgr::instance().removeAll(); - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); CommandMgr::instance().deregisterAll(); - CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND); + UnixCommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND); server_.reset(); reset(); @@ -222,7 +223,7 @@ public: ASSERT_EQ(0, status) << txt->str(); // Now check that the socket was indeed open. - ASSERT_GT(isc::config::CommandMgr::instance().getControlSocketFD(), -1); + ASSERT_GT(isc::config::UnixCommandMgr::instance().getControlSocketFD(), -1); } /// @brief Reset hooks data @@ -2254,7 +2255,7 @@ TEST_F(CtrlChannelDhcpv4SrvTest, connectionTimeoutPartialCommand) { // Set connection timeout to 2s to prevent long waiting time for the // timeout during this test. const unsigned short timeout = 2000; - CommandMgr::instance().setConnectionTimeout(timeout); + UnixCommandMgr::instance().setConnectionTimeout(timeout); // Server's response will be assigned to this variable. std::string response; @@ -2308,7 +2309,7 @@ TEST_F(CtrlChannelDhcpv4SrvTest, connectionTimeoutNoData) { // Set connection timeout to 2s to prevent long waiting time for the // timeout during this test. const unsigned short timeout = 2000; - CommandMgr::instance().setConnectionTimeout(timeout); + UnixCommandMgr::instance().setConnectionTimeout(timeout); // Server's response will be assigned to this variable. std::string response; diff --git a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc index 0b50668dd4..0dccec9914 100644 --- a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc @@ -8,7 +8,7 @@ #include <asiolink/io_address.h> #include <cc/command_interpreter.h> -#include <config/command_mgr.h> +#include <config/unix_command_mgr.h> #include <config_backend/base_config_backend.h> #include <dhcp4/dhcp4_log.h> #include <dhcp4/dhcp4_srv.h> @@ -2965,10 +2965,10 @@ Dhcpv4SrvTest::loadConfigFile(const string& path) { TimerMgr::instance()->unregisterTimers(); // Close the command socket (if it exists). - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); // Reset CommandMgr IO service. - CommandMgr::instance().setIOService(IOServicePtr()); + UnixCommandMgr::instance().setIOService(IOServicePtr()); // Reset DatabaseConnection IO service. DatabaseConnection::setIOService(IOServicePtr()); diff --git a/src/bin/dhcp4/tests/dhcp4_test_utils.h b/src/bin/dhcp4/tests/dhcp4_test_utils.h index 269e6906f0..9988e3631c 100644 --- a/src/bin/dhcp4/tests/dhcp4_test_utils.h +++ b/src/bin/dhcp4/tests/dhcp4_test_utils.h @@ -28,6 +28,7 @@ #include <asiolink/io_address.h> #include <cc/command_interpreter.h> #include <config/command_mgr.h> +#include <config/unix_command_mgr.h> #include <util/multi_threading_mgr.h> #include <list> @@ -136,7 +137,7 @@ public: dhcp::TimerMgr::instance()->setIOService(getIOService()); - config::CommandMgr::instance().setIOService(getIOService()); + config::UnixCommandMgr::instance().setIOService(getIOService()); } /// @brief Returns fixed server identifier assigned to the naked server diff --git a/src/bin/dhcp6/ctrl_dhcp6_srv.cc b/src/bin/dhcp6/ctrl_dhcp6_srv.cc index 43eb8392c8..635212eda5 100644 --- a/src/bin/dhcp6/ctrl_dhcp6_srv.cc +++ b/src/bin/dhcp6/ctrl_dhcp6_srv.cc @@ -12,6 +12,7 @@ #include <cc/data.h> #include <config/command_mgr.h> #include <config/http_command_mgr.h> +#include <config/unix_command_mgr.h> #include <cryptolink/crypto_hash.h> #include <dhcp/libdhcp++.h> #include <dhcp6/ctrl_dhcp6_srv.h> @@ -1106,7 +1107,7 @@ ControlledDhcpv6Srv::ControlledDhcpv6Srv(uint16_t server_port /*= DHCP6_SERVER_P TimerMgr::instance()->setIOService(getIOService()); // Command managers use IO service to run asynchronous socket operations. - CommandMgr::instance().setIOService(getIOService()); + UnixCommandMgr::instance().setIOService(getIOService()); HttpCommandMgr::instance().setIOService(getIOService()); // Set the HTTP default socket address to the IPv6 (vs IPv4) loopback. @@ -1220,7 +1221,7 @@ ControlledDhcpv6Srv::~ControlledDhcpv6Srv() { cleanup(); // Close command sockets. - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); HttpCommandMgr::instance().close(); // Deregister any registered commands (please keep in alphabetic order) diff --git a/src/bin/dhcp6/json_config_parser.cc b/src/bin/dhcp6/json_config_parser.cc index b9d26a0a1e..eb5923a5de 100644 --- a/src/bin/dhcp6/json_config_parser.cc +++ b/src/bin/dhcp6/json_config_parser.cc @@ -12,6 +12,7 @@ #include <cc/command_interpreter.h> #include <config/command_mgr.h> #include <config/http_command_mgr.h> +#include <config/unix_command_mgr.h> #include <database/database_connection.h> #include <database/dbaccess_parser.h> #include <dhcp6/ctrl_dhcp6_srv.h> @@ -442,14 +443,14 @@ void configureCommandChannel() { // receive the configuration result. if (!sock_cfg || !current_sock_cfg || sock_changed) { // Close the existing socket (if any). - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); if (sock_cfg) { // This will create a control socket and install the external // socket in IfaceMgr. That socket will be monitored when // Dhcp6Srv::receivePacket() calls IfaceMgr::receive6() and // callback in CommandMgr will be called, if necessary. - CommandMgr::instance().openCommandSocket(sock_cfg); + UnixCommandMgr::instance().openCommandSocket(sock_cfg); } } diff --git a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc index fc1271b070..131bf179aa 100644 --- a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc @@ -10,6 +10,7 @@ #include <cc/command_interpreter.h> #include <config/command_mgr.h> #include <config/timeouts.h> +#include <config/unix_command_mgr.h> #include <dhcp/libdhcp++.h> #include <dhcp/testutils/iface_mgr_test_config.h> #include <dhcpsrv/cfgmgr.h> @@ -111,7 +112,7 @@ public: LeaseMgrFactory::destroy(); StatsMgr::instance().removeAll(); CommandMgr::instance().deregisterAll(); - CommandMgr::instance().setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT); + UnixCommandMgr::instance().setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT); reset(); } @@ -167,9 +168,9 @@ public: LeaseMgrFactory::destroy(); StatsMgr::instance().removeAll(); - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); CommandMgr::instance().deregisterAll(); - CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND); + UnixCommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND); server_.reset(); reset(); @@ -258,7 +259,7 @@ public: ASSERT_EQ(0, status) << txt->str(); // Now check that the socket was indeed open. - ASSERT_GT(isc::config::CommandMgr::instance().getControlSocketFD(), -1); + ASSERT_GT(isc::config::UnixCommandMgr::instance().getControlSocketFD(), -1); } /// @brief Reset @@ -2289,7 +2290,7 @@ TEST_F(CtrlChannelDhcpv6SrvTest, connectionTimeoutPartialCommand) { // Set connection timeout to 2s to prevent long waiting time for the // timeout during this test. const unsigned short timeout = 2000; - CommandMgr::instance().setConnectionTimeout(timeout); + UnixCommandMgr::instance().setConnectionTimeout(timeout); // Server's response will be assigned to this variable. std::string response; @@ -2343,7 +2344,7 @@ TEST_F(CtrlChannelDhcpv6SrvTest, connectionTimeoutNoData) { // Set connection timeout to 2s to prevent long waiting time for the // timeout during this test. const unsigned short timeout = 2000; - CommandMgr::instance().setConnectionTimeout(timeout); + UnixCommandMgr::instance().setConnectionTimeout(timeout); // Server's response will be assigned to this variable. std::string response; diff --git a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc index 0a3d1feefc..d9648bf4ad 100644 --- a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc @@ -9,6 +9,7 @@ #include <asiolink/io_address.h> #include <cc/command_interpreter.h> #include <config/command_mgr.h> +#include <config/unix_command_mgr.h> #include <config_backend/base_config_backend.h> #include <dhcp6/json_config_parser.h> #include <dhcp6/tests/dhcp6_test_utils.h> @@ -329,10 +330,10 @@ Dhcpv6SrvTest::loadConfigFile(const string& path) { TimerMgr::instance()->unregisterTimers(); // Close the command socket (if it exists). - CommandMgr::instance().closeCommandSocket(); + UnixCommandMgr::instance().closeCommandSocket(); // Reset CommandMgr IO service. - CommandMgr::instance().setIOService(IOServicePtr()); + UnixCommandMgr::instance().setIOService(IOServicePtr()); // Reset DatabaseConnection IO service. DatabaseConnection::setIOService(IOServicePtr()); diff --git a/src/bin/dhcp6/tests/dhcp6_test_utils.h b/src/bin/dhcp6/tests/dhcp6_test_utils.h index fd7a11f474..b0e23488a1 100644 --- a/src/bin/dhcp6/tests/dhcp6_test_utils.h +++ b/src/bin/dhcp6/tests/dhcp6_test_utils.h @@ -31,6 +31,7 @@ #include <dhcpsrv/lease_mgr_factory.h> #include <hooks/hooks_manager.h> #include <config/command_mgr.h> +#include <config/unix_command_mgr.h> #include <util/multi_threading_mgr.h> #include <testutils/log_utils.h> @@ -145,7 +146,7 @@ public: dhcp::TimerMgr::instance()->setIOService(getIOService()); - config::CommandMgr::instance().setIOService(getIOService()); + config::UnixCommandMgr::instance().setIOService(getIOService()); } /// @brief fakes packet reception diff --git a/src/lib/config/Makefile.am b/src/lib/config/Makefile.am index fab1236701..b31544365f 100644 --- a/src/lib/config/Makefile.am +++ b/src/lib/config/Makefile.am @@ -10,6 +10,7 @@ libkea_cfgclient_la_SOURCES = cmds_impl.h libkea_cfgclient_la_SOURCES += base_command_mgr.cc base_command_mgr.h libkea_cfgclient_la_SOURCES += client_connection.cc client_connection.h libkea_cfgclient_la_SOURCES += command_mgr.cc command_mgr.h +libkea_cfgclient_la_SOURCES += unix_command_mgr.cc unix_command_mgr.h libkea_cfgclient_la_SOURCES += config_log.h config_log.cc libkea_cfgclient_la_SOURCES += config_messages.h config_messages.cc libkea_cfgclient_la_SOURCES += hooked_command_mgr.cc hooked_command_mgr.h diff --git a/src/lib/config/command-socket.dox b/src/lib/config/command-socket.dox index 3b5150afa3..7413746576 100644 --- a/src/lib/config/command-socket.dox +++ b/src/lib/config/command-socket.dox @@ -1,4 +1,4 @@ -// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2015-2024 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 @@ -18,8 +18,7 @@ envisioned are: reconfiguration, statistics retrieval and manipulation, and shutdown. Communication over Control Channel is conducted using JSON structures. -As of Kea 0.9.2, the only supported communication channel is UNIX stream -socket, but additional types may be added in the future. +The supported communication channel is UNIX stream and HTTP/HTTPS sockets. If configured, Kea will open a socket and will listen for any incoming connections. A process connecting to this socket is expected to send JSON @@ -62,9 +61,9 @@ encountered. - arguments - is a map of additional data values returned by the server, specific to the command issue. The map is always present, even if it contains no data values. -@section ctrlSocketClient Using Control Channel +@section ctrlSocketClient Using UNIX Control Channel -Here are two examples of how to access the Control Channel: +Here are two examples of how to access the UNIX Control Channel: 1. Use socat tool, which is available in many Linux and BSD distributions. See http://www.dest-unreach.org/socat/ for details. To use it: @@ -147,30 +146,33 @@ There are 3 main methods that are expected to be used by developers: - @ref isc::config::CommandMgr::processCommand, which allows handling specified command. -There are also two methods for managing control sockets. They are not expected -to be used directly, unless someone implements a new Control Channel (e.g. TCP -or HTTPS connection): - -- @ref isc::config::CommandMgr::openCommandSocket that passes structure defined - in the configuration file. Currently only two parameters are supported: socket-type - (which must contain value 'unix') and socket-name (which contains unix path for - the named socket to be created). -- @ref isc::config::CommandMgr::closeCommandSocket() - it is used to close the - socket. - Kea servers use @c CommandMgr to register handlers for various commands they support natively. However, it is possible extend a set of supported commands using hooks framework. See @ref hooksdgCommandHandlers how to implement support for your own control commands in Kea. +@section unixCtrlSocketImpl UNIX Control Channel Implementation + +UNIX Control Channel is implemented in @ref isc::config::UnixCommandMgr. +It is a singleton class providing two methods for managing control sockets. +They are not expected to be used directly. + +- @ref isc::config::UnixCommandMgr::openCommandSocket that passes + structure defined in the configuration file. Currently only two + parameters are supported: socket-type (which must contain value + 'unix') and socket-name (which contains unix path for the named + socket to be created). +- @ref isc::config::UnixCommandMgr::closeCommandSocket() - it is used + to close the socket. + @section ctrlSocketConnections Accepting connections -The @ref isc::config::CommandMgr is implemented using boost ASIO and uses +The @ref isc::config::UnixCommandMgr is implemented using boost ASIO and uses asynchronous calls to accept new connections and receive commands from the controlling clients. ASIO uses IO service object to run asynchronous calls. -Thus, before the server can use the @ref isc::config::CommandMgr it must +Thus, before the server can use the @ref isc::config::UnixCommandMgr it must provide it with a common instance of the @ref isc::asiolink::IOService -object using @ref isc::config::CommandMgr::setIOService. The server's +object using @ref isc::config::UnixCommandMgr::setIOService. The server's main loop must contain calls to @ref isc::asiolink::IOService::run or @ref isc::asiolink::IOService::poll or their variants to invoke Command Manager's handlers as required for processing control requests. diff --git a/src/lib/config/command_mgr.cc b/src/lib/config/command_mgr.cc index cf21a697ab..13a94e0f0c 100644 --- a/src/lib/config/command_mgr.cc +++ b/src/lib/config/command_mgr.cc @@ -6,640 +6,12 @@ #include <config.h> -#include <asiolink/asio_wrapper.h> -#include <asiolink/interval_timer.h> -#include <asiolink/io_service.h> -#include <asiolink/unix_domain_socket.h> -#include <asiolink/unix_domain_socket_acceptor.h> -#include <asiolink/unix_domain_socket_endpoint.h> #include <config/command_mgr.h> -#include <cc/data.h> -#include <cc/command_interpreter.h> -#include <cc/json_feed.h> -#include <dhcp/iface_mgr.h> -#include <config/config_log.h> -#include <config/timeouts.h> -#include <util/watch_socket.h> -#include <boost/enable_shared_from_this.hpp> -#include <array> -#include <functional> -#include <unistd.h> -#include <sys/file.h> - -using namespace isc; -using namespace isc::asiolink; -using namespace isc::config; -using namespace isc::data; -namespace ph = std::placeholders; - -namespace { - -/// @brief Maximum size of the data chunk sent/received over the socket. -const size_t BUF_SIZE = 32768; - -class ConnectionPool; - -/// @brief Represents a single connection over control socket. -/// -/// An instance of this object is created when the @c CommandMgr acceptor -/// receives new connection from a controlling client. -class Connection : public boost::enable_shared_from_this<Connection> { -public: - - /// @brief Constructor. - /// - /// This constructor registers a socket of this connection in the Interface - /// Manager to cause the blocking call to @c select() to return as soon as - /// a transmission over the control socket is received. - /// - /// It installs two external sockets on the @IfaceMgr to break synchronous - /// calls to @select(). The @c WatchSocket is used for send operations - /// over the connection. The native socket is used for signaling reads - /// over the connection. - /// - /// @param io_service IOService object used to handle the asio operations - /// @param socket Pointer to the object representing a socket which is used - /// for data transmission. - /// @param connection_pool Reference to the connection pool to which this - /// connection belongs. - /// @param timeout Connection timeout (in seconds). - Connection(const IOServicePtr& io_service, - const boost::shared_ptr<UnixDomainSocket>& socket, - ConnectionPool& connection_pool, - const long timeout) - : socket_(socket), timeout_timer_(io_service), timeout_(timeout), - buf_(), response_(), connection_pool_(connection_pool), feed_(), - response_in_progress_(false), watch_socket_(new util::WatchSocket()) { - - LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_OPENED) - .arg(socket_->getNative()); - - // Callback value of 0 is used to indicate that callback function is - // not installed. - isc::dhcp::IfaceMgr::instance().addExternalSocket(watch_socket_->getSelectFd(), 0); - isc::dhcp::IfaceMgr::instance().addExternalSocket(socket_->getNative(), 0); - - // Initialize state model for receiving and preparsing commands. - feed_.initModel(); - - // Start timer for detecting timeouts. - scheduleTimer(); - } - - /// @brief Destructor. - /// - /// Cancels timeout timer if one is scheduled. - ~Connection() { - timeout_timer_.cancel(); - } - - /// @brief This method schedules timer or reschedules existing timer. - void scheduleTimer() { - timeout_timer_.setup(std::bind(&Connection::timeoutHandler, this), - timeout_, IntervalTimer::ONE_SHOT); - } - - /// @brief Close current connection. - /// - /// Connection is not closed if the invocation of this method is a result of - /// server reconfiguration. The connection will be closed once a response is - /// sent to the client. Closing a socket during processing a request would - /// cause the server to not send a response to the client. - void stop() { - if (!response_in_progress_) { - LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_CLOSED) - .arg(socket_->getNative()); - - isc::dhcp::IfaceMgr::instance().deleteExternalSocket(watch_socket_->getSelectFd()); - isc::dhcp::IfaceMgr::instance().deleteExternalSocket(socket_->getNative()); - - // Close watch socket and log errors if occur. - std::string watch_error; - if (!watch_socket_->closeSocket(watch_error)) { - LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_CLOSE_ERROR) - .arg(watch_error); - } - - socket_->close(); - timeout_timer_.cancel(); - } - } - - /// @brief Gracefully terminates current connection. - /// - /// This method should be called prior to closing the socket to initiate - /// graceful shutdown. - void terminate(); - - /// @brief Start asynchronous read over the unix domain socket. - /// - /// This method doesn't block. Once the transmission is received over the - /// socket, the @c Connection::receiveHandler callback is invoked to - /// process received data. - void doReceive() { - socket_->asyncReceive(&buf_[0], sizeof(buf_), - std::bind(&Connection::receiveHandler, - shared_from_this(), ph::_1, ph::_2)); - } - - /// @brief Starts asynchronous send over the unix domain socket. - /// - /// This method doesn't block. Once the send operation (that covers the whole - /// data if it's small or first BUF_SIZE bytes if its large) is completed, the - /// @c Connection::sendHandler callback is invoked. That handler will either - /// close the connection gracefully if all data has been sent, or will - /// call @ref doSend() again to send the next chunk of data. - void doSend() { - size_t chunk_size = (response_.size() < BUF_SIZE) ? response_.size() : BUF_SIZE; - socket_->asyncSend(&response_[0], chunk_size, - std::bind(&Connection::sendHandler, shared_from_this(), ph::_1, ph::_2)); - - // Asynchronous send has been scheduled and we need to indicate this - // to break the synchronous select(). The handler should clear this - // status when invoked. - try { - watch_socket_->markReady(); - - } catch (const std::exception& ex) { - LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_MARK_READY_ERROR) - .arg(ex.what()); - } - } - - /// @brief Handler invoked when the data is received over the control - /// socket. - /// - /// It collects received data into the @c isc::config::JSONFeed object and - /// schedules additional asynchronous read of data if this object signals - /// that command is incomplete. When the entire command is received, the - /// handler processes this command and asynchronously responds to the - /// controlling client. - // - /// - /// @param ec Error code. - /// @param bytes_transferred Number of bytes received. - void receiveHandler(const boost::system::error_code& ec, - size_t bytes_transferred); - - /// @brief Handler invoked when the data is sent over the control socket. - /// - /// If there are still data to be sent, another asynchronous send is - /// scheduled. When the entire command is sent, the connection is shutdown - /// and closed. - /// - /// @param ec Error code. - /// @param bytes_transferred Number of bytes sent. - void sendHandler(const boost::system::error_code& ec, - size_t bytes_transferred); - - /// @brief Handler invoked when timeout has occurred. - /// - /// Asynchronously sends a response to the client indicating that the - /// timeout has occurred. - void timeoutHandler(); - -private: - - /// @brief Pointer to the socket used for transmission. - boost::shared_ptr<UnixDomainSocket> socket_; - - /// @brief Interval timer used to detect connection timeouts. - IntervalTimer timeout_timer_; - - /// @brief Connection timeout (in milliseconds) - long timeout_; - - /// @brief Buffer used for received data. - std::array<char, BUF_SIZE> buf_; - - /// @brief Response created by the server. - std::string response_; - - /// @brief Reference to the pool of connections. - ConnectionPool& connection_pool_; - - /// @brief State model used to receive data over the connection and detect - /// when the command ends. - JSONFeed feed_; - - /// @brief Boolean flag indicating if the request to stop connection is a - /// result of server reconfiguration. - bool response_in_progress_; - - /// @brief Pointer to watch socket instance used to signal that the socket - /// is ready for read or write. - util::WatchSocketPtr watch_socket_; -}; - -/// @brief Pointer to the @c Connection. -typedef boost::shared_ptr<Connection> ConnectionPtr; - -/// @brief Holds all open connections. -class ConnectionPool { -public: - - /// @brief Starts new connection. - /// - /// @param connection Pointer to the new connection object. - void start(const ConnectionPtr& connection) { - connection->doReceive(); - connections_.insert(connection); - } - - /// @brief Stops running connection. - /// - /// @param connection Pointer to the new connection object. - void stop(const ConnectionPtr& connection) { - try { - connection->stop(); - connections_.erase(connection); - } catch (const std::exception& ex) { - LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_CLOSE_FAIL) - .arg(ex.what()); - } - } - - /// @brief Stops all connections which are allowed to stop. - void stopAll() { - for (auto const& conn : connections_) { - conn->stop(); - } - connections_.clear(); - } - -private: - - /// @brief Pool of connections. - std::set<ConnectionPtr> connections_; - -}; - -void -Connection::terminate() { - try { - socket_->shutdown(); - - } catch (const std::exception& ex) { - LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_SHUTDOWN_FAIL) - .arg(ex.what()); - } -} - -void -Connection::receiveHandler(const boost::system::error_code& ec, - size_t bytes_transferred) { - if (ec) { - if (ec.value() == boost::asio::error::eof) { - std::stringstream os; - if (feed_.getProcessedText().empty()) { - os << "no input data to discard"; - } else { - os << "discarding partial command of " - << feed_.getProcessedText().size() << " bytes"; - } - - // Foreign host has closed the connection. We should remove it from the - // connection pool. - LOG_INFO(command_logger, COMMAND_SOCKET_CLOSED_BY_FOREIGN_HOST) - .arg(socket_->getNative()).arg(os.str()); - } else if (ec.value() != boost::asio::error::operation_aborted) { - LOG_ERROR(command_logger, COMMAND_SOCKET_READ_FAIL) - .arg(ec.value()).arg(socket_->getNative()); - } - - connection_pool_.stop(shared_from_this()); - return; - - } else if (bytes_transferred == 0) { - // Nothing received. Close the connection. - connection_pool_.stop(shared_from_this()); - return; - } - - LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_READ) - .arg(bytes_transferred).arg(socket_->getNative()); - - // Reschedule the timer because the transaction is ongoing. - scheduleTimer(); - - ConstElementPtr cmd; - ConstElementPtr rsp; - - try { - // Received some data over the socket. Append them to the JSON feed - // to see if we have reached the end of command. - feed_.postBuffer(&buf_[0], bytes_transferred); - feed_.poll(); - // If we haven't yet received the full command, continue receiving. - if (feed_.needData()) { - doReceive(); - return; - } - - // Received entire command. Parse the command into JSON. - if (feed_.feedOk()) { - cmd = feed_.toElement(); - response_in_progress_ = true; - - // Cancel the timer to make sure that long lasting command - // processing doesn't cause the timeout. - timeout_timer_.cancel(); - - // If successful, then process it as a command. - rsp = CommandMgr::instance().processCommand(cmd); - - response_in_progress_ = false; - - } else { - // Failed to parse command as JSON or process the received command. - // This exception will be caught below and the error response will - // be sent. - isc_throw(BadValue, feed_.getErrorMessage()); - } - - } catch (const Exception& ex) { - LOG_WARN(command_logger, COMMAND_PROCESS_ERROR1).arg(ex.what()); - rsp = createAnswer(CONTROL_RESULT_ERROR, std::string(ex.what())); - } - - // No response generated. Connection will be closed. - if (!rsp) { - LOG_WARN(command_logger, COMMAND_RESPONSE_ERROR) - .arg(cmd ? cmd->str() : "unknown"); - rsp = createAnswer(CONTROL_RESULT_ERROR, - "internal server error: no response generated"); - - } else { - - // Reschedule the timer as it may be either canceled or need to be - // updated to not timeout before we manage to the send the reply. - scheduleTimer(); - - // Let's convert JSON response to text. Note that at this stage - // the rsp pointer is always set. - response_ = rsp->str(); - - doSend(); - return; - } - - // Close the connection if we have sent the entire response. - connection_pool_.stop(shared_from_this()); -} - -void -Connection::sendHandler(const boost::system::error_code& ec, - size_t bytes_transferred) { - // Clear the watch socket so as the future send operation can mark it - // again to interrupt the synchronous select() call. - try { - watch_socket_->clearReady(); - - } catch (const std::exception& ex) { - LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_CLEAR_ERROR) - .arg(ex.what()); - } - - if (ec) { - // If an error occurred, log this error and stop the connection. - if (ec.value() != boost::asio::error::operation_aborted) { - LOG_ERROR(command_logger, COMMAND_SOCKET_WRITE_FAIL) - .arg(socket_->getNative()).arg(ec.message()); - } - - } else { - - // Reschedule the timer because the transaction is ongoing. - scheduleTimer(); - - // No error. We are in a process of sending a response. Need to - // remove the chunk that we have managed to sent with the previous - // attempt. - response_.erase(0, bytes_transferred); - - LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_WRITE) - .arg(bytes_transferred).arg(response_.size()) - .arg(socket_->getNative()); - - // Check if there is any data left to be sent and sent it. - if (!response_.empty()) { - doSend(); - return; - } - - // Gracefully shutdown the connection and close the socket if - // we have sent the whole response. - terminate(); - } - - // All data sent or an error has occurred. Close the connection. - connection_pool_.stop(shared_from_this()); -} - -void -Connection::timeoutHandler() { - LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_TIMEOUT) - .arg(socket_->getNative()); - - try { - socket_->cancel(); - - } catch (const std::exception& ex) { - LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_CANCEL_FAIL) - .arg(socket_->getNative()) - .arg(ex.what()); - } - - std::stringstream os; - os << "Connection over control channel timed out"; - if (!feed_.getProcessedText().empty()) { - os << ", discarded partial command of " - << feed_.getProcessedText().size() << " bytes"; - } - - ConstElementPtr rsp = createAnswer(CONTROL_RESULT_ERROR, os.str()); - response_ = rsp->str(); - doSend(); -} - -} namespace isc { namespace config { -/// @brief Implementation of the @c CommandMgr. -class CommandMgrImpl { -public: - - /// @brief Constructor. - CommandMgrImpl() - : io_service_(), acceptor_(), socket_(), socket_name_(), - connection_pool_(), timeout_(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND) { - } - - /// @brief Opens acceptor service allowing the control clients to connect. - /// - /// @param socket_info Configuration information for the control socket. - /// @throw BadSocketInfo When socket configuration is invalid. - /// @throw SocketError When socket operation fails. - void openCommandSocket(const isc::data::ConstElementPtr& socket_info); - - /// @brief Asynchronously accepts next connection. - void doAccept(); - - /// @brief Returns the lock file name - std::string getLockName() { - return (std::string(socket_name_ + ".lock")); - } - - /// @brief Pointer to the IO service used by the server process for running - /// asynchronous tasks. - IOServicePtr io_service_; - - /// @brief Pointer to the acceptor service. - boost::shared_ptr<UnixDomainSocketAcceptor> acceptor_; - - /// @brief Pointer to the socket into which the new connection is accepted. - boost::shared_ptr<UnixDomainSocket> socket_; - - /// @brief Path to the unix domain socket descriptor. - /// - /// This is used to remove the socket file once the connection terminates. - std::string socket_name_; - - /// @brief Pool of connections. - ConnectionPool connection_pool_; - - /// @brief Connection timeout - long timeout_; -}; - -void -CommandMgrImpl::openCommandSocket(const isc::data::ConstElementPtr& socket_info) { - socket_name_.clear(); - - if(!socket_info) { - isc_throw(BadSocketInfo, "Missing socket_info parameters, can't create socket."); - } - - ConstElementPtr type = socket_info->get("socket-type"); - if (!type) { - isc_throw(BadSocketInfo, "Mandatory 'socket-type' parameter missing"); - } - - // Only supporting unix sockets right now. - if (type->stringValue() != "unix") { - isc_throw(BadSocketInfo, "Invalid 'socket-type' parameter value " - << type->stringValue()); - } - - // UNIX socket is requested. It takes one parameter: socket-name that - // specifies UNIX path of the socket. - ConstElementPtr name = socket_info->get("socket-name"); - if (!name) { - isc_throw(BadSocketInfo, "Mandatory 'socket-name' parameter missing"); - } - - if (name->getType() != Element::string) { - isc_throw(BadSocketInfo, "'socket-name' parameter expected to be a string"); - } - - socket_name_ = name->stringValue(); - - // First let's open lock file. - std::string lock_name = getLockName(); - int lock_fd = open(lock_name.c_str(), O_RDONLY | O_CREAT, 0600); - if (lock_fd == -1) { - std::string errmsg = strerror(errno); - isc_throw(SocketError, "cannot create socket lockfile, " - << lock_name << ", : " << errmsg); - } - - // Try to acquire lock. If we can't somebody else is actively - // using it. - int ret = flock(lock_fd, LOCK_EX | LOCK_NB); - if (ret != 0) { - std::string errmsg = strerror(errno); - isc_throw(SocketError, "cannot lock socket lockfile, " - << lock_name << ", : " << errmsg); - } - - // We have the lock, so let's remove the pre-existing socket - // file if it exists. - static_cast<void>(::remove(socket_name_.c_str())); - - LOG_INFO(command_logger, COMMAND_ACCEPTOR_START) - .arg(socket_name_); - - try { - // Start asynchronous acceptor service. - acceptor_.reset(new UnixDomainSocketAcceptor(io_service_)); - UnixDomainSocketEndpoint endpoint(socket_name_); - acceptor_->open(endpoint); - acceptor_->bind(endpoint); - acceptor_->listen(); - // Install this socket in Interface Manager. - isc::dhcp::IfaceMgr::instance().addExternalSocket(acceptor_->getNative(), 0); - - doAccept(); - - } catch (const std::exception& ex) { - isc_throw(SocketError, ex.what()); - } -} - -void -CommandMgrImpl::doAccept() { - // Create a socket into which the acceptor will accept new connection. - socket_.reset(new UnixDomainSocket(io_service_)); - acceptor_->asyncAccept(*socket_, [this](const boost::system::error_code& ec) { - if (!ec) { - // New connection is arriving. Start asynchronous transmission. - ConnectionPtr connection(new Connection(io_service_, socket_, - connection_pool_, - timeout_)); - connection_pool_.start(connection); - - } else if (ec.value() != boost::asio::error::operation_aborted) { - LOG_ERROR(command_logger, COMMAND_SOCKET_ACCEPT_FAIL) - .arg(acceptor_->getNative()).arg(ec.message()); - } - - // Unless we're stopping the service, start accepting connections again. - if (ec.value() != boost::asio::error::operation_aborted) { - doAccept(); - } - }); -} - -CommandMgr::CommandMgr() - : HookedCommandMgr(), impl_(new CommandMgrImpl()) { -} - -void -CommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info) { - impl_->openCommandSocket(socket_info); -} - -void CommandMgr::closeCommandSocket() { - // Close acceptor if the acceptor is open. - if (impl_->acceptor_ && impl_->acceptor_->isOpen()) { - isc::dhcp::IfaceMgr::instance().deleteExternalSocket(impl_->acceptor_->getNative()); - impl_->acceptor_->close(); - static_cast<void>(::remove(impl_->socket_name_.c_str())); - static_cast<void>(::remove(impl_->getLockName().c_str())); - } - - // Stop all connections which can be closed. The only connection that won't - // be closed is the one over which we have received a request to reconfigure - // the server. This connection will be held until the CommandMgr responds to - // such request. - impl_->connection_pool_.stopAll(); -} - -int -CommandMgr::getControlSocketFD() { - return (impl_->acceptor_ ? impl_->acceptor_->getNative() : -1); +CommandMgr::CommandMgr() : HookedCommandMgr() { } CommandMgr& @@ -648,15 +20,5 @@ CommandMgr::instance() { return (cmd_mgr); } -void -CommandMgr::setIOService(const IOServicePtr& io_service) { - impl_->io_service_ = io_service; -} - -void -CommandMgr::setConnectionTimeout(const long timeout) { - impl_->timeout_ = timeout; -} - } // end of isc::config } // end of isc diff --git a/src/lib/config/command_mgr.h b/src/lib/config/command_mgr.h index 91f2c8f39a..10f2f0a12d 100644 --- a/src/lib/config/command_mgr.h +++ b/src/lib/config/command_mgr.h @@ -7,33 +7,12 @@ #ifndef COMMAND_MGR_H #define COMMAND_MGR_H -#include <asiolink/io_service.h> -#include <cc/data.h> #include <config/hooked_command_mgr.h> -#include <exceptions/exceptions.h> #include <boost/noncopyable.hpp> -#include <boost/shared_ptr.hpp> namespace isc { namespace config { -/// @brief An exception indicating that specified socket parameters are invalid -class BadSocketInfo : public Exception { -public: - BadSocketInfo(const char* file, size_t line, const char* what) : - isc::Exception(file, line, what) { }; -}; - -/// @brief An exception indicating a problem with socket operation -class SocketError : public Exception { -public: - SocketError(const char* file, size_t line, const char* what) : - isc::Exception(file, line, what) { }; -}; - - -class CommandMgrImpl; - /// @brief Commands Manager implementation for the Kea servers. /// /// This class extends @ref BaseCommandMgr with the ability to receive and @@ -44,48 +23,13 @@ public: /// @brief CommandMgr is a singleton class. This method returns reference /// to its sole instance. /// - /// @return the only existing instance of the manager + /// @return the only existing instance of the manager. static CommandMgr& instance(); - /// @brief Sets IO service to be used by the command manager. - /// - /// The server should use this method to provide the Command Manager with the - /// common IO service used by the server. - /// @param io_service Pointer to the IO service. - void setIOService(const asiolink::IOServicePtr& io_service); - - /// @brief Override default connection timeout. - /// - /// @param timeout New connection timeout in milliseconds. - void setConnectionTimeout(const long timeout); - - /// @brief Opens control socket with parameters specified in socket_info - /// - /// Currently supported types are: - /// - unix (required parameters: socket-type: unix, socket-name:/unix/path) - /// - /// @throw BadSocketInfo When socket configuration is invalid. - /// @throw SocketError When socket operation fails. - /// - /// @param socket_info Configuration information for the control socket. - void - openCommandSocket(const isc::data::ConstElementPtr& socket_info); - - /// @brief Shuts down any open control sockets - void closeCommandSocket(); - - /// @brief Returns control socket descriptor - /// - /// This method should be used only in tests. - int getControlSocketFD(); - private: - /// @brief Private constructor + /// @brief Private constructor. CommandMgr(); - - /// @brief Pointer to the implementation of the @ref CommandMgr. - boost::shared_ptr<CommandMgrImpl> impl_; }; } // end of isc::config namespace diff --git a/src/lib/config/http_command_mgr.cc b/src/lib/config/http_command_mgr.cc index fafe50eb11..2f396ae83e 100644 --- a/src/lib/config/http_command_mgr.cc +++ b/src/lib/config/http_command_mgr.cc @@ -215,8 +215,7 @@ HttpCommandMgr::instance() { return (http_cmd_mgr); } -HttpCommandMgr::HttpCommandMgr() - : HookedCommandMgr(), impl_(new HttpCommandMgrImpl()) { +HttpCommandMgr::HttpCommandMgr() : impl_(new HttpCommandMgrImpl()) { } void diff --git a/src/lib/config/http_command_mgr.h b/src/lib/config/http_command_mgr.h index 9cd51707a6..12b2bf3b9f 100644 --- a/src/lib/config/http_command_mgr.h +++ b/src/lib/config/http_command_mgr.h @@ -9,7 +9,6 @@ #include <asiolink/io_service.h> #include <config/http_command_config.h> -#include <config/hooked_command_mgr.h> #include <http/listener.h> #include <boost/noncopyable.hpp> @@ -21,8 +20,8 @@ class HttpCommandMgrImpl; /// @brief HTTP Commands Manager implementation for the Kea servers. /// -/// Similar to @c CommandMgr but using HTTP/HTTPS instead of UNIX sockets. -class HttpCommandMgr : public HookedCommandMgr, public boost::noncopyable { +/// Similar to @c UnixCommandMgr but using HTTP/HTTPS instead of UNIX sockets. +class HttpCommandMgr : public boost::noncopyable { public: /// @brief HttpCommandMgr is a singleton class. This method @@ -31,7 +30,7 @@ public: /// @return The only existing instance of the manager. static HttpCommandMgr& instance(); - /// @brief Sets IO service to be used by the command manager. + /// @brief Sets IO service to be used by the http command manager. /// /// The server should use this method to provide the Command /// Manager with the common IO service used by the server. @@ -56,12 +55,12 @@ public: /// @param use_external True (default) add external sockets. void addExternalSockets(bool use_external = true); - /// @brief Configure control socket from configuration. + /// @brief Configure http control socket from configuration. /// - /// @param config Configuration of the control socket. + /// @param config Configuration of the control http socket. void configure(HttpCommandConfigPtr config); - /// @brief Close control socket. + /// @brief Close http control socket. /// /// @note When remove is false @c garbageCollectListeners must /// be called after. diff --git a/src/lib/config/tests/Makefile.am b/src/lib/config/tests/Makefile.am index 41077ce953..3a95de4d2d 100644 --- a/src/lib/config/tests/Makefile.am +++ b/src/lib/config/tests/Makefile.am @@ -21,6 +21,7 @@ TESTS += run_unittests run_unittests_SOURCES = client_connection_unittests.cc run_unittests_SOURCES += run_unittests.cc run_unittests_SOURCES += command_mgr_unittests.cc +run_unittests_SOURCES += unix_command_mgr_unittests.cc run_unittests_SOURCES += cmd_http_listener_unittests.cc run_unittests_SOURCES += cmd_response_creator_unittests.cc run_unittests_SOURCES += cmd_response_creator_factory_unittests.cc diff --git a/src/lib/config/tests/command_mgr_unittests.cc b/src/lib/config/tests/command_mgr_unittests.cc index 492bcfa825..289548521f 100644 --- a/src/lib/config/tests/command_mgr_unittests.cc +++ b/src/lib/config/tests/command_mgr_unittests.cc @@ -8,8 +8,6 @@ #include <gtest/gtest.h> -#include <testutils/sandbox.h> -#include <asiolink/io_service.h> #include <config/base_command_mgr.h> #include <config/command_mgr.h> #include <config/hooked_command_mgr.h> @@ -20,7 +18,6 @@ #include <string> #include <vector> -using namespace isc::asiolink; using namespace isc::config; using namespace isc::data; using namespace isc::hooks; @@ -29,14 +26,8 @@ using namespace std; // Test class for Command Manager class CommandMgrTest : public ::testing::Test { public: - isc::test::Sandbox sandbox; - /// Default constructor - CommandMgrTest() - : io_service_(new IOService()) { - - CommandMgr::instance().setIOService(io_service_); - + CommandMgrTest() { handler_name_ = ""; handler_params_ = ElementPtr(); handler_called_ = false; @@ -45,7 +36,6 @@ public: processed_log_ = ""; CommandMgr::instance().deregisterAll(); - CommandMgr::instance().closeCommandSocket(); resetCalloutIndicators(); } @@ -53,23 +43,9 @@ public: /// Default destructor virtual ~CommandMgrTest() { CommandMgr::instance().deregisterAll(); - CommandMgr::instance().closeCommandSocket(); resetCalloutIndicators(); } - /// @brief Returns socket path (using either hardcoded path or env variable) - /// @return path to the unix socket - std::string getSocketPath() { - std::string socket_path; - const char* env = getenv("KEA_SOCKET_TEST_DIR"); - if (env) { - socket_path = std::string(env) + "/test-socket"; - } else { - socket_path = sandbox.join("test-socket"); - } - return (socket_path); - } - /// @brief Resets indicators related to callout invocation. /// /// It also removes any registered callouts. @@ -150,9 +126,6 @@ public: return (0); } - /// @brief IO service used by these tests. - IOServicePtr io_service_; - /// @brief Name of the command (used in my_handler) static std::string handler_name_; @@ -427,46 +400,6 @@ TEST_F(CommandMgrTest, delegateListCommands) { EXPECT_EQ("my-command-bis", command_names_list[2]); } -// This test verifies that a Unix socket can be opened properly and that input -// parameters (socket-type and socket-name) are verified. -TEST_F(CommandMgrTest, unixCreate) { - // Null pointer is obviously a bad idea. - EXPECT_THROW(CommandMgr::instance().openCommandSocket(ConstElementPtr()), - isc::config::BadSocketInfo); - - // So is passing no parameters. - ElementPtr socket_info = Element::createMap(); - EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), - isc::config::BadSocketInfo); - - // We don't support ipx sockets - socket_info->set("socket-type", Element::create("ipx")); - EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), - isc::config::BadSocketInfo); - - socket_info->set("socket-type", Element::create("unix")); - EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), - isc::config::BadSocketInfo); - - socket_info->set("socket-name", Element::create(getSocketPath())); - EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info)); - EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0); - - // It should be possible to close the socket. - EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket()); -} - -// This test checks that when unix path is too long, the socket cannot be opened. -TEST_F(CommandMgrTest, unixCreateTooLong) { - ElementPtr socket_info = Element::fromJSON("{ \"socket-type\": \"unix\"," - "\"socket-name\": \"/tmp/toolongtoolongtoolongtoolongtoolongtoolong" - "toolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolong" - "\" }"); - - EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), - SocketError); -} - // This test verifies that a registered callout for the command_processed // hookpoint is invoked and passed the correct information. TEST_F(CommandMgrTest, commandProcessedHook) { @@ -542,28 +475,3 @@ TEST_F(CommandMgrTest, commandProcessedHookReplaceResponse) { "{ \"result\": 2, \"text\": \"'change-response' command not supported.\" }", processed_log_); } - -// Verifies that a socket cannot be concurrently opened more than once. -TEST_F(CommandMgrTest, exclusiveOpen) { - // Pass in valid parameters. - ElementPtr socket_info = Element::createMap(); - socket_info->set("socket-type", Element::create("unix")); - socket_info->set("socket-name", Element::create(getSocketPath())); - - EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info)); - EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0); - - // Should not be able to open it twice. - EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info), - isc::config::SocketError); - - // Now let's close it. - EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket()); - - // Should be able to re-open it now. - EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info)); - EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0); - - // Now let's close it. - EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket()); -} diff --git a/src/lib/config/tests/unix_command_mgr_unittests.cc b/src/lib/config/tests/unix_command_mgr_unittests.cc new file mode 100644 index 0000000000..9b59fb6aee --- /dev/null +++ b/src/lib/config/tests/unix_command_mgr_unittests.cc @@ -0,0 +1,117 @@ +// Copyright (C) 2015-2024 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 <gtest/gtest.h> + +#include <testutils/sandbox.h> +#include <asiolink/io_service.h> +#include <config/unix_command_mgr.h> +#include <string> + +using namespace isc::asiolink; +using namespace isc::config; +using namespace isc::data; +using namespace std; + +// Test class for Unix Command Manager +class UnixCommandMgrTest : public ::testing::Test { +public: + isc::test::Sandbox sandbox; + + /// Default constructor + UnixCommandMgrTest() : io_service_(new IOService()) { + UnixCommandMgr::instance().setIOService(io_service_); + UnixCommandMgr::instance().closeCommandSocket(); + } + + /// Default destructor + virtual ~UnixCommandMgrTest() { + UnixCommandMgr::instance().closeCommandSocket(); + } + + /// @brief Returns socket path (using either hardcoded path or env variable) + /// @return path to the unix socket + std::string getSocketPath() { + std::string socket_path; + const char* env = getenv("KEA_SOCKET_TEST_DIR"); + if (env) { + socket_path = std::string(env) + "/test-socket"; + } else { + socket_path = sandbox.join("test-socket"); + } + return (socket_path); + } + + /// @brief IO service used by these tests. + IOServicePtr io_service_; +}; + +// This test verifies that a Unix socket can be opened properly and that input +// parameters (socket-type and socket-name) are verified. +TEST_F(UnixCommandMgrTest, unixCreate) { + // Null pointer is obviously a bad idea. + EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(ConstElementPtr()), + isc::config::BadSocketInfo); + + // So is passing no parameters. + ElementPtr socket_info = Element::createMap(); + EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info), + isc::config::BadSocketInfo); + + // We don't support ipx sockets + socket_info->set("socket-type", Element::create("ipx")); + EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info), + isc::config::BadSocketInfo); + + socket_info->set("socket-type", Element::create("unix")); + EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info), + isc::config::BadSocketInfo); + + socket_info->set("socket-name", Element::create(getSocketPath())); + EXPECT_NO_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info)); + EXPECT_GE(UnixCommandMgr::instance().getControlSocketFD(), 0); + + // It should be possible to close the socket. + EXPECT_NO_THROW(UnixCommandMgr::instance().closeCommandSocket()); +} + +// This test checks that when unix path is too long, the socket cannot be opened. +TEST_F(UnixCommandMgrTest, unixCreateTooLong) { + ElementPtr socket_info = Element::fromJSON("{ \"socket-type\": \"unix\"," + "\"socket-name\": \"/tmp/toolongtoolongtoolongtoolongtoolongtoolong" + "toolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolong" + "\" }"); + + EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info), + SocketError); +} + +// Verifies that a socket cannot be concurrently opened more than once. +TEST_F(UnixCommandMgrTest, exclusiveOpen) { + // Pass in valid parameters. + ElementPtr socket_info = Element::createMap(); + socket_info->set("socket-type", Element::create("unix")); + socket_info->set("socket-name", Element::create(getSocketPath())); + + EXPECT_NO_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info)); + EXPECT_GE(UnixCommandMgr::instance().getControlSocketFD(), 0); + + // Should not be able to open it twice. + EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info), + isc::config::SocketError); + + // Now let's close it. + EXPECT_NO_THROW(UnixCommandMgr::instance().closeCommandSocket()); + + // Should be able to re-open it now. + EXPECT_NO_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info)); + EXPECT_GE(UnixCommandMgr::instance().getControlSocketFD(), 0); + + // Now let's close it. + EXPECT_NO_THROW(UnixCommandMgr::instance().closeCommandSocket()); +} diff --git a/src/lib/config/unix_command_mgr.cc b/src/lib/config/unix_command_mgr.cc new file mode 100644 index 0000000000..94ed6f0a39 --- /dev/null +++ b/src/lib/config/unix_command_mgr.cc @@ -0,0 +1,663 @@ +// Copyright (C) 2015-2024 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/asio_wrapper.h> +#include <asiolink/interval_timer.h> +#include <asiolink/io_service.h> +#include <asiolink/unix_domain_socket.h> +#include <asiolink/unix_domain_socket_acceptor.h> +#include <asiolink/unix_domain_socket_endpoint.h> +#include <config/command_mgr.h> +#include <config/unix_command_mgr.h> +#include <cc/data.h> +#include <cc/command_interpreter.h> +#include <cc/json_feed.h> +#include <dhcp/iface_mgr.h> +#include <config/config_log.h> +#include <config/timeouts.h> +#include <util/watch_socket.h> +#include <boost/enable_shared_from_this.hpp> +#include <array> +#include <functional> +#include <unistd.h> +#include <sys/file.h> + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::config; +using namespace isc::data; +namespace ph = std::placeholders; + +namespace { + +/// @brief Maximum size of the data chunk sent/received over the socket. +const size_t BUF_SIZE = 32768; + +class ConnectionPool; + +/// @brief Represents a single connection over control socket. +/// +/// An instance of this object is created when the @c CommandMgr acceptor +/// receives new connection from a controlling client. +class Connection : public boost::enable_shared_from_this<Connection> { +public: + + /// @brief Constructor. + /// + /// This constructor registers a socket of this connection in the Interface + /// Manager to cause the blocking call to @c select() to return as soon as + /// a transmission over the control socket is received. + /// + /// It installs two external sockets on the @IfaceMgr to break synchronous + /// calls to @select(). The @c WatchSocket is used for send operations + /// over the connection. The native socket is used for signaling reads + /// over the connection. + /// + /// @param io_service IOService object used to handle the asio operations + /// @param socket Pointer to the object representing a socket which is used + /// for data transmission. + /// @param connection_pool Reference to the connection pool to which this + /// connection belongs. + /// @param timeout Connection timeout (in seconds). + Connection(const IOServicePtr& io_service, + const boost::shared_ptr<UnixDomainSocket>& socket, + ConnectionPool& connection_pool, + const long timeout) + : socket_(socket), timeout_timer_(io_service), timeout_(timeout), + buf_(), response_(), connection_pool_(connection_pool), feed_(), + response_in_progress_(false), watch_socket_(new util::WatchSocket()) { + + LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_OPENED) + .arg(socket_->getNative()); + + // Callback value of 0 is used to indicate that callback function is + // not installed. + isc::dhcp::IfaceMgr::instance().addExternalSocket(watch_socket_->getSelectFd(), 0); + isc::dhcp::IfaceMgr::instance().addExternalSocket(socket_->getNative(), 0); + + // Initialize state model for receiving and preparsing commands. + feed_.initModel(); + + // Start timer for detecting timeouts. + scheduleTimer(); + } + + /// @brief Destructor. + /// + /// Cancels timeout timer if one is scheduled. + ~Connection() { + timeout_timer_.cancel(); + } + + /// @brief This method schedules timer or reschedules existing timer. + void scheduleTimer() { + timeout_timer_.setup(std::bind(&Connection::timeoutHandler, this), + timeout_, IntervalTimer::ONE_SHOT); + } + + /// @brief Close current connection. + /// + /// Connection is not closed if the invocation of this method is a result of + /// server reconfiguration. The connection will be closed once a response is + /// sent to the client. Closing a socket during processing a request would + /// cause the server to not send a response to the client. + void stop() { + if (!response_in_progress_) { + LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_CLOSED) + .arg(socket_->getNative()); + + isc::dhcp::IfaceMgr::instance().deleteExternalSocket(watch_socket_->getSelectFd()); + isc::dhcp::IfaceMgr::instance().deleteExternalSocket(socket_->getNative()); + + // Close watch socket and log errors if occur. + std::string watch_error; + if (!watch_socket_->closeSocket(watch_error)) { + LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_CLOSE_ERROR) + .arg(watch_error); + } + + socket_->close(); + timeout_timer_.cancel(); + } + } + + /// @brief Gracefully terminates current connection. + /// + /// This method should be called prior to closing the socket to initiate + /// graceful shutdown. + void terminate(); + + /// @brief Start asynchronous read over the unix domain socket. + /// + /// This method doesn't block. Once the transmission is received over the + /// socket, the @c Connection::receiveHandler callback is invoked to + /// process received data. + void doReceive() { + socket_->asyncReceive(&buf_[0], sizeof(buf_), + std::bind(&Connection::receiveHandler, + shared_from_this(), ph::_1, ph::_2)); + } + + /// @brief Starts asynchronous send over the unix domain socket. + /// + /// This method doesn't block. Once the send operation (that covers the whole + /// data if it's small or first BUF_SIZE bytes if its large) is completed, the + /// @c Connection::sendHandler callback is invoked. That handler will either + /// close the connection gracefully if all data has been sent, or will + /// call @ref doSend() again to send the next chunk of data. + void doSend() { + size_t chunk_size = (response_.size() < BUF_SIZE) ? response_.size() : BUF_SIZE; + socket_->asyncSend(&response_[0], chunk_size, + std::bind(&Connection::sendHandler, shared_from_this(), ph::_1, ph::_2)); + + // Asynchronous send has been scheduled and we need to indicate this + // to break the synchronous select(). The handler should clear this + // status when invoked. + try { + watch_socket_->markReady(); + + } catch (const std::exception& ex) { + LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_MARK_READY_ERROR) + .arg(ex.what()); + } + } + + /// @brief Handler invoked when the data is received over the control + /// socket. + /// + /// It collects received data into the @c isc::config::JSONFeed object and + /// schedules additional asynchronous read of data if this object signals + /// that command is incomplete. When the entire command is received, the + /// handler processes this command and asynchronously responds to the + /// controlling client. + // + /// + /// @param ec Error code. + /// @param bytes_transferred Number of bytes received. + void receiveHandler(const boost::system::error_code& ec, + size_t bytes_transferred); + + /// @brief Handler invoked when the data is sent over the control socket. + /// + /// If there are still data to be sent, another asynchronous send is + /// scheduled. When the entire command is sent, the connection is shutdown + /// and closed. + /// + /// @param ec Error code. + /// @param bytes_transferred Number of bytes sent. + void sendHandler(const boost::system::error_code& ec, + size_t bytes_transferred); + + /// @brief Handler invoked when timeout has occurred. + /// + /// Asynchronously sends a response to the client indicating that the + /// timeout has occurred. + void timeoutHandler(); + +private: + + /// @brief Pointer to the socket used for transmission. + boost::shared_ptr<UnixDomainSocket> socket_; + + /// @brief Interval timer used to detect connection timeouts. + IntervalTimer timeout_timer_; + + /// @brief Connection timeout (in milliseconds) + long timeout_; + + /// @brief Buffer used for received data. + std::array<char, BUF_SIZE> buf_; + + /// @brief Response created by the server. + std::string response_; + + /// @brief Reference to the pool of connections. + ConnectionPool& connection_pool_; + + /// @brief State model used to receive data over the connection and detect + /// when the command ends. + JSONFeed feed_; + + /// @brief Boolean flag indicating if the request to stop connection is a + /// result of server reconfiguration. + bool response_in_progress_; + + /// @brief Pointer to watch socket instance used to signal that the socket + /// is ready for read or write. + util::WatchSocketPtr watch_socket_; +}; + +/// @brief Pointer to the @c Connection. +typedef boost::shared_ptr<Connection> ConnectionPtr; + +/// @brief Holds all open connections. +class ConnectionPool { +public: + + /// @brief Starts new connection. + /// + /// @param connection Pointer to the new connection object. + void start(const ConnectionPtr& connection) { + connection->doReceive(); + connections_.insert(connection); + } + + /// @brief Stops running connection. + /// + /// @param connection Pointer to the new connection object. + void stop(const ConnectionPtr& connection) { + try { + connection->stop(); + connections_.erase(connection); + } catch (const std::exception& ex) { + LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_CLOSE_FAIL) + .arg(ex.what()); + } + } + + /// @brief Stops all connections which are allowed to stop. + void stopAll() { + for (auto const& conn : connections_) { + conn->stop(); + } + connections_.clear(); + } + +private: + + /// @brief Pool of connections. + std::set<ConnectionPtr> connections_; + +}; + +void +Connection::terminate() { + try { + socket_->shutdown(); + + } catch (const std::exception& ex) { + LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_SHUTDOWN_FAIL) + .arg(ex.what()); + } +} + +void +Connection::receiveHandler(const boost::system::error_code& ec, + size_t bytes_transferred) { + if (ec) { + if (ec.value() == boost::asio::error::eof) { + std::stringstream os; + if (feed_.getProcessedText().empty()) { + os << "no input data to discard"; + } else { + os << "discarding partial command of " + << feed_.getProcessedText().size() << " bytes"; + } + + // Foreign host has closed the connection. We should remove it from the + // connection pool. + LOG_INFO(command_logger, COMMAND_SOCKET_CLOSED_BY_FOREIGN_HOST) + .arg(socket_->getNative()).arg(os.str()); + } else if (ec.value() != boost::asio::error::operation_aborted) { + LOG_ERROR(command_logger, COMMAND_SOCKET_READ_FAIL) + .arg(ec.value()).arg(socket_->getNative()); + } + + connection_pool_.stop(shared_from_this()); + return; + + } else if (bytes_transferred == 0) { + // Nothing received. Close the connection. + connection_pool_.stop(shared_from_this()); + return; + } + + LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_READ) + .arg(bytes_transferred).arg(socket_->getNative()); + + // Reschedule the timer because the transaction is ongoing. + scheduleTimer(); + + ConstElementPtr cmd; + ConstElementPtr rsp; + + try { + // Received some data over the socket. Append them to the JSON feed + // to see if we have reached the end of command. + feed_.postBuffer(&buf_[0], bytes_transferred); + feed_.poll(); + // If we haven't yet received the full command, continue receiving. + if (feed_.needData()) { + doReceive(); + return; + } + + // Received entire command. Parse the command into JSON. + if (feed_.feedOk()) { + cmd = feed_.toElement(); + response_in_progress_ = true; + + // Cancel the timer to make sure that long lasting command + // processing doesn't cause the timeout. + timeout_timer_.cancel(); + + // If successful, then process it as a command. + rsp = CommandMgr::instance().processCommand(cmd); + + response_in_progress_ = false; + + } else { + // Failed to parse command as JSON or process the received command. + // This exception will be caught below and the error response will + // be sent. + isc_throw(BadValue, feed_.getErrorMessage()); + } + + } catch (const Exception& ex) { + LOG_WARN(command_logger, COMMAND_PROCESS_ERROR1).arg(ex.what()); + rsp = createAnswer(CONTROL_RESULT_ERROR, std::string(ex.what())); + } + + // No response generated. Connection will be closed. + if (!rsp) { + LOG_WARN(command_logger, COMMAND_RESPONSE_ERROR) + .arg(cmd ? cmd->str() : "unknown"); + rsp = createAnswer(CONTROL_RESULT_ERROR, + "internal server error: no response generated"); + + } else { + + // Reschedule the timer as it may be either canceled or need to be + // updated to not timeout before we manage to the send the reply. + scheduleTimer(); + + // Let's convert JSON response to text. Note that at this stage + // the rsp pointer is always set. + response_ = rsp->str(); + + doSend(); + return; + } + + // Close the connection if we have sent the entire response. + connection_pool_.stop(shared_from_this()); +} + +void +Connection::sendHandler(const boost::system::error_code& ec, + size_t bytes_transferred) { + // Clear the watch socket so as the future send operation can mark it + // again to interrupt the synchronous select() call. + try { + watch_socket_->clearReady(); + + } catch (const std::exception& ex) { + LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_CLEAR_ERROR) + .arg(ex.what()); + } + + if (ec) { + // If an error occurred, log this error and stop the connection. + if (ec.value() != boost::asio::error::operation_aborted) { + LOG_ERROR(command_logger, COMMAND_SOCKET_WRITE_FAIL) + .arg(socket_->getNative()).arg(ec.message()); + } + + } else { + + // Reschedule the timer because the transaction is ongoing. + scheduleTimer(); + + // No error. We are in a process of sending a response. Need to + // remove the chunk that we have managed to sent with the previous + // attempt. + response_.erase(0, bytes_transferred); + + LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_WRITE) + .arg(bytes_transferred).arg(response_.size()) + .arg(socket_->getNative()); + + // Check if there is any data left to be sent and sent it. + if (!response_.empty()) { + doSend(); + return; + } + + // Gracefully shutdown the connection and close the socket if + // we have sent the whole response. + terminate(); + } + + // All data sent or an error has occurred. Close the connection. + connection_pool_.stop(shared_from_this()); +} + +void +Connection::timeoutHandler() { + LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_TIMEOUT) + .arg(socket_->getNative()); + + try { + socket_->cancel(); + + } catch (const std::exception& ex) { + LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_CANCEL_FAIL) + .arg(socket_->getNative()) + .arg(ex.what()); + } + + std::stringstream os; + os << "Connection over control channel timed out"; + if (!feed_.getProcessedText().empty()) { + os << ", discarded partial command of " + << feed_.getProcessedText().size() << " bytes"; + } + + ConstElementPtr rsp = createAnswer(CONTROL_RESULT_ERROR, os.str()); + response_ = rsp->str(); + doSend(); +} + +} + +namespace isc { +namespace config { + +/// @brief Implementation of the @c UnixCommandMgr. +class UnixCommandMgrImpl { +public: + + /// @brief Constructor. + UnixCommandMgrImpl() + : io_service_(), acceptor_(), socket_(), socket_name_(), + connection_pool_(), timeout_(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND) { + } + + /// @brief Opens acceptor service allowing the control clients to connect. + /// + /// @param socket_info Configuration information for the control socket. + /// @throw BadSocketInfo When socket configuration is invalid. + /// @throw SocketError When socket operation fails. + void openCommandSocket(const isc::data::ConstElementPtr& socket_info); + + /// @brief Asynchronously accepts next connection. + void doAccept(); + + /// @brief Returns the lock file name + std::string getLockName() { + return (std::string(socket_name_ + ".lock")); + } + + /// @brief Pointer to the IO service used by the server process for running + /// asynchronous tasks. + IOServicePtr io_service_; + + /// @brief Pointer to the acceptor service. + boost::shared_ptr<UnixDomainSocketAcceptor> acceptor_; + + /// @brief Pointer to the socket into which the new connection is accepted. + boost::shared_ptr<UnixDomainSocket> socket_; + + /// @brief Path to the unix domain socket descriptor. + /// + /// This is used to remove the socket file once the connection terminates. + std::string socket_name_; + + /// @brief Pool of connections. + ConnectionPool connection_pool_; + + /// @brief Connection timeout + long timeout_; +}; + +void +UnixCommandMgrImpl::openCommandSocket(const isc::data::ConstElementPtr& socket_info) { + socket_name_.clear(); + + if(!socket_info) { + isc_throw(BadSocketInfo, "Missing socket_info parameters, can't create socket."); + } + + ConstElementPtr type = socket_info->get("socket-type"); + if (!type) { + isc_throw(BadSocketInfo, "Mandatory 'socket-type' parameter missing"); + } + + // Only supporting unix sockets right now. + if (type->stringValue() != "unix") { + isc_throw(BadSocketInfo, "Invalid 'socket-type' parameter value " + << type->stringValue()); + } + + // UNIX socket is requested. It takes one parameter: socket-name that + // specifies UNIX path of the socket. + ConstElementPtr name = socket_info->get("socket-name"); + if (!name) { + isc_throw(BadSocketInfo, "Mandatory 'socket-name' parameter missing"); + } + + if (name->getType() != Element::string) { + isc_throw(BadSocketInfo, "'socket-name' parameter expected to be a string"); + } + + socket_name_ = name->stringValue(); + + // First let's open lock file. + std::string lock_name = getLockName(); + int lock_fd = open(lock_name.c_str(), O_RDONLY | O_CREAT, 0600); + if (lock_fd == -1) { + std::string errmsg = strerror(errno); + isc_throw(SocketError, "cannot create socket lockfile, " + << lock_name << ", : " << errmsg); + } + + // Try to acquire lock. If we can't somebody else is actively + // using it. + int ret = flock(lock_fd, LOCK_EX | LOCK_NB); + if (ret != 0) { + std::string errmsg = strerror(errno); + isc_throw(SocketError, "cannot lock socket lockfile, " + << lock_name << ", : " << errmsg); + } + + // We have the lock, so let's remove the pre-existing socket + // file if it exists. + static_cast<void>(::remove(socket_name_.c_str())); + + LOG_INFO(command_logger, COMMAND_ACCEPTOR_START) + .arg(socket_name_); + + try { + // Start asynchronous acceptor service. + acceptor_.reset(new UnixDomainSocketAcceptor(io_service_)); + UnixDomainSocketEndpoint endpoint(socket_name_); + acceptor_->open(endpoint); + acceptor_->bind(endpoint); + acceptor_->listen(); + // Install this socket in Interface Manager. + isc::dhcp::IfaceMgr::instance().addExternalSocket(acceptor_->getNative(), 0); + + doAccept(); + + } catch (const std::exception& ex) { + isc_throw(SocketError, ex.what()); + } +} + +void +UnixCommandMgrImpl::doAccept() { + // Create a socket into which the acceptor will accept new connection. + socket_.reset(new UnixDomainSocket(io_service_)); + acceptor_->asyncAccept(*socket_, [this](const boost::system::error_code& ec) { + if (!ec) { + // New connection is arriving. Start asynchronous transmission. + ConnectionPtr connection(new Connection(io_service_, socket_, + connection_pool_, + timeout_)); + connection_pool_.start(connection); + + } else if (ec.value() != boost::asio::error::operation_aborted) { + LOG_ERROR(command_logger, COMMAND_SOCKET_ACCEPT_FAIL) + .arg(acceptor_->getNative()).arg(ec.message()); + } + + // Unless we're stopping the service, start accepting connections again. + if (ec.value() != boost::asio::error::operation_aborted) { + doAccept(); + } + }); +} + +UnixCommandMgr::UnixCommandMgr() : impl_(new UnixCommandMgrImpl()) { +} + +void +UnixCommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info) { + impl_->openCommandSocket(socket_info); +} + +void +UnixCommandMgr::closeCommandSocket() { + // Close acceptor if the acceptor is open. + if (impl_->acceptor_ && impl_->acceptor_->isOpen()) { + isc::dhcp::IfaceMgr::instance().deleteExternalSocket(impl_->acceptor_->getNative()); + impl_->acceptor_->close(); + static_cast<void>(::remove(impl_->socket_name_.c_str())); + static_cast<void>(::remove(impl_->getLockName().c_str())); + } + + // Stop all connections which can be closed. The only connection that won't + // be closed is the one over which we have received a request to reconfigure + // the server. This connection will be held until the UnixCommandMgr + // responds to such request. + impl_->connection_pool_.stopAll(); +} + +int +UnixCommandMgr::getControlSocketFD() { + return (impl_->acceptor_ ? impl_->acceptor_->getNative() : -1); +} + +UnixCommandMgr& +UnixCommandMgr::instance() { + static UnixCommandMgr cmd_mgr; + return (cmd_mgr); +} + +void +UnixCommandMgr::setIOService(const IOServicePtr& io_service) { + impl_->io_service_ = io_service; +} + +void +UnixCommandMgr::setConnectionTimeout(const long timeout) { + impl_->timeout_ = timeout; +} + +} // end of isc::config +} // end of isc diff --git a/src/lib/config/unix_command_mgr.h b/src/lib/config/unix_command_mgr.h new file mode 100644 index 0000000000..1bed1e532e --- /dev/null +++ b/src/lib/config/unix_command_mgr.h @@ -0,0 +1,88 @@ +// Copyright (C) 2015-2024 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/. + +#ifndef UNIX_COMMAND_MGR_H +#define UNIX_COMMAND_MGR_H + +#include <asiolink/io_service.h> +#include <cc/data.h> +#include <exceptions/exceptions.h> +#include <boost/noncopyable.hpp> +#include <boost/shared_ptr.hpp> + +namespace isc { +namespace config { + +/// @brief An exception indicating that specified socket parameters are invalid +class BadSocketInfo : public Exception { +public: + BadSocketInfo(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief An exception indicating a problem with socket operation +class SocketError : public Exception { +public: + SocketError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +class UnixCommandMgrImpl; + +/// @brief Unix Commands Manager implementation for the Kea servers. +/// +/// This class receives and responds to commands over unix domain sockets. +class UnixCommandMgr : public boost::noncopyable { +public: + + /// @brief UnixCommandMgr is a singleton class. This method + /// returns reference to its sole instance. + /// + /// @return the only existing instance of the manager. + static UnixCommandMgr& instance(); + + /// @brief Sets IO service to be used by the unix command manager. + /// + /// The server should use this method to provide the Unix Command + /// Manager with the common IO service used by the server. + /// @param io_service Pointer to the IO service. + void setIOService(const asiolink::IOServicePtr& io_service); + + /// @brief Override default connection timeout. + /// + /// @param timeout New connection timeout in milliseconds. + void setConnectionTimeout(const long timeout); + + /// @brief Opens unix control socket with parameters specified in socket_info + /// (required parameters: socket-type: unix, socket-name:/unix/path). + /// + /// @throw BadSocketInfo When socket configuration is invalid. + /// @throw SocketError When socket operation fails. + /// + /// @param socket_info Configuration information for the unix control socket. + void + openCommandSocket(const isc::data::ConstElementPtr& socket_info); + + /// @brief Shuts down any open unix control sockets + void closeCommandSocket(); + + /// @brief Returns unix control socket descriptor + /// + /// This method should be used only in tests. + int getControlSocketFD(); + +private: + + /// @brief Private constructor. + UnixCommandMgr(); + + /// @brief Pointer to the implementation of the @ref UnixCommandMgr. + boost::shared_ptr<UnixCommandMgrImpl> impl_; +}; + +} // end of isc::config namespace +} // end of isc namespace +#endif |