// Copyright (C) 2014-2022 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace isc::asiolink; using namespace isc::config; using namespace isc::data; using namespace isc::db; using namespace isc::dhcp; using namespace isc::hooks; using namespace isc::stats; using namespace isc::util; using namespace std; namespace ph = std::placeholders; namespace { /// Structure that holds registered hook indexes. struct CtrlDhcp4Hooks { int hooks_index_dhcp4_srv_configured_; /// Constructor that registers hook points for the DHCPv4 server. CtrlDhcp4Hooks() { hooks_index_dhcp4_srv_configured_ = HooksManager::registerHook("dhcp4_srv_configured"); } }; // Declare a Hooks object. As this is outside any function or method, it // will be instantiated (and the constructor run) when the module is loaded. // As a result, the hook indexes will be defined before any method in this // module is called. CtrlDhcp4Hooks Hooks; /// @brief Signals handler for DHCPv4 server. /// /// This signal handler handles the following signals received by the DHCPv4 /// server process: /// - SIGHUP - triggers server's dynamic reconfiguration. /// - SIGTERM - triggers server's shut down. /// - SIGINT - triggers server's shut down. /// /// @param signo Signal number received. void signalHandler(int signo) { // SIGHUP signals a request to reconfigure the server. if (signo == SIGHUP) { ControlledDhcpv4Srv::processCommand("config-reload", ConstElementPtr()); } else if ((signo == SIGTERM) || (signo == SIGINT)) { ControlledDhcpv4Srv::processCommand("shutdown", ConstElementPtr()); } } } namespace isc { namespace dhcp { ControlledDhcpv4Srv* ControlledDhcpv4Srv::server_ = NULL; void ControlledDhcpv4Srv::init(const std::string& file_name) { // Keep the call timestamp. start_ = boost::posix_time::second_clock::universal_time(); // Configure the server using JSON file. ConstElementPtr result = loadConfigFile(file_name); int rcode; ConstElementPtr comment = isc::config::parseAnswer(rcode, result); if (rcode != CONTROL_RESULT_SUCCESS) { string reason = comment ? comment->stringValue() : "no details available"; isc_throw(isc::BadValue, reason); } // We don't need to call openActiveSockets() or startD2() as these // methods are called in processConfig() which is called by // processCommand("config-set", ...) // Set signal handlers. When the SIGHUP is received by the process // the server reconfiguration will be triggered. When SIGTERM or // SIGINT will be received, the server will start shutting down. signal_set_.reset(new IOSignalSet(getIOService(), signalHandler)); signal_set_->add(SIGINT); signal_set_->add(SIGHUP); signal_set_->add(SIGTERM); } void ControlledDhcpv4Srv::cleanup() { // Nothing to do here. No need to disconnect from anything. } ConstElementPtr ControlledDhcpv4Srv::loadConfigFile(const std::string& file_name) { // This is a configuration backend implementation that reads the // configuration from a JSON file. isc::data::ConstElementPtr json; isc::data::ConstElementPtr result; // Basic sanity check: file name must not be empty. try { if (file_name.empty()) { // Basic sanity check: file name must not be empty. isc_throw(isc::BadValue, "JSON configuration file not specified." " Please use -c command line option."); } // Read contents of the file and parse it as JSON Parser4Context parser; json = parser.parseFile(file_name, Parser4Context::PARSER_DHCP4); if (!json) { isc_throw(isc::BadValue, "no configuration found"); } // Let's do sanity check before we call json->get() which // works only for map. if (json->getType() != isc::data::Element::map) { isc_throw(isc::BadValue, "Configuration file is expected to be " "a map, i.e., start with { and end with } and contain " "at least an entry called 'Dhcp4' that itself is a map. " << file_name << " is a valid JSON, but its top element is not a map." " Did you forget to add { } around your configuration?"); } // Use parsed JSON structures to configure the server result = ControlledDhcpv4Srv::processCommand("config-set", json); if (!result) { // Undetermined status of the configuration. This should never // happen, but as the configureDhcp4Server returns a pointer, it is // theoretically possible that it will return NULL. isc_throw(isc::BadValue, "undefined result of " "processCommand(\"config-set\", json)"); } // Now check is the returned result is successful (rcode=0) or not // (see @ref isc::config::parseAnswer). int rcode; ConstElementPtr comment = isc::config::parseAnswer(rcode, result); if (rcode != CONTROL_RESULT_SUCCESS) { string reason = comment ? comment->stringValue() : "no details available"; isc_throw(isc::BadValue, reason); } } catch (const std::exception& ex) { // If configuration failed at any stage, we drop the staging // configuration and continue to use the previous one. CfgMgr::instance().rollback(); LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL) .arg(file_name).arg(ex.what()); isc_throw(isc::BadValue, "configuration error using file '" << file_name << "': " << ex.what()); } LOG_WARN(dhcp4_logger, DHCP4_MULTI_THREADING_INFO) .arg(MultiThreadingMgr::instance().getMode() ? "yes" : "no") .arg(MultiThreadingMgr::instance().getThreadPoolSize()) .arg(MultiThreadingMgr::instance().getPacketQueueSize()); return (result); } ConstElementPtr ControlledDhcpv4Srv::commandShutdownHandler(const string&, ConstElementPtr args) { if (!ControlledDhcpv4Srv::getInstance()) { LOG_WARN(dhcp4_logger, DHCP4_NOT_RUNNING); return (createAnswer(CONTROL_RESULT_ERROR, "Shutdown failure.")); } int exit_value = 0; if (args) { // @todo Should we go ahead and shutdown even if the args are invalid? if (args->getType() != Element::map) { return (createAnswer(CONTROL_RESULT_ERROR, "Argument must be a map")); } ConstElementPtr param = args->get("exit-value"); if (param) { if (param->getType() != Element::integer) { return (createAnswer(CONTROL_RESULT_ERROR, "parameter 'exit-value' is not an integer")); } exit_value = param->intValue(); } } ControlledDhcpv4Srv::getInstance()->shutdownServer(exit_value); return (createAnswer(CONTROL_RESULT_SUCCESS, "Shutting down.")); } ConstElementPtr ControlledDhcpv4Srv::commandLibReloadHandler(const string&, ConstElementPtr) { // stop thread pool (if running) MultiThreadingCriticalSection cs; // Clear the packet queue. MultiThreadingMgr::instance().getThreadPool().reset(); try { /// Get list of currently loaded libraries and reload them. HookLibsCollection loaded = HooksManager::getLibraryInfo(); HooksManager::prepareUnloadLibraries(); static_cast(HooksManager::unloadLibraries()); bool status = HooksManager::loadLibraries(loaded); if (!status) { isc_throw(Unexpected, "Failed to reload hooks libraries."); } } catch (const std::exception& ex) { LOG_ERROR(dhcp4_logger, DHCP4_HOOKS_LIBS_RELOAD_FAIL); ConstElementPtr answer = isc::config::createAnswer(1, ex.what()); return (answer); } ConstElementPtr answer = isc::config::createAnswer(0, "Hooks libraries successfully reloaded."); return (answer); } ConstElementPtr ControlledDhcpv4Srv::commandConfigReloadHandler(const string&, ConstElementPtr /*args*/) { // Get configuration file name. std::string file = ControlledDhcpv4Srv::getInstance()->getConfigFile(); try { LOG_INFO(dhcp4_logger, DHCP4_DYNAMIC_RECONFIGURATION).arg(file); auto result = loadConfigFile(file); LOG_INFO(dhcp4_logger, DHCP4_DYNAMIC_RECONFIGURATION_SUCCESS).arg(file); return (result); } catch (const std::exception& ex) { // Log the unsuccessful reconfiguration. The reason for failure // should be already logged. Don't rethrow an exception so as // the server keeps working. LOG_FATAL(dhcp4_logger, DHCP4_DYNAMIC_RECONFIGURATION_FAIL) .arg(file); return (createAnswer(CONTROL_RESULT_ERROR, "Config reload failed: " + string(ex.what()))); } } ConstElementPtr ControlledDhcpv4Srv::commandConfigGetHandler(const string&, ConstElementPtr /*args*/) { ConstElementPtr config = CfgMgr::instance().getCurrentCfg()->toElement(); return (createAnswer(0, config)); } ConstElementPtr ControlledDhcpv4Srv::commandConfigWriteHandler(const string&, ConstElementPtr args) { string filename; if (args) { if (args->getType() != Element::map) { return (createAnswer(CONTROL_RESULT_ERROR, "Argument must be a map")); } ConstElementPtr filename_param = args->get("filename"); if (filename_param) { if (filename_param->getType() != Element::string) { return (createAnswer(CONTROL_RESULT_ERROR, "passed parameter 'filename' is not a string")); } filename = filename_param->stringValue(); } } if (filename.empty()) { // filename parameter was not specified, so let's use whatever we remember // from the command-line filename = getConfigFile(); } if (filename.empty()) { return (createAnswer(CONTROL_RESULT_ERROR, "Unable to determine filename." "Please specify filename explicitly.")); } // Ok, it's time to write the file. size_t size = 0; try { ConstElementPtr cfg = CfgMgr::instance().getCurrentCfg()->toElement(); size = writeConfigFile(filename, cfg); } catch (const isc::Exception& ex) { return (createAnswer(CONTROL_RESULT_ERROR, string("Error during write-config:") + ex.what())); } if (size == 0) { return (createAnswer(CONTROL_RESULT_ERROR, "Error writing configuration to " + filename)); } // Ok, it's time to return the successful response. ElementPtr params = Element::createMap(); params->set("size", Element::create(static_cast(size))); params->set("filename", Element::create(filename)); return (createAnswer(CONTROL_RESULT_SUCCESS, "Configuration written to " + filename + " successful", params)); } ConstElementPtr ControlledDhcpv4Srv::commandConfigSetHandler(const string&, ConstElementPtr args) { const int status_code = CONTROL_RESULT_ERROR; ConstElementPtr dhcp4; string message; // Command arguments are expected to be: // { "Dhcp4": { ... } } if (!args) { message = "Missing mandatory 'arguments' parameter."; } else { dhcp4 = args->get("Dhcp4"); if (!dhcp4) { message = "Missing mandatory 'Dhcp4' parameter."; } else if (dhcp4->getType() != Element::map) { message = "'Dhcp4' parameter expected to be a map."; } } // Check unsupported objects. if (message.empty()) { for (auto obj : args->mapValue()) { const string& obj_name = obj.first; if (obj_name != "Dhcp4") { LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_UNSUPPORTED_OBJECT) .arg(obj_name); if (message.empty()) { message = "Unsupported '" + obj_name + "' parameter"; } else { message += " (and '" + obj_name + "')"; } } } if (!message.empty()) { message += "."; } } if (!message.empty()) { // Something is amiss with arguments, return a failure response. ConstElementPtr result = isc::config::createAnswer(status_code, message); return (result); } // stop thread pool (if running) MultiThreadingCriticalSection cs; // disable multi-threading (it will be applied by new configuration) // this must be done in order to properly handle MT to ST transition // when 'multi-threading' structure is missing from new config MultiThreadingMgr::instance().apply(false, 0, 0); // We are starting the configuration process so we should remove any // staging configuration that has been created during previous // configuration attempts. CfgMgr::instance().rollback(); // Parse the logger configuration explicitly into the staging config. // Note this does not alter the current loggers, they remain in // effect until we apply the logging config below. If no logging // is supplied logging will revert to default logging. Daemon::configureLogger(dhcp4, CfgMgr::instance().getStagingCfg()); // Let's apply the new logging. We do it early, so we'll be able to print // out what exactly is wrong with the new config in case of problems. CfgMgr::instance().getStagingCfg()->applyLoggingCfg(); // Now we configure the server proper. ConstElementPtr result = processConfig(dhcp4); // If the configuration parsed successfully, apply the new logger // configuration and the commit the new configuration. We apply // the logging first in case there's a configuration failure. int rcode = 0; isc::config::parseAnswer(rcode, result); if (rcode == CONTROL_RESULT_SUCCESS) { CfgMgr::instance().getStagingCfg()->applyLoggingCfg(); // Use new configuration. CfgMgr::instance().commit(); } else { // Ok, we applied the logging from the upcoming configuration, but // there were problems with the config. As such, we need to back off // and revert to the previous logging configuration. CfgMgr::instance().getCurrentCfg()->applyLoggingCfg(); if (CfgMgr::instance().getCurrentCfg()->getSequence() != 0) { // Not initial configuration so someone can believe we reverted // to the previous configuration. It is not the case so be clear // about this. LOG_FATAL(dhcp4_logger, DHCP4_CONFIG_UNRECOVERABLE_ERROR); } } return (result); } ConstElementPtr ControlledDhcpv4Srv::commandConfigTestHandler(const string&, ConstElementPtr args) { const int status_code = CONTROL_RESULT_ERROR; // 1 indicates an error ConstElementPtr dhcp4; string message; // Command arguments are expected to be: // { "Dhcp4": { ... } } if (!args) { message = "Missing mandatory 'arguments' parameter."; } else { dhcp4 = args->get("Dhcp4"); if (!dhcp4) { message = "Missing mandatory 'Dhcp4' parameter."; } else if (dhcp4->getType() != Element::map) { message = "'Dhcp4' parameter expected to be a map."; } } // Check unsupported objects. if (message.empty()) { for (auto obj : args->mapValue()) { const string& obj_name = obj.first; if (obj_name != "Dhcp4") { LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_UNSUPPORTED_OBJECT) .arg(obj_name); if (message.empty()) { message = "Unsupported '" + obj_name + "' parameter"; } else { message += " (and '" + obj_name + "')"; } } } if (!message.empty()) { message += "."; } } if (!message.empty()) { // Something is amiss with arguments, return a failure response. ConstElementPtr result = isc::config::createAnswer(status_code, message); return (result); } // stop thread pool (if running) MultiThreadingCriticalSection cs; // We are starting the configuration process so we should remove any // staging configuration that has been created during previous // configuration attempts. CfgMgr::instance().rollback(); // Now we check the server proper. return (checkConfig(dhcp4)); } ConstElementPtr ControlledDhcpv4Srv::commandDhcpDisableHandler(const std::string&, ConstElementPtr args) { std::ostringstream message; int64_t max_period = 0; std::string origin; // If the args map does not contain 'origin' parameter, the default type // will be used (user command). NetworkState::Origin type = NetworkState::Origin::USER_COMMAND; // Parse arguments to see if the 'max-period' or 'origin' parameters have // been specified. if (args) { // Arguments must be a map. if (args->getType() != Element::map) { message << "arguments for the 'dhcp-disable' command must be a map"; } else { ConstElementPtr max_period_element = args->get("max-period"); // max-period is optional. if (max_period_element) { // It must be an integer, if specified. if (max_period_element->getType() != Element::integer) { message << "'max-period' argument must be a number"; } else { // It must be positive integer. max_period = max_period_element->intValue(); if (max_period <= 0) { message << "'max-period' must be positive integer"; } } } ConstElementPtr origin_element = args->get("origin"); // The 'origin' parameter is optional. if (origin_element) { // It must be a string, if specified. if (origin_element->getType() != Element::string) { message << "'origin' argument must be a string"; } else { origin = origin_element->stringValue(); if (origin == "ha-partner") { type = NetworkState::Origin::HA_COMMAND; } else if (origin != "user") { if (origin.empty()) { origin = "(empty string)"; } message << "invalid value used for 'origin' parameter: " << origin; } } } } } // No error occurred, so let's disable the service. if (message.tellp() == 0) { message << "DHCPv4 service disabled"; if (max_period > 0) { message << " for " << max_period << " seconds"; // The user specified that the DHCP service should resume not // later than in max-period seconds. If the 'dhcp-enable' command // is not sent, the DHCP service will resume automatically. network_state_->delayedEnableAll(static_cast(max_period), type); } network_state_->disableService(type); // Success. return (config::createAnswer(CONTROL_RESULT_SUCCESS, message.str())); } // Failure. return (config::createAnswer(CONTROL_RESULT_ERROR, message.str())); } ConstElementPtr ControlledDhcpv4Srv::commandDhcpEnableHandler(const std::string&, ConstElementPtr args) { std::ostringstream message; std::string origin; // If the args map does not contain 'origin' parameter, the default type // will be used (user command). NetworkState::Origin type = NetworkState::Origin::USER_COMMAND; // Parse arguments to see if the 'origin' parameter has been specified. if (args) { // Arguments must be a map. if (args->getType() != Element::map) { message << "arguments for the 'dhcp-enable' command must be a map"; } else { ConstElementPtr origin_element = args->get("origin"); // The 'origin' parameter is optional. if (origin_element) { // It must be a string, if specified. if (origin_element->getType() != Element::string) { message << "'origin' argument must be a string"; } else { origin = origin_element->stringValue(); if (origin == "ha-partner") { type = NetworkState::Origin::HA_COMMAND; } else if (origin != "user") { if (origin.empty()) { origin = "(empty string)"; } message << "invalid value used for 'origin' parameter: " << origin; } } } } } // No error occurred, so let's enable the service. if (message.tellp() == 0) { network_state_->enableService(type); // Success. return (config::createAnswer(CONTROL_RESULT_SUCCESS, "DHCP service successfully enabled")); } // Failure. return (config::createAnswer(CONTROL_RESULT_ERROR, message.str())); } ConstElementPtr ControlledDhcpv4Srv::commandVersionGetHandler(const string&, ConstElementPtr) { ElementPtr extended = Element::create(Dhcpv4Srv::getVersion(true)); ElementPtr arguments = Element::createMap(); arguments->set("extended", extended); ConstElementPtr answer = isc::config::createAnswer(0, Dhcpv4Srv::getVersion(false), arguments); return (answer); } ConstElementPtr ControlledDhcpv4Srv::commandBuildReportHandler(const string&, ConstElementPtr) { ConstElementPtr answer = isc::config::createAnswer(0, isc::detail::getConfigReport()); return (answer); } ConstElementPtr ControlledDhcpv4Srv::commandLeasesReclaimHandler(const string&, ConstElementPtr args) { int status_code = CONTROL_RESULT_ERROR; string message; // args must be { "remove": } if (!args) { message = "Missing mandatory 'remove' parameter."; } else { ConstElementPtr remove_name = args->get("remove"); if (!remove_name) { message = "Missing mandatory 'remove' parameter."; } else if (remove_name->getType() != Element::boolean) { message = "'remove' parameter expected to be a boolean."; } else { bool remove_lease = remove_name->boolValue(); server_->alloc_engine_->reclaimExpiredLeases4(0, 0, remove_lease); status_code = 0; message = "Reclamation of expired leases is complete."; } } ConstElementPtr answer = isc::config::createAnswer(status_code, message); return (answer); } ConstElementPtr ControlledDhcpv4Srv::commandServerTagGetHandler(const std::string&, ConstElementPtr) { const std::string& tag = CfgMgr::instance().getCurrentCfg()->getServerTag(); ElementPtr response = Element::createMap(); response->set("server-tag", Element::create(tag)); return (createAnswer(CONTROL_RESULT_SUCCESS, response)); } ConstElementPtr ControlledDhcpv4Srv::commandConfigBackendPullHandler(const std::string&, ConstElementPtr) { auto ctl_info = CfgMgr::instance().getCurrentCfg()->getConfigControlInfo(); if (!ctl_info) { return (createAnswer(CONTROL_RESULT_EMPTY, "No config backend.")); } // stop thread pool (if running) MultiThreadingCriticalSection cs; // Reschedule the periodic CB fetch. if (TimerMgr::instance()->isTimerRegistered("Dhcp4CBFetchTimer")) { TimerMgr::instance()->cancel("Dhcp4CBFetchTimer"); TimerMgr::instance()->setup("Dhcp4CBFetchTimer"); } // Code from cbFetchUpdates. // The configuration to use is the current one because this is called // after the configuration manager commit. try { auto srv_cfg = CfgMgr::instance().getCurrentCfg(); auto mode = CBControlDHCPv4::FetchMode::FETCH_UPDATE; server_->getCBControl()->databaseConfigFetch(srv_cfg, mode); } catch (const std::exception& ex) { LOG_ERROR(dhcp4_logger, DHCP4_CB_ON_DEMAND_FETCH_UPDATES_FAIL) .arg(ex.what()); return (createAnswer(CONTROL_RESULT_ERROR, "On demand configuration update failed: " + string(ex.what()))); } return (createAnswer(CONTROL_RESULT_SUCCESS, "On demand configuration update successful.")); } ConstElementPtr ControlledDhcpv4Srv::commandStatusGetHandler(const string&, ConstElementPtr /*args*/) { ElementPtr status = Element::createMap(); status->set("pid", Element::create(static_cast(getpid()))); auto now = boost::posix_time::second_clock::universal_time(); // Sanity check: start_ is always initialized. if (!start_.is_not_a_date_time()) { auto uptime = now - start_; status->set("uptime", Element::create(uptime.total_seconds())); } auto last_commit = CfgMgr::instance().getCurrentCfg()->getLastCommitTime(); if (!last_commit.is_not_a_date_time()) { auto reload = now - last_commit; status->set("reload", Element::create(reload.total_seconds())); } auto& mt_mgr = MultiThreadingMgr::instance(); if (mt_mgr.getMode()) { status->set("multi-threading-enabled", Element::create(true)); status->set("thread-pool-size", Element::create(static_cast( MultiThreadingMgr::instance().getThreadPoolSize()))); status->set("packet-queue-size", Element::create(static_cast( MultiThreadingMgr::instance().getPacketQueueSize()))); ElementPtr queue_stats = Element::createList(); queue_stats->add(Element::create(mt_mgr.getThreadPool().getQueueStat(10))); queue_stats->add(Element::create(mt_mgr.getThreadPool().getQueueStat(100))); queue_stats->add(Element::create(mt_mgr.getThreadPool().getQueueStat(1000))); status->set("packet-queue-statistics", queue_stats); } else { status->set("multi-threading-enabled", Element::create(false)); } // Iterate through the interfaces and get all the errors. ElementPtr socket_errors(Element::createList()); for (IfacePtr const& interface : IfaceMgr::instance().getIfaces()) { for (std::string const& error : interface->getErrors()) { socket_errors->add(Element::create(error)); } } // Abstract the information from all sockets into a single status. ElementPtr sockets(Element::createMap()); if (socket_errors->empty()) { sockets->set("status", Element::create("ready")); } else { ReconnectCtlPtr const reconnect_ctl( CfgMgr::instance().getCurrentCfg()->getCfgIface()->getReconnectCtl()); if (reconnect_ctl && reconnect_ctl->retriesLeft()) { sockets->set("status", Element::create("retrying")); } else { sockets->set("status", Element::create("failed")); } sockets->set("errors", socket_errors); } status->set("sockets", sockets); return (createAnswer(0, status)); } ConstElementPtr ControlledDhcpv4Srv::commandStatisticSetMaxSampleCountAllHandler(const string&, ConstElementPtr args) { StatsMgr& stats_mgr = StatsMgr::instance(); ConstElementPtr answer = stats_mgr.statisticSetMaxSampleCountAllHandler(args); // Update the default parameter. long max_samples = stats_mgr.getMaxSampleCountDefault(); CfgMgr::instance().getCurrentCfg()->addConfiguredGlobal( "statistic-default-sample-count", Element::create(max_samples)); return (answer); } ConstElementPtr ControlledDhcpv4Srv::commandStatisticSetMaxSampleAgeAllHandler(const string&, ConstElementPtr args) { StatsMgr& stats_mgr = StatsMgr::instance(); ConstElementPtr answer = stats_mgr.statisticSetMaxSampleAgeAllHandler(args); // Update the default parameter. auto duration = stats_mgr.getMaxSampleAgeDefault(); long max_age = toSeconds(duration); CfgMgr::instance().getCurrentCfg()->addConfiguredGlobal( "statistic-default-sample-age", Element::create(max_age)); return (answer); } ConstElementPtr ControlledDhcpv4Srv::processCommand(const string& command, ConstElementPtr args) { string txt = args ? args->str() : "(none)"; LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_COMMAND_RECEIVED) .arg(command).arg(txt); ControlledDhcpv4Srv* srv = ControlledDhcpv4Srv::getInstance(); if (!srv) { ConstElementPtr no_srv = isc::config::createAnswer(1, "Server object not initialized, so can't process command '" + command + "', arguments: '" + txt + "'."); return (no_srv); } try { if (command == "shutdown") { return (srv->commandShutdownHandler(command, args)); } else if (command == "libreload") { return (srv->commandLibReloadHandler(command, args)); } else if (command == "config-reload") { return (srv->commandConfigReloadHandler(command, args)); } else if (command == "config-set") { return (srv->commandConfigSetHandler(command, args)); } else if (command == "config-get") { return (srv->commandConfigGetHandler(command, args)); } else if (command == "config-test") { return (srv->commandConfigTestHandler(command, args)); } else if (command == "dhcp-disable") { return (srv->commandDhcpDisableHandler(command, args)); } else if (command == "dhcp-enable") { return (srv->commandDhcpEnableHandler(command, args)); } else if (command == "version-get") { return (srv->commandVersionGetHandler(command, args)); } else if (command == "build-report") { return (srv->commandBuildReportHandler(command, args)); } else if (command == "leases-reclaim") { return (srv->commandLeasesReclaimHandler(command, args)); } else if (command == "config-write") { return (srv->commandConfigWriteHandler(command, args)); } else if (command == "server-tag-get") { return (srv->commandServerTagGetHandler(command, args)); } else if (command == "config-backend-pull") { return (srv->commandConfigBackendPullHandler(command, args)); } else if (command == "status-get") { return (srv->commandStatusGetHandler(command, args)); } return (isc::config::createAnswer(1, "Unrecognized command:" + command)); } catch (const isc::Exception& ex) { return (isc::config::createAnswer(1, "Error while processing command '" + command + "':" + ex.what() + ", params: '" + txt + "'")); } } isc::data::ConstElementPtr ControlledDhcpv4Srv::processConfig(isc::data::ConstElementPtr config) { ControlledDhcpv4Srv* srv = ControlledDhcpv4Srv::getInstance(); // Single stream instance used in all error clauses std::ostringstream err; if (!srv) { err << "Server object not initialized, can't process config."; return (isc::config::createAnswer(1, err.str())); } LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_RECEIVED) .arg(srv->redactConfig(config)->str()); ConstElementPtr answer = configureDhcp4Server(*srv, config); // Check that configuration was successful. If not, do not reopen sockets // and don't bother with DDNS stuff. try { int rcode = 0; isc::config::parseAnswer(rcode, answer); if (rcode != 0) { return (answer); } } catch (const std::exception& ex) { err << "Failed to process configuration:" << ex.what(); return (isc::config::createAnswer(1, err.str())); } // Re-open lease and host database with new parameters. try { DatabaseConnection::db_lost_callback_ = std::bind(&ControlledDhcpv4Srv::dbLostCallback, srv, ph::_1); DatabaseConnection::db_recovered_callback_ = std::bind(&ControlledDhcpv4Srv::dbRecoveredCallback, srv, ph::_1); DatabaseConnection::db_failed_callback_ = std::bind(&ControlledDhcpv4Srv::dbFailedCallback, srv, ph::_1); CfgDbAccessPtr cfg_db = CfgMgr::instance().getStagingCfg()->getCfgDbAccess(); cfg_db->setAppendedParameters("universe=4"); cfg_db->createManagers(); // Reset counters related to connections as all managers have been recreated. srv->getNetworkState()->reset(NetworkState::Origin::DB_CONNECTION); } catch (const std::exception& ex) { err << "Unable to open database: " << ex.what(); return (isc::config::createAnswer(1, err.str())); } // Server will start DDNS communications if its enabled. try { srv->startD2(); } catch (const std::exception& ex) { err << "Error starting DHCP_DDNS client after server reconfiguration: " << ex.what(); return (isc::config::createAnswer(1, err.str())); } // Setup DHCPv4-over-DHCPv6 IPC try { Dhcp4to6Ipc::instance().open(); } catch (const std::exception& ex) { err << "error starting DHCPv4-over-DHCPv6 IPC " " after server reconfiguration: " << ex.what(); return (isc::config::createAnswer(1, err.str())); } // Configure DHCP packet queueing try { data::ConstElementPtr qc; qc = CfgMgr::instance().getStagingCfg()->getDHCPQueueControl(); if (IfaceMgr::instance().configureDHCPPacketQueue(AF_INET, qc)) { LOG_INFO(dhcp4_logger, DHCP4_CONFIG_PACKET_QUEUE) .arg(IfaceMgr::instance().getPacketQueue4()->getInfoStr()); } } catch (const std::exception& ex) { err << "Error setting packet queue controls after server reconfiguration: " << ex.what(); return (isc::config::createAnswer(1, err.str())); } // Configure a callback to shut down the server when the bind socket // attempts exceeded. CfgIface::open_sockets_failed_callback_ = std::bind(&ControlledDhcpv4Srv::openSocketsFailedCallback, srv, ph::_1); // Configuration may change active interfaces. Therefore, we have to reopen // sockets according to new configuration. It is possible that this // operation will fail for some interfaces but the openSockets function // guards against exceptions and invokes a callback function to // log warnings. Since we allow that this fails for some interfaces there // is no need to rollback configuration if socket fails to open on any // of the interfaces. CfgMgr::instance().getStagingCfg()->getCfgIface()-> openSockets(AF_INET, srv->getServerPort(), getInstance()->useBroadcast()); // Install the timers for handling leases reclamation. try { CfgMgr::instance().getStagingCfg()->getCfgExpiration()-> setupTimers(&ControlledDhcpv4Srv::reclaimExpiredLeases, &ControlledDhcpv4Srv::deleteExpiredReclaimedLeases, server_); } catch (const std::exception& ex) { err << "unable to setup timers for periodically running the" " reclamation of the expired leases: " << ex.what() << "."; return (isc::config::createAnswer(1, err.str())); } // Setup config backend polling, if configured for it. auto ctl_info = CfgMgr::instance().getStagingCfg()->getConfigControlInfo(); if (ctl_info) { long fetch_time = static_cast(ctl_info->getConfigFetchWaitTime()); // Only schedule the CB fetch timer if the fetch wait time is greater // than 0. if (fetch_time > 0) { // When we run unit tests, we want to use milliseconds unit for the // specified interval. Otherwise, we use seconds. Note that using // milliseconds as a unit in unit tests prevents us from waiting 1 // second on more before the timer goes off. Instead, we wait one // millisecond which significantly reduces the test time. if (!server_->inTestMode()) { fetch_time = 1000 * fetch_time; } boost::shared_ptr failure_count(new unsigned(0)); TimerMgr::instance()-> registerTimer("Dhcp4CBFetchTimer", std::bind(&ControlledDhcpv4Srv::cbFetchUpdates, server_, CfgMgr::instance().getStagingCfg(), failure_count), fetch_time, asiolink::IntervalTimer::ONE_SHOT); TimerMgr::instance()->setup("Dhcp4CBFetchTimer"); } } // Finally, we can commit runtime option definitions in libdhcp++. This is // exception free. LibDHCP::commitRuntimeOptionDefs(); // This hook point notifies hooks libraries that the configuration of the // DHCPv4 server has completed. It provides the hook library with the pointer // to the common IO service object, new server configuration in the JSON // format and with the pointer to the configuration storage where the // parsed configuration is stored. if (HooksManager::calloutsPresent(Hooks.hooks_index_dhcp4_srv_configured_)) { CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle(); callout_handle->setArgument("io_context", srv->getIOService()); callout_handle->setArgument("network_state", srv->getNetworkState()); callout_handle->setArgument("json_config", config); callout_handle->setArgument("server_config", CfgMgr::instance().getStagingCfg()); HooksManager::callCallouts(Hooks.hooks_index_dhcp4_srv_configured_, *callout_handle); // If next step is DROP, report a configuration error. if (callout_handle->getStatus() == CalloutHandle::NEXT_STEP_DROP) { string error; try { callout_handle->getArgument("error", error); } catch (NoSuchArgument const& ex) { error = "unknown error"; } return (isc::config::createAnswer(CONTROL_RESULT_ERROR, error)); } } // Apply multi threading settings. // @note These settings are applied/updated only if no errors occur while // applying the new configuration. // @todo This should be fixed. try { CfgMultiThreading::apply(CfgMgr::instance().getStagingCfg()->getDHCPMultiThreading()); } catch (const std::exception& ex) { err << "Error applying multi threading settings: " << ex.what(); return (isc::config::createAnswer(CONTROL_RESULT_ERROR, err.str())); } return (answer); } isc::data::ConstElementPtr ControlledDhcpv4Srv::checkConfig(isc::data::ConstElementPtr config) { LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_RECEIVED) .arg(redactConfig(config)->str()); ControlledDhcpv4Srv* srv = ControlledDhcpv4Srv::getInstance(); // Single stream instance used in all error clauses std::ostringstream err; if (!srv) { err << "Server object not initialized, can't process config."; return (isc::config::createAnswer(1, err.str())); } return (configureDhcp4Server(*srv, config, true)); } ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t server_port /*= DHCP4_SERVER_PORT*/, uint16_t client_port /*= 0*/) : Dhcpv4Srv(server_port, client_port), timer_mgr_(TimerMgr::instance()) { if (getInstance()) { isc_throw(InvalidOperation, "There is another Dhcpv4Srv instance already."); } server_ = this; // remember this instance for later use in handlers // TimerMgr uses IO service to run asynchronous timers. TimerMgr::instance()->setIOService(getIOService()); // CommandMgr uses IO service to run asynchronous socket operations. CommandMgr::instance().setIOService(getIOService()); // LeaseMgr uses IO service to run asynchronous timers. LeaseMgr::setIOService(getIOService()); // HostMgr uses IO service to run asynchronous timers. HostMgr::setIOService(getIOService()); // These are the commands always supported by the DHCPv4 server. // Please keep the list in alphabetic order. CommandMgr::instance().registerCommand("build-report", std::bind(&ControlledDhcpv4Srv::commandBuildReportHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("config-backend-pull", std::bind(&ControlledDhcpv4Srv::commandConfigBackendPullHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("config-get", std::bind(&ControlledDhcpv4Srv::commandConfigGetHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("config-reload", std::bind(&ControlledDhcpv4Srv::commandConfigReloadHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("config-set", std::bind(&ControlledDhcpv4Srv::commandConfigSetHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("config-test", std::bind(&ControlledDhcpv4Srv::commandConfigTestHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("config-write", std::bind(&ControlledDhcpv4Srv::commandConfigWriteHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("dhcp-enable", std::bind(&ControlledDhcpv4Srv::commandDhcpEnableHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("dhcp-disable", std::bind(&ControlledDhcpv4Srv::commandDhcpDisableHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("libreload", std::bind(&ControlledDhcpv4Srv::commandLibReloadHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("leases-reclaim", std::bind(&ControlledDhcpv4Srv::commandLeasesReclaimHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("server-tag-get", std::bind(&ControlledDhcpv4Srv::commandServerTagGetHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("shutdown", std::bind(&ControlledDhcpv4Srv::commandShutdownHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("status-get", std::bind(&ControlledDhcpv4Srv::commandStatusGetHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("version-get", std::bind(&ControlledDhcpv4Srv::commandVersionGetHandler, this, ph::_1, ph::_2)); // Register statistic related commands CommandMgr::instance().registerCommand("statistic-get", std::bind(&StatsMgr::statisticGetHandler, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-reset", std::bind(&StatsMgr::statisticResetHandler, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-remove", std::bind(&StatsMgr::statisticRemoveHandler, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-get-all", std::bind(&StatsMgr::statisticGetAllHandler, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-reset-all", std::bind(&StatsMgr::statisticResetAllHandler, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-remove-all", std::bind(&StatsMgr::statisticRemoveAllHandler, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-sample-age-set", std::bind(&StatsMgr::statisticSetMaxSampleAgeHandler, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-sample-age-set-all", std::bind(&ControlledDhcpv4Srv::commandStatisticSetMaxSampleAgeAllHandler, this, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-sample-count-set", std::bind(&StatsMgr::statisticSetMaxSampleCountHandler, ph::_1, ph::_2)); CommandMgr::instance().registerCommand("statistic-sample-count-set-all", std::bind(&ControlledDhcpv4Srv::commandStatisticSetMaxSampleCountAllHandler, this, ph::_1, ph::_2)); } void ControlledDhcpv4Srv::shutdownServer(int exit_value) { setExitValue(exit_value); getIOService()->stop(); // Stop ASIO transmissions shutdown(); // Initiate DHCPv4 shutdown procedure. } ControlledDhcpv4Srv::~ControlledDhcpv4Srv() { try { LeaseMgrFactory::destroy(); HostMgr::create(); cleanup(); // The closure captures either a shared pointer (memory leak) // or a raw pointer (pointing to a deleted object). DatabaseConnection::db_lost_callback_ = 0; DatabaseConnection::db_recovered_callback_ = 0; DatabaseConnection::db_failed_callback_ = 0; timer_mgr_->unregisterTimers(); // Close the command socket (if it exists). CommandMgr::instance().closeCommandSocket(); // Deregister any registered commands (please keep in alphabetic order) CommandMgr::instance().deregisterCommand("build-report"); CommandMgr::instance().deregisterCommand("config-backend-pull"); CommandMgr::instance().deregisterCommand("config-get"); CommandMgr::instance().deregisterCommand("config-reload"); CommandMgr::instance().deregisterCommand("config-set"); CommandMgr::instance().deregisterCommand("config-test"); CommandMgr::instance().deregisterCommand("config-write"); CommandMgr::instance().deregisterCommand("dhcp-disable"); CommandMgr::instance().deregisterCommand("dhcp-enable"); CommandMgr::instance().deregisterCommand("leases-reclaim"); CommandMgr::instance().deregisterCommand("libreload"); CommandMgr::instance().deregisterCommand("server-tag-get"); CommandMgr::instance().deregisterCommand("shutdown"); CommandMgr::instance().deregisterCommand("statistic-get"); CommandMgr::instance().deregisterCommand("statistic-get-all"); CommandMgr::instance().deregisterCommand("statistic-remove"); CommandMgr::instance().deregisterCommand("statistic-remove-all"); CommandMgr::instance().deregisterCommand("statistic-reset"); CommandMgr::instance().deregisterCommand("statistic-reset-all"); CommandMgr::instance().deregisterCommand("statistic-sample-age-set"); CommandMgr::instance().deregisterCommand("statistic-sample-age-set-all"); CommandMgr::instance().deregisterCommand("statistic-sample-count-set"); CommandMgr::instance().deregisterCommand("statistic-sample-count-set-all"); CommandMgr::instance().deregisterCommand("status-get"); CommandMgr::instance().deregisterCommand("version-get"); // LeaseMgr uses IO service to run asynchronous timers. LeaseMgr::setIOService(IOServicePtr()); // HostMgr uses IO service to run asynchronous timers. HostMgr::setIOService(IOServicePtr()); } catch (...) { // Don't want to throw exceptions from the destructor. The server // is shutting down anyway. ; } server_ = NULL; // forget this instance. There should be no callback anymore // at this stage anyway. } void ControlledDhcpv4Srv::reclaimExpiredLeases(const size_t max_leases, const uint16_t timeout, const bool remove_lease, const uint16_t max_unwarned_cycles) { try { server_->alloc_engine_->reclaimExpiredLeases4(max_leases, timeout, remove_lease, max_unwarned_cycles); } catch (const std::exception& ex) { LOG_ERROR(dhcp4_logger, DHCP4_RECLAIM_EXPIRED_LEASES_FAIL) .arg(ex.what()); } // We're using the ONE_SHOT timer so there is a need to re-schedule it. TimerMgr::instance()->setup(CfgExpiration::RECLAIM_EXPIRED_TIMER_NAME); } void ControlledDhcpv4Srv::deleteExpiredReclaimedLeases(const uint32_t secs) { server_->alloc_engine_->deleteExpiredReclaimedLeases4(secs); // We're using the ONE_SHOT timer so there is a need to re-schedule it. TimerMgr::instance()->setup(CfgExpiration::FLUSH_RECLAIMED_TIMER_NAME); } bool ControlledDhcpv4Srv::dbLostCallback(ReconnectCtlPtr db_reconnect_ctl) { if (!db_reconnect_ctl) { // This should never happen LOG_ERROR(dhcp4_logger, DHCP4_DB_RECONNECT_NO_DB_CTL); return (false); } // Disable service until the connection is recovered. if (db_reconnect_ctl->retriesLeft() == db_reconnect_ctl->maxRetries() && db_reconnect_ctl->alterServiceState()) { network_state_->disableService(NetworkState::Origin::DB_CONNECTION); } LOG_INFO(dhcp4_logger, DHCP4_DB_RECONNECT_LOST_CONNECTION); // If reconnect isn't enabled log it, initiate a shutdown if needed and // return false. if (!db_reconnect_ctl->retriesLeft() || !db_reconnect_ctl->retryInterval()) { LOG_INFO(dhcp4_logger, DHCP4_DB_RECONNECT_DISABLED) .arg(db_reconnect_ctl->retriesLeft()) .arg(db_reconnect_ctl->retryInterval()); if (db_reconnect_ctl->exitOnFailure()) { shutdownServer(EXIT_FAILURE); } return (false); } return (true); } bool ControlledDhcpv4Srv::dbRecoveredCallback(ReconnectCtlPtr db_reconnect_ctl) { if (!db_reconnect_ctl) { // This should never happen LOG_ERROR(dhcp4_logger, DHCP4_DB_RECONNECT_NO_DB_CTL); return (false); } // Enable service after the connection is recovered. if (db_reconnect_ctl->alterServiceState()) { network_state_->enableService(NetworkState::Origin::DB_CONNECTION); } LOG_INFO(dhcp4_logger, DHCP4_DB_RECONNECT_SUCCEEDED); db_reconnect_ctl->resetRetries(); return (true); } bool ControlledDhcpv4Srv::dbFailedCallback(ReconnectCtlPtr db_reconnect_ctl) { if (!db_reconnect_ctl) { // This should never happen LOG_ERROR(dhcp4_logger, DHCP4_DB_RECONNECT_NO_DB_CTL); return (false); } LOG_INFO(dhcp4_logger, DHCP4_DB_RECONNECT_FAILED) .arg(db_reconnect_ctl->maxRetries()); if (db_reconnect_ctl->exitOnFailure()) { shutdownServer(EXIT_FAILURE); } return (true); } void ControlledDhcpv4Srv::openSocketsFailedCallback(ReconnectCtlPtr reconnect_ctl) { if (!reconnect_ctl) { // This should never happen LOG_ERROR(dhcp4_logger, DHCP4_OPEN_SOCKETS_NO_RECONNECT_CTL); return; } LOG_INFO(dhcp4_logger, DHCP4_OPEN_SOCKETS_FAILED) .arg(reconnect_ctl->maxRetries()); if (reconnect_ctl->exitOnFailure()) { shutdownServer(EXIT_FAILURE); } } void ControlledDhcpv4Srv::cbFetchUpdates(const SrvConfigPtr& srv_cfg, boost::shared_ptr failure_count) { // stop thread pool (if running) MultiThreadingCriticalSection cs; try { // Fetch any configuration backend updates since our last fetch. server_->getCBControl()->databaseConfigFetch(srv_cfg, CBControlDHCPv4::FetchMode::FETCH_UPDATE); (*failure_count) = 0; } catch (const std::exception& ex) { LOG_ERROR(dhcp4_logger, DHCP4_CB_PERIODIC_FETCH_UPDATES_FAIL) .arg(ex.what()); // We allow at most 10 consecutive failures after which we stop // making further attempts to fetch the configuration updates. // Let's return without re-scheduling the timer. if (++(*failure_count) > 10) { LOG_ERROR(dhcp4_logger, DHCP4_CB_PERIODIC_FETCH_UPDATES_RETRIES_EXHAUSTED); return; } } // Reschedule the timer to fetch new updates or re-try if // the previous attempt resulted in an error. if (TimerMgr::instance()->isTimerRegistered("Dhcp4CBFetchTimer")) { TimerMgr::instance()->setup("Dhcp4CBFetchTimer"); } } } // namespace dhcp } // namespace isc