From: Razvan Becheriu Date: Thu, 28 Aug 2025 08:48:14 +0000 (+0300) Subject: [#3986] check if LFC is running on config-set X-Git-Tag: Kea-3.1.2~94 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3cf23c76ec7eca3bcee6a2e1883c8514ae9ae9de;p=thirdparty%2Fkea.git [#3986] check if LFC is running on config-set --- diff --git a/src/bin/dhcp4/ctrl_dhcp4_srv.cc b/src/bin/dhcp4/ctrl_dhcp4_srv.cc index f62a41f0be..1d59e0b7ac 100644 --- a/src/bin/dhcp4/ctrl_dhcp4_srv.cc +++ b/src/bin/dhcp4/ctrl_dhcp4_srv.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -374,6 +376,21 @@ ControlledDhcpv4Srv::commandConfigSetHandler(const string&, return (result); } + ConstElementPtr lease_database = dhcp4->get("lease-database"); + if (lease_database) { + db::DbAccessParser parser; + std::string access_string; + parser.parse(access_string, lease_database); + auto params = parser.getDbAccessParameters(); + if (params["type"] == "memfile") { + string file_name = params["name"]; + if (Memfile_LeaseMgr::isLFCProcessRunning(file_name, Memfile_LeaseMgr::V4)) { + return (isc::config::createAnswer(CONTROL_RESULT_ERROR, + "Can not update configuration while lease file cleanup process is running.")); + } + } + } + // stop thread pool (if running) MultiThreadingCriticalSection cs; @@ -396,7 +413,7 @@ ControlledDhcpv4Srv::commandConfigSetHandler(const string&, ConstElementPtr result = processConfig(dhcp4); // If the configuration parsed successfully, apply the new logger - // configuration and the commit the new configuration. We apply + // configuration and then commit the new configuration. We apply // the logging first in case there's a configuration failure. int rcode = 0; isc::config::parseAnswer(rcode, result); diff --git a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc index d0f717b996..ab0800d145 100644 --- a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc @@ -10,8 +10,9 @@ #include #include #include -#include #include +#include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +51,7 @@ using namespace isc; using namespace isc::asiolink; using namespace isc::config; using namespace isc::data; +using namespace isc::db; using namespace isc::dhcp; using namespace isc::dhcp::test; using namespace isc::hooks; @@ -102,7 +105,7 @@ public: }; /// @brief Fixture class intended for testing control channel in the DHCPv4Srv -class CtrlChannelDhcpv4SrvTest : public ::testing::Test { +class CtrlChannelDhcpv4SrvTest : public BaseServerTest { public: isc::test::Sandbox sandbox; @@ -457,6 +460,18 @@ public: } return (createAnswer(CONTROL_RESULT_SUCCESS, arguments)); } + + /// @brief Return path to the lease file used by unit tests. + /// + /// @param filename Name of the lease file appended to the path to the + /// directory where test data is held. + /// + /// @return Full path to the lease file. + static std::string getLeaseFilePath(const std::string& filename) { + std::ostringstream s; + s << CfgMgr::instance().getDataDir() << "/" << filename; + return (s.str()); + } }; TEST_F(CtrlChannelDhcpv4SrvTest, commands) { @@ -875,6 +890,158 @@ TEST_F(CtrlChannelDhcpv4SrvTest, configSet) { CfgMgr::instance().clear(); } +// Check that the "config-set" fails when LFC is running. +TEST_F(CtrlChannelDhcpv4SrvTest, configSetLFCRunning) { + setLogTestPath("/dev"); + createUnixChannelServer(); + + // Define strings to permutate the config arguments + // (Note the line feeds makes errors easy to find) + string set_config_txt = "{ \"command\": \"config-set\" \n"; + string args_txt = " \"arguments\": { \n"; + string dhcp4_cfg_txt = + " \"Dhcp4\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet4\": [ \n"; + string subnet1 = + " {\"subnet\": \"192.2.0.0/24\", \"id\": 1, \n" + " \"pools\": [{ \"pool\": \"192.2.0.1-192.2.0.50\" }]}\n"; + string subnet2 = + " {\"subnet\": \"192.2.1.0/24\", \"id\": 2, \n" + " \"pools\": [{ \"pool\": \"192.2.1.1-192.2.1.50\" }]}\n"; + string bad_subnet = + " {\"comment\": \"192.2.2.0/24\", \"id\": 10, \n" + " \"pools\": [{ \"pool\": \"192.2.2.1-192.2.2.50\" }]}\n"; + string subnet_footer = + " ] \n"; + string option_def = + " ,\"option-def\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"type\": \"uint32\",\n" + " \"array\": false,\n" + " \"record-types\": \"\",\n" + " \"space\": \"dhcp4\",\n" + " \"encapsulate\": \"\"\n" + " }\n" + "]\n"; + string option_data = + " ,\"option-data\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"space\": \"dhcp4\",\n" + " \"csv-format\": true,\n" + " \"data\": \"12345\"\n" + " }\n" + "]\n"; + string control_socket_header = + " ,\"control-socket\": { \n" + " \"socket-type\": \"unix\", \n" + " \"socket-name\": \""; + string control_socket_footer = + "\" \n} \n"; + string logger_txt = + " ,\"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output-options\": [{ \n" + " \"output\": \"/dev/null\", \n" + " \"maxsize\": 0" + " }] \n" + " }] \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << set_config_txt << "," + << args_txt + << dhcp4_cfg_txt + << subnet1 + << subnet_footer + << option_def + << option_data + << control_socket_header + << socket_path_ + << control_socket_footer + << logger_txt + << "}\n" // close dhcp4 + << "}}"; + + // Send the config-set command + std::string response; + sendUnixCommand(os.str(), response); + + // Verify the configuration was successful. The config contains random + // socket name (/tmp/kea-/kea4.sock), so the + // hash will be different each time. As such, we can do simplified checks: + // - verify the "result": 0 is there + // - verify the "text": "Configuration successful." is there + EXPECT_NE(response.find("\"result\": 0"), std::string::npos); + EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), std::string::npos); + + // Check that the config was indeed applied. + const Subnet4Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + OptionDefinitionPtr def = + LibDHCP::getRuntimeOptionDef(DHCP4_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Create the backend configuration. + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "4"; + pmap["name"] = getLeaseFilePath("kea-leases4.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + // Send the config-set command + sendUnixCommand(os.str(), response); + + // Should fail with an error + EXPECT_EQ("{ \"result\": 1, " + "\"text\": \"Can not update configuration while lease file cleanup process is running.\" }", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + def = LibDHCP::getRuntimeOptionDef(DHCP4_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Verify the control channel socket exists. + ASSERT_TRUE(fileExists(socket_path_)); + + // Clean up after the test. + CfgMgr::instance().clear(); +} + // Tests if the server returns its configuration using config-get. // Note there are separate tests that verify if toElement() called by the // config-get handler are actually converting the configuration correctly. @@ -1642,6 +1809,59 @@ TEST_F(CtrlChannelDhcpv4SrvTest, configReloadBrokenFile) { ::remove("test7.json"); } +// Tests if config-reload attempts to reload a file while LFC is running +TEST_F(CtrlChannelDhcpv4SrvTest, configReloadLFCRunning) { + createUnixChannelServer(); + std::string response; + + // This is normally set to whatever value is passed to -c when the server is + // started, but we're not starting it that way, so need to set it by hand. + server_->setConfigFile("test8.json"); + + // Ok, enough fooling around. Let's create a valid config. + const std::string cfg_txt = + "{ \"Dhcp4\": {" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"subnet4\": [" + " { \"id\": 1, \"subnet\": \"192.0.2.0/24\" }," + " { \"id\": 2, \"subnet\": \"192.0.3.0/24\" }" + " ]," + " \"valid-lifetime\": 4000," + " \"lease-database\": {" + " \"type\": \"memfile\", \"persist\": false }" + "} }"; + ofstream f("test8.json", ios::trunc); + f << cfg_txt; + f.close(); + + // Create the backend configuration. + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "4"; + pmap["name"] = getLeaseFilePath("kea-leases4.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + // Now tell Kea to reload its config. + sendUnixCommand("{ \"command\": \"config-reload\" }", response); + + // Verify the reload will fail. + EXPECT_EQ("{ \"result\": 1, \"text\": \"Config reload failed: configuration error using file 'test8.json': " + "Can not update configuration while lease file cleanup process is running.\" }", + response); + + ::remove("test8.json"); +} + // Tests if config-reload attempts to reload a file and reports that the // file is loaded correctly. TEST_F(CtrlChannelDhcpv4SrvTest, configReloadValid) { diff --git a/src/bin/dhcp4/tests/http_control_socket_unittest.cc b/src/bin/dhcp4/tests/http_control_socket_unittest.cc index 67126f8726..947482be54 100644 --- a/src/bin/dhcp4/tests/http_control_socket_unittest.cc +++ b/src/bin/dhcp4/tests/http_control_socket_unittest.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +54,7 @@ using namespace isc::asiolink; using namespace isc::asiolink::test; using namespace isc::config; using namespace isc::data; +using namespace isc::db; using namespace isc::dhcp; using namespace isc::dhcp::test; using namespace isc::hooks; @@ -85,7 +88,7 @@ public: }; /// @brief Base fixture class intended for testing HTTP/HTTPS control channel. -class BaseCtrlChannelDhcpv4Test : public ::testing::Test { +class BaseCtrlChannelDhcpv4Test : public BaseServerTest { public: /// @brief List of interfaces (defaults to "*"). @@ -529,6 +532,9 @@ public: // file is not a valid JSON. void testConfigReloadBrokenFile(); + // Tests if config-reload attempts to reload a file while LFC is running. + void testConfigReloadLFCRunning(); + // Tests if config-reload attempts to reload a file and reports that the // file is loaded correctly. void testConfigReloadValid(); @@ -575,6 +581,18 @@ public: // This test verifies that the server signals timeout if the transmission // takes too long, having received no data from the client. void testConnectionTimeoutNoData(); + + /// @brief Return path to the lease file used by unit tests. + /// + /// @param filename Name of the lease file appended to the path to the + /// directory where test data is held. + /// + /// @return Full path to the lease file. + static std::string getLeaseFilePath(const std::string& filename) { + std::ostringstream s; + s << CfgMgr::instance().getDataDir() << "/" << filename; + return (s.str()); + } }; /// @brief Fixture class intended for testing HTTP control channel. @@ -1179,6 +1197,152 @@ TEST_F(HttpCtrlChannelDhcpv4Test, configSet) { CfgMgr::instance().clear(); } +// Check that the "config-set" fails when LFC is running. +TEST_F(HttpCtrlChannelDhcpv4Test, configSetLFCRunning) { + createHttpChannelServer(); + + // Define strings to permutate the config arguments + // (Note the line feeds makes errors easy to find) + string config_set_txt = "{ \"command\": \"config-set\" \n"; + string args_txt = " \"arguments\": { \n"; + string dhcp4_cfg_txt = + " \"Dhcp4\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet4\": [ \n"; + string subnet1 = + " {\"subnet\": \"192.2.0.0/24\", \"id\": 1, \n" + " \"pools\": [{ \"pool\": \"192.2.0.1-192.2.0.50\" }]}\n"; + string subnet2 = + " {\"subnet\": \"192.2.1.0/24\", \"id\": 2, \n" + " \"pools\": [{ \"pool\": \"192.2.1.1-192.2.1.50\" }]}\n"; + string bad_subnet = + " {\"comment\": \"192.2.2.0/24\", \"id\": 10, \n" + " \"pools\": [{ \"pool\": \"192.2.2.1-192.2.2.50\" }]}\n"; + string subnet_footer = + " ] \n"; + string option_def = + " ,\"option-def\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"type\": \"uint32\",\n" + " \"array\": false,\n" + " \"record-types\": \"\",\n" + " \"space\": \"dhcp4\",\n" + " \"encapsulate\": \"\"\n" + " }\n" + "]\n"; + string option_data = + " ,\"option-data\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"space\": \"dhcp4\",\n" + " \"csv-format\": true,\n" + " \"data\": \"12345\"\n" + " }\n" + "]\n"; + string control_socket = + " ,\"control-socket\": { \n" + " \"socket-type\": \"http\", \n" + " \"socket-address\": \"127.0.0.1\", \n" + " \"socket-port\": 18124 \n" + " } \n"; + string logger_txt = + " ,\"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output-options\": [{ \n" + " \"output\": \"/dev/null\", \n" + " \"maxsize\": 0" + " }] \n" + " }] \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << config_set_txt << "," + << args_txt + << dhcp4_cfg_txt + << subnet1 + << subnet_footer + << option_def + << option_data + << control_socket + << logger_txt + << "}\n" // close dhcp4 + << "}}"; + + // Send the config-set command + std::string response; + sendHttpCommand(os.str(), response); + EXPECT_EQ("[ { \"arguments\": { \"hash\": \"CEA228E014102D623F40E6E9C68EC166A6021A81E220A15276F8C87F68866FDF\" }, \"result\": 0, \"text\": \"Configuration successful.\" } ]", + response); + + // Check that the config was indeed applied. + const Subnet4Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + OptionDefinitionPtr def = + LibDHCP::getRuntimeOptionDef(DHCP4_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Create the backend configuration. + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "4"; + pmap["name"] = getLeaseFilePath("kea-leases4.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + // Send the config-set command + sendHttpCommand(os.str(), response); + + // Should fail with an error + EXPECT_EQ("[ { \"result\": 1, " + "\"text\": \"Can not update configuration while lease file cleanup process is running.\" } ]", + response); + + // Verify the HTTP control channel socket exists. + EXPECT_TRUE(HttpCommandMgr::instance().getHttpListener()); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + def = LibDHCP::getRuntimeOptionDef(DHCP4_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Verify the HTTP control channel socket exists. + EXPECT_TRUE(HttpCommandMgr::instance().getHttpListener()); + + // Clean up after the test. + CfgMgr::instance().clear(); +} + // Check that the "config-set" command will replace current configuration. TEST_F(HttpsCtrlChannelDhcpv4Test, configSet) { createHttpChannelServer(); @@ -1364,6 +1528,161 @@ TEST_F(HttpsCtrlChannelDhcpv4Test, configSet) { CfgMgr::instance().clear(); } +// Check that the "config-set" fails when LFC is running. +TEST_F(HttpsCtrlChannelDhcpv4Test, configSetLFCRunning) { + createHttpChannelServer(); + + // Define strings to permutate the config arguments + // (Note the line feeds makes errors easy to find) + string ca_dir(string(TEST_CA_DIR)); + string config_set_txt = "{ \"command\": \"config-set\" \n"; + string args_txt = " \"arguments\": { \n"; + string dhcp4_cfg_txt = + " \"Dhcp4\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet4\": [ \n"; + string subnet1 = + " {\"subnet\": \"192.2.0.0/24\", \"id\": 1, \n" + " \"pools\": [{ \"pool\": \"192.2.0.1-192.2.0.50\" }]}\n"; + string subnet2 = + " {\"subnet\": \"192.2.1.0/24\", \"id\": 2, \n" + " \"pools\": [{ \"pool\": \"192.2.1.1-192.2.1.50\" }]}\n"; + string bad_subnet = + " {\"comment\": \"192.2.2.0/24\", \"id\": 10, \n" + " \"pools\": [{ \"pool\": \"192.2.2.1-192.2.2.50\" }]}\n"; + string subnet_footer = + " ] \n"; + string option_def = + " ,\"option-def\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"type\": \"uint32\",\n" + " \"array\": false,\n" + " \"record-types\": \"\",\n" + " \"space\": \"dhcp4\",\n" + " \"encapsulate\": \"\"\n" + " }\n" + "]\n"; + string option_data = + " ,\"option-data\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"space\": \"dhcp4\",\n" + " \"csv-format\": true,\n" + " \"data\": \"12345\"\n" + " }\n" + "]\n"; + string control_socket_header = + " ,\"control-socket\": { \n" + " \"socket-type\": \"http\", \n" + " \"socket-address\": \"127.0.0.1\", \n" + " \"socket-port\": 18124, \n"; + string control_socket_footer = + " } \n"; + string logger_txt = + " ,\"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output-options\": [{ \n" + " \"output\": \"/dev/null\", \n" + " \"maxsize\": 0" + " }] \n" + " }] \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << config_set_txt << "," + << args_txt + << dhcp4_cfg_txt + << subnet1 + << subnet_footer + << option_def + << option_data + << control_socket_header + << " \"trust-anchor\": \"" << ca_dir << "/kea-ca.crt\", \n" + << " \"cert-file\": \"" << ca_dir << "/kea-server.crt\", \n" + << " \"key-file\": \"" << ca_dir << "/kea-server.key\" \n" + << control_socket_footer + << logger_txt + << "}\n" // close dhcp4 + << "}}"; + + // Send the config-set command + std::string response; + sendHttpCommand(os.str(), response); + // Verify the configuration was successful. The config contains random + // file paths (CA directory), so the hash will be different each time. + // As such, we can do simplified checks: + // - verify the "result": 0 is there + // - verify the "text": "Configuration successful." is there + EXPECT_NE(response.find("\"result\": 0"), std::string::npos); + EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), + std::string::npos); + + // Check that the config was indeed applied. + const Subnet4Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + OptionDefinitionPtr def = + LibDHCP::getRuntimeOptionDef(DHCP4_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Create the backend configuration. + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "4"; + pmap["name"] = getLeaseFilePath("kea-leases4.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + // Send the config-set command + sendHttpCommand(os.str(), response); + + // Should fail with an error + EXPECT_EQ("[ { \"result\": 1, " + "\"text\": \"Can not update configuration while lease file cleanup process is running.\" } ]", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll(); + EXPECT_EQ(1, subnets->size()); + + def = LibDHCP::getRuntimeOptionDef(DHCP4_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Verify the HTTP control channel socket exists. + EXPECT_TRUE(HttpCommandMgr::instance().getHttpListener()); + + // Clean up after the test. + CfgMgr::instance().clear(); +} + // Tests if the server returns its configuration using config-get. // Note there are separate tests that verify if toElement() called by the // config-get handler are actually converting the configuration correctly. @@ -2508,6 +2827,68 @@ TEST_F(HttpsCtrlChannelDhcpv4Test, configReloadBrokenFile) { testConfigReloadBrokenFile(); } +// Tests if config-reload attempts to reload a file while LFC is running. +void +BaseCtrlChannelDhcpv4Test::testConfigReloadLFCRunning() { + createHttpChannelServer(); + std::string response; + + // This is normally set to whatever value is passed to -c when the server is + // started, but we're not starting it that way, so need to set it by hand. + server_->setConfigFile("test8.json"); + + // Ok, enough fooling around. Let's create a valid config. + const std::string cfg_txt = + "{ \"Dhcp4\": {" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"subnet4\": [" + " { \"id\": 1, \"subnet\": \"192.0.2.0/24\" }," + " { \"id\": 2, \"subnet\": \"192.0.3.0/24\" }" + " ]," + " \"valid-lifetime\": 4000," + " \"lease-database\": {" + " \"type\": \"memfile\", \"persist\": false }" + "} }"; + ofstream f("test8.json", ios::trunc); + f << cfg_txt; + f.close(); + + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "4"; + pmap["name"] = getLeaseFilePath("kea-leases4.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + + // This command should reload test8.json config. + sendHttpCommand("{ \"command\": \"config-reload\" }", response); + + // Verify the reload will fail. + EXPECT_EQ("[ { \"result\": 1, \"text\": \"Config reload failed: configuration error using file 'test8.json': " + "Can not update configuration while lease file cleanup process is running.\" } ]", + response); + + ::remove("test8.json"); +} + +TEST_F(HttpCtrlChannelDhcpv4Test, configReloadLFCRunning) { + testConfigReloadLFCRunning(); +} + +TEST_F(HttpsCtrlChannelDhcpv4Test, configReloadLFCRunning) { + testConfigReloadLFCRunning(); +} + // Tests if config-reload attempts to reload a file and reports that the // file is loaded correctly. void diff --git a/src/bin/dhcp6/ctrl_dhcp6_srv.cc b/src/bin/dhcp6/ctrl_dhcp6_srv.cc index 3ff5f0e53e..9d2dcdf6e9 100644 --- a/src/bin/dhcp6/ctrl_dhcp6_srv.cc +++ b/src/bin/dhcp6/ctrl_dhcp6_srv.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -377,6 +379,21 @@ ControlledDhcpv6Srv::commandConfigSetHandler(const string&, return (result); } + ConstElementPtr lease_database = dhcp6->get("lease-database"); + if (lease_database) { + db::DbAccessParser parser; + std::string access_string; + parser.parse(access_string, lease_database); + auto params = parser.getDbAccessParameters(); + if (params["type"] == "memfile") { + string file_name = params["name"]; + if (Memfile_LeaseMgr::isLFCProcessRunning(file_name, Memfile_LeaseMgr::V6)) { + return (isc::config::createAnswer(CONTROL_RESULT_ERROR, + "Can not update configuration while lease file cleanup process is running.")); + } + } + } + // stop thread pool (if running) MultiThreadingCriticalSection cs; @@ -399,7 +416,7 @@ ControlledDhcpv6Srv::commandConfigSetHandler(const string&, ConstElementPtr result = processConfig(dhcp6); // If the configuration parsed successfully, apply the new logger - // configuration and the commit the new configuration. We apply + // configuration and then commit the new configuration. We apply // the logging first in case there's a configuration failure. int rcode = 0; isc::config::parseAnswer(rcode, result); diff --git a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc index 3b9df41e2d..d6ca8608c8 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 #include #include +#include #include #include #include @@ -17,10 +18,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -49,6 +52,7 @@ using namespace isc; using namespace isc::asiolink; using namespace isc::config; using namespace isc::data; +using namespace isc::db; using namespace isc::dhcp; using namespace isc::dhcp::test; using namespace isc::hooks; @@ -459,6 +463,18 @@ public: } return (createAnswer(CONTROL_RESULT_SUCCESS, arguments)); } + + /// @brief Return path to the lease file used by unit tests. + /// + /// @param filename Name of the lease file appended to the path to the + /// directory where test data is held. + /// + /// @return Full path to the lease file. + static std::string getLeaseFilePath(const std::string& filename) { + std::ostringstream s; + s << CfgMgr::instance().getDataDir() << "/" << filename; + return (s.str()); + } }; TEST_F(CtrlDhcpv6SrvTest, commands) { @@ -885,6 +901,159 @@ TEST_F(CtrlChannelDhcpv6SrvTest, configSet) { CfgMgr::instance().clear(); } +// Check that the "config-set" fails when LFC is running. +TEST_F(CtrlChannelDhcpv6SrvTest, configSetLFCRunning) { + setLogTestPath("/dev"); + createUnixChannelServer(); + + // Define strings to permutate the config arguments + // (Note the line feeds makes errors easy to find) + string set_config_txt = "{ \"command\": \"config-set\" \n"; + string args_txt = " \"arguments\": { \n"; + string dhcp6_cfg_txt = + " \"Dhcp6\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"preferred-lifetime\": 3000, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet6\": [ \n"; + string subnet1 = + " {\"subnet\": \"3002::/64\", \"id\": 1, \n" + " \"pools\": [{ \"pool\": \"3002::100-3002::200\" }]}\n"; + string subnet2 = + " {\"subnet\": \"3003::/64\", \"id\": 2, \n" + " \"pools\": [{ \"pool\": \"3003::100-3003::200\" }]}\n"; + string bad_subnet = + " {\"comment\": \"3005::/64\", \"id\": 10, \n" + " \"pools\": [{ \"pool\": \"3005::100-3005::200\" }]}\n"; + string subnet_footer = + " ] \n"; + string option_def = + " ,\"option-def\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"type\": \"uint32\",\n" + " \"array\": false,\n" + " \"record-types\": \"\",\n" + " \"space\": \"dhcp6\",\n" + " \"encapsulate\": \"\"\n" + " }\n" + "]\n"; + string option_data = + " ,\"option-data\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"space\": \"dhcp6\",\n" + " \"csv-format\": true,\n" + " \"data\": \"12345\"\n" + " }\n" + "]\n"; + string control_socket_header = + " ,\"control-socket\": { \n" + " \"socket-type\": \"unix\", \n" + " \"socket-name\": \""; + string control_socket_footer = + "\" \n} \n"; + string logger_txt = + " ,\"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output-options\": [{ \n" + " \"output\": \"/dev/null\", \n" + " \"maxsize\": 0" + " }] \n" + " }] \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << set_config_txt << "," + << args_txt + << dhcp6_cfg_txt + << subnet1 + << subnet_footer + << option_def + << option_data + << control_socket_header + << socket_path_ + << control_socket_footer + << logger_txt + << "}\n" // close dhcp6 + << "}}"; + + // Send the config-set command + std::string response; + sendUnixCommand(os.str(), response); + + // Verify the configuration was successful. The config contains random + // socket name (/tmp/kea-/kea6.sock), so the + // hash will be different each time. As such, we can do simplified checks: + // - verify the "result": 0 is there + // - verify the "text": "Configuration successful." is there + EXPECT_NE(response.find("\"result\": 0"), std::string::npos); + EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), std::string::npos); + + // Check that the config was indeed applied. + const Subnet6Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + OptionDefinitionPtr def = + LibDHCP::getRuntimeOptionDef(DHCP6_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Create the backend configuration. + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "6"; + pmap["name"] = getLeaseFilePath("kea-leases6.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + // Send the config-set command + sendUnixCommand(os.str(), response); + + // Should fail with an error + EXPECT_EQ("{ \"result\": 1, " + "\"text\": \"Can not update configuration while lease file cleanup process is running.\" }", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + def = LibDHCP::getRuntimeOptionDef(DHCP6_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Verify the control channel socket exists. + ASSERT_TRUE(fileExists(socket_path_)); + + // Clean up after the test. + CfgMgr::instance().clear(); +} + // Tests if the server returns its configuration using config-get. // Note there are separate tests that verify if toElement() called by the // config-get handler are actually converting the configuration correctly. @@ -1644,6 +1813,58 @@ TEST_F(CtrlChannelDhcpv6SrvTest, configReloadBrokenFile) { ::remove("test7.json"); } +// Tests if config-reload attempts to reload a file while LFC is running +TEST_F(CtrlChannelDhcpv6SrvTest, configReloadLFCRunning) { + createUnixChannelServer(); + std::string response; + + // This is normally set to whatever value is passed to -c when the server is + // started, but we're not starting it that way, so need to set it by hand. + server_->setConfigFile("test8.json"); + + // Ok, enough fooling around. Let's create a valid config. + const std::string cfg_txt = + "{ \"Dhcp6\": {" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"subnet6\": [" + " { \"subnet\": \"2001:db8:1::/64\", \"id\": 1 }," + " { \"subnet\": \"2001:db8:2::/64\", \"id\": 2 }" + " ]," + " \"lease-database\": {" + " \"type\": \"memfile\", \"persist\": false }" + "} }"; + ofstream f("test8.json", ios::trunc); + f << cfg_txt; + f.close(); + + // Create the backend configuration. + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "6"; + pmap["name"] = getLeaseFilePath("kea-leases6.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + // Now tell Kea to reload its config. + sendUnixCommand("{ \"command\": \"config-reload\" }", response); + + // Verify the reload will fail. + EXPECT_EQ("{ \"result\": 1, \"text\": \"Config reload failed: configuration error using file 'test8.json': " + "Can not update configuration while lease file cleanup process is running.\" }", + response); + + ::remove("test8.json"); +} + // Tests if config-reload attempts to reload a file and reports that the // file is loaded correctly. TEST_F(CtrlChannelDhcpv6SrvTest, configReloadValid) { diff --git a/src/bin/dhcp6/tests/http_control_socket_unittest.cc b/src/bin/dhcp6/tests/http_control_socket_unittest.cc index b13634ca3c..1fc1bfc792 100644 --- a/src/bin/dhcp6/tests/http_control_socket_unittest.cc +++ b/src/bin/dhcp6/tests/http_control_socket_unittest.cc @@ -13,12 +13,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -26,6 +28,7 @@ #include #include #include +#include #include #include @@ -52,6 +55,7 @@ using namespace isc::asiolink; using namespace isc::asiolink::test; using namespace isc::config; using namespace isc::data; +using namespace isc::db; using namespace isc::dhcp; using namespace isc::dhcp::test; using namespace isc::hooks; @@ -539,6 +543,9 @@ public: // file is not a valid JSON. void testConfigReloadBrokenFile(); + // Tests if config-reload attempts to reload a file while LFC is running. + void testConfigReloadLFCRunning(); + // Tests if config-reload attempts to reload a file and reports that the // file is loaded correctly. void testConfigReloadValid(); @@ -585,6 +592,18 @@ public: // This test verifies that the server signals timeout if the transmission // takes too long, having received no data from the client. void testConnectionTimeoutNoData(); + + /// @brief Return path to the lease file used by unit tests. + /// + /// @param filename Name of the lease file appended to the path to the + /// directory where test data is held. + /// + /// @return Full path to the lease file. + static std::string getLeaseFilePath(const std::string& filename) { + std::ostringstream s; + s << CfgMgr::instance().getDataDir() << "/" << filename; + return (s.str()); + } }; /// @brief Fixture class intended for testing HTTP control channel. @@ -1196,6 +1215,153 @@ TEST_F(HttpCtrlChannelDhcpv6Test, configSet) { CfgMgr::instance().clear(); } +// Check that the "config-set" fails when LFC is running. +TEST_F(HttpCtrlChannelDhcpv6Test, configSetLFCRunning) { + createHttpChannelServer(); + + // Define strings to permutate the config arguments + // (Note the line feeds makes errors easy to find) + string config_set_txt = "{ \"command\": \"config-set\" \n"; + string args_txt = " \"arguments\": { \n"; + string dhcp6_cfg_txt = + " \"Dhcp6\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"preferred-lifetime\": 3000, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet6\": [ \n"; + string subnet1 = + " {\"subnet\": \"3002::/64\", \"id\": 1, \n" + " \"pools\": [{ \"pool\": \"3002::100-3002::200\" }]}\n"; + string subnet2 = + " {\"subnet\": \"3003::/64\", \"id\": 2, \n" + " \"pools\": [{ \"pool\": \"3003::100-3003::200\" }]}\n"; + string bad_subnet = + " {\"comment\": \"3005::/64\", \"id\": 10, \n" + " \"pools\": [{ \"pool\": \"3005::100-3005::200\" }]}\n"; + string subnet_footer = + " ] \n"; + string option_def = + " ,\"option-def\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"type\": \"uint32\",\n" + " \"array\": false,\n" + " \"record-types\": \"\",\n" + " \"space\": \"dhcp6\",\n" + " \"encapsulate\": \"\"\n" + " }\n" + "]\n"; + string option_data = + " ,\"option-data\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"space\": \"dhcp6\",\n" + " \"csv-format\": true,\n" + " \"data\": \"12345\"\n" + " }\n" + "]\n"; + string control_socket = + " ,\"control-socket\": { \n" + " \"socket-type\": \"http\", \n" + " \"socket-address\": \"::1\", \n" + " \"socket-port\": 18126 \n" + " } \n"; + string logger_txt = + " ,\"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output-options\": [{ \n" + " \"output\": \"/dev/null\", \n" + " \"maxsize\": 0" + " }] \n" + " }] \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << config_set_txt << "," + << args_txt + << dhcp6_cfg_txt + << subnet1 + << subnet_footer + << option_def + << option_data + << control_socket + << logger_txt + << "}\n" // close dhcp6 + << "}}"; + + // Send the config-set command + std::string response; + sendHttpCommand(os.str(), response); + EXPECT_EQ("[ { \"arguments\": { \"hash\": \"7B1A2256CDB80F66DEBFC9C86D1210717A1F2DB45BEF532C30865FD50ECCDC3D\" }, \"result\": 0, \"text\": \"Configuration successful.\" } ]", + response); + + // Check that the config was indeed applied. + const Subnet6Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + OptionDefinitionPtr def = + LibDHCP::getRuntimeOptionDef(DHCP6_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Create the backend configuration. + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "6"; + pmap["name"] = getLeaseFilePath("kea-leases6.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + // Send the config-set command + sendHttpCommand(os.str(), response); + + // Should fail with an error + EXPECT_EQ("[ { \"result\": 1, " + "\"text\": \"Can not update configuration while lease file cleanup process is running.\" } ]", + response); + + // Verify the HTTP control channel socket exists. + EXPECT_TRUE(HttpCommandMgr::instance().getHttpListener()); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + def = LibDHCP::getRuntimeOptionDef(DHCP6_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Verify the HTTP control channel socket exists. + EXPECT_TRUE(HttpCommandMgr::instance().getHttpListener()); + + // Clean up after the test. + CfgMgr::instance().clear(); +} + // Check that the "config-set" command will replace current configuration. TEST_F(HttpsCtrlChannelDhcpv6Test, configSet) { createHttpChannelServer(); @@ -1382,6 +1548,162 @@ TEST_F(HttpsCtrlChannelDhcpv6Test, configSet) { CfgMgr::instance().clear(); } +// Check that the "config-set" fails when LFC is running. +TEST_F(HttpsCtrlChannelDhcpv6Test, configSetLFCRunning) { + createHttpChannelServer(); + + // Define strings to permutate the config arguments + // (Note the line feeds makes errors easy to find) + string ca_dir(string(TEST_CA_DIR)); + string config_set_txt = "{ \"command\": \"config-set\" \n"; + string args_txt = " \"arguments\": { \n"; + string dhcp6_cfg_txt = + " \"Dhcp6\": { \n" + " \"interfaces-config\": { \n" + " \"interfaces\": [\"*\"] \n" + " }, \n" + " \"preferred-lifetime\": 3000, \n" + " \"valid-lifetime\": 4000, \n" + " \"renew-timer\": 1000, \n" + " \"rebind-timer\": 2000, \n" + " \"lease-database\": { \n" + " \"type\": \"memfile\", \n" + " \"persist\":false, \n" + " \"lfc-interval\": 0 \n" + " }, \n" + " \"expired-leases-processing\": { \n" + " \"reclaim-timer-wait-time\": 0, \n" + " \"hold-reclaimed-time\": 0, \n" + " \"flush-reclaimed-timer-wait-time\": 0 \n" + " }," + " \"subnet6\": [ \n"; + string subnet1 = + " {\"subnet\": \"3002::/64\", \"id\": 1, \n" + " \"pools\": [{ \"pool\": \"3002::100-3002::200\" }]}\n"; + string subnet2 = + " {\"subnet\": \"3003::/64\", \"id\": 2, \n" + " \"pools\": [{ \"pool\": \"3003::100-3003::200\" }]}\n"; + string bad_subnet = + " {\"comment\": \"3005::/64\", \"id\": 10, \n" + " \"pools\": [{ \"pool\": \"3005::100-3005::200\" }]}\n"; + string subnet_footer = + " ] \n"; + string option_def = + " ,\"option-def\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"type\": \"uint32\",\n" + " \"array\": false,\n" + " \"record-types\": \"\",\n" + " \"space\": \"dhcp6\",\n" + " \"encapsulate\": \"\"\n" + " }\n" + "]\n"; + string option_data = + " ,\"option-data\": [\n" + " {\n" + " \"name\": \"foo\",\n" + " \"code\": 163,\n" + " \"space\": \"dhcp6\",\n" + " \"csv-format\": true,\n" + " \"data\": \"12345\"\n" + " }\n" + "]\n"; + string control_socket_header = + " ,\"control-socket\": { \n" + " \"socket-type\": \"http\", \n" + " \"socket-address\": \"::1\", \n" + " \"socket-port\": 18126, \n"; + string control_socket_footer = + " } \n"; + string logger_txt = + " ,\"loggers\": [ { \n" + " \"name\": \"kea\", \n" + " \"severity\": \"FATAL\", \n" + " \"output-options\": [{ \n" + " \"output\": \"/dev/null\", \n" + " \"maxsize\": 0" + " }] \n" + " }] \n"; + + std::ostringstream os; + + // Create a valid config with all the parts should parse + os << config_set_txt << "," + << args_txt + << dhcp6_cfg_txt + << subnet1 + << subnet_footer + << option_def + << option_data + << control_socket_header + << " \"trust-anchor\": \"" << ca_dir << "/kea-ca.crt\", \n" + << " \"cert-file\": \"" << ca_dir << "/kea-server.crt\", \n" + << " \"key-file\": \"" << ca_dir << "/kea-server.key\" \n" + << control_socket_footer + << logger_txt + << "}\n" // close dhcp6 + << "}}"; + + // Send the config-set command + std::string response; + sendHttpCommand(os.str(), response); + // Verify the configuration was successful. The config contains random + // file paths (CA directory), so the hash will be different each time. + // As such, we can do simplified checks: + // - verify the "result": 0 is there + // - verify the "text": "Configuration successful." is there + EXPECT_NE(response.find("\"result\": 0"), std::string::npos); + EXPECT_NE(response.find("\"text\": \"Configuration successful.\""), + std::string::npos); + + // Check that the config was indeed applied. + const Subnet6Collection* subnets = + CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + OptionDefinitionPtr def = + LibDHCP::getRuntimeOptionDef(DHCP6_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Create the backend configuration. + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "6"; + pmap["name"] = getLeaseFilePath("kea-leases6.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + // Send the config-set command + sendHttpCommand(os.str(), response); + + // Should fail with an error + EXPECT_EQ("[ { \"result\": 1, " + "\"text\": \"Can not update configuration while lease file cleanup process is running.\" } ]", + response); + + // Check that the config was not lost + subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll(); + EXPECT_EQ(1, subnets->size()); + + def = LibDHCP::getRuntimeOptionDef(DHCP6_OPTION_SPACE, 163); + ASSERT_TRUE(def); + + // Verify the HTTP control channel socket exists. + EXPECT_TRUE(HttpCommandMgr::instance().getHttpListener()); + + // Clean up after the test. + CfgMgr::instance().clear(); +} + // Tests if the server returns its configuration using config-get. // Note there are separate tests that verify if toElement() called by the // config-get handler are actually converting the configuration correctly. @@ -2505,6 +2827,67 @@ TEST_F(HttpsCtrlChannelDhcpv6Test, configReloadBrokenFile) { testConfigReloadBrokenFile(); } +// Tests if config-reload attempts to reload a file while LFC is running. +void +BaseCtrlChannelDhcpv6Test::testConfigReloadLFCRunning() { + createHttpChannelServer(); + std::string response; + + // This is normally set to whatever value is passed to -c when the server is + // started, but we're not starting it that way, so need to set it by hand. + server_->setConfigFile("test8.json"); + + // Ok, enough fooling around. Let's create a valid config. + const std::string cfg_txt = + "{ \"Dhcp6\": {" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"subnet6\": [" + " { \"subnet\": \"2001:db8:1::/64\", \"id\": 1 }," + " { \"subnet\": \"2001:db8:2::/64\", \"id\": 2 }" + " ]," + " \"lease-database\": {" + " \"type\": \"memfile\", \"persist\": false }" + "} }"; + ofstream f("test8.json", ios::trunc); + f << cfg_txt; + f.close(); + + DatabaseConnection::ParameterMap pmap; + pmap["type"] = "memfile"; + pmap["universe"] = "6"; + pmap["name"] = getLeaseFilePath("kea-leases6.csv"); + pmap["lfc-interval"] = "1"; + + // Create a pid file holding the PID of the current process. Choosing the + // pid of the current process guarantees that when the backend starts up + // the process is alive. + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(pmap["name"], Memfile_LeaseMgr::FILE_PID)); + pid_file.write(); + + std::unique_ptr p(static_cast(&pid_file), [](void* p) { reinterpret_cast(p)->deleteFile(); }); + + + // This command should reload test8.json config. + sendHttpCommand("{ \"command\": \"config-reload\" }", response); + + // Verify the reload will fail. + EXPECT_EQ("[ { \"result\": 1, \"text\": \"Config reload failed: configuration error using file 'test8.json': " + "Can not update configuration while lease file cleanup process is running.\" } ]", + response); + + ::remove("test8.json"); +} + +TEST_F(HttpCtrlChannelDhcpv6Test, configReloadLFCRunning) { + testConfigReloadLFCRunning(); +} + +TEST_F(HttpsCtrlChannelDhcpv6Test, configReloadLFCRunning) { + testConfigReloadLFCRunning(); +} + // Tests if config-reload attempts to reload a file and reports that the // file is loaded correctly. void diff --git a/src/hooks/dhcp/forensic_log/tests/meson.build b/src/hooks/dhcp/forensic_log/tests/meson.build index 5f71ab5200..35f92feca9 100644 --- a/src/hooks/dhcp/forensic_log/tests/meson.build +++ b/src/hooks/dhcp/forensic_log/tests/meson.build @@ -51,5 +51,7 @@ test( 'dhcp-forensic-log-tests', dhcp_forensic_log_tests, protocol: 'gtest', + is_parallel: false, + priority: -1, timeout: 60, ) diff --git a/src/lib/dhcpsrv/memfile_lease_mgr.cc b/src/lib/dhcpsrv/memfile_lease_mgr.cc index e6096e9564..f858b8c788 100644 --- a/src/lib/dhcpsrv/memfile_lease_mgr.cc +++ b/src/lib/dhcpsrv/memfile_lease_mgr.cc @@ -1008,7 +1008,7 @@ Memfile_LeaseMgr::Memfile_LeaseMgr(const DatabaseConnection::ParameterMap& param std::string file4 = initLeaseFilePath(V4); if (!file4.empty()) { conversion_needed = loadLeasesFromFiles(file4, + CSVLeaseFile4>(V4, file4, lease_file4_, storage4_); static_cast(extractExtendedInfo4(false, false)); @@ -1017,7 +1017,7 @@ Memfile_LeaseMgr::Memfile_LeaseMgr(const DatabaseConnection::ParameterMap& param std::string file6 = initLeaseFilePath(V6); if (!file6.empty()) { conversion_needed = loadLeasesFromFiles(file6, + CSVLeaseFile6>(V6, file6, lease_file6_, storage6_); buildExtendedInfoTables6(); @@ -2260,6 +2260,16 @@ Memfile_LeaseMgr::rollback() { DHCPSRV_MEMFILE_ROLLBACK); } +bool +Memfile_LeaseMgr::isLFCProcessRunning(const std::string file_name, Universe u) { + std::string lease_file(file_name); + if (lease_file.empty()) { + lease_file = Memfile_LeaseMgr::getDefaultLeaseFilePath(u); + } + PIDFile pid_file(Memfile_LeaseMgr::appendSuffix(lease_file, FILE_PID)); + return (pid_file.check()); +} + std::string Memfile_LeaseMgr::appendSuffix(const std::string& file_name, const LFCFileType& file_type) { @@ -2290,7 +2300,7 @@ Memfile_LeaseMgr::appendSuffix(const std::string& file_name, std::string Memfile_LeaseMgr::getDefaultLeaseFilePath(Universe u, - std::string filename /* = "" */) const { + std::string filename /* = "" */) { std::ostringstream s;; s << CfgMgr::instance().getDataDir(); if (filename.empty()) { @@ -2350,7 +2360,7 @@ Memfile_LeaseMgr::initLeaseFilePath(Universe u) { lease_file = conn_.getParameter("name"); } catch (const Exception&) { // Not specified, use the default. - return (getDefaultLeaseFilePath(u)); + return (Memfile_LeaseMgr::getDefaultLeaseFilePath(u)); } try { @@ -2365,7 +2375,7 @@ Memfile_LeaseMgr::initLeaseFilePath(Universe u) { template bool -Memfile_LeaseMgr::loadLeasesFromFiles(const std::string& filename, +Memfile_LeaseMgr::loadLeasesFromFiles(Universe u, const std::string& filename, boost::shared_ptr& lease_file, StorageType& storage) { // Check if the instance of the LFC is running right now. If it is @@ -2374,8 +2384,7 @@ Memfile_LeaseMgr::loadLeasesFromFiles(const std::string& filename, // it should go through. /// @todo Consider applying a timeout for an LFC and retry when this /// timeout elapses. - PIDFile pid_file(appendSuffix(filename, FILE_PID)); - if (pid_file.check()) { + if (Memfile_LeaseMgr::isLFCProcessRunning(filename, u)) { isc_throw(DbOpenError, "unable to load leases from files while the " "lease file cleanup is in progress"); } @@ -2413,14 +2422,14 @@ Memfile_LeaseMgr::loadLeasesFromFiles(const std::string& filename, } else { // If the leasefile.completed doesn't exist, let's load the leases // from leasefile.2 and leasefile.1, if they exist. - lease_file.reset(new LeaseFileType(appendSuffix(filename, FILE_PREVIOUS))); + lease_file.reset(new LeaseFileType(Memfile_LeaseMgr::appendSuffix(filename, FILE_PREVIOUS))); if (lease_file->exists()) { LeaseFileLoader::load(*lease_file, storage, max_row_errors); conversion_needed = conversion_needed || lease_file->needsConversion(); } - lease_file.reset(new LeaseFileType(appendSuffix(filename, FILE_INPUT))); + lease_file.reset(new LeaseFileType(Memfile_LeaseMgr::appendSuffix(filename, FILE_INPUT))); if (lease_file->exists()) { LeaseFileLoader::load(*lease_file, storage, max_row_errors); @@ -2500,8 +2509,8 @@ Memfile_LeaseMgr::lfcExecute(boost::shared_ptr& lease_file) { // is an indication that another LFC instance may be in progress or // may be stalled. In that case we don't want to rotate the current // lease file to avoid overriding the contents of the existing file. - CSVFile lease_file_finish(appendSuffix(lease_file->getFilename(), FILE_FINISH)); - CSVFile lease_file_copy(appendSuffix(lease_file->getFilename(), FILE_INPUT)); + CSVFile lease_file_finish(Memfile_LeaseMgr::appendSuffix(lease_file->getFilename(), FILE_FINISH)); + CSVFile lease_file_copy(Memfile_LeaseMgr::appendSuffix(lease_file->getFilename(), FILE_INPUT)); if (!lease_file_finish.exists() && !lease_file_copy.exists()) { // Close the current file so as we can move it to the copy file. lease_file->close(); diff --git a/src/lib/dhcpsrv/memfile_lease_mgr.h b/src/lib/dhcpsrv/memfile_lease_mgr.h index 42c809380f..cd3213ec9b 100644 --- a/src/lib/dhcpsrv/memfile_lease_mgr.h +++ b/src/lib/dhcpsrv/memfile_lease_mgr.h @@ -963,6 +963,11 @@ public: /// support transactions, this is a no-op. virtual void rollback() override; + /// @brief Check if LFC is running. + /// + /// @return True if LFC is running, false otherwise (memfile only). + static bool isLFCProcessRunning(const std::string file_name, Universe u); + //@} /// @name Public type and method used to determine file names for LFC. @@ -1012,8 +1017,8 @@ public: /// /// @param u Universe (V4 or V6). /// @param filename optional filename to use. - std::string getDefaultLeaseFilePath(Universe u, - const std::string filename = "") const; + static std::string getDefaultLeaseFilePath(Universe u, + const std::string filename = ""); /// @brief Returns an absolute path to the lease file. /// @@ -1099,6 +1104,7 @@ private: /// @todo Consider implementing delaying the lease files loading when /// the LFC is in progress by the specified amount of time. /// + /// @param u Universe (V4 or V6). /// @param filename Name of the lease file. /// @param lease_file An object representing a lease file to which /// the server will store lease updates. @@ -1114,7 +1120,7 @@ private: /// @throw DbOpenError when it is found that the LFC is in progress. template - bool loadLeasesFromFiles(const std::string& filename, + bool loadLeasesFromFiles(Universe u, const std::string& filename, boost::shared_ptr& lease_file, StorageType& storage); diff --git a/src/lib/util/pid_file.cc b/src/lib/util/pid_file.cc index ef519b3d87..89d2681ebc 100644 --- a/src/lib/util/pid_file.cc +++ b/src/lib/util/pid_file.cc @@ -31,7 +31,7 @@ PIDFile::check() const { // If we weren't able to open the file treat // it as if the process wasn't running if (!fs.is_open()) { - return (false); + return (0); } // Try to get the pid, get the status and get rid of the file