// Copyright (C) 2015-2023 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 using namespace std; using namespace isc::util; using namespace isc::dhcp; using namespace isc::log; namespace { /// @brief Maximum number of errors to allow when reading leases from the file. const uint32_t MAX_LEASE_ERRORS = 100; }; // namespace anonymous namespace isc { namespace lfc { /// @brief Defines the application name, it may be used to locate /// configuration data and appears in log statements. const char* LFCController::lfc_app_name_ = "DhcpLFC"; /// @brief Defines the executable name. const char* LFCController::lfc_bin_name_ = "kea-lfc"; LFCController::LFCController() : protocol_version_(0), verbose_(false), config_file_(""), previous_file_(""), copy_file_(""), output_file_(""), finish_file_(""), pid_file_("") { } LFCController::~LFCController() { } void LFCController::launch(int argc, char* argv[], const bool test_mode) { bool do_rotate = true; // It would be nice to set up the logger as the first step // in the process, but we don't know where to send logging // info until after we have parsed our arguments. As we // don't currently log anything when trying to parse the // arguments we do the parse before the logging setup. If // we do decide to log something then the code will need // to move around a bit. try { parseArgs(argc, argv); } catch (const InvalidUsage& ex) { usage(ex.what()); throw; // rethrow it } // Start up the logging system. startLogger(test_mode); LOG_INFO(lfc_logger, LFC_START); // verify we are the only instance PIDFile pid_file(pid_file_); try { if (pid_file.check()) { // Already running instance, bail out LOG_FATAL(lfc_logger, LFC_RUNNING); return; } // create the pid file for this instance pid_file.write(); } catch (const PIDFileError& pid_ex) { LOG_FATAL(lfc_logger, LFC_FAIL_PID_CREATE).arg(pid_ex.what()); return; } // If we don't have a finish file do the processing. We // don't know the exact type of the finish file here but // all we care about is if it exists so that's okay CSVFile lf_finish(getFinishFile()); if (!lf_finish.exists()) { LOG_INFO(lfc_logger, LFC_PROCESSING) .arg(previous_file_) .arg(copy_file_); try { if (getProtocolVersion() == 4) { processLeases(); } else { processLeases(); } } catch (const std::exception& proc_ex) { // We don't want to do the cleanup but do want to get rid of the pid do_rotate = false; LOG_FATAL(lfc_logger, LFC_FAIL_PROCESS).arg(proc_ex.what()); } } // If do_rotate is true We either already had a finish file or // were able to create one. We now want to do the file cleanup, // we don't want to return after the catch as we // still need to cleanup the pid file if (do_rotate) { LOG_INFO(lfc_logger, LFC_ROTATING); try { fileRotate(); } catch (const RunTimeFail& run_ex) { LOG_FATAL(lfc_logger, LFC_FAIL_ROTATE).arg(run_ex.what()); } } // delete the pid file for this instance try { pid_file.deleteFile(); } catch (const PIDFileError& pid_ex) { LOG_FATAL(lfc_logger, LFC_FAIL_PID_DEL).arg(pid_ex.what()); } LOG_INFO(lfc_logger, LFC_TERMINATE); } void LFCController::parseArgs(int argc, char* argv[]) { int ch; opterr = 0; optind = 1; while ((ch = getopt(argc, argv, ":46dhvVWp:x:i:o:c:f:")) != -1) { switch (ch) { case '4': // Process DHCPv4 lease files. protocol_version_ = 4; break; case '6': // Process DHCPv6 lease files. protocol_version_ = 6; break; case 'v': // Print just Kea version and exit. std::cout << getVersion(false) << std::endl; exit(EXIT_SUCCESS); case 'V': // Print extended Kea version and exit. std::cout << getVersion(true) << std::endl; exit(EXIT_SUCCESS); case 'W': // Display the configuration report and exit. std::cout << isc::detail::getConfigReport() << std::endl; exit(EXIT_SUCCESS); case 'd': // Verbose output. verbose_ = true; break; case 'p': // PID file name. if (optarg == NULL) { isc_throw(InvalidUsage, "PID file name missing"); } pid_file_ = optarg; break; case 'x': // Previous (or ex) file name. if (optarg == NULL) { isc_throw(InvalidUsage, "Previous (ex) file name missing"); } previous_file_ = optarg; break; case 'i': // Copy file name. if (optarg == NULL) { isc_throw(InvalidUsage, "Copy file name missing"); } copy_file_ = optarg; break; case 'o': // Output file name. if (optarg == NULL) { isc_throw(InvalidUsage, "Output file name missing"); } output_file_ = optarg; break; case 'f': // Finish file name. if (optarg == NULL) { isc_throw(InvalidUsage, "Finish file name missing"); } finish_file_ = optarg; break; case 'c': // Configuration file name if (optarg == NULL) { isc_throw(InvalidUsage, "Configuration file name missing"); } config_file_ = optarg; break; case 'h': usage(""); exit(EXIT_SUCCESS); case '?': // Unknown argument // note this will catch all the previous ... name missing isc_throw(InvalidUsage, "Unknown argument"); case ':': // Missing option argument isc_throw(InvalidUsage, "Missing option argument"); default: // I don't think we should get here as the unknown arguments // and missing options cases should cover everything else isc_throw(InvalidUsage, "Invalid command line"); } } // Check for extraneous parameters. if (argc > optind) { isc_throw(InvalidUsage, "Extraneous parameters."); } if (protocol_version_ == 0) { isc_throw(InvalidUsage, "DHCP version required"); } if (pid_file_.empty()) { isc_throw(InvalidUsage, "PID file not specified"); } if (previous_file_.empty()) { isc_throw(InvalidUsage, "Previous file not specified"); } if (copy_file_.empty()) { isc_throw(InvalidUsage, "Copy file not specified"); } if (output_file_.empty()) { isc_throw(InvalidUsage, "Output file not specified"); } if (finish_file_.empty()) { isc_throw(InvalidUsage, "Finish file not specified"); } if (config_file_.empty()) { isc_throw(InvalidUsage, "Config file not specified"); } // If verbose is set echo the input information if (verbose_) { std::cout << "Protocol version: DHCPv" << protocol_version_ << std::endl << "Previous or ex lease file: " << previous_file_ << std::endl << "Copy lease file: " << copy_file_ << std::endl << "Output lease file: " << output_file_ << std::endl << "Finish file: " << finish_file_ << std::endl << "Config file: " << config_file_ << std::endl << "PID file: " << pid_file_ << std::endl << std::endl; } } void LFCController::usage(const std::string& text) { if (!text.empty()) { std::cerr << "Usage error: " << text << std::endl; } std::cerr << "Usage: " << lfc_bin_name_ << std::endl << " [-4|-6] -p file -x file -i file -o file -f file -c file" << std::endl << " -4 or -6 clean a set of v4 or v6 lease files" << std::endl << " -p : PID file" << std::endl << " -x : previous or ex lease file" << std::endl << " -i : copy of lease file" << std::endl << " -o : output lease file" << std::endl << " -f : finish file" << std::endl << " -c : configuration file" << std::endl << " -v: print version number and exit" << std::endl << " -V: print extended version information and exit" << std::endl << " -d: optional, verbose output " << std::endl << " -h: print this message " << std::endl << std::endl; } std::string LFCController::getVersion(const bool extended) const{ std::stringstream version_stream; version_stream << VERSION; if (extended) { std::string db_version; if (protocol_version_ == 4) { db_version = Memfile_LeaseMgr::getDBVersion(Memfile_LeaseMgr::V4); } else if (protocol_version_ == 6) { db_version = Memfile_LeaseMgr::getDBVersion(Memfile_LeaseMgr::V6); } if (!db_version.empty()) { db_version = "database: " + db_version; } version_stream << std::endl << EXTENDED_VERSION << std::endl << db_version; } return (version_stream.str()); } template void LFCController::processLeases() const { StorageType storage; // If a previous file exists read the entries into storage LeaseFileType lf_prev(getPreviousFile()); if (lf_prev.exists()) { LeaseFileLoader::load(lf_prev, storage, MAX_LEASE_ERRORS); } // Follow that with the copy of the current lease file LeaseFileType lf_copy(getCopyFile()); if (lf_copy.exists()) { LeaseFileLoader::load(lf_copy, storage, MAX_LEASE_ERRORS); } // Write the result out to the output file LeaseFileType lf_output(getOutputFile()); LeaseFileLoader::write(lf_output, storage); // If desired log the stats LOG_INFO(lfc_logger, LFC_READ_STATS) .arg(lf_prev.getReadLeases() + lf_copy.getReadLeases()) .arg(lf_prev.getReads() + lf_copy.getReads()) .arg(lf_prev.getReadErrs() + lf_copy.getReadErrs()); LOG_INFO(lfc_logger, LFC_WRITE_STATS) .arg(lf_output.getWriteLeases()) .arg(lf_output.getWrites()) .arg(lf_output.getWriteErrs()); // Once we've finished the output file move it to the complete file if (rename(getOutputFile().c_str(), getFinishFile().c_str()) != 0) { isc_throw(RunTimeFail, "Unable to move output (" << output_file_ << ") to complete (" << finish_file_ << ") error: " << strerror(errno)); } } void LFCController::fileRotate() const { // Remove the old previous file if ((remove(getPreviousFile().c_str()) != 0) && (errno != ENOENT)) { isc_throw(RunTimeFail, "Unable to delete previous file '" << previous_file_ << "' error: " << strerror(errno)); } // Remove the copy file if ((remove(getCopyFile().c_str()) != 0) && (errno != ENOENT)) { isc_throw(RunTimeFail, "Unable to delete copy file '" << copy_file_ << "' error: " << strerror(errno)); } // Rename the finish file to be the previous file if (rename(finish_file_.c_str(), previous_file_.c_str()) != 0) { isc_throw(RunTimeFail, "Unable to move finish (" << finish_file_ << ") to previous (" << previous_file_ << ") error: " << strerror(errno)); } } void LFCController::startLogger(const bool test_mode) const { // If we are running in test mode use the environment variables // else use our defaults if (test_mode) { initLogger(); } else { OutputOption option; LoggerManager manager; initLogger(lfc_app_name_, INFO, 0, NULL, false); // Prepare the objects to define the logging specification LoggerSpecification spec(getRootLoggerName(), keaLoggerSeverity(INFO), keaLoggerDbglevel(0)); // If we are running in verbose (debugging) mode // we send the output to the console, otherwise // by default we send it to the SYSLOG if (verbose_) { option.destination = OutputOption::DEST_CONSOLE; } else { option.destination = OutputOption::DEST_SYSLOG; } // ... and set the destination spec.addOutputOption(option); manager.process(spec); } } }; // namespace isc::lfc }; // namespace isc