diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc index e579975503..ca86f745ac 100644 --- a/src/bin/dhcp4/dhcp4_srv.cc +++ b/src/bin/dhcp4/dhcp4_srv.cc @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -5130,6 +5131,13 @@ Dhcpv4Srv::getVersion(bool extended) { tmp << endl << "- " << version; } } + info = BackendStoreFactory::getDBVersions(); + if (info.size()) { + tmp << endl << "forensic backends:"; + for (auto const& version : info) { + tmp << endl << "- " << version; + } + } // @todo: more details about database runtime } diff --git a/src/bin/dhcp4/json_config_parser.cc b/src/bin/dhcp4/json_config_parser.cc index 3f0e252cd6..77b111a397 100644 --- a/src/bin/dhcp4/json_config_parser.cc +++ b/src/bin/dhcp4/json_config_parser.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -928,6 +929,9 @@ configureDhcp4Server(Dhcpv4Srv& server, isc::data::ConstElementPtr config_set, // Log the list of known backends. HostDataSourceFactory::logRegistered(); + // Log the list of known backends. + BackendStoreFactory::logRegistered(); + // Moved from the commit block to add the config backend indication. if (status_code == CONTROL_RESULT_SUCCESS && (!check_only || extra_checks)) { try { diff --git a/src/bin/dhcp6/dhcp6_srv.cc b/src/bin/dhcp6/dhcp6_srv.cc index 6b6afbe2f0..bc764bd812 100644 --- a/src/bin/dhcp6/dhcp6_srv.cc +++ b/src/bin/dhcp6/dhcp6_srv.cc @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -5078,6 +5079,13 @@ Dhcpv6Srv::getVersion(bool extended) { tmp << endl << "- " << version; } } + info = BackendStoreFactory::getDBVersions(); + if (info.size()) { + tmp << endl << "forensic backends:"; + for (auto const& version : info) { + tmp << endl << "- " << version; + } + } // @todo: more details about database runtime } diff --git a/src/bin/dhcp6/json_config_parser.cc b/src/bin/dhcp6/json_config_parser.cc index b65faec709..b2622a9291 100644 --- a/src/bin/dhcp6/json_config_parser.cc +++ b/src/bin/dhcp6/json_config_parser.cc @@ -21,7 +21,8 @@ #include #include #include -#include +#include +#include #include #include #include @@ -1063,6 +1064,9 @@ configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set, // Log the list of known backends. HostDataSourceFactory::logRegistered(); + // Log the list of known backends. + BackendStoreFactory::logRegistered(); + // Moved from the commit block to add the config backend indication. if (status_code == CONTROL_RESULT_SUCCESS && (!check_only || extra_checks)) { try { diff --git a/src/hooks/d2/gss_tsig/tests/gss_tsig_callouts_unittests.cc b/src/hooks/d2/gss_tsig/tests/gss_tsig_callouts_unittests.cc index d49bceee2a..769a239572 100644 --- a/src/hooks/d2/gss_tsig/tests/gss_tsig_callouts_unittests.cc +++ b/src/hooks/d2/gss_tsig/tests/gss_tsig_callouts_unittests.cc @@ -4,11 +4,6 @@ // 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/. -/// @file This file contains tests which verify control command legal file entry -/// generation and callout: command_processed. -/// These tests assume the legal log library is linked in, not loaded. -/// This allows a great deal more flexibility in testing, such as overriding -/// and accessing the BackendStore::instance(). /// The load and unload callouts are exercised in ../libloadtests, which /// actually uses the HooksManager to load and unload the library. diff --git a/src/hooks/dhcp/forensic_log/Makefile.am b/src/hooks/dhcp/forensic_log/Makefile.am index 2a98af890f..c5a747a3c8 100644 --- a/src/hooks/dhcp/forensic_log/Makefile.am +++ b/src/hooks/dhcp/forensic_log/Makefile.am @@ -5,12 +5,6 @@ legal_log_dir = @localstatedir@/lib/@PACKAGE@ AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) AM_CPPFLAGS += -DLEGAL_LOG_DIR="\"$(legal_log_dir)\"" -if HAVE_MYSQL -AM_CPPFLAGS += $(MYSQL_CPPFLAGS) -endif -if HAVE_PGSQL -AM_CPPFLAGS += $(PGSQL_CPPFLAGS) -endif AM_CXXFLAGS = $(KEA_CXXFLAGS) @@ -38,20 +32,10 @@ liblegl_la_SOURCES += lease4_callouts.cc liblegl_la_SOURCES += lease6_callouts.cc liblegl_la_SOURCES += legal_log_log.cc legal_log_log.h liblegl_la_SOURCES += legal_log_messages.cc legal_log_messages.h -liblegl_la_SOURCES += legal_log_db_log.cc legal_log_db_log.h -liblegl_la_SOURCES += backend_store.cc backend_store.h liblegl_la_SOURCES += rotating_file.cc rotating_file.h liblegl_la_SOURCES += subnets_user_context.h liblegl_la_SOURCES += version.cc -if HAVE_MYSQL -liblegl_la_SOURCES += mysql_legal_log.cc mysql_legal_log.h -endif - -if HAVE_PGSQL -liblegl_la_SOURCES += pgsql_legal_log.cc pgsql_legal_log.h -endif - liblegl_la_CXXFLAGS = $(AM_CXXFLAGS) liblegl_la_CPPFLAGS = $(AM_CPPFLAGS) @@ -69,14 +53,6 @@ libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la - -if HAVE_MYSQL -libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/mysql/libkea-mysql.la -endif -if HAVE_PGSQL -libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/pgsql/libkea-pgsql.la -endif - libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/database/libkea-database.la libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la @@ -86,12 +62,6 @@ libdhcp_legal_log_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-excepti libdhcp_legal_log_la_LIBADD += $(LOG4CPLUS_LIBS) libdhcp_legal_log_la_LIBADD += $(BOOST_LIBS) libdhcp_legal_log_la_LIBADD += $(CRYPTO_LIBS) -if HAVE_MYSQL -libdhcp_legal_log_la_LDFLAGS += $(MYSQL_LIBS) -endif -if HAVE_PGSQL -libdhcp_legal_log_la_LDFLAGS += $(PGSQL_LIBS) -endif # Doxygen documentation diff --git a/src/hooks/dhcp/forensic_log/command_callouts.cc b/src/hooks/dhcp/forensic_log/command_callouts.cc index 239189b60e..2c46f0ba2e 100644 --- a/src/hooks/dhcp/forensic_log/command_callouts.cc +++ b/src/hooks/dhcp/forensic_log/command_callouts.cc @@ -18,7 +18,7 @@ #include #include #include -#include +#include #include #include @@ -119,7 +119,7 @@ void addDuration(ostringstream& os, ConstElementPtr& arguments) { if (!getOptionalInt(arguments, "valid-lft", duration)) { int64_t expire = 0; if (getOptionalInt(arguments, "expire", expire)) { - duration = expire - BackendStore::instance()->now().tv_sec; + duration = expire - BackendStoreFactory::instance()->now().tv_sec; } } @@ -181,12 +181,13 @@ bool checkLoggingEnabledSubnet4(ConstElementPtr& arguments) { /// @brief Handle lease4 related commands. /// +/// @param handle CalloutHandle which provides access to context. /// @param name The command name. /// @param arguments The command arguments. /// @param response The command response. int handleLease4Cmds(string& name, ConstElementPtr& arguments, ConstElementPtr& /*response*/) { - if (!BackendStore::instance()) { + if (!BackendStoreFactory::instance()) { LOG_ERROR(legal_log_logger, LEGAL_LOG_COMMAND_NO_LEGAL_STORE); return (1); @@ -244,7 +245,7 @@ int handleLease4Cmds(string& name, ConstElementPtr& arguments, } } - BackendStore::instance()->writeln(os.str(), osa.str()); + BackendStoreFactory::instance()->writeln(os.str(), osa.str()); } catch (const exception& ex) { LOG_ERROR(legal_log_logger, LEGAL_LOG_COMMAND_WRITE_ERROR) .arg(ex.what()); @@ -286,7 +287,7 @@ bool checkLoggingEnabledSubnet6(ConstElementPtr& arguments) { /// @param response The command response. int handleLease6Cmds(string& name, ConstElementPtr& arguments, ConstElementPtr& response) { - if (!BackendStore::instance()) { + if (!BackendStoreFactory::instance()) { LOG_ERROR(legal_log_logger, LEGAL_LOG_COMMAND_NO_LEGAL_STORE); return (1); @@ -456,7 +457,7 @@ int handleLease6Cmds(string& name, ConstElementPtr& arguments, return (status); } - BackendStore::instance()->writeln(os.str(), osa.str()); + BackendStoreFactory::instance()->writeln(os.str(), osa.str()); } catch (const exception& ex) { LOG_ERROR(legal_log_logger, LEGAL_LOG_COMMAND_WRITE_ERROR) .arg(ex.what()); @@ -480,7 +481,7 @@ int handleLease6Cmds(string& name, ConstElementPtr& arguments, /// /// @return 0 upon success, non-zero otherwise. int command_processed(CalloutHandle& handle) { - if (!BackendStore::instance()) { + if (!BackendStoreFactory::instance()) { LOG_ERROR(legal_log_logger, LEGAL_LOG_COMMAND_NO_LEGAL_STORE); return (1); diff --git a/src/hooks/dhcp/forensic_log/lease4_callouts.cc b/src/hooks/dhcp/forensic_log/lease4_callouts.cc index 738eebf8f2..d6fd039f58 100644 --- a/src/hooks/dhcp/forensic_log/lease4_callouts.cc +++ b/src/hooks/dhcp/forensic_log/lease4_callouts.cc @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include #include @@ -38,13 +38,13 @@ bool getCustomEntry(const Pkt4Ptr& query, const Pkt4Ptr& response, const Lease4Ptr& /*lease*/, std::string& value) { bool using_custom_format = false; - auto expression = BackendStore::instance()->getRequestFormatExpression(); + auto expression = BackendStoreFactory::instance()->getRequestFormatExpression(); if (expression && query) { value = evaluateString(*expression, *query); using_custom_format = true; } - expression = BackendStore::instance()->getResponseFormatExpression(); + expression = BackendStoreFactory::instance()->getResponseFormatExpression(); if (expression && response) { value += evaluateString(*expression, *response); using_custom_format = true; @@ -214,7 +214,7 @@ std::string genLease4Entry(const Pkt4Ptr& query, /// /// @return returns 0 upon success, non-zero otherwise int legalLog4Handler(CalloutHandle& handle, const Action& action) { - if (!BackendStore::instance()) { + if (!BackendStoreFactory::instance()) { LOG_ERROR(legal_log_logger, LEGAL_LOG_LEASE4_NO_LEGAL_STORE); return (1); @@ -239,7 +239,7 @@ int legalLog4Handler(CalloutHandle& handle, const Action& action) { ConstSubnet4Ptr subnet = cfg->getBySubnetId(lease->subnet_id_); if (!isLoggingDisabled(subnet)) { - BackendStore::instance()->writeln(genLease4Entry(query, response, + BackendStoreFactory::instance()->writeln(genLease4Entry(query, response, lease, action), lease->addr_.toText()); } diff --git a/src/hooks/dhcp/forensic_log/lease6_callouts.cc b/src/hooks/dhcp/forensic_log/lease6_callouts.cc index 892ecaaf58..e96b4d95c3 100644 --- a/src/hooks/dhcp/forensic_log/lease6_callouts.cc +++ b/src/hooks/dhcp/forensic_log/lease6_callouts.cc @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include #include @@ -475,6 +475,7 @@ void replaceTokensForLease(isc::dhcp::ExpressionPtr& expression, /// @brief Create custom log entry for the current lease. /// +/// @param handle CalloutHandle which provides access to context. /// @param query The query received by the server. /// @param response The response of the server. /// @param lease The current lease generating this log entry. @@ -483,7 +484,7 @@ bool getCustomEntry(const Pkt6Ptr& query, const Pkt6Ptr& response, const Lease6Ptr& lease, std::string& value) { bool using_custom_format = false; - auto expression = BackendStore::instance()->getRequestFormatExpression(); + auto expression = BackendStoreFactory::instance()->getRequestFormatExpression(); if (expression && query) { replaceTokensForLease(expression, lease); @@ -491,7 +492,7 @@ bool getCustomEntry(const Pkt6Ptr& query, const Pkt6Ptr& response, using_custom_format = true; } - expression = BackendStore::instance()->getResponseFormatExpression(); + expression = BackendStoreFactory::instance()->getResponseFormatExpression(); if (expression && response) { replaceTokensForLease(expression, lease); @@ -668,7 +669,7 @@ std::string genLease6Entry(const Pkt6Ptr& query, /// /// @return returns 0 upon success, non-zero otherwise int legalLog6Handler(CalloutHandle& handle, const Action& action) { - if (!BackendStore::instance()) { + if (!BackendStoreFactory::instance()) { LOG_ERROR(legal_log_logger, LEGAL_LOG_LEASE6_NO_LEGAL_STORE); return (1); } @@ -692,7 +693,7 @@ int legalLog6Handler(CalloutHandle& handle, const Action& action) { ConstSubnet6Ptr subnet = cfg->getBySubnetId(lease->subnet_id_); if (!isLoggingDisabled(subnet)) { - BackendStore::instance()->writeln(genLease6Entry(query, response, + BackendStoreFactory::instance()->writeln(genLease6Entry(query, response, lease, action), genLease6Addr(lease)); } diff --git a/src/hooks/dhcp/forensic_log/legal_log_db_log.cc b/src/hooks/dhcp/forensic_log/legal_log_db_log.cc deleted file mode 100644 index 657a13d241..0000000000 --- a/src/hooks/dhcp/forensic_log/legal_log_db_log.cc +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (C) 2018-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/. - -/// Defines the logger used by the NSAS - -#include - -#include -#include - -using namespace isc::db; -using namespace std; - -namespace isc { - -namespace db { - -extern mutex db_logger_mutex; - -} // namespace db - -namespace legal_log { - -const DbLogger::MessageMap legal_log_db_message_map = { - { DB_INVALID_ACCESS, LEGAL_LOG_INVALID_ACCESS }, - - { PGSQL_DEALLOC_ERROR, LEGAL_LOG_PGSQL_DEALLOC_ERROR }, - { PGSQL_FATAL_ERROR, LEGAL_LOG_PGSQL_FATAL_ERROR }, - { PGSQL_START_TRANSACTION, LEGAL_LOG_PGSQL_START_TRANSACTION }, - { PGSQL_COMMIT, LEGAL_LOG_PGSQL_COMMIT }, - { PGSQL_ROLLBACK, LEGAL_LOG_PGSQL_ROLLBACK }, - - { MYSQL_FATAL_ERROR, LEGAL_LOG_MYSQL_FATAL_ERROR }, - { MYSQL_START_TRANSACTION, LEGAL_LOG_MYSQL_START_TRANSACTION }, - { MYSQL_COMMIT, LEGAL_LOG_MYSQL_COMMIT }, - { MYSQL_ROLLBACK, LEGAL_LOG_MYSQL_ROLLBACK }, -}; - -DbLogger legal_log_db_logger(legal_log_logger, legal_log_db_message_map); - -LegalLogDbLogger::LegalLogDbLogger() { - lock_guard lk(db_logger_mutex); - db_logger_stack.push_back(legal_log_db_logger); -} - -LegalLogDbLogger::~LegalLogDbLogger() { - lock_guard lk(db_logger_mutex); - db_logger_stack.pop_back(); -} - -} // namespace legal_log -} // namespace isc diff --git a/src/hooks/dhcp/forensic_log/legal_log_messages.cc b/src/hooks/dhcp/forensic_log/legal_log_messages.cc index cc9b5d2c3c..c32dcd4264 100644 --- a/src/hooks/dhcp/forensic_log/legal_log_messages.cc +++ b/src/hooks/dhcp/forensic_log/legal_log_messages.cc @@ -7,39 +7,14 @@ extern const isc::log::MessageID LEGAL_LOG_COMMAND_NO_LEGAL_STORE = "LEGAL_LOG_COMMAND_NO_LEGAL_STORE"; extern const isc::log::MessageID LEGAL_LOG_COMMAND_WRITE_ERROR = "LEGAL_LOG_COMMAND_WRITE_ERROR"; extern const isc::log::MessageID LEGAL_LOG_DB_OPEN_CONNECTION_WITH_RETRY_FAILED = "LEGAL_LOG_DB_OPEN_CONNECTION_WITH_RETRY_FAILED"; -extern const isc::log::MessageID LEGAL_LOG_INVALID_ACCESS = "LEGAL_LOG_INVALID_ACCESS"; extern const isc::log::MessageID LEGAL_LOG_LEASE4_NO_LEGAL_STORE = "LEGAL_LOG_LEASE4_NO_LEGAL_STORE"; extern const isc::log::MessageID LEGAL_LOG_LEASE4_WRITE_ERROR = "LEGAL_LOG_LEASE4_WRITE_ERROR"; extern const isc::log::MessageID LEGAL_LOG_LEASE6_NO_LEGAL_STORE = "LEGAL_LOG_LEASE6_NO_LEGAL_STORE"; extern const isc::log::MessageID LEGAL_LOG_LEASE6_WRITE_ERROR = "LEGAL_LOG_LEASE6_WRITE_ERROR"; extern const isc::log::MessageID LEGAL_LOG_LOAD_ERROR = "LEGAL_LOG_LOAD_ERROR"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_COMMIT = "LEGAL_LOG_MYSQL_COMMIT"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB = "LEGAL_LOG_MYSQL_DB"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED = "LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE = "LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED = "LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_FATAL_ERROR = "LEGAL_LOG_MYSQL_FATAL_ERROR"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_GET_VERSION = "LEGAL_LOG_MYSQL_GET_VERSION"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_INSERT_LOG = "LEGAL_LOG_MYSQL_INSERT_LOG"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_NO_TLS = "LEGAL_LOG_MYSQL_NO_TLS"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_ROLLBACK = "LEGAL_LOG_MYSQL_ROLLBACK"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_START_TRANSACTION = "LEGAL_LOG_MYSQL_START_TRANSACTION"; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_TLS_CIPHER = "LEGAL_LOG_MYSQL_TLS_CIPHER"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_COMMIT = "LEGAL_LOG_PGSQL_COMMIT"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB = "LEGAL_LOG_PGSQL_DB"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED = "LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE = "LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED = "LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DEALLOC_ERROR = "LEGAL_LOG_PGSQL_DEALLOC_ERROR"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_FATAL_ERROR = "LEGAL_LOG_PGSQL_FATAL_ERROR"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_GET_VERSION = "LEGAL_LOG_PGSQL_GET_VERSION"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_INSERT_LOG = "LEGAL_LOG_PGSQL_INSERT_LOG"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_NO_TLS_SUPPORT = "LEGAL_LOG_PGSQL_NO_TLS_SUPPORT"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_ROLLBACK = "LEGAL_LOG_PGSQL_ROLLBACK"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_START_TRANSACTION = "LEGAL_LOG_PGSQL_START_TRANSACTION"; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_TLS_SUPPORT = "LEGAL_LOG_PGSQL_TLS_SUPPORT"; extern const isc::log::MessageID LEGAL_LOG_STORE_CLOSED = "LEGAL_LOG_STORE_CLOSED"; extern const isc::log::MessageID LEGAL_LOG_STORE_CLOSE_ERROR = "LEGAL_LOG_STORE_CLOSE_ERROR"; +extern const isc::log::MessageID LEGAL_LOG_STORE_OPEN = "LEGAL_LOG_STORE_OPEN"; extern const isc::log::MessageID LEGAL_LOG_STORE_OPENED = "LEGAL_LOG_STORE_OPENED"; extern const isc::log::MessageID LEGAL_LOG_UNLOAD_ERROR = "LEGAL_LOG_UNLOAD_ERROR"; @@ -49,39 +24,14 @@ const char* values[] = { "LEGAL_LOG_COMMAND_NO_LEGAL_STORE", "LegalStore instance is null", "LEGAL_LOG_COMMAND_WRITE_ERROR", "Could not write command entry to the legal store: %1", "LEGAL_LOG_DB_OPEN_CONNECTION_WITH_RETRY_FAILED", "Failed to connect to database: %1 with error: %2", - "LEGAL_LOG_INVALID_ACCESS", "invalid database access string: %1", "LEGAL_LOG_LEASE4_NO_LEGAL_STORE", "LegalStore instance is null", "LEGAL_LOG_LEASE4_WRITE_ERROR", "Could not write to the legal store: %1", "LEGAL_LOG_LEASE6_NO_LEGAL_STORE", "LegalStore instance is null", "LEGAL_LOG_LEASE6_WRITE_ERROR", "Could not write to the legal store: %1", "LEGAL_LOG_LOAD_ERROR", "LEGAL LOGGING DISABLED! An error occurred loading the library: %1", - "LEGAL_LOG_MYSQL_COMMIT", "committing to MySQL database", - "LEGAL_LOG_MYSQL_DB", "opening MySQL log database: %1", - "LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED", "database reconnect failed: %1", - "LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE", "scheduling attempt %1 of %2 in %3 milliseconds", - "LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED", "maximum number of database reconnect attempts: %1, has been exhausted without success", - "LEGAL_LOG_MYSQL_FATAL_ERROR", "Unrecoverable MySQL error occurred: %1 for <%2>, reason: %3 (error code: %4).", - "LEGAL_LOG_MYSQL_GET_VERSION", "obtaining schema version information", - "LEGAL_LOG_MYSQL_INSERT_LOG", "Adding a log entry to the database: %1", - "LEGAL_LOG_MYSQL_NO_TLS", "TLS was required but is not used", - "LEGAL_LOG_MYSQL_ROLLBACK", "rolling back MySQL database", - "LEGAL_LOG_MYSQL_START_TRANSACTION", "starting new MySQL transaction", - "LEGAL_LOG_MYSQL_TLS_CIPHER", "TLS cipher: %1", - "LEGAL_LOG_PGSQL_COMMIT", "committing to PostgreSQL database", - "LEGAL_LOG_PGSQL_DB", "opening PostgreSQL log database: %1", - "LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED", "database reconnect failed: %1", - "LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE", "scheduling attempt %1 of %2 in %3 milliseconds", - "LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED", "maximum number of database reconnect attempts: %1, has been exhausted without success", - "LEGAL_LOG_PGSQL_DEALLOC_ERROR", "An error occurred deallocating SQL statements while closing the PostgreSQL log database: %1", - "LEGAL_LOG_PGSQL_FATAL_ERROR", "Unrecoverable PostgreSQL error occurred: Statement: <%1>, reason: %2 (error code: %3).", - "LEGAL_LOG_PGSQL_GET_VERSION", "obtaining schema version information", - "LEGAL_LOG_PGSQL_INSERT_LOG", "Adding a log entry to the database: %1", - "LEGAL_LOG_PGSQL_NO_TLS_SUPPORT", "Attempt to configure TLS (unsupported for PostgreSQL): %1", - "LEGAL_LOG_PGSQL_ROLLBACK", "rolling back PostgreSQL database", - "LEGAL_LOG_PGSQL_START_TRANSACTION", "starting a new PostgreSQL transaction", - "LEGAL_LOG_PGSQL_TLS_SUPPORT", "Attempt to configure TLS: %1", "LEGAL_LOG_STORE_CLOSED", "Legal store closed: %1", "LEGAL_LOG_STORE_CLOSE_ERROR", "An error occurred closing the store: %1, error: %2", + "LEGAL_LOG_STORE_OPEN", "opening Legal Log file: %1", "LEGAL_LOG_STORE_OPENED", "Legal store opened: %1", "LEGAL_LOG_UNLOAD_ERROR", "An error occurred unloading the library: %1", NULL diff --git a/src/hooks/dhcp/forensic_log/legal_log_messages.h b/src/hooks/dhcp/forensic_log/legal_log_messages.h index 1519188c6d..ebdb761148 100644 --- a/src/hooks/dhcp/forensic_log/legal_log_messages.h +++ b/src/hooks/dhcp/forensic_log/legal_log_messages.h @@ -8,39 +8,14 @@ extern const isc::log::MessageID LEGAL_LOG_COMMAND_NO_LEGAL_STORE; extern const isc::log::MessageID LEGAL_LOG_COMMAND_WRITE_ERROR; extern const isc::log::MessageID LEGAL_LOG_DB_OPEN_CONNECTION_WITH_RETRY_FAILED; -extern const isc::log::MessageID LEGAL_LOG_INVALID_ACCESS; extern const isc::log::MessageID LEGAL_LOG_LEASE4_NO_LEGAL_STORE; extern const isc::log::MessageID LEGAL_LOG_LEASE4_WRITE_ERROR; extern const isc::log::MessageID LEGAL_LOG_LEASE6_NO_LEGAL_STORE; extern const isc::log::MessageID LEGAL_LOG_LEASE6_WRITE_ERROR; extern const isc::log::MessageID LEGAL_LOG_LOAD_ERROR; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_COMMIT; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_FATAL_ERROR; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_GET_VERSION; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_INSERT_LOG; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_NO_TLS; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_ROLLBACK; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_START_TRANSACTION; -extern const isc::log::MessageID LEGAL_LOG_MYSQL_TLS_CIPHER; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_COMMIT; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_DEALLOC_ERROR; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_FATAL_ERROR; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_GET_VERSION; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_INSERT_LOG; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_NO_TLS_SUPPORT; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_ROLLBACK; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_START_TRANSACTION; -extern const isc::log::MessageID LEGAL_LOG_PGSQL_TLS_SUPPORT; extern const isc::log::MessageID LEGAL_LOG_STORE_CLOSED; extern const isc::log::MessageID LEGAL_LOG_STORE_CLOSE_ERROR; +extern const isc::log::MessageID LEGAL_LOG_STORE_OPEN; extern const isc::log::MessageID LEGAL_LOG_STORE_OPENED; extern const isc::log::MessageID LEGAL_LOG_UNLOAD_ERROR; diff --git a/src/hooks/dhcp/forensic_log/legal_log_messages.mes b/src/hooks/dhcp/forensic_log/legal_log_messages.mes index 4eadc1c547..b6ac0f1e4b 100644 --- a/src/hooks/dhcp/forensic_log/legal_log_messages.mes +++ b/src/hooks/dhcp/forensic_log/legal_log_messages.mes @@ -21,12 +21,6 @@ the store database. The operation started a retry to connect procedure. The database access string with password redacted is logged, along with the error and details for the reconnect procedure. -% LEGAL_LOG_INVALID_ACCESS invalid database access string: %1 -This is logged when an attempt has been made to parse a database access string -and the attempt ended in error. The access string in question - which -should be of the form 'keyword=value keyword=value...' is included in -the message. - % LEGAL_LOG_LEASE4_NO_LEGAL_STORE LegalStore instance is null This is an error message issued when the Legal Log library attempted to write a IPv4 lease entry to the legal store and the store instance @@ -54,133 +48,6 @@ This is an error message issued when the DHCP Legal Log library could not be loaded. The exact cause should be explained in the log message. No existing stores will be altered, nor any legal logging entries emitted. -% LEGAL_LOG_MYSQL_COMMIT committing to MySQL database -The code has issued a commit call. All outstanding transactions will be -committed to the database. Note that depending on the MySQL settings, -the committal may not include a write to disk. - -% LEGAL_LOG_MYSQL_DB opening MySQL log database: %1 -This informational message is logged when a legal log hook library is -about to open a MySQL log database. The parameters of the -connection including database name and username needed to access it -(but not the password if any) are logged. - -% LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED database reconnect failed: %1 -An error message issued when an attempt to reconnect has failed. - -% LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE scheduling attempt %1 of %2 in %3 milliseconds -An info message issued when the server is scheduling the next attempt to reconnect -to the database. This occurs when the server has lost database connectivity and -is attempting to reconnect automatically. - -% LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED maximum number of database reconnect attempts: %1, has been exhausted without success -An error message issued when the server failed to reconnect. Loss of connectivity -is typically a network or database server issue. - -% LEGAL_LOG_MYSQL_FATAL_ERROR Unrecoverable MySQL error occurred: %1 for <%2>, reason: %3 (error code: %4). -An error message indicating that communication with the MySQL database server -has been lost. When this occurs the server exits immediately with a non-zero -exit code. This is most likely due to a network issue. - -% LEGAL_LOG_MYSQL_GET_VERSION obtaining schema version information -Logged at debug log level 50. -A debug message issued when the server is about to obtain schema version -information from the MySQL database. - -% LEGAL_LOG_MYSQL_INSERT_LOG Adding a log entry to the database: %1 -Logged at debug log level 50. -An informational message logged when a log entry is inserted. - -% LEGAL_LOG_MYSQL_NO_TLS TLS was required but is not used -This error message is issued when TLS for the connection was required but -TLS is not used. - -% LEGAL_LOG_MYSQL_ROLLBACK rolling back MySQL database -The code has issued a rollback call. All outstanding transaction will -be rolled back and not committed to the database. - -% LEGAL_LOG_MYSQL_START_TRANSACTION starting new MySQL transaction -A debug message issued when a new MySQL transaction is being started. -This message is typically not issued when inserting data into a -single table because the server doesn't explicitly start -transactions in this case. This message is issued when data is -inserted into multiple tables with multiple INSERT statements -and there may be a need to rollback the whole transaction if -any of these INSERT statements fail. - -% LEGAL_LOG_MYSQL_TLS_CIPHER TLS cipher: %1 -Logged at debug log level 50. -A debug message issued when a new MySQL connected is created with TLS. -The TLS cipher name is logged. - -% LEGAL_LOG_PGSQL_COMMIT committing to PostgreSQL database -The code has issued a commit call. All outstanding transactions will be -committed to the database. Note that depending on the PostgreSQL settings, -the committal may not include a write to disk. - -% LEGAL_LOG_PGSQL_DB opening PostgreSQL log database: %1 -This informational message is logged when a legal log hook library is -about to open a PostgreSQL log database. The parameters of the -connection including database name and username needed to access it -(but not the password if any) are logged. - -% LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED database reconnect failed: %1 -An error message issued when an attempt to reconnect has failed. - -% LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE scheduling attempt %1 of %2 in %3 milliseconds -An info message issued when the server is scheduling the next attempt to reconnect -to the database. This occurs when the server has lost database connectivity and -is attempting to reconnect automatically. - -% LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED maximum number of database reconnect attempts: %1, has been exhausted without success -An error message issued when the server failed to reconnect. Loss of connectivity -is typically a network or database server issue. - -% LEGAL_LOG_PGSQL_DEALLOC_ERROR An error occurred deallocating SQL statements while closing the PostgreSQL log database: %1 -This is an error message issued when a legal log hook library experienced -and error freeing database SQL resources as part of closing its connection to -the PostgreSQL database. The connection is closed as part of normal server -shutdown. This error is most likely a programmatic issue that is highly -unlikely to occur or negatively impact server operation. - -% LEGAL_LOG_PGSQL_FATAL_ERROR Unrecoverable PostgreSQL error occurred: Statement: <%1>, reason: %2 (error code: %3). -An error message indicating that communication with the MySQL database server -has been lost. When this occurs the server exits immediately with a non-zero -exit code. This is most likely due to a network issue. - -% LEGAL_LOG_PGSQL_GET_VERSION obtaining schema version information -Logged at debug log level 50. -A debug message issued when the server is about to obtain schema version -information from the PostgreSQL database. - -% LEGAL_LOG_PGSQL_INSERT_LOG Adding a log entry to the database: %1 -Logged at debug log level 50. -An informational message logged when a log entry is inserted. - -% LEGAL_LOG_PGSQL_NO_TLS_SUPPORT Attempt to configure TLS (unsupported for PostgreSQL): %1 -This error message is printed when TLS support was required in the Kea -configuration: Kea was built with this feature disabled for PostgreSQL. -The parameters of the connection are logged. - -% LEGAL_LOG_PGSQL_ROLLBACK rolling back PostgreSQL database -The code has issued a rollback call. All outstanding transaction will -be rolled back and not committed to the database. - -% LEGAL_LOG_PGSQL_START_TRANSACTION starting a new PostgreSQL transaction -A debug message issued when a new PostgreSQL transaction is being started. -This message is typically not issued when inserting data into a -single table because the server doesn't explicitly start -transactions in this case. This message is issued when data is -inserted into multiple tables with multiple INSERT statements -and there may be a need to rollback the whole transaction if -any of these INSERT statements fail. - -% LEGAL_LOG_PGSQL_TLS_SUPPORT Attempt to configure TLS: %1 -This informational message is printed when TLS support was required in -the Kea configuration: The TLS support in PostgreSQL will be initialized but -its configuration is fully managed outside the C API. -The parameters of the connection are logged. - % LEGAL_LOG_STORE_CLOSED Legal store closed: %1 This is an informational message issued when the Legal Log library has successfully closed the legal store. @@ -194,6 +61,11 @@ should not affect the store content or subsequent legal store operations. This is an informational message issued when the Legal Log library has successfully opened the legal store. +% LEGAL_LOG_STORE_OPEN opening Legal Log file: %1 +This informational message is logged when a DHCP server (either V4 or +V6) is about to open a legal log file. The parameters of +the backend are logged. + % LEGAL_LOG_UNLOAD_ERROR An error occurred unloading the library: %1 This is an error message issued when an error occurs while unloading the Legal Log library. This is unlikely to occur and normal operations of the diff --git a/src/hooks/dhcp/forensic_log/libloadtests/Makefile.am b/src/hooks/dhcp/forensic_log/libloadtests/Makefile.am index 725ae7b5a2..3aa0e71bd5 100644 --- a/src/hooks/dhcp/forensic_log/libloadtests/Makefile.am +++ b/src/hooks/dhcp/forensic_log/libloadtests/Makefile.am @@ -1,12 +1,21 @@ SUBDIRS = . AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += -I$(top_builddir)/src -I$(top_srcdir)/src AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/forensic_log -I$(top_srcdir)/src/hooks/dhcp/forensic_log AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/hooks/dhcp/forensic_log/libloadtests\" AM_CPPFLAGS += -DLEGAL_LOG_LIB_SO=\"$(abs_top_builddir)/src/hooks/dhcp/forensic_log/.libs/libdhcp_legal_log.so\" AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" +if HAVE_MYSQL +AM_CPPFLAGS += $(MYSQL_CPPFLAGS) +endif + +if HAVE_PGSQL +AM_CPPFLAGS += $(PGSQL_CPPFLAGS) +endif + AM_CXXFLAGS = $(KEA_CXXFLAGS) # Some versions of GCC warn about some versions of Boost regarding @@ -39,7 +48,17 @@ libdhcp_legal_log_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_L libdhcp_legal_log_unittests_CXXFLAGS = $(AM_CXXFLAGS) -libdhcp_legal_log_unittests_LDADD = $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la +libdhcp_legal_log_unittests_LDADD = + +if HAVE_PGSQL +libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/hooks/dhcp/pgsql/libpgsql.la +endif + +if HAVE_MYSQL +libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/hooks/dhcp/mysql/libmysql.la +endif + +libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la diff --git a/src/hooks/dhcp/forensic_log/libloadtests/load_unload_unittests.cc b/src/hooks/dhcp/forensic_log/libloadtests/load_unload_unittests.cc index 2f6f50c23f..6cd980c16e 100644 --- a/src/hooks/dhcp/forensic_log/libloadtests/load_unload_unittests.cc +++ b/src/hooks/dhcp/forensic_log/libloadtests/load_unload_unittests.cc @@ -17,16 +17,17 @@ #include #include #include +#include #ifdef HAVE_MYSQL #include +#include #endif #ifdef HAVE_PGSQL #include +#include #endif -#include - #include #include @@ -37,6 +38,7 @@ using namespace std; using namespace isc; +using namespace isc::dhcp; using namespace hooks; namespace { @@ -262,6 +264,9 @@ public: // If data wipe enabled, delete transient data otherwise destroy the schema isc::db::test::destroyPgSQLSchema(); } + + /// @brief Initializer. + PgSqlForensicBackendInit init_; }; // Verifies that the load callout with correct PostgreSQL setup instantiates. @@ -317,6 +322,9 @@ public: // If data wipe enabled, delete transient data otherwise destroy the schema isc::db::test::destroyMySQLSchema(); } + + /// @brief Initializer. + MySqlForensicBackendInit init_; }; // Verifies that the load callout with correct MySQL setup instantiates. diff --git a/src/hooks/dhcp/forensic_log/libloadtests/meson.build b/src/hooks/dhcp/forensic_log/libloadtests/meson.build index 6833f0434e..a031acbbbc 100644 --- a/src/hooks/dhcp/forensic_log/libloadtests/meson.build +++ b/src/hooks/dhcp/forensic_log/libloadtests/meson.build @@ -8,6 +8,19 @@ dhcp_forensic_log_lltests_libs = [ kea_pgsql_testutils_lib, kea_database_testutils_lib, ] + +dependencies = [gtest, crypto] + +if mysql.found() + dependencies += [mysql] + dhcp_forensic_log_lltests_libs += [kea_mysql_testutils_lib] +endif + +if postgresql.found() + dependencies += [postgresql] + dhcp_forensic_log_lltests_libs += [kea_pgsql_testutils_lib] +endif + dhcp_forensic_log_libloadtests = executable( 'dhcp-forensic-log-libload-tests', 'load_unload_unittests.cc', @@ -16,10 +29,11 @@ dhcp_forensic_log_libloadtests = executable( f'-DTEST_DATA_BUILDDIR="@current_build_dir@"', f'-DLEGAL_LOG_LIB_SO="@TOP_BUILD_DIR@/src/hooks/dhcp/forensic_log/libdhcp_legal_log.so"', ], - dependencies: [gtest, crypto], - include_directories: [include_directories('.'), include_directories('..')] + INCLUDES, + dependencies: dependencies, + include_directories: [include_directories('.'), include_directories('..'), include_directories('../..')] + INCLUDES, link_with: [dhcp_forensic_log_lltests_libs] + LIBS_BUILT_SO_FAR, ) + test( 'dhcp-forensic-log-libloadtests', dhcp_forensic_log_libloadtests, diff --git a/src/hooks/dhcp/forensic_log/load_unload.cc b/src/hooks/dhcp/forensic_log/load_unload.cc index 01b071ecce..6e49e1ac82 100644 --- a/src/hooks/dhcp/forensic_log/load_unload.cc +++ b/src/hooks/dhcp/forensic_log/load_unload.cc @@ -16,7 +16,8 @@ #include #include #include -#include +#include +#include #include @@ -48,7 +49,7 @@ int load(LibraryHandle& handle) { try { // Make the hook library not loadable by d2 or ca. uint16_t family = CfgMgr::instance().getFamily(); - const std::string& proc_name = Daemon::getProcName(); + const string& proc_name = Daemon::getProcName(); if (family == AF_INET) { if (proc_name != "kea-dhcp4") { isc_throw(isc::Unexpected, "Bad process name: " << proc_name @@ -61,26 +62,19 @@ int load(LibraryHandle& handle) { } } + BackendStoreFactory::registerBackendFactory("logfile", RotatingFile::factory); + // Get and decode parameters. ConstElementPtr const& parameters(handle.getParameters()); - - if (!parameters || !parameters->get("type") || - parameters->get("type")->stringValue() == "logfile") { - BackendStore::parseFile(parameters); - } else { - BackendStore::parseDatabase(parameters); - } + DatabaseConnection::ParameterMap map; try { - BackendStore::parseExtraParameters(parameters); - - BackendStore::setConfig(parameters); - - BackendStore::instance()->open(); + BackendStore::parseConfig(parameters, map); + BackendStoreFactory::addBackend(map); } catch (const isc::db::DbOpenErrorWithRetry& err) { - std::string redacted; + string redacted; try { - redacted = DatabaseConnection::redactedAccessString(BackendStore::getParameters()); + redacted = DatabaseConnection::redactedAccessString(map); } catch (...) { } LOG_INFO(legal_log_logger, LEGAL_LOG_DB_OPEN_CONNECTION_WITH_RETRY_FAILED) @@ -108,13 +102,9 @@ int unload() { // Since it's "global" Let's explicitly destroy it now rather // than indeterminately. Note, BackendStore destructor will close // the store. - BackendStore::instance().reset(); - IOServicePtr io_service = BackendStore::getIOService(); - if (io_service) { - IOServiceMgr::instance().unregisterIOService(io_service); - io_service->stopAndPoll(); - BackendStore::setIOService(IOServicePtr()); - } + BackendStoreFactory::instance().reset(); + + BackendStoreFactory::unregisterBackendFactory("logfile"); } catch (const std::exception& ex) { // On the off chance something goes awry, catch it and log it. // @todo Not sure if we should return a non-zero result or not. @@ -128,40 +118,14 @@ int unload() { /// @brief dhcp4_srv_configured callout implementation. /// /// @param handle callout handle. -int dhcp4_srv_configured(CalloutHandle& handle) { - try { - BackendStore::setIOService(IOServicePtr(new IOService())); - IOServiceMgr::instance().registerIOService(BackendStore::getIOService()); - } catch (const std::exception& ex) { - LOG_ERROR(legal_log_logger, LEGAL_LOG_LOAD_ERROR) - .arg(ex.what()); - handle.setStatus(isc::hooks::CalloutHandle::NEXT_STEP_DROP); - ostringstream os; - os << "Error: " << ex.what(); - string error(os.str()); - handle.setArgument("error", error); - return (1); - } +int dhcp4_srv_configured(CalloutHandle& /* handle */) { return (0); } /// @brief dhcp6_srv_configured callout implementation. /// /// @param handle callout handle. -int dhcp6_srv_configured(CalloutHandle& handle) { - try { - BackendStore::setIOService(IOServicePtr(new IOService())); - IOServiceMgr::instance().registerIOService(BackendStore::getIOService()); - } catch (const std::exception& ex) { - LOG_ERROR(legal_log_logger, LEGAL_LOG_LOAD_ERROR) - .arg(ex.what()); - handle.setStatus(isc::hooks::CalloutHandle::NEXT_STEP_DROP); - ostringstream os; - os << "Error: " << ex.what(); - string error(os.str()); - handle.setArgument("error", error); - return (1); - } +int dhcp6_srv_configured(CalloutHandle& /* handle */) { return (0); } diff --git a/src/hooks/dhcp/forensic_log/meson.build b/src/hooks/dhcp/forensic_log/meson.build index c3751c3f0a..f7fd9ad909 100644 --- a/src/hooks/dhcp/forensic_log/meson.build +++ b/src/hooks/dhcp/forensic_log/meson.build @@ -6,16 +6,12 @@ endif dhcp_forensic_log_lib = shared_library( 'dhcp_legal_log', - 'backend_store.cc', 'command_callouts.cc', 'lease4_callouts.cc', 'lease6_callouts.cc', - 'legal_log_db_log.cc', 'legal_log_log.cc', 'legal_log_messages.cc', 'load_unload.cc', - 'mysql_legal_log.cc', - 'pgsql_legal_log.cc', 'rotating_file.cc', 'version.cc', cpp_args: [f'-DLEGAL_LOG_DIR="@LEGAL_LOG_DIR@"'], diff --git a/src/hooks/dhcp/forensic_log/rotating_file.cc b/src/hooks/dhcp/forensic_log/rotating_file.cc index fa7107f519..2ba9a2862b 100644 --- a/src/hooks/dhcp/forensic_log/rotating_file.cc +++ b/src/hooks/dhcp/forensic_log/rotating_file.cc @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -23,19 +22,76 @@ using namespace isc::asiolink; using namespace isc::util; +using namespace isc::dhcp; +using namespace isc::data; +using namespace isc::db; +using namespace std; namespace isc { namespace legal_log { +RotatingFile::RotatingFile(const DatabaseConnection::ParameterMap& parameters) + : BackendStore(parameters), time_unit_(TimeUnit::Day), count_(1), timestamp_(0) { + apply(parameters); +} -RotatingFile::RotatingFile(const std::string& path, - const std::string& base_name, - const TimeUnit unit, - const uint32_t count, - const std::string& prerotate, - const std::string& postrotate) - : path_(path), base_name_(base_name), time_unit_(unit), count_(count), - file_name_(), file_(), prerotate_(prerotate), postrotate_(postrotate), - timestamp_(0), mutex_() { +void +RotatingFile::apply(const DatabaseConnection::ParameterMap& parameters) { + string path(LEGAL_LOG_DIR); + string base("kea-legal"); + RotatingFile::TimeUnit unit(RotatingFile::TimeUnit::Day); + int64_t count(1); + string count_str; + string prerotate; + string postrotate; + + // Prioritize parameters. + if (parameters.find("path") != parameters.end()) { + path = parameters.at("path"); + } + if (parameters.find("base-name") != parameters.end()) { + base = parameters.at("base-name"); + } + if (parameters.find("time-unit") != parameters.end()) { + string time_unit(parameters.at("time-unit")); + + if (time_unit == "second") { + unit = RotatingFile::TimeUnit::Second; + } else if (time_unit == "day") { + unit = RotatingFile::TimeUnit::Day; + } else if (time_unit == "month") { + unit = RotatingFile::TimeUnit::Month; + } else if (time_unit == "year") { + unit = RotatingFile::TimeUnit::Year; + } else { + isc_throw(BadValue, "unknown time unit type: " << time_unit + << ", expected one of: second, day, month, year"); + } + } + if (parameters.find("count") != parameters.end()) { + try { + count = boost::lexical_cast(parameters.at("count")); + } catch (...) { + isc_throw(BadValue, "bad value: " << parameters.at("count") << " for count parameter"); + } + if ((count < 0) || + (count > numeric_limits::max())) { + isc_throw(OutOfRange, "count value: " << count + << " is out of range, expected value: 0.." + << numeric_limits::max()); + } + } + if (parameters.find("prerotate") != parameters.end()) { + prerotate = parameters.at("prerotate"); + } + if (parameters.find("postrotate") != parameters.end()) { + postrotate = parameters.at("postrotate"); + } + path_ = path; + base_name_ = base; + time_unit_ = unit; + count_ = static_cast(count); + prerotate_ = prerotate; + postrotate_ = postrotate; if (path_.empty()) { isc_throw(BackendStoreError, "path cannot be blank"); @@ -45,17 +101,17 @@ RotatingFile::RotatingFile(const std::string& path, isc_throw(BackendStoreError, "file name cannot be blank"); } - if (!prerotate.empty()) { + if (!prerotate_.empty()) { try { - ProcessSpawn process(ProcessSpawn::ASYNC, prerotate); + ProcessSpawn process(ProcessSpawn::ASYNC, prerotate_); } catch (const isc::Exception& ex) { isc_throw(BackendStoreError, "Invalid 'prerotate' parameter: " << ex.what()); } } - if (!postrotate.empty()) { + if (!postrotate_.empty()) { try { - ProcessSpawn process(ProcessSpawn::ASYNC, postrotate); + ProcessSpawn process(ProcessSpawn::ASYNC, postrotate_); } catch (const isc::Exception& ex) { isc_throw(BackendStoreError, "Invalid 'postrotate' parameter: " << ex.what()); } @@ -66,24 +122,24 @@ RotatingFile::~RotatingFile() { close(); }; -std::string +string RotatingFile::getYearMonthDay(const struct tm& time_info) { char buffer[128]; strftime(buffer, sizeof(buffer), "%Y%m%d", &time_info); - return (std::string(buffer)); + return (string(buffer)); } void RotatingFile::updateFileNameAndTimestamp(struct tm& time_info, bool use_existing) { - std::ostringstream stream; - std::string name = base_name_ + "."; + ostringstream stream; + string name = base_name_ + "."; stream << path_ << "/"; if (time_unit_ == TimeUnit::Second) { time_t timestamp = mktime(&time_info); - std::ostringstream name_stream; - name_stream << std::right << std::setfill('0') << std::setw(20) + ostringstream name_stream; + name_stream << right << setfill('0') << setw(20) << static_cast(timestamp); name += "T"; name += name_stream.str(); @@ -107,14 +163,14 @@ RotatingFile::useExistingFiles(struct tm& time_info) { return; } - std::unique_ptr defer(dir, [](DIR* d) { closedir(d); }); + unique_ptr defer(dir, [](DIR* d) { closedir(d); }); // Set of sorted files by name. - std::set files; + set files; // Add only files of interest that could be used to append logging data. for (struct dirent* dent = readdir(dir); dent; dent = readdir(dir)) { - std::string name(dent->d_name); + string name(dent->d_name); // Supported file formats are: 'base-name.YYYYMMDD.txt' and // 'base-name.TXXXXXXXXXXXXXXXXXXXX.txt'. if ((name.size() != (base_name_.size() + sizeof(".YYYYMMDD.txt") - 1)) && @@ -127,7 +183,7 @@ RotatingFile::useExistingFiles(struct tm& time_info) { continue; } - std::string file = name.substr(0, name.size() - 4); + string file = name.substr(0, name.size() - 4); // Skip files that are not beginning with base name. if (base_name_ != file.substr(0, base_name_.size())) { @@ -148,7 +204,7 @@ RotatingFile::useExistingFiles(struct tm& time_info) { continue; } for (; index < tag_size; ++index) { - if (!std::isdigit(file.at(index))) { + if (!isdigit(file.at(index))) { break; } } @@ -162,7 +218,7 @@ RotatingFile::useExistingFiles(struct tm& time_info) { return; } - std::string file = *files.rbegin(); + string file = *files.rbegin(); if (time_unit_ == TimeUnit::Second) { time_t file_timestamp; @@ -227,7 +283,7 @@ void RotatingFile::openInternal(struct tm& time_info, bool use_existing) { updateFileNameAndTimestamp(time_info, use_existing); // Open the file - file_.open(file_name_.c_str(), std::ofstream::app); + file_.open(file_name_.c_str(), ofstream::app); int sav_error = errno; if (!file_.is_open()) { isc_throw(BackendStoreError, "cannot open file:" << file_name_ @@ -310,9 +366,9 @@ RotatingFile::rotate() { } void -RotatingFile::writeln(const std::string& text, const std::string&) { +RotatingFile::writeln(const string& text, const string&) { if (util::MultiThreadingMgr::instance().getMode()) { - std::lock_guard lock(mutex_); + lock_guard lock(mutex_); writelnInternal(text); } else { writelnInternal(text); @@ -320,7 +376,7 @@ RotatingFile::writeln(const std::string& text, const std::string&) { } void -RotatingFile::writelnInternal(const std::string& text) { +RotatingFile::writelnInternal(const string& text) { if (text.empty()) { return; } @@ -328,10 +384,10 @@ RotatingFile::writelnInternal(const std::string& text) { // Call rotate in case we've crossed days since we last wrote. rotate(); - std::string timestamp = getNowString(); - std::stringstream ss(text); - for (std::string line; std::getline(ss, line, '\n');) { - file_ << timestamp << " " << line << std::endl; + string timestamp = getNowString(); + stringstream ss(text); + for (string line; getline(ss, line, '\n');) { + file_ << timestamp << " " << line << endl; } int sav_error = errno; if (!file_.good()) { @@ -354,7 +410,7 @@ RotatingFile::close() { file_.flush(); file_.close(); } - } catch (const std::exception& ex) { + } catch (const exception& ex) { // Highly unlikely to occur but let's at least spit out an error. // Beyond that we swallow it for tidiness. LOG_ERROR(legal_log_logger, LEGAL_LOG_STORE_CLOSE_ERROR) @@ -362,5 +418,12 @@ RotatingFile::close() { } } +BackendStorePtr +RotatingFile::factory(const DatabaseConnection::ParameterMap& parameters) { + LOG_INFO(legal_log_logger, LEGAL_LOG_STORE_OPEN) + .arg(DatabaseConnection::redactedAccessString(parameters)); + return (BackendStorePtr(new RotatingFile(parameters))); +} + } // namespace legal_log } // namespace isc diff --git a/src/hooks/dhcp/forensic_log/rotating_file.h b/src/hooks/dhcp/forensic_log/rotating_file.h index f76b2bc174..62fddf0b30 100644 --- a/src/hooks/dhcp/forensic_log/rotating_file.h +++ b/src/hooks/dhcp/forensic_log/rotating_file.h @@ -7,6 +7,8 @@ #ifndef ROTATING_FILE_H #define ROTATING_FILE_H +#include + #include /// @file rotating_file.h Defines the class, RotatingFile, which implements @@ -45,7 +47,7 @@ namespace legal_log { /// /// The class implements virtual methods in facilitate unit testing /// and derives from @c BackendStore abstract class. -class RotatingFile : public BackendStore { +class RotatingFile : public isc::dhcp::BackendStore { public: /// @brief Time unit type used to rotate file. @@ -74,18 +76,35 @@ public: /// @param postrotate The script to be run after opening the new file. /// /// @throw BackendStoreError if given file name is empty. - RotatingFile(const std::string& path, - const std::string& base_name, - const TimeUnit unit = TimeUnit::Day, - const uint32_t count = 1, - const std::string& prerotate = "", - const std::string& postrotate = ""); + RotatingFile(const isc::db::DatabaseConnection::ParameterMap& parameters); /// @brief Destructor. /// /// The destructor does call the close method. virtual ~RotatingFile(); + /// @brief Parse file specification and create forensic store backed. + /// + /// It supports the following parameters via the Hook Library Parameter + /// mechanism: + /// + /// @b path - Directory in which the legal file(s) will be written. + /// The default value is "/var/lib/kea". The directory must exist. + /// + /// @b base-name - An arbitrary value which is used in conjunction + /// with current system date to form the current legal file name. + /// It defaults to "kea-legal". + /// + /// Legal file names will have the form: + /// + /// /..txt + /// /..txt + /// + /// @param parameters The library parameters. + /// + /// @return The RotatingFile forensic store backend. + void apply(const isc::db::DatabaseConnection::ParameterMap& parameters); + /// @brief Opens the current file for writing. /// /// Forms the current file name if using seconds as time units: @@ -150,6 +169,20 @@ public: return (file_name_); } + /// @brief Returns the file base name. + /// + /// @return The file base name. + std::string getBaseName() const { + return (base_name_); + } + + /// @brief Returns the file path. + /// + /// @return The file path. + std::string getPath() const { + return (path_); + } + /// @brief Build the year-month-day string from a date. /// /// @param time_info The time info to be converted to string. @@ -232,6 +265,29 @@ private: /// @brief Mutex to protect output. std::mutex mutex_; + +public: + /// @brief Factory class method. + /// + /// @param parameters A data structure relating keywords and values + /// concerned with the database. + /// + /// @return The Rotating File Store Backend. + static isc::dhcp::BackendStorePtr + factory(const isc::db::DatabaseConnection::ParameterMap& parameters); +}; + +/// @brief Initialization structure used to register and deregister RotateFile Forensic Store. +struct RotatingFileInit { + // Constructor registers + RotatingFileInit() { + isc::dhcp::BackendStoreFactory::registerBackendFactory("logfile", RotatingFile::factory); + } + + // Destructor deregisters + ~RotatingFileInit() { + isc::dhcp::BackendStoreFactory::unregisterBackendFactory("logfile"); + } }; } // namespace legal_log diff --git a/src/hooks/dhcp/forensic_log/tests/Makefile.am b/src/hooks/dhcp/forensic_log/tests/Makefile.am index f5b6dfc282..aea42eb7b8 100644 --- a/src/hooks/dhcp/forensic_log/tests/Makefile.am +++ b/src/hooks/dhcp/forensic_log/tests/Makefile.am @@ -12,15 +12,6 @@ AM_CPPFLAGS += -DFORENSIC_POSTROTATE_TEST_SH=\"$(abs_top_builddir)/src/hooks/dhc AM_CPPFLAGS += -DINVALID_FORENSIC_POSTROTATE_TEST_SH=\"$(abs_top_srcdir)/README\" AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" AM_CPPFLAGS += -DLEGAL_LOG_DIR="\"$(legal_log_dir)\"" -TEST_CA_DIR = $(top_srcdir)/src/lib/asiolink/testutils/ca -AM_CPPFLAGS += -DTEST_CA_DIR=\"$(TEST_CA_DIR)\" -if HAVE_MYSQL -AM_CPPFLAGS += $(MYSQL_CPPFLAGS) -endif -if HAVE_PGSQL -AM_CPPFLAGS += $(PGSQL_CPPFLAGS) -endif - AM_CXXFLAGS = $(KEA_CXXFLAGS) # Some versions of GCC warn about some versions of Boost regarding @@ -52,13 +43,6 @@ libdhcp_legal_log_unittests_SOURCES += legal_log6_unittests.cc libdhcp_legal_log_unittests_SOURCES += rotating_file_unittests.cc libdhcp_legal_log_unittests_SOURCES += test_utils.h -if HAVE_MYSQL -libdhcp_legal_log_unittests_SOURCES += mysql_unittests.cc -endif -if HAVE_PGSQL -libdhcp_legal_log_unittests_SOURCES += pgsql_unittests.cc -endif - libdhcp_legal_log_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES) libdhcp_legal_log_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) @@ -73,17 +57,6 @@ libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la - -if HAVE_PGSQL -libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/pgsql/testutils/libpgsqltest.la -libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/pgsql/libkea-pgsql.la -endif - -if HAVE_MYSQL -libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/mysql/testutils/libmysqltest.la -libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/mysql/libkea-mysql.la -endif - libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/database/testutils/libdatabasetest.la libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la libdhcp_legal_log_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la @@ -96,13 +69,6 @@ libdhcp_legal_log_unittests_LDADD += $(LOG4CPLUS_LIBS) libdhcp_legal_log_unittests_LDADD += $(CRYPTO_LIBS) libdhcp_legal_log_unittests_LDADD += $(BOOST_LIBS) libdhcp_legal_log_unittests_LDADD += $(GTEST_LDADD) - -if HAVE_MYSQL -libdhcp_legal_log_unittests_LDADD += $(MYSQL_LIBS) -endif -if HAVE_PGSQL -libdhcp_legal_log_unittests_LDADD += $(PGSQL_LIBS) -endif endif noinst_PROGRAMS = $(TESTS) diff --git a/src/hooks/dhcp/forensic_log/tests/backend_store_unittests.cc b/src/hooks/dhcp/forensic_log/tests/backend_store_unittests.cc index b089144809..0e50c4c9d0 100644 --- a/src/hooks/dhcp/forensic_log/tests/backend_store_unittests.cc +++ b/src/hooks/dhcp/forensic_log/tests/backend_store_unittests.cc @@ -11,9 +11,11 @@ #include #include -#include #include -#include +#include +#include +#include +#include #include @@ -22,17 +24,50 @@ using namespace isc; using namespace isc::data; using namespace isc::db; +using namespace isc::dhcp; using namespace isc::legal_log; +using namespace std; namespace { +const DbLogger::MessageMap legal_log_db_message_map = { +}; + +DbLogger legal_log_db_logger(legal_log_logger, legal_log_db_message_map); + /// @brief Test fixture struct BackendStoreTest : ::testing::Test { - /// @brief Called before each test - void SetUp() final override { + /// @brief Destructor. + virtual ~BackendStoreTest() = default; + + /// @brief Called before each test. + virtual void SetUp() final override { // Clean up from past tests. - BackendStore::instance().reset(); + BackendStoreFactory::delAllBackends(); } + + /// @brief Called after each test. + virtual void TearDown() { + // Clean up from past tests. + BackendStoreFactory::delAllBackends(); + reset(); + } + + /// @brief Removes files that may be left over from previous tests + void reset() { + std::ostringstream stream; + stream << "rm " << TEST_DATA_BUILDDIR << "/" << "*-legal" + << ".*.txt 2>/dev/null"; + int result = ::system(stream.str().c_str()); + if (result != 0) { + // Ignore the result, because it may well be non-zero value when + // files to be removed don't exist. + ; + } + } + + /// @brief Initializer. + RotatingFileInit init_; }; // Verifies output of genDurationString() @@ -60,7 +95,7 @@ TEST_F(BackendStoreTest, legalLogDbLogger) { EXPECT_EQ(1, db_logger_stack.size()); // Push local logger - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(legal_log_db_logger); // Check now we have a second logger EXPECT_EQ(2, db_logger_stack.size()); @@ -72,7 +107,7 @@ TEST_F(BackendStoreTest, legalLogDbLogger) { // Open a try block to check it works with it try { EXPECT_EQ(1, db_logger_stack.size()); - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(legal_log_db_logger); EXPECT_EQ(2, db_logger_stack.size()); } catch (const std::exception&) { ADD_FAILURE() << "no exception was raised"; @@ -82,7 +117,7 @@ TEST_F(BackendStoreTest, legalLogDbLogger) { // Another check with an exception now try { EXPECT_EQ(1, db_logger_stack.size()); - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(legal_log_db_logger); EXPECT_EQ(2, db_logger_stack.size()); isc_throw(Unexpected, "for testing"); ADD_FAILURE() << "an exception was raised"; @@ -102,86 +137,105 @@ TEST_F(BackendStoreTest, emptyVectorDump) { // Verify that parsing extra parameters for rotate file works TEST_F(BackendStoreTest, parseExtraRotatingFileParameters) { + isc::db::DatabaseConnection::ParameterMap map; + EXPECT_NO_THROW(BackendStore::parseConfig(ConstElementPtr(), map)); + map["path"] = TEST_DATA_BUILDDIR; + EXPECT_NO_THROW(BackendStoreFactory::addBackend(map)); + EXPECT_TRUE(BackendStoreFactory::instance()); + RotatingFile& rotating_file = dynamic_cast(*BackendStoreFactory::instance()); + ElementPtr params = Element::createMap(); params->set("path", Element::create("path")); params->set("base-name", Element::create("name")); params->set("time-unit", Element::create(0)); - EXPECT_THROW(BackendStore::parseFile(params), TypeError); - EXPECT_FALSE(BackendStore::instance()); + EXPECT_THROW(BackendStore::parseFile(params, map), TypeError); params->set("time-unit", Element::create("nothing")); - EXPECT_THROW(BackendStore::parseFile(params), BadValue); - EXPECT_FALSE(BackendStore::instance()); + EXPECT_NO_THROW(BackendStore::parseFile(params, map)); + EXPECT_THROW(rotating_file.apply(map), BadValue); params->set("time-unit", Element::create("second")); params->set("count", Element::create("")); - EXPECT_THROW(BackendStore::parseFile(params), TypeError); - EXPECT_FALSE(BackendStore::instance()); + EXPECT_THROW(BackendStore::parseFile(params, map), TypeError); params->set("time-unit", Element::create("day")); params->set("count", Element::create(-1)); - EXPECT_THROW(BackendStore::parseFile(params), OutOfRange); - EXPECT_FALSE(BackendStore::instance()); + EXPECT_THROW(BackendStore::parseFile(params, map), OutOfRange); params->set("time-unit", Element::create("month")); params->set("count", Element::create(static_cast(1) << 32)); - EXPECT_THROW(BackendStore::parseFile(params), OutOfRange); - EXPECT_FALSE(BackendStore::instance()); + EXPECT_THROW(BackendStore::parseFile(params, map), OutOfRange); params->set("time-unit", Element::create("year")); params->set("count", Element::create(1)); params->set("prerotate", Element::create(FORENSIC_PREROTATE_TEST_SH)); params->set("postrotate", Element::create(FORENSIC_POSTROTATE_TEST_SH)); - EXPECT_NO_THROW(BackendStore::parseFile(params)); - EXPECT_TRUE(BackendStore::instance()); + EXPECT_NO_THROW(BackendStore::parseFile(params, map)); + EXPECT_NO_THROW(rotating_file.apply(map)); } // Verify that parsing extra parameters works TEST_F(BackendStoreTest, parseExtraParameters) { - ASSERT_NO_THROW(BackendStore::instance().reset(new RotatingFile("path", "name"))); + db::DatabaseConnection::ParameterMap map; + map["path"] = "path"; + map["base-name"] = "name"; + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new RotatingFile(map))); ElementPtr params = Element::createMap(); params->set("request-parser-format", Element::create("'request'")); params->set("response-parser-format", Element::create("'response'")); params->set("timestamp-format", Element::create("timestamp")); - EXPECT_NO_THROW(BackendStore::parseExtraParameters(params)); - auto request_format = BackendStore::instance()->getRequestFormatExpression(); + map.clear(); + map["type"] = "logfile"; + map["path"] = TEST_DATA_BUILDDIR; + EXPECT_NO_THROW(BackendStore::parseExtraParameters(params, map)); + EXPECT_NO_THROW(BackendStoreFactory::addBackend(map)); + EXPECT_TRUE(BackendStoreFactory::instance()); + + auto request_format = BackendStoreFactory::instance()->getRequestFormatExpression(); EXPECT_TRUE(request_format); - auto response_format = BackendStore::instance()->getResponseFormatExpression(); + auto response_format = BackendStoreFactory::instance()->getResponseFormatExpression(); EXPECT_TRUE(response_format); EXPECT_NE(request_format, response_format); - auto timestamp_format = BackendStore::instance()->getTimestampFormat(); + auto timestamp_format = BackendStoreFactory::instance()->getTimestampFormat(); EXPECT_EQ(timestamp_format, "timestamp"); } TEST_F(BackendStoreTest, fileNoParameters) { - EXPECT_NO_THROW(BackendStore::parseFile(ConstElementPtr())); - EXPECT_TRUE(BackendStore::instance()); - EXPECT_EQ(BackendStore::getParameters()["path"], LEGAL_LOG_DIR); + db::DatabaseConnection::ParameterMap map; + EXPECT_NO_THROW(BackendStore::parseFile(ConstElementPtr(), map)); + map["path"] = TEST_DATA_BUILDDIR; + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new RotatingFile(map))); + ASSERT_NO_THROW(BackendStoreFactory::instance()->open()); + RotatingFile& rotating_file = dynamic_cast(*BackendStoreFactory::instance()); + map.clear(); + rotating_file.apply(map); + EXPECT_EQ(rotating_file.getPath(), LEGAL_LOG_DIR); + EXPECT_EQ(rotating_file.getBaseName(), "kea-legal"); } TEST_F(BackendStoreTest, databaseNoParameters) { - EXPECT_THROW(BackendStore::parseDatabase(ConstElementPtr()), BadValue); - EXPECT_FALSE(BackendStore::instance()); + db::DatabaseConnection::ParameterMap map; + EXPECT_THROW(BackendStore::parseDatabase(ConstElementPtr(), map), BadValue); } TEST_F(BackendStoreTest, wrongDatabaseType) { + db::DatabaseConnection::ParameterMap map; ElementPtr parameters = Element::createMap(); parameters->set("type", Element::create("")); - EXPECT_THROW_MSG(BackendStore::parseDatabase(parameters), InvalidType, - "Database access parameter 'type' does not specify a " - "supported database backend: "); - EXPECT_FALSE(BackendStore::instance()); + EXPECT_NO_THROW(BackendStore::parseDatabase(parameters, map)); + EXPECT_THROW_MSG(BackendStoreFactory::addBackend(map), InvalidType, + "The type of the forensic store backend: '' is not supported"); + EXPECT_FALSE(BackendStoreFactory::instance()); parameters->set("type", Element::create("awesomesql")); - EXPECT_THROW_MSG(BackendStore::parseDatabase(parameters), InvalidType, - "Database access parameter 'type' does not specify a " - "supported database backend: awesomesql"); - EXPECT_FALSE(BackendStore::instance()); + EXPECT_NO_THROW(BackendStore::parseDatabase(parameters, map)); + EXPECT_THROW_MSG(BackendStoreFactory::addBackend(map), InvalidType, + "The type of the forensic store backend: 'awesomesql' is not supported"); } } // end of anonymous namespace diff --git a/src/hooks/dhcp/forensic_log/tests/command_log_unittests.cc b/src/hooks/dhcp/forensic_log/tests/command_log_unittests.cc index 772a818633..da21bfd64a 100644 --- a/src/hooks/dhcp/forensic_log/tests/command_log_unittests.cc +++ b/src/hooks/dhcp/forensic_log/tests/command_log_unittests.cc @@ -8,7 +8,7 @@ /// generation and callout: command_processed. /// These tests assume the legal log library is linked in, not loaded. /// This allows a great deal more flexibility in testing, such as overriding -/// and accessing the BackendStore::instance(). +/// and accessing the BackendStoreFactory::instance(). /// The load and unload callouts are exercised in ../libloadtests, which /// actually uses the HooksManager to load and unload the library. @@ -557,7 +557,6 @@ TEST(CommandCalloutFuncs, getOptionalStringTest) { ASSERT_NO_THROW(ret = getOptionalString(arguments, "num", value)); EXPECT_FALSE(ret); EXPECT_EQ("", value); - } // Exercises the getOptionInt() function @@ -582,7 +581,6 @@ TEST(CommandCalloutFuncs, getOptionalIntTest) { ASSERT_NO_THROW(ret = getOptionalInt(arguments, "not-there", value)); EXPECT_FALSE(ret); EXPECT_EQ(0, value); - } // Exercises the isPrefix() function @@ -613,7 +611,7 @@ TEST(CommandCalloutFuncs, isPrefixTest) { // Exercises the addDuration() function TEST_F(CalloutTest, addDurationTest) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Should generate duration text of 1 day based on valid-lft ConstElementPtr arguments; @@ -631,7 +629,7 @@ TEST_F(CalloutTest, addDurationTest) { EXPECT_EQ(os.str(), ""); // Should have a duration of 2 days based on expire timestamp. - int64_t expire = BackendStore::instance()->now().tv_sec + 172800; + int64_t expire = BackendStoreFactory::instance()->now().tv_sec + 172800; os.str(""); os << "{ \"expire\":" << expire << "}"; ASSERT_NO_THROW(arguments = Element::fromJSON(os.str())); @@ -643,7 +641,7 @@ TEST_F(CalloutTest, addDurationTest) { // Exercises the addContext() function TEST_F(CalloutTest, addContext) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Should not generate user context text. string args = "{ \"some-other\": 86400 }"; @@ -679,7 +677,7 @@ TEST_F(CalloutTest, addContext) { // Iterates over a list of valid command/argument combinations and verifies that // each produces the expected log in the log file TEST_F(CalloutTest, validCommandEntries) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandle handle(getCalloutManager()); @@ -717,17 +715,17 @@ TEST_F(CalloutTest, validCommandEntries) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. - string today_now_string = BackendStore::instance()->getNowString(); + string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Iterates over a list of valid command/argument combinations and verifies that // each produces the expected log in the log file TEST_F(CalloutTest, responseWithErrorsLease6BulkApplyCommandEntries) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandle handle(getCalloutManager()); @@ -828,17 +826,17 @@ TEST_F(CalloutTest, responseWithErrorsLease6BulkApplyCommandEntries) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. - string today_now_string = BackendStore::instance()->getNowString(); + string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // This test verifies that it is possible to disable logging for selected IPv4 // subnets. TEST_F(CalloutTest, disableLoggingForSubnet4) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Create a subnet with user context disabling legal logging. Subnet4Ptr subnet4(new Subnet4(IOAddress("192.0.2.0"), 24, 30, 40, 50, @@ -903,17 +901,17 @@ TEST_F(CalloutTest, disableLoggingForSubnet4) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. - string today_now_string = BackendStore::instance()->getNowString(); + string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // This test verifies that it is possible to disable logging for selected IPv6 // subnets. TEST_F(CalloutTest, disableLoggingForSubnet6) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Create a subnet with user context disabling legal logging. Subnet6Ptr subnet6(new Subnet6(IOAddress("2001:db8::"), 48, 30, 40, 50, 60, @@ -976,16 +974,16 @@ TEST_F(CalloutTest, disableLoggingForSubnet6) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. - string today_now_string = BackendStore::instance()->getNowString(); + string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Tests that a command with a failed result code does not generate a log entry TEST_F(CalloutTest, failedCommand) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandle handle(getCalloutManager()); @@ -1018,10 +1016,10 @@ TEST_F(CalloutTest, failedCommand) { ASSERT_EQ(0, ret); // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content has only the one expected line. - string today_now_string = BackendStore::instance()->getNowString(); + string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } diff --git a/src/hooks/dhcp/forensic_log/tests/legal_log4_unittests.cc b/src/hooks/dhcp/forensic_log/tests/legal_log4_unittests.cc index 89e5e7b949..7dd3cc5450 100644 --- a/src/hooks/dhcp/forensic_log/tests/legal_log4_unittests.cc +++ b/src/hooks/dhcp/forensic_log/tests/legal_log4_unittests.cc @@ -8,7 +8,7 @@ /// generation as well as tests which exercise v4 callouts: leases4_committed /// and pkt4_send. These tests assume the legal log library /// is linked in, not loaded. This allows a great deal more flexibility -/// in testing, such as overriding and accessing the BackendStore::instance(). +/// in testing, such as overriding and accessing the BackendStoreFactory::instance(). /// The load and unload callouts are exercised in ../libloadtests, which /// actually uses the HooksManager to load and unload the library. @@ -317,7 +317,7 @@ struct CalloutTestv4 : CalloutTest { // Verifies legal entry content for directly connected clients TEST_F(CalloutTestv4, directClient4) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); std::string entry; @@ -439,9 +439,9 @@ TEST_F(CalloutTestv4, directClient4) { // Verifies legal entry content for directly connected clients TEST_F(CalloutTestv4, directClient4CustomLoggingFormatRequestOnly) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); - BackendStore::instance()->setRequestFormatExpression(request_format_only_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_only_); std::string entry; @@ -561,10 +561,10 @@ TEST_F(CalloutTestv4, directClient4CustomLoggingFormatRequestOnly) { // Verifies legal entry content for directly connected clients TEST_F(CalloutTestv4, directClient4CustomLoggingFormatRequestAndResponse) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); - BackendStore::instance()->setRequestFormatExpression(request_format_); - BackendStore::instance()->setResponseFormatExpression(response_format_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_); + BackendStoreFactory::instance()->setResponseFormatExpression(response_format_); std::string entry; @@ -688,7 +688,7 @@ TEST_F(CalloutTestv4, directClient4CustomLoggingFormatRequestAndResponse) { // Verifies legal entry content for relayed clients // Checks with and without RAI and its suboptions TEST_F(CalloutTestv4, relayedClient4) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); std::string entry; @@ -786,9 +786,9 @@ TEST_F(CalloutTestv4, relayedClient4) { // Verifies legal entry content for relayed clients // Checks with and without RAI and its suboptions TEST_F(CalloutTestv4, relayedClient4CustomLoggingFormatRequestOnly) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); - BackendStore::instance()->setRequestFormatExpression(request_format_only_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_only_); std::string entry; @@ -881,10 +881,10 @@ TEST_F(CalloutTestv4, relayedClient4CustomLoggingFormatRequestOnly) { // Verifies legal entry content for relayed clients // Checks with and without RAI and its suboptions TEST_F(CalloutTestv4, relayedClient4CustomLoggingFormatRequestAndResponse) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); - BackendStore::instance()->setRequestFormatExpression(request_format_); - BackendStore::instance()->setResponseFormatExpression(response_format_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_); + BackendStoreFactory::instance()->setResponseFormatExpression(response_format_); std::string entry; @@ -979,7 +979,7 @@ TEST_F(CalloutTestv4, relayedClient4CustomLoggingFormatRequestAndResponse) { // Verifies printable items TEST_F(CalloutTestv4, printable4) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); request_->setGiaddr(isc::asiolink::IOAddress("192.2.16.33")); const uint8_t clientid[] = { 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72 }; @@ -1030,14 +1030,14 @@ TEST_F(CalloutTestv4, printable4) { entry); } -// Verifies that legalLog4Handler() detects a null BackendStore::instance() +// Verifies that legalLog4Handler() detects a null BackendStoreFactory::instance() TEST_F(CalloutTestv4, noRotatingFileTest4) { // Make a callout handle CalloutHandle handle(getCalloutManager()); handle.setArgument("lease4", lease_); handle.setArgument("query4", request_); - // The function should fail when there's no BackendStore::instance(). + // The function should fail when there's no BackendStoreFactory::instance(). int ret; ASSERT_NO_THROW(ret = legalLog4Handler(handle, Action::ASSIGN)); EXPECT_EQ(1, ret); @@ -1046,7 +1046,7 @@ TEST_F(CalloutTestv4, noRotatingFileTest4) { // Verifies that the pkt4_receive callout creates an empty context which can be // used in all other hook points. TEST_F(CalloutTestv4, pkt4_receive) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(discover_); @@ -1132,12 +1132,12 @@ TEST_F(CalloutTestv4, pkt4_receive) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. We should have no entry. std::vectorlines; - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileNotCreated(genName(today())); } @@ -1149,7 +1149,7 @@ TEST_F(CalloutTestv4, pkt4_receive) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv4, leases4_committed) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(request_); @@ -1241,7 +1241,7 @@ TEST_F(CalloutTestv4, leases4_committed) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. We should have two entries: // 192.2.1.111 and 192.2.3.122. @@ -1256,14 +1256,14 @@ TEST_F(CalloutTestv4, leases4_committed) { " to a device with hardware address:" " hwtype=1 08:00:2b:02:3f:4e"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Verifies that the lease4_renew callout generates the correct entry // in the legal file given a Pkt4 and Lease4 TEST_F(CalloutTestv4, lease4_renew) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(request_); @@ -1355,7 +1355,7 @@ TEST_F(CalloutTestv4, lease4_renew) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -1369,21 +1369,21 @@ TEST_F(CalloutTestv4, lease4_renew) { " to a device with hardware address:" " hwtype=1 08:00:2b:02:3f:4e"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Verifies that the lease4_renew callout generates the correct entry // in the legal file given a Pkt4 and Lease4 TEST_F(CalloutTestv4, customRequestLoggingFormat_lease4_renew) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(request_); std::string format = "ifelse(pkt4.msgtype == 5, concat('Assigned address: ', addrtotext(pkt4.yiaddr)), '')"; - BackendStore::instance()->setResponseFormatExpression(format); + BackendStoreFactory::instance()->setResponseFormatExpression(format); int ret; @@ -1413,20 +1413,20 @@ TEST_F(CalloutTestv4, customRequestLoggingFormat_lease4_renew) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("Assigned address: 192.2.1.100"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Verifies that the lease4_release callout generates the correct entry // in the legal file given a Pkt4 and Lease4 TEST_F(CalloutTestv4, lease4_release) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(release_); @@ -1485,7 +1485,7 @@ TEST_F(CalloutTestv4, lease4_release) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -1497,21 +1497,21 @@ TEST_F(CalloutTestv4, lease4_release) { " from a device with hardware address:" " hwtype=1 08:00:2b:02:3f:4e"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Verifies that the lease4_release callout generates the correct entry // in the legal file given a Pkt4 and Lease4 TEST_F(CalloutTestv4, customRequestLoggingFormat_lease4_release) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(release_); std::string format = "ifelse(pkt4.msgtype == 7, concat('Released address: ', addrtotext(pkt4.ciaddr)), '')"; - BackendStore::instance()->setRequestFormatExpression(format); + BackendStoreFactory::instance()->setRequestFormatExpression(format); int ret; @@ -1528,20 +1528,20 @@ TEST_F(CalloutTestv4, customRequestLoggingFormat_lease4_release) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("Released address: 192.2.1.100"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Verifies that the lease4_decline callout generates the correct entry // in the legal file given a Pkt4 and Lease4 TEST_F(CalloutTestv4, lease4_decline) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(decline_); @@ -1600,7 +1600,7 @@ TEST_F(CalloutTestv4, lease4_decline) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -1612,21 +1612,21 @@ TEST_F(CalloutTestv4, lease4_decline) { " from a device with hardware address:" " hwtype=1 08:00:2b:02:3f:4e"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Verifies that the lease4_decline callout generates the correct entry // in the legal file given a Pkt4 and Lease4 TEST_F(CalloutTestv4, customRequestLoggingFormat_lease4_decline) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(decline_); std::string format = "ifelse(pkt4.msgtype == 4, concat('Declined address: ', addrtotext(option[50].hex)), '')"; - BackendStore::instance()->setRequestFormatExpression(format); + BackendStoreFactory::instance()->setRequestFormatExpression(format); int ret; @@ -1643,26 +1643,26 @@ TEST_F(CalloutTestv4, customRequestLoggingFormat_lease4_decline) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("Declined address: 192.2.1.100"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Verifies that the custom format logs on multiple lines. TEST_F(CalloutTestv4, customRequestLoggingFormatMultipleLines) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(decline_); std::string format = "ifelse(pkt4.msgtype == 4, 'first line' + 0x0a + 'second line', '')"; - BackendStore::instance()->setRequestFormatExpression(format); + BackendStoreFactory::instance()->setRequestFormatExpression(format); int ret; @@ -1679,14 +1679,14 @@ TEST_F(CalloutTestv4, customRequestLoggingFormatMultipleLines) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("first line"); lines.push_back("second line"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } diff --git a/src/hooks/dhcp/forensic_log/tests/legal_log6_unittests.cc b/src/hooks/dhcp/forensic_log/tests/legal_log6_unittests.cc index 9270a80ee1..c113057c5f 100644 --- a/src/hooks/dhcp/forensic_log/tests/legal_log6_unittests.cc +++ b/src/hooks/dhcp/forensic_log/tests/legal_log6_unittests.cc @@ -8,7 +8,7 @@ /// generation as well as tests which exercise v6 callouts: leases6_committed /// and pkt6_send. These tests assume the legal log library /// is linked in, not loaded. This allows a great deal more flexibility -/// in testing, such as overriding and accessing the BackendStore::instance(). +/// in testing, such as overriding and accessing the BackendStoreFactory::instance(). /// The load and unload callouts are exercised in ../libloadtests, which /// actually uses the HooksManager to load and unload the library. @@ -412,7 +412,7 @@ TEST(Lease6FuncTest, hwaddrSourceToString ) { // Verifies DHCPv6 entries for directly connected clients TEST_F(CalloutTestv6, directClient6NA) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); std::string entry; @@ -468,11 +468,11 @@ TEST_F(CalloutTestv6, directClient6NA) { // Verifies DHCPv6 entries for directly connected clients TEST_F(CalloutTestv6, directClient6NACustomLoggingFormatRequestOnly) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_only_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_only_); std::string entry; @@ -513,12 +513,12 @@ TEST_F(CalloutTestv6, directClient6NACustomLoggingFormatRequestOnly) { // Verifies DHCPv6 entries for directly connected clients TEST_F(CalloutTestv6, directClient6NACustomLoggingFormatRequestAndResponse) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_); - BackendStore::instance()->setResponseFormatExpression(response_format_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_); + BackendStoreFactory::instance()->setResponseFormatExpression(response_format_); std::string entry; @@ -560,7 +560,7 @@ TEST_F(CalloutTestv6, directClient6NACustomLoggingFormatRequestAndResponse) { // Verifies DHCPv6 entries for relayed clients TEST_F(CalloutTestv6, relayedClient6NA) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); std::string entry; @@ -677,11 +677,11 @@ TEST_F(CalloutTestv6, relayedClient6NA) { // Verifies DHCPv6 entries for relayed clients TEST_F(CalloutTestv6, relayedClient6NACustomLoggingFormatRequestOnly) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_only_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_only_); std::string entry; @@ -786,12 +786,12 @@ TEST_F(CalloutTestv6, relayedClient6NACustomLoggingFormatRequestOnly) { // Verifies DHCPv6 entries for relayed clients TEST_F(CalloutTestv6, relayedClient6NACustomLoggingFormatRequestAndResponse) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_); - BackendStore::instance()->setResponseFormatExpression(response_format_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_); + BackendStoreFactory::instance()->setResponseFormatExpression(response_format_); std::string entry; @@ -909,7 +909,7 @@ TEST_F(CalloutTestv6, relayedClient6NACustomLoggingFormatRequestAndResponse) { // Verifies DHCPv6 entries for directly connected clients TEST_F(CalloutTestv6, directClient6PD) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); std::string entry; @@ -959,11 +959,11 @@ TEST_F(CalloutTestv6, directClient6PD) { // Verifies DHCPv6 entries for directly connected clients TEST_F(CalloutTestv6, directClient6PDCustomLoggingFormatRequestOnly) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_only_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_only_); std::string entry; @@ -998,12 +998,12 @@ TEST_F(CalloutTestv6, directClient6PDCustomLoggingFormatRequestOnly) { // Verifies DHCPv6 entries for directly connected clients TEST_F(CalloutTestv6, directClient6PDCustomLoggingFormatRequestAndResponse) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_); - BackendStore::instance()->setResponseFormatExpression(response_format_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_); + BackendStoreFactory::instance()->setResponseFormatExpression(response_format_); std::string entry; @@ -1039,7 +1039,7 @@ TEST_F(CalloutTestv6, directClient6PDCustomLoggingFormatRequestAndResponse) { // Verifies DHCPv6 entries for relayed clients TEST_F(CalloutTestv6, relayedClient6PD) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); std::string entry; @@ -1156,11 +1156,11 @@ TEST_F(CalloutTestv6, relayedClient6PD) { // Verifies DHCPv6 entries for relayed clients TEST_F(CalloutTestv6, relayedClient6PDCustomLoggingFormatRequestOnly) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_only_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_only_); std::string entry; @@ -1265,12 +1265,12 @@ TEST_F(CalloutTestv6, relayedClient6PDCustomLoggingFormatRequestOnly) { // Verifies DHCPv6 entries for relayed clients TEST_F(CalloutTestv6, relayedClient6PDCustomLoggingFormatRequestAndResponse) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_); - BackendStore::instance()->setResponseFormatExpression(response_format_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_); + BackendStoreFactory::instance()->setResponseFormatExpression(response_format_); std::string entry; @@ -1388,7 +1388,7 @@ TEST_F(CalloutTestv6, relayedClient6PDCustomLoggingFormatRequestAndResponse) { // Verifies printable id handling TEST_F(CalloutTestv6, printable6) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); Pkt6::RelayInfo relay1; relay1.msg_type_ = DHCPV6_RELAY_FORW; @@ -1430,14 +1430,14 @@ TEST_F(CalloutTestv6, printable6) { entry); } -// Verifies that legalLog6Handler() detects a null BackendStore::instance() +// Verifies that legalLog6Handler() detects a null BackendStoreFactory::instance() TEST_F(CalloutTestv6, noRotatingFileTest6) { // Make a callout handle CalloutHandle handle(getCalloutManager()); handle.setArgument("lease6", lease_na_); handle.setArgument("query6", request_na_); - // The function should fail when there's no BackendStore::instance(). + // The function should fail when there's no BackendStoreFactory::instance(). int ret; ASSERT_NO_THROW(ret = legalLog6Handler(handle, Action::ASSIGN)); EXPECT_EQ(1, ret); @@ -1446,7 +1446,7 @@ TEST_F(CalloutTestv6, noRotatingFileTest6) { // Verifies that the pkt6_receive callout creates an empty context which can be // used in all other hook points. TEST_F(CalloutTestv6, pkt6_receive) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(solicit_na_); @@ -1537,12 +1537,12 @@ TEST_F(CalloutTestv6, pkt6_receive) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileNotCreated(genName(today())); } @@ -1555,7 +1555,7 @@ TEST_F(CalloutTestv6, pkt6_receive) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, leases6_committed) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(request_na_); @@ -1652,7 +1652,7 @@ TEST_F(CalloutTestv6, leases6_committed) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -1664,7 +1664,7 @@ TEST_F(CalloutTestv6, leases6_committed) { " for 0 hrs 11 mins 53 secs" " to a device with DUID: 17:34:e2:ff:09:92:54"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } @@ -1673,7 +1673,7 @@ TEST_F(CalloutTestv6, leases6_committed) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, lease6_renew) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(renew_na_); @@ -1770,7 +1770,7 @@ TEST_F(CalloutTestv6, lease6_renew) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -1782,7 +1782,7 @@ TEST_F(CalloutTestv6, lease6_renew) { " for 0 hrs 11 mins 53 secs" " to a device with DUID: 17:34:e2:ff:09:92:54"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } @@ -1791,7 +1791,7 @@ TEST_F(CalloutTestv6, lease6_renew) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_renew) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); @@ -1800,7 +1800,7 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_renew) { std::string format = "ifelse(pkt6.msgtype == 5, concat('Assigned address: ', addrtotext(substring(option[3].option[5].hex, 0, 16))), '')"; - BackendStore::instance()->setRequestFormatExpression(format); + BackendStoreFactory::instance()->setRequestFormatExpression(format); int ret; @@ -1831,13 +1831,13 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_renew) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("Assigned address: 2001:db8:1::"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } @@ -1846,7 +1846,7 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_renew) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, lease6_rebind) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(rebind_na_); @@ -1943,7 +1943,7 @@ TEST_F(CalloutTestv6, lease6_rebind) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -1955,7 +1955,7 @@ TEST_F(CalloutTestv6, lease6_rebind) { " for 0 hrs 11 mins 53 secs" " to a device with DUID: 17:34:e2:ff:09:92:54"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } @@ -1964,7 +1964,7 @@ TEST_F(CalloutTestv6, lease6_rebind) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_rebind) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); @@ -1973,7 +1973,7 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_rebind) { std::string format = "ifelse(pkt6.msgtype == 6, concat('Assigned address: ', addrtotext(substring(option[3].option[5].hex, 0, 16))), '')"; - BackendStore::instance()->setRequestFormatExpression(format); + BackendStoreFactory::instance()->setRequestFormatExpression(format); int ret; @@ -2004,13 +2004,13 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_rebind) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("Assigned address: 2001:db8:1::"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } @@ -2019,7 +2019,7 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_rebind) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, lease6_release) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(release_na_); @@ -2104,7 +2104,7 @@ TEST_F(CalloutTestv6, lease6_release) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -2114,7 +2114,7 @@ TEST_F(CalloutTestv6, lease6_release) { lines.push_back("Address: 2001:db8:3::1 has been released" " from a device with DUID: 17:34:e2:ff:09:92:54"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } @@ -2123,7 +2123,7 @@ TEST_F(CalloutTestv6, lease6_release) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_release) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); @@ -2132,7 +2132,7 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_release) { std::string format = "ifelse(pkt6.msgtype == 8, concat('Released address: ', addrtotext(substring(option[3].option[5].hex, 0, 16))), '')"; - BackendStore::instance()->setRequestFormatExpression(format); + BackendStoreFactory::instance()->setRequestFormatExpression(format); int ret; @@ -2157,13 +2157,13 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_release) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("Released address: 2001:db8:1::"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } @@ -2172,7 +2172,7 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_release) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, lease6_decline) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(decline_); @@ -2257,7 +2257,7 @@ TEST_F(CalloutTestv6, lease6_decline) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -2267,7 +2267,7 @@ TEST_F(CalloutTestv6, lease6_decline) { lines.push_back("Address: 2001:db8:3::1 has been released" " from a device with DUID: 17:34:e2:ff:09:92:54"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } @@ -2276,7 +2276,7 @@ TEST_F(CalloutTestv6, lease6_decline) { // Note we don't bother testing multiple entries or rotation as this is done // during RotatingFile testing. TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_decline) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); @@ -2285,7 +2285,7 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_decline) { std::string format = "ifelse(pkt6.msgtype == 9, concat('Declined address: ', addrtotext(substring(option[3].option[5].hex, 0, 16))), '')"; - BackendStore::instance()->setRequestFormatExpression(format); + BackendStoreFactory::instance()->setRequestFormatExpression(format); int ret; @@ -2310,19 +2310,19 @@ TEST_F(CalloutTestv6, customRequestLoggingFormat_lease6_decline) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("Declined address: 2001:db8:1::"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } // Verifies that the custom format logs on multiple lines. TEST_F(CalloutTestv6, customRequestLoggingFormatMultipleLines) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); CfgMgr::instance().setFamily(AF_INET6); @@ -2331,7 +2331,7 @@ TEST_F(CalloutTestv6, customRequestLoggingFormatMultipleLines) { std::string format = "ifelse(pkt6.msgtype == 9, 'first line' + 0x0a + 'second line', '')"; - BackendStore::instance()->setRequestFormatExpression(format); + BackendStoreFactory::instance()->setRequestFormatExpression(format); int ret; @@ -2356,26 +2356,26 @@ TEST_F(CalloutTestv6, customRequestLoggingFormatMultipleLines) { } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; lines.push_back("first line"); lines.push_back("second line"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } TEST_F(CalloutTestv6, multipleAddressesAndPrefixesCustomLoggingFormatRequestOnly) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(request_na_); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_only_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_only_); int ret; @@ -2517,7 +2517,7 @@ TEST_F(CalloutTestv6, multipleAddressesAndPrefixesCustomLoggingFormatRequestOnly } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -2534,20 +2534,20 @@ TEST_F(CalloutTestv6, multipleAddressesAndPrefixesCustomLoggingFormatRequestOnly lines.push_back("Prefix: 2001:db8:2::/64 has been assigned for 256 seconds" " to a device with DUID: 17:34:e2:ff:09:92:54"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } TEST_F(CalloutTestv6, multipleAddressesAndPrefixesCustomLoggingFormatRequestAndResponse) { - ASSERT_NO_THROW(BackendStore::instance().reset(new TestableRotatingFile(time_))); + ASSERT_NO_THROW(BackendStoreFactory::instance().reset(new TestableRotatingFile(time_))); // Make a callout handle CalloutHandlePtr handle = getCalloutHandle(request_na_); CfgMgr::instance().setFamily(AF_INET6); - BackendStore::instance()->setRequestFormatExpression(request_format_); - BackendStore::instance()->setResponseFormatExpression(response_format_); + BackendStoreFactory::instance()->setRequestFormatExpression(request_format_); + BackendStoreFactory::instance()->setResponseFormatExpression(response_format_); int ret; @@ -2689,7 +2689,7 @@ TEST_F(CalloutTestv6, multipleAddressesAndPrefixesCustomLoggingFormatRequestAndR } // Close it to flush any unwritten data - BackendStore::instance()->close(); + BackendStoreFactory::instance()->close(); // Verify that the file content is correct. std::vectorlines; @@ -2706,7 +2706,7 @@ TEST_F(CalloutTestv6, multipleAddressesAndPrefixesCustomLoggingFormatRequestAndR lines.push_back("Prefix: 2001:db8:4::/64 has been assigned for 512 seconds" " to a device with DUID: 17:34:e2:ff:09:92:54"); - std::string today_now_string = BackendStore::instance()->getNowString(); + std::string today_now_string = BackendStoreFactory::instance()->getNowString(); checkFileLines(genName(today()), today_now_string, lines); } diff --git a/src/hooks/dhcp/forensic_log/tests/meson.build b/src/hooks/dhcp/forensic_log/tests/meson.build index 3f498d6996..291c4984ba 100644 --- a/src/hooks/dhcp/forensic_log/tests/meson.build +++ b/src/hooks/dhcp/forensic_log/tests/meson.build @@ -34,18 +34,6 @@ sources = [ 'run_unittests.cc', ] -if mysql.found() - dependencies += [mysql] - dhcp_forensic_log_tests_libs += [kea_mysql_testutils_lib] - sources += ['mysql_unittests.cc'] -endif - -if postgresql.found() - dependencies += [postgresql] - dhcp_forensic_log_tests_libs += [kea_pgsql_testutils_lib] - sources += ['pgsql_unittests.cc'] -endif - dhcp_forensic_log_tests = executable( sources, cpp_args: [ @@ -55,7 +43,6 @@ dhcp_forensic_log_tests = executable( f'-DFORENSIC_POSTROTATE_TEST_SH="@current_build_dir@/forensic_postrotate_test.sh"', f'-DINVALID_FORENSIC_POSTROTATE_TEST_SH="@TOP_SOURCE_DIR@/README"', f'-DLEGAL_LOG_DIR="@LEGAL_LOG_DIR@"', - f'-DTEST_CA_DIR="@TEST_CA_DIR@"', ], dependencies: dependencies, include_directories: [include_directories('.'), include_directories('..')] + INCLUDES, diff --git a/src/hooks/dhcp/forensic_log/tests/rotating_file_unittests.cc b/src/hooks/dhcp/forensic_log/tests/rotating_file_unittests.cc index 3570c982a7..389066fad8 100644 --- a/src/hooks/dhcp/forensic_log/tests/rotating_file_unittests.cc +++ b/src/hooks/dhcp/forensic_log/tests/rotating_file_unittests.cc @@ -28,41 +28,49 @@ namespace { /// @brief Tests the RotatingFile constructor. TEST_F(RotatingFileTest, invalidConstruction) { + db::DatabaseConnection::ParameterMap map; + BackendStorePtr p; // Verify that a RotatingFile with empty path is rejected. - ASSERT_THROW_MSG(RotatingFile("", "legal"), BackendStoreError, + map["path"] = ""; + map["base"] = "legal"; + ASSERT_THROW_MSG(p = BackendStorePtr(new RotatingFile(map)), BackendStoreError, "path cannot be blank"); + map["path"] = TEST_DATA_BUILDDIR; + map["base-name"] = ""; // Verify that a RotatingFile with an empty base name is rejected. - ASSERT_THROW_MSG(RotatingFile(TEST_DATA_BUILDDIR, ""), BackendStoreError, + ASSERT_THROW_MSG(p = BackendStorePtr(new RotatingFile(map)), BackendStoreError, "file name cannot be blank"); std::string name = "invalid"; + map["path"] = TEST_DATA_BUILDDIR; + map["base-name"] = "legal"; + map["unit"] = "day"; + + map["prerotate"] = "invalid"; + map["postrotate"] = ""; // Verify that a RotatingFile with an invalid prerotate action is rejected. - ASSERT_THROW(RotatingFile(TEST_DATA_BUILDDIR, "legal", - RotatingFile::TimeUnit::Day, 1, name, ""), - BackendStoreError); + ASSERT_THROW(p = BackendStorePtr(new RotatingFile(map)), BackendStoreError); + map["prerotate"] = ""; + map["postrotate"] = "invalid"; // Verify that a RotatingFile with an invalid postrotate action is rejected. - ASSERT_THROW(RotatingFile(TEST_DATA_BUILDDIR, "legal", - RotatingFile::TimeUnit::Day, 1, "", name), - BackendStoreError); + ASSERT_THROW(p = BackendStorePtr(new RotatingFile(map)), BackendStoreError); - name = INVALID_FORENSIC_PREROTATE_TEST_SH; + map["prerotate"] = INVALID_FORENSIC_PREROTATE_TEST_SH; + map["postrotate"] = ""; // Verify that a RotatingFile with a non executable prerotate action is // rejected. - ASSERT_THROW(RotatingFile(TEST_DATA_BUILDDIR, "legal", - RotatingFile::TimeUnit::Day, 1, name, ""), - BackendStoreError); + ASSERT_THROW(p = BackendStorePtr(new RotatingFile(map)), BackendStoreError); - name = INVALID_FORENSIC_POSTROTATE_TEST_SH; + map["prerotate"] = ""; + map["postrotate"] = INVALID_FORENSIC_POSTROTATE_TEST_SH; // Verify that a RotatingFile with a non executable postrotate action is // rejected. - ASSERT_THROW(RotatingFile(TEST_DATA_BUILDDIR, "legal", - RotatingFile::TimeUnit::Day, 1, "", name), - BackendStoreError); + ASSERT_THROW(p = BackendStorePtr(new RotatingFile(map)), BackendStoreError); } /// @brief Tests #5579 fix. @@ -151,10 +159,19 @@ TEST_F(RotatingFileTest, nowString) { // Try with an alternative format set via a load-time parameter. Use a few // of the more obscure strftime format specifiers to verify that it's // actually different from the "plain" %Y-%m-%d %M:%M:%S %Z format. - BackendStore::instance() = rotating_file_; + BackendStoreFactory::instance() = rotating_file_; data::ElementPtr params = data::Element::createMap(); params->set("timestamp-format", data::Element::create("%A%t%w %F%%")); - ASSERT_NO_THROW_LOG(rotating_file_->parseExtraParameters(params)); + + db::DatabaseConnection::ParameterMap map; + EXPECT_NO_THROW(BackendStore::parseExtraParameters(params, map)); + map["type"] = "logfile"; + map["path"] = TEST_DATA_BUILDDIR; + EXPECT_NO_THROW(BackendStoreFactory::addBackend(map)); + EXPECT_TRUE(BackendStoreFactory::instance()); + + rotating_file_->setTimestampFormat(BackendStoreFactory::instance()->getTimestampFormat()); + ASSERT_NO_THROW_LOG(now_string = rotating_file_->getNowString()); EXPECT_EQ("Monday\t1 2016-05-02%", now_string); } @@ -1114,7 +1131,7 @@ TEST_F(RotatingFileTest, prerotateActions) { ASSERT_NO_THROW_LOG(rotating_file_.reset(new TestableRotatingFile(time_, RotatingFile::TimeUnit::Second, 5, FORENSIC_PREROTATE_TEST_SH, ""))); - BackendStore::instance() = rotating_file_; + BackendStoreFactory::instance() = rotating_file_; // Open the file ASSERT_NO_THROW_LOG(rotating_file_->open()); @@ -1156,7 +1173,7 @@ TEST_F(RotatingFileTest, postrotateActions) { ASSERT_NO_THROW_LOG(rotating_file_.reset(new TestableRotatingFile(time_, RotatingFile::TimeUnit::Second, 5, "", FORENSIC_POSTROTATE_TEST_SH))); - BackendStore::instance() = rotating_file_; + BackendStoreFactory::instance() = rotating_file_; // Open the file ASSERT_NO_THROW_LOG(rotating_file_->open()); @@ -1199,7 +1216,7 @@ TEST_F(RotatingFileTest, prerotateAndPostrotateActions) { FORENSIC_PREROTATE_TEST_SH, FORENSIC_POSTROTATE_TEST_SH))); - BackendStore::instance() = rotating_file_; + BackendStoreFactory::instance() = rotating_file_; // Open the file ASSERT_NO_THROW_LOG(rotating_file_->open()); diff --git a/src/hooks/dhcp/forensic_log/tests/test_utils.h b/src/hooks/dhcp/forensic_log/tests/test_utils.h index b5ee6045bb..d07151c315 100644 --- a/src/hooks/dhcp/forensic_log/tests/test_utils.h +++ b/src/hooks/dhcp/forensic_log/tests/test_utils.h @@ -10,15 +10,9 @@ #include #include #include -#include +#include #include #include -#ifdef HAVE_MYSQL -#include -#endif -#ifdef HAVE_PGSQL -#include -#endif #include @@ -29,11 +23,15 @@ #include #include #include +#include #include using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; using namespace hooks; using namespace legal_log; +using namespace std; namespace isc { namespace legal_log { @@ -60,10 +58,36 @@ public: TestableRotatingFile(struct tm time, const TimeUnit unit = TimeUnit::Day, const uint32_t count = 1, - const std::string& prerotate = "", - const std::string& postrotate = "") - : RotatingFile(TEST_DATA_BUILDDIR, "legal", unit, count, prerotate, - postrotate), time_(time) { + const string& prerotate = "", + const string& postrotate = "", + db::DatabaseConnection::ParameterMap map = db::DatabaseConnection::ParameterMap()) + : RotatingFile(map), time_(time) { + ElementPtr parameters = Element::createMap(); + parameters->set("count", Element::create(count)); + BackendStore::parseFile(parameters, map); + map["path"] = TEST_DATA_BUILDDIR; + map["base-name"] = "legal"; + map["prerotate"] = prerotate; + map["postrotate"] = postrotate; + string time_unit = "day"; + switch (unit) { + case RotatingFile::TimeUnit::Second: + time_unit = "second"; + break; + case RotatingFile::TimeUnit::Day: + time_unit = "day"; + break; + case RotatingFile::TimeUnit::Month: + time_unit = "month"; + break; + case RotatingFile::TimeUnit::Year: + time_unit = "year"; + break; + default: + break; + } + map["time-unit"] = time_unit; + apply(map); } /// @brief Destructor. @@ -105,7 +129,7 @@ public: mutable struct tm time_; /// @brief List of created files that need to be removed after test ends - std::set file_list_; + set file_list_; /// @brief Make protected methods visible using RotatingFile::rotate; @@ -126,7 +150,22 @@ public: } /// @brief Destructor - virtual ~RotatingFileTest() = default; + virtual ~RotatingFileTest() { + reset(); + } + + /// @brief Removes files that may be left over from previous tests + void reset() { + std::ostringstream stream; + stream << "rm " << TEST_DATA_BUILDDIR << "/" << "*-legal" + << ".*.txt 2>/dev/null"; + int result = ::system(stream.str().c_str()); + if (result != 0) { + // Ignore the result, because it may well be non-zero value when + // files to be removed don't exist. + ; + } + } /// @brief Single instance of IOService. static asiolink::IOServicePtr getIOService() { @@ -135,8 +174,8 @@ public: } /// @brief Wait for file to be created or exit with timeout after 3 seconds. - void waitForFile(const std::string& name) { - std::ifstream test_log; + void waitForFile(const string& name) { + ifstream test_log; time_t now(time(0)); while (true) { test_log.open(name); @@ -151,8 +190,8 @@ public: /// @brief Called before each test virtual void SetUp() override { // Clean up from past tests. - BackendStore::instance().reset(); - BackendStore::setIOService(getIOService()); + BackendStoreFactory::instance().reset(); + db::DatabaseConnection::setIOService(getIOService()); asiolink::ProcessSpawn::setIOService(getIOService()); } @@ -206,7 +245,7 @@ public: /// @brief Checks if the given file exists /// /// @return true if the file exists, false if it does not - bool fileExists(const std::string& filename) { + bool fileExists(const string& filename) { struct stat statbuf; if (stat(filename.c_str(), &statbuf) == 0) { return (true); @@ -228,12 +267,12 @@ public: /// /// @param day date to use in the file name /// @return the generated file name - std::string genName(const boost::gregorian::date& day) { + string genName(const boost::gregorian::date& day) { boost::gregorian::date::ymd_type ymd = day.year_month_day(); - std::ostringstream stream; + ostringstream stream; stream << TEST_DATA_BUILDDIR << "/" << "legal" << "." << ymd.year - << std::right << std::setfill('0') << std::setw(2) - << ymd.month.as_number() << std::setw(2) << ymd.day << ".txt"; + << right << setfill('0') << setw(2) + << ymd.month.as_number() << setw(2) << ymd.day << ".txt"; return (stream.str()); } @@ -243,11 +282,11 @@ public: /// /// @param time - time to use in the file name /// @return - the generated file name - std::string genName(struct tm& time) { + string genName(struct tm& time) { time_t timestamp = mktime(&time); - std::ostringstream stream; + ostringstream stream; stream << TEST_DATA_BUILDDIR << "/" << "legal.T" - << std::right << std::setfill('0') << std::setw(20) + << right << setfill('0') << setw(20) << static_cast(timestamp) << ".txt"; return (stream.str()); } @@ -259,10 +298,10 @@ public: /// @param file_name name of the file to read /// @param expected_lines a vector of the lines expected to be found /// in the file (entries DO NOT include EOL) - void checkFileLines(const std::string& file_name, - const std::string& now_string, - const std::vector& expected_lines) { - std::ifstream is; + void checkFileLines(const string& file_name, + const string& now_string, + const vector& expected_lines) { + ifstream is; is.open(file_name.c_str()); ASSERT_TRUE(is.good()) << "Could not open file: " << file_name; @@ -275,7 +314,7 @@ public: ASSERT_TRUE(i < expected_lines.size()) << "Too many entries in file: " << file_name; - std::string cmp_line = now_string + " " + expected_lines[i]; + string cmp_line = now_string + " " + expected_lines[i]; ASSERT_EQ(cmp_line, buf) << "line mismatch in: " << file_name << " at line:" << i; @@ -292,8 +331,8 @@ public: /// Passes if the given file does not exist. Fails otherwise. /// /// @param file_name name of the file to check - void checkFileNotCreated(const std::string& file_name) { - std::ifstream is; + void checkFileNotCreated(const string& file_name) { + ifstream is; is.open(file_name.c_str()); ASSERT_FALSE(is.good()) << "The file is present: " << file_name; } @@ -303,14 +342,14 @@ public: /// @param use_time used instead of time() as the input time_t /// from which to derive the timezone /// @return The current timezone - std::string getTimezone(time_t use_time = 0) { + string getTimezone(time_t use_time = 0) { char buffer[16]; struct tm time_info = RotatingFileTest::getTime(use_time); strftime(buffer, sizeof(buffer), "%Z", &time_info); - return (std::string(buffer)); + return (string(buffer)); } /// @brief Return current time @@ -345,6 +384,9 @@ public: /// @brief The current date struct tm time_; + + /// @brief Initializer. + RotatingFileInit init_; }; /// @brief Test fixture for exercising Legal library callouts @@ -357,7 +399,7 @@ public: /// over from previous tests CalloutTest() : RotatingFileTest(), co_manager_(new CalloutManager(1)) { - BackendStore::instance().reset(); + BackendStoreFactory::instance().reset(); isc::dhcp::CfgMgr::instance().clear(); isc::dhcp::CfgMgr::instance().setFamily(AF_INET); } @@ -365,7 +407,7 @@ public: /// @brief Destructor /// Removes files that may be left over from previous tests virtual ~CalloutTest() { - BackendStore::instance().reset(); + BackendStoreFactory::instance().reset(); isc::dhcp::CfgMgr::instance().clear(); isc::dhcp::CfgMgr::instance().setFamily(AF_INET); } @@ -380,291 +422,6 @@ private: boost::shared_ptr co_manager_; }; -/// @brief Helper class to execute a SQL tool and get results -class runSQL { -public: - /// @brief Constructor - runSQL() { - reset(); - } - - /// @brief Destructor - virtual ~runSQL() = default; - - /// @brief Reset everything - void reset() { - command_ = ""; - query_ = ""; - result_ = 0; - output_.clear(); - } - - /// @brief Get command - /// @return the command - const std::string& getCommand() const { - return (command_); - } - - /// @brief Set command - /// @param the command - void setCommand(const std::string& command) { - command_ = command; - } - - /// @brief Get query - /// @return the query - const std::string& getQuery() const { - return (query_); - } - - /// @brief Set query - /// @param the query - void setQuery(const std::string& query) { - query_ = query; - } - - /// @brief Get result - /// @return the exit code - int getResult() const { - return (result_); - } - - /// @brief Get raw output - /// @return the unprocessed output - std::vector getRawOutput() { - return (output_); - } - - /// @brief Process output - /// @param output reference to the string vector to fill with output - /// @return true if processing was successful - virtual bool getOutput(std::vector& output) = 0; - - /// @brief Execute - /// @throw std::system_error according to boost documentation - void execute() { - // Reading stream - FILE* istream; - size_t buffer_size = 1024; - char buffer[buffer_size + 1]; - - // Child process - std::string cmd = command_ + "\"" + query_ + "\""; - istream = popen(cmd.c_str(), "r"); - - // Check errors - if (!istream) { - result_ = -1; - return; - } else { - result_ = ferror(istream); - if (result_) { - return; - } - } - - // Read output - while (!feof(istream) && fgets(buffer, buffer_size, istream)) { - output_.push_back(std::string(buffer)); - } - - // Wait for children to terminate and get its exit code - result_ = pclose(istream); - } - -private: - // @brief command - std::string command_; - - // @brief query - std::string query_; - - // @brief exit code aka result - int result_; - - // @brief output - std::vector output_; -}; - -/// @brief Test fixture for testing database backend connection recovery. -class LegalLogDbLostCallbackTest : public ::testing::Test { -public: - - /// @brief Constructor. - LegalLogDbLostCallbackTest() - : db_lost_callback_called_(0), db_recovered_callback_called_(0), - db_failed_callback_called_(0), - io_service_(boost::make_shared()) { - isc::db::DatabaseConnection::db_lost_callback_ = 0; - isc::db::DatabaseConnection::db_recovered_callback_ = 0; - isc::db::DatabaseConnection::db_failed_callback_ = 0; - isc::db::DatabaseConnection::setIOService(io_service_); - BackendStore::setIOService(io_service_); - isc::asiolink::ProcessSpawn::setIOService(io_service_); - isc::dhcp::TimerMgr::instance()->setIOService(io_service_); - BackendStore::instance().reset(); - } - - /// @brief Destructor. - virtual ~LegalLogDbLostCallbackTest() { - isc::db::DatabaseConnection::db_lost_callback_ = 0; - isc::db::DatabaseConnection::db_recovered_callback_ = 0; - isc::db::DatabaseConnection::db_failed_callback_ = 0; - isc::db::DatabaseConnection::setIOService(isc::asiolink::IOServicePtr()); - BackendStore::setIOService(isc::asiolink::IOServicePtr()); - isc::asiolink::ProcessSpawn::setIOService(isc::asiolink::IOServicePtr()); - isc::dhcp::TimerMgr::instance()->unregisterTimers(); - BackendStore::instance().reset(); - } - - /// @brief Prepares the class for a test. - /// - /// Invoked by gtest prior test entry, we create the - /// appropriate schema and create a basic DB manager to - /// wipe out any prior instance - virtual void SetUp() = 0; - - /// @brief Pre-text exit clean up - /// - /// Invoked by gtest upon test exit, we destroy the schema - /// we created. - virtual void TearDown() = 0; - - /// @brief Method which returns the back end specific connection - /// string - virtual std::string validConnectString() = 0; - - /// @brief Method which returns invalid back end specific connection - /// string - virtual std::string invalidConnectString() = 0; - - /// @brief Verifies the Backend Store behavior if DB connection can not be - /// established but succeeds on retry - /// - /// This function creates a Backend Store with a back end that supports - /// connectivity lost callback. It verifies that connectivity is unavailable - /// and then recovered on retry: - /// -# The registered DbLostCallback was invoked - /// -# The registered DbRecoveredCallback was invoked - virtual void testRetryOpenDbLostAndRecoveredCallback() = 0; - - /// @brief Verifies the Backend Store behavior if DB connection can not be - /// established but fails on retry - /// - /// This function creates a Backend Store with a back end that supports - /// connectivity lost callback. It verifies that connectivity is unavailable - /// and then fails again on retry: - /// -# The registered DbLostCallback was invoked - /// -# The registered DbFailedCallback was invoked - virtual void testRetryOpenDbLostAndFailedCallback() = 0; - - /// @brief Verifies the Backend Store behavior if DB connection can not be - /// established but succeeds on retry - /// - /// This function creates a Backend Store with a back end that supports - /// connectivity lost callback. It verifies that connectivity is unavailable - /// and then recovered on retry: - /// -# The registered DbLostCallback was invoked - /// -# The registered DbRecoveredCallback was invoked after two reconnect - /// attempts (once failing and second triggered by timer) - virtual void testRetryOpenDbLostAndRecoveredAfterTimeoutCallback() = 0; - - /// @brief Verifies the Backend Store behavior if DB connection can not be - /// established but fails on retry - /// - /// This function creates a Backend Store with a back end that supports - /// connectivity lost callback. It verifies that connectivity is unavailable - /// and then fails again on retry: - /// -# The registered DbLostCallback was invoked - /// -# The registered DbFailedCallback was invoked after two reconnect - /// attempts (once failing and second triggered by timer) - virtual void testRetryOpenDbLostAndFailedAfterTimeoutCallback() = 0; - - /// @brief Verifies open failures do NOT invoke db lost callback - /// - /// The db lost callback should only be invoked after successfully - /// opening the DB and then subsequently losing it. Failing to - /// open should be handled directly by the application layer. - virtual void testNoCallbackOnOpenFailure() = 0; - - /// @brief Verifies the Backend Store behavior if DB connection is lost - /// - /// This function creates a Backend Store with a back end that supports - /// connectivity lost callback. It verifies connectivity by issuing a known - /// valid query. Next it simulates connectivity lost by identifying and - /// closing the socket connection to the Backend Store. It then reissues the - /// query and verifies that: - /// -# The Query throws DbOperationError (rather than exiting) - /// -# The registered DbLostCallback was invoked - /// -# The registered DbRecoveredCallback was invoked - virtual void testDbLostAndRecoveredCallback() = 0; - - /// @brief Verifies the Backend Store behavior if DB connection is lost - /// - /// This function creates a Backend Store with a back end that supports - /// connectivity lost callback. It verifies connectivity by issuing a known - /// valid query. Next it simulates connectivity lost by identifying and - /// closing the socket connection to the Backend Store. It then reissues the - /// query and verifies that: - /// -# The Query throws DbOperationError (rather than exiting) - /// -# The registered DbLostCallback was invoked - /// -# The registered DbFailedCallback was invoked - virtual void testDbLostAndFailedCallback() = 0; - - /// @brief Verifies the Backend Store behavior if DB connection is lost - /// - /// This function creates a Backend Store with a back end that supports - /// connectivity lost callback. It verifies connectivity by issuing a known - /// valid query. Next it simulates connectivity lost by identifying and - /// closing the socket connection to the Backend Store. It then reissues the - /// query and verifies that: - /// -# The Query throws DbOperationError (rather than exiting) - /// -# The registered DbLostCallback was invoked - /// -# The registered DbRecoveredCallback was invoked after two reconnect - /// attempts (once failing and second triggered by timer) - virtual void testDbLostAndRecoveredAfterTimeoutCallback() = 0; - - /// @brief Verifies the Backend Store behavior if DB connection is lost - /// - /// This function creates a Backend Store with a back end that supports - /// connectivity lost callback. It verifies connectivity by issuing a known - /// valid query. Next it simulates connectivity lost by identifying and - /// closing the socket connection to the Backend Store. It then reissues the - /// query and verifies that: - /// -# The Query throws DbOperationError (rather than exiting) - /// -# The registered DbLostCallback was invoked - /// -# The registered DbFailedCallback was invoked after two reconnect - /// attempts (once failing and second triggered by timer) - virtual void testDbLostAndFailedAfterTimeoutCallback() = 0; - - /// @brief Callback function registered with the Backend Store - bool db_lost_callback(util::ReconnectCtlPtr /* not_used */) { - return (++db_lost_callback_called_); - } - - /// @brief Flag used to detect calls to db_lost_callback function - uint32_t db_lost_callback_called_; - - /// @brief Callback function registered with the Backend Store - bool db_recovered_callback(util::ReconnectCtlPtr /* not_used */) { - return (++db_recovered_callback_called_); - } - - /// @brief Flag used to detect calls to db_recovered_callback function - uint32_t db_recovered_callback_called_; - - /// @brief Callback function registered with the Backend Store - bool db_failed_callback(util::ReconnectCtlPtr /* not_used */) { - return (++db_failed_callback_called_); - } - - /// @brief Flag used to detect calls to db_failed_callback function - uint32_t db_failed_callback_called_; - - /// The IOService object, used for all ASIO operations. - isc::asiolink::IOServicePtr io_service_; -}; - } // namespace legal_log } // namespace isc diff --git a/src/hooks/dhcp/forensic_log/testutils/mysql_legl_schema.h b/src/hooks/dhcp/forensic_log/testutils/mysql_legl_schema.h deleted file mode 100644 index f9ebfaafc1..0000000000 --- a/src/hooks/dhcp/forensic_log/testutils/mysql_legl_schema.h +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (C) 2018-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/. - -#ifndef TEST_LEGL_MYSQL_SCHEMA_H -#define TEST_LEGL_MYSQL_SCHEMA_H - -#include - -namespace isc { -namespace legal_log { -namespace test { - -/// @brief Clear everything from the database -/// -/// Submits the current schema drop script: -/// -/// /mysql/legldb_drop.mysql -/// -/// to the unit test MySQL database. If the script fails, the invoking test -/// will fail. The output of stderr is suppressed unless the parameter, -/// show_err is true. -/// -/// @param show_err flag which governs whether or not stderr is suppressed. -void destroyMySQLLegLSchema(bool show_err = false) { - isc::db::test::runMySQLScript(DATABASE_SCRIPTS_DIR, - "mysql/legldb_drop.mysql", - show_err); -} - -/// @brief Create the MySQL Schema -/// -/// Submits the current schema creation script: -/// -/// /mysql/legldb_create.mysql -/// -/// to the unit test MySQL database. If the script fails, the invoking test -/// will fail. The output of stderr is suppressed unless the parameter, -/// show_err is true. -/// -/// @param show_err flag which governs whether or not stderr is suppressed. -void createMySQLLegLSchema(bool show_err = false) { - isc::db::test::runMySQLScript(DATABASE_SCRIPTS_DIR, - "mysql/legldb_create.mysql", - show_err); -} - -}; -}; -}; - -#endif diff --git a/src/hooks/dhcp/forensic_log/testutils/pgsql_legl_schema.h b/src/hooks/dhcp/forensic_log/testutils/pgsql_legl_schema.h deleted file mode 100644 index 01e73ce277..0000000000 --- a/src/hooks/dhcp/forensic_log/testutils/pgsql_legl_schema.h +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (C) 2018-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/. - -#ifndef TEST_PGSQL_LEGL_SCHEMA_H -#define TEST_PGSQL_LEGL_SCHEMA_H - -#include - -namespace isc { -namespace legal_log { -namespace test { - -/// @brief Clear everything from the database -/// -/// Submits the current schema drop script: -/// -/// /pgsql/legldb_drop.pgsql -/// -/// to the unit test Postgresql database. If the script fails, the invoking -/// test will fail. The output of stderr is suppressed unless the parameter, -/// show_err is true. -/// -/// @param show_err flag which governs whether or not stderr is suppressed. -void destroyPgSQLLegLSchema(bool show_err = false) { - isc::db::test::runPgSQLScript(DATABASE_SCRIPTS_DIR, - "pgsql/legldb_drop.pgsql", - show_err); -} - -/// @brief Create the Postgresql Schema -/// -/// Submits the current schema creation script: -/// -/// /pgsql/legldb_create.pgsql -/// -/// to the unit test Postgresql database. If the script fails, the invoking -/// test will fail. The output of stderr is suppressed unless the parameter, -/// show_err is true. -/// -/// @param show_err flag which governs whether or not stderr is suppressed. -void createPgSQLLegLSchema(bool show_err = false) { - isc::db::test::runPgSQLScript(DATABASE_SCRIPTS_DIR, - "pgsql/legldb_create.pgsql", - show_err); -} - -}; -}; -}; - -#endif diff --git a/src/hooks/dhcp/mysql/.gitattributes b/src/hooks/dhcp/mysql/.gitattributes index 4686716df9..cc433fc55c 100644 --- a/src/hooks/dhcp/mysql/.gitattributes +++ b/src/hooks/dhcp/mysql/.gitattributes @@ -1,5 +1,7 @@ /mysql_cb_messages.cc -diff merge=ours /mysql_cb_messages.h -diff merge=ours +/mysql_fb_messages.cc -diff merge=ours +/mysql_fb_messages.h -diff merge=ours /mysql_hb_messages.cc -diff merge=ours /mysql_hb_messages.h -diff merge=ours /mysql_lb_messages.cc -diff merge=ours diff --git a/src/hooks/dhcp/mysql/Makefile.am b/src/hooks/dhcp/mysql/Makefile.am index 6faae540a0..f12dd0f1b8 100644 --- a/src/hooks/dhcp/mysql/Makefile.am +++ b/src/hooks/dhcp/mysql/Makefile.am @@ -6,6 +6,7 @@ AM_CXXFLAGS = $(KEA_CXXFLAGS) # Ensure that the message file is included in the distribution EXTRA_DIST = mysql_cb_messages.mes +EXTRA_DIST += mysql_fb_messages.mes EXTRA_DIST += mysql_hb_messages.mes EXTRA_DIST += mysql_lb_messages.mes @@ -21,6 +22,8 @@ libmysql_la_SOURCES += mysql_cb_dhcp6.cc mysql_cb_dhcp6.h libmysql_la_SOURCES += mysql_cb_impl.cc mysql_cb_impl.h libmysql_la_SOURCES += mysql_cb_messages.cc mysql_cb_messages.h libmysql_la_SOURCES += mysql_cb_log.cc mysql_cb_log.h +libmysql_la_SOURCES += mysql_fb_messages.cc mysql_fb_messages.h +libmysql_la_SOURCES += mysql_fb_log.cc mysql_fb_log.h libmysql_la_SOURCES += mysql_query_macros_dhcp.h libmysql_la_SOURCES += mysql_hb_log.cc mysql_hb_log.h libmysql_la_SOURCES += mysql_hb_messages.cc mysql_hb_messages.h @@ -28,6 +31,7 @@ libmysql_la_SOURCES += mysql_host_data_source.cc mysql_host_data_source.h libmysql_la_SOURCES += mysql_lb_log.cc mysql_lb_log.h libmysql_la_SOURCES += mysql_lb_messages.cc mysql_lb_messages.h libmysql_la_SOURCES += mysql_lease_mgr.cc mysql_lease_mgr.h +libmysql_la_SOURCES += mysql_legal_log.cc mysql_legal_log.h libmysql_la_SOURCES += version.cc libmysql_la_CXXFLAGS = $(AM_CXXFLAGS) @@ -72,6 +76,7 @@ libdhcp_mysql_la_LIBADD += $(BOOST_LIBS) # reconfigure, a new target messages-clean has been added. maintainer-clean-local: rm -f mysql_cb_messages.h mysql_cb_messages.cc + rm -f mysql_fb_messages.h mysql_fb_messages.cc rm -f mysql_hb_messages.h mysql_hb_messages.cc rm -f mysql_lb_messages.h mysql_lb_messages.cc @@ -87,6 +92,7 @@ if GENERATE_MESSAGES # Define rule to build logging source files from message file messages: mysql_cb_messages.h mysql_cb_messages.cc \ + mysql_fb_messages.h mysql_fb_messages.cc \ mysql_hb_messages.h mysql_hb_messages.cc \ mysql_lb_messages.h mysql_lb_messages.cc @echo Message files regenerated @@ -95,6 +101,10 @@ mysql_cb_messages.h mysql_cb_messages.cc: mysql_cb_messages.mes (cd $(top_srcdir); \ $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/mysql/mysql_cb_messages.mes) +mysql_fb_messages.h mysql_fb_messages.cc: mysql_fb_messages.mes + (cd $(top_srcdir); \ + $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/mysql/mysql_fb_messages.mes) + mysql_hb_messages.h mysql_hb_messages.cc: mysql_hb_messages.mes (cd $(top_srcdir); \ $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/mysql/mysql_hb_messages.mes) @@ -106,6 +116,7 @@ mysql_lb_messages.h mysql_lb_messages.cc: mysql_lb_messages.mes else messages: mysql_cb_messages.h mysql_cb_messages.cc \ + mysql_fb_messages.h mysql_fb_messages.cc \ mysql_hb_messages.h mysql_hb_messages.cc \ mysql_lb_messages.h mysql_lb_messages.cc @echo Messages generation disabled. Configure with --enable-generate-messages to enable it. diff --git a/src/hooks/dhcp/mysql/meson.build b/src/hooks/dhcp/mysql/meson.build index c7c8640e47..b9c90295c6 100644 --- a/src/hooks/dhcp/mysql/meson.build +++ b/src/hooks/dhcp/mysql/meson.build @@ -10,12 +10,15 @@ dhcp_mysql_lib = shared_library( 'mysql_cb_impl.cc', 'mysql_cb_log.cc', 'mysql_cb_messages.cc', + 'mysql_fb_log.cc', + 'mysql_fb_messages.cc', 'mysql_hb_log.cc', 'mysql_hb_messages.cc', 'mysql_host_data_source.cc', 'mysql_lb_log.cc', 'mysql_lb_messages.cc', 'mysql_lease_mgr.cc', + 'mysql_legal_log.cc', 'version.cc', dependencies: [crypto, mysql], include_directories: [include_directories('.')] + INCLUDES, @@ -42,6 +45,16 @@ if KEA_MSG_COMPILER.found() ], ) TARGETS_GEN_MESSAGES += [target_gen_messages] + target_gen_messages = run_target( + 'src-hooks-dhcp-mysql-mysql_fb_messages', + command: [ + CD_AND_RUN, + TOP_SOURCE_DIR, + KEA_MSG_COMPILER, + 'src/hooks/dhcp/mysql/mysql_fb_messages.mes', + ], + ) + TARGETS_GEN_MESSAGES += [target_gen_messages] target_gen_messages = run_target( 'src-hooks-dhcp-mysql-mysql_hb_messages', command: [ diff --git a/src/hooks/dhcp/mysql/mysql_callouts.cc b/src/hooks/dhcp/mysql/mysql_callouts.cc index e3427acfe6..bf195fe8b5 100644 --- a/src/hooks/dhcp/mysql/mysql_callouts.cc +++ b/src/hooks/dhcp/mysql/mysql_callouts.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -63,6 +64,12 @@ int load(LibraryHandle& /* handle */) { MySqlConfigBackendDHCPv4::registerBackendType(); MySqlConfigBackendDHCPv6::registerBackendType(); + // Register MySQL FB factories with Backend Store managers. + BackendStoreFactory::registerBackendFactory("mysql", + MySqlStore::factory, + true, + MySqlStore::getDBVersion); + // Register MySQL HB factories with Host Managers HostDataSourceFactory::registerFactory("mysql", MySqlHostDataSource::factory, @@ -116,6 +123,9 @@ int unload() { MySqlConfigBackendImpl::setIOService(IOServicePtr()); } + // Unregister the factories and remove MySQL backends + BackendStoreFactory::unregisterBackendFactory("mysql", true); + // Unregister the factories and remove MySQL backends HostDataSourceFactory::deregisterFactory("mysql", true); diff --git a/src/hooks/dhcp/mysql/mysql_fb_log.cc b/src/hooks/dhcp/mysql/mysql_fb_log.cc new file mode 100644 index 0000000000..b1b4ece6dd --- /dev/null +++ b/src/hooks/dhcp/mysql/mysql_fb_log.cc @@ -0,0 +1,40 @@ +// Copyright (C) 2019-2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include + +using namespace isc::db; + +namespace isc { +namespace dhcp { + +extern const int MYSQL_FB_DBG_TRACE = isc::log::DBGLVL_TRACE_BASIC; +extern const int MYSQL_FB_DBG_RESULTS = isc::log::DBGLVL_TRACE_BASIC_DATA; +extern const int MYSQL_FB_DBG_TRACE_DETAIL = isc::log::DBGLVL_TRACE_DETAIL; +extern const int MYSQL_FB_DBG_TRACE_DETAIL_DATA = + isc::log::DBGLVL_TRACE_DETAIL_DATA; +extern const int MYSQL_FB_DBG_HOOKS = isc::log::DBGLVL_TRACE_BASIC; + +isc::log::Logger mysql_fb_logger("mysql-fb-hooks"); + +isc::log::Logger mysql_legal_log_logger("mysql-legal-log"); + +const isc::db::DbLogger::MessageMap mysql_legal_log_db_message_map = { + { DB_INVALID_ACCESS, LEGAL_LOG_MYSQL_INVALID_ACCESS }, + + { MYSQL_FATAL_ERROR, LEGAL_LOG_MYSQL_FATAL_ERROR }, + { MYSQL_START_TRANSACTION, LEGAL_LOG_MYSQL_START_TRANSACTION }, + { MYSQL_COMMIT, LEGAL_LOG_MYSQL_COMMIT }, + { MYSQL_ROLLBACK, LEGAL_LOG_MYSQL_ROLLBACK }, +}; + +isc::db::DbLogger mysql_legal_log_db_logger(mysql_legal_log_logger, mysql_legal_log_db_message_map); + +} // namespace dhcp +} // namespace isc diff --git a/src/hooks/dhcp/mysql/mysql_fb_log.h b/src/hooks/dhcp/mysql/mysql_fb_log.h new file mode 100644 index 0000000000..148fe00982 --- /dev/null +++ b/src/hooks/dhcp/mysql/mysql_fb_log.h @@ -0,0 +1,72 @@ +// Copyright (C) 2019-2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef MYSQL_FB_LOG_H +#define MYSQL_FB_LOG_H + +#include +#include +#include +#include + +namespace isc { +namespace dhcp { + +///@{ +/// @brief MySQL Forensic Mgr logging levels +/// +/// Defines the levels used to output debug messages in the MySQL Forensic Mgr +/// Note that higher numbers equate to more verbose (and detailed) output. + +/// @brief Traces normal operations +/// +/// E.g. sending a query to the database etc. +extern const int MYSQL_FB_DBG_TRACE; + +/// @brief Records the results of the lookups +/// +/// Using the example of tracing queries from the backend database, this will +/// just record the summary results. +extern const int MYSQL_FB_DBG_RESULTS; + +/// @brief Additional information +/// +/// Record detailed tracing. This is generally reserved for tracing access to +/// the forensic database. +extern const int MYSQL_FB_DBG_TRACE_DETAIL; + +/// @brief Additional information +/// +/// Record detailed (and verbose) data on the server. +extern const int MYSQL_FB_DBG_TRACE_DETAIL_DATA; + +// Trace hook related operations +extern const int MYSQL_FB_DBG_HOOKS; +///@} + +/// @brief MySQL Forensic Mgr Logger +/// +/// Define the logger used to log messages. We could define it in multiple +/// modules, but defining in a single module and linking to it saves time and +/// space. +extern isc::log::Logger mysql_fb_logger; + +/// @brief Legal Log Logger +/// +/// Define the logger used to log messages. We could define it in multiple +/// modules, but defining in a single module and linking to it saves time and +/// space. +extern isc::log::Logger mysql_legal_log_logger; + +/// @brief Legal log database Logger +/// +/// It is the default database logger. +extern isc::db::DbLogger mysql_legal_log_db_logger; + +} // namespace dhcp +} // namespace isc + +#endif diff --git a/src/hooks/dhcp/mysql/mysql_fb_messages.cc b/src/hooks/dhcp/mysql/mysql_fb_messages.cc new file mode 100644 index 0000000000..828c8bd67b --- /dev/null +++ b/src/hooks/dhcp/mysql/mysql_fb_messages.cc @@ -0,0 +1,49 @@ +// File created from src/hooks/dhcp/mysql/mysql_fb_messages.mes + +#include +#include +#include + +namespace isc { +namespace dhcp { + +extern const isc::log::MessageID LEGAL_LOG_MYSQL_COMMIT = "LEGAL_LOG_MYSQL_COMMIT"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED = "LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE = "LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED = "LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_FATAL_ERROR = "LEGAL_LOG_MYSQL_FATAL_ERROR"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_GET_VERSION = "LEGAL_LOG_MYSQL_GET_VERSION"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_INSERT_LOG = "LEGAL_LOG_MYSQL_INSERT_LOG"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_INVALID_ACCESS = "LEGAL_LOG_MYSQL_INVALID_ACCESS"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_NO_TLS = "LEGAL_LOG_MYSQL_NO_TLS"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_ROLLBACK = "LEGAL_LOG_MYSQL_ROLLBACK"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_START_TRANSACTION = "LEGAL_LOG_MYSQL_START_TRANSACTION"; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_TLS_CIPHER = "LEGAL_LOG_MYSQL_TLS_CIPHER"; +extern const isc::log::MessageID MYSQL_FB_DB = "MYSQL_FB_DB"; + +} // namespace dhcp +} // namespace isc + +namespace { + +const char* values[] = { + "LEGAL_LOG_MYSQL_COMMIT", "committing to MySQL database", + "LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED", "database reconnect failed: %1", + "LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE", "scheduling attempt %1 of %2 in %3 milliseconds", + "LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED", "maximum number of database reconnect attempts: %1, has been exhausted without success", + "LEGAL_LOG_MYSQL_FATAL_ERROR", "Unrecoverable MySQL error occurred: %1 for <%2>, reason: %3 (error code: %4).", + "LEGAL_LOG_MYSQL_GET_VERSION", "obtaining schema version information", + "LEGAL_LOG_MYSQL_INSERT_LOG", "Adding a log entry to the database: %1", + "LEGAL_LOG_MYSQL_INVALID_ACCESS", "invalid database access string: %1", + "LEGAL_LOG_MYSQL_NO_TLS", "TLS was required but is not used", + "LEGAL_LOG_MYSQL_ROLLBACK", "rolling back MySQL database", + "LEGAL_LOG_MYSQL_START_TRANSACTION", "starting new MySQL transaction", + "LEGAL_LOG_MYSQL_TLS_CIPHER", "TLS cipher: %1", + "MYSQL_FB_DB", "opening MySQL log database: %1", + NULL +}; + +const isc::log::MessageInitializer initializer(values); + +} // Anonymous namespace + diff --git a/src/hooks/dhcp/mysql/mysql_fb_messages.h b/src/hooks/dhcp/mysql/mysql_fb_messages.h new file mode 100644 index 0000000000..5121990fc3 --- /dev/null +++ b/src/hooks/dhcp/mysql/mysql_fb_messages.h @@ -0,0 +1,28 @@ +// File created from src/hooks/dhcp/mysql/mysql_fb_messages.mes + +#ifndef MYSQL_FB_MESSAGES_H +#define MYSQL_FB_MESSAGES_H + +#include + +namespace isc { +namespace dhcp { + +extern const isc::log::MessageID LEGAL_LOG_MYSQL_COMMIT; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_FATAL_ERROR; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_GET_VERSION; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_INSERT_LOG; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_INVALID_ACCESS; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_NO_TLS; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_ROLLBACK; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_START_TRANSACTION; +extern const isc::log::MessageID LEGAL_LOG_MYSQL_TLS_CIPHER; +extern const isc::log::MessageID MYSQL_FB_DB; + +} // namespace dhcp +} // namespace isc + +#endif // MYSQL_FB_MESSAGES_H diff --git a/src/hooks/dhcp/mysql/mysql_fb_messages.mes b/src/hooks/dhcp/mysql/mysql_fb_messages.mes new file mode 100644 index 0000000000..64cc9a875d --- /dev/null +++ b/src/hooks/dhcp/mysql/mysql_fb_messages.mes @@ -0,0 +1,72 @@ +# Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +$NAMESPACE isc::dhcp + +% LEGAL_LOG_MYSQL_COMMIT committing to MySQL database +The code has issued a commit call. All outstanding transactions will be +committed to the database. Note that depending on the MySQL settings, +the committal may not include a write to disk. + +% MYSQL_FB_DB opening MySQL log database: %1 +This informational message is logged when a legal log hook library is +about to open a MySQL log database. The parameters of the +connection including database name and username needed to access it +(but not the password if any) are logged. + +% LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED database reconnect failed: %1 +An error message issued when an attempt to reconnect has failed. + +% LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE scheduling attempt %1 of %2 in %3 milliseconds +An info message issued when the server is scheduling the next attempt to reconnect +to the database. This occurs when the server has lost database connectivity and +is attempting to reconnect automatically. + +% LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED maximum number of database reconnect attempts: %1, has been exhausted without success +An error message issued when the server failed to reconnect. Loss of connectivity +is typically a network or database server issue. + +% LEGAL_LOG_MYSQL_FATAL_ERROR Unrecoverable MySQL error occurred: %1 for <%2>, reason: %3 (error code: %4). +An error message indicating that communication with the MySQL database server +has been lost. When this occurs the server exits immediately with a non-zero +exit code. This is most likely due to a network issue. + +% LEGAL_LOG_MYSQL_GET_VERSION obtaining schema version information +Logged at debug log level 50. +A debug message issued when the server is about to obtain schema version +information from the MySQL database. + +% LEGAL_LOG_MYSQL_INSERT_LOG Adding a log entry to the database: %1 +Logged at debug log level 50. +An informational message logged when a log entry is inserted. + +% LEGAL_LOG_MYSQL_INVALID_ACCESS invalid database access string: %1 +This is logged when an attempt has been made to parse a database access string +and the attempt ended in error. The access string in question - which +should be of the form 'keyword=value keyword=value...' is included in +the message. + +% LEGAL_LOG_MYSQL_NO_TLS TLS was required but is not used +This error message is issued when TLS for the connection was required but +TLS is not used. + +% LEGAL_LOG_MYSQL_ROLLBACK rolling back MySQL database +The code has issued a rollback call. All outstanding transaction will +be rolled back and not committed to the database. + +% LEGAL_LOG_MYSQL_START_TRANSACTION starting new MySQL transaction +A debug message issued when a new MySQL transaction is being started. +This message is typically not issued when inserting data into a +single table because the server doesn't explicitly start +transactions in this case. This message is issued when data is +inserted into multiple tables with multiple INSERT statements +and there may be a need to rollback the whole transaction if +any of these INSERT statements fail. + +% LEGAL_LOG_MYSQL_TLS_CIPHER TLS cipher: %1 +Logged at debug log level 50. +A debug message issued when a new MySQL connected is created with TLS. +The TLS cipher name is logged. diff --git a/src/hooks/dhcp/mysql/mysql_host_data_source.cc b/src/hooks/dhcp/mysql/mysql_host_data_source.cc index 119ed3c99e..900bb5a9b6 100644 --- a/src/hooks/dhcp/mysql/mysql_host_data_source.cc +++ b/src/hooks/dhcp/mysql/mysql_host_data_source.cc @@ -1706,6 +1706,8 @@ public: MySqlIPv6ReservationExchange() : host_id_(0), prefix_len_(0), type_(0), iaid_(0), resv_(IPv6Resrv::TYPE_NA, asiolink::IOAddress("::"), 128), + addr6_length_(0), + excluded_prefix_length_(0), excluded_prefix_len_(0) { // Reset error table. diff --git a/src/hooks/dhcp/forensic_log/mysql_legal_log.cc b/src/hooks/dhcp/mysql/mysql_legal_log.cc similarity index 86% rename from src/hooks/dhcp/forensic_log/mysql_legal_log.cc rename to src/hooks/dhcp/mysql/mysql_legal_log.cc index 36c357bf7b..795e53e009 100644 --- a/src/hooks/dhcp/forensic_log/mysql_legal_log.cc +++ b/src/hooks/dhcp/mysql/mysql_legal_log.cc @@ -6,8 +6,10 @@ #include -#include +#include #include +#include +#include #include #include #include @@ -25,7 +27,6 @@ using namespace isc; using namespace isc::db; using namespace isc::dhcp; -using namespace isc::legal_log; using namespace isc::util; using namespace std; @@ -41,7 +42,7 @@ tagged_statements = { { }; namespace isc { -namespace legal_log { +namespace dhcp { /// @brief Supports exchanging log entries with MySQL. class MySqlLegLExchange { @@ -230,7 +231,7 @@ MySqlStoreContext::MySqlStoreContext(const DatabaseConnection::ParameterMap& par // MySqlStoreContextAlloc Constructor and Destructor -MySqlStore::MySqlStoreContextAlloc::MySqlStoreContextAlloc(const MySqlStore& store) +MySqlStore::MySqlStoreContextAlloc::MySqlStoreContextAlloc(MySqlStore& store) : ctx_(), store_(store) { if (MultiThreadingMgr::instance().getMode()) { @@ -260,17 +261,18 @@ MySqlStore::MySqlStoreContextAlloc::~MySqlStoreContextAlloc() { // multi-threaded lock_guard lock(store_.pool_->mutex_); store_.pool_->pool_.push_back(ctx_); + if (ctx_->conn_.isUnusable()) { + store_.unusable_ = true; + } + } else if (ctx_->conn_.isUnusable()) { + store_.unusable_ = true; } - // If running in single-threaded mode, there's nothing to do here. } // MySqlStore MySqlStore::MySqlStore(const DatabaseConnection::ParameterMap& parameters) - : timer_name_("") { - - // Store connection parameters. - BackendStore::setParameters(parameters); + : BackendStore(parameters), timer_name_(""), unusable_(false) { // Create unique timer name per instance. timer_name_ = "MySqlLegalStore["; @@ -279,14 +281,14 @@ MySqlStore::MySqlStore(const DatabaseConnection::ParameterMap& parameters) } void MySqlStore::open() { - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(mysql_legal_log_db_logger); // Test schema version first. std::pair code_version(MYSQL_SCHEMA_VERSION_MAJOR, MYSQL_SCHEMA_VERSION_MINOR); - BackendStorePtr store = BackendStore::instance(); - BackendStore::instance().reset(); + BackendStorePtr store = BackendStoreFactory::instance(); + BackendStoreFactory::instance().reset(); string timer_name; bool retry = false; @@ -308,7 +310,7 @@ void MySqlStore::open() { << db_version.second); } - BackendStore::instance() = store; + BackendStoreFactory::instance() = store; // Create an initial context. pool_.reset(new MySqlStoreContextPool()); @@ -319,8 +321,8 @@ void MySqlStore::open() { MySqlStoreContextPtr MySqlStore::createContext() const { - MySqlStoreContextPtr ctx(new MySqlStoreContext(BackendStore::getParameters(), - IOServiceAccessorPtr(new IOServiceAccessor(&MySqlStore::getIOService)), + MySqlStoreContextPtr ctx(new MySqlStoreContext(getParameters(), + IOServiceAccessorPtr(new IOServiceAccessor(&DatabaseConnection::getIOService)), &MySqlStore::dbReconnect)); // Open the database. @@ -330,9 +332,9 @@ MySqlStore::createContext() const { if (ctx->conn_.getTls()) { std::string cipher = ctx->conn_.getTlsCipher(); if (cipher.empty()) { - LOG_ERROR(legal_log_logger, LEGAL_LOG_MYSQL_NO_TLS); + LOG_ERROR(mysql_fb_logger, LEGAL_LOG_MYSQL_NO_TLS); } else { - LOG_DEBUG(legal_log_logger, DB_DBG_TRACE_DETAIL, + LOG_DEBUG(mysql_fb_logger, DB_DBG_TRACE_DETAIL, LEGAL_LOG_MYSQL_TLS_CIPHER) .arg(cipher); } @@ -367,10 +369,10 @@ MySqlStore::writeln(const string& text, const std::string& addr) { return; } - LOG_DEBUG(legal_log_logger, DB_DBG_TRACE_DETAIL, + LOG_DEBUG(mysql_fb_logger, DB_DBG_TRACE_DETAIL, LEGAL_LOG_MYSQL_INSERT_LOG).arg(text); - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(mysql_legal_log_db_logger); // Get a context MySqlStoreContextAlloc get_context(*this); @@ -395,15 +397,24 @@ MySqlStore::writeln(const string& text, const std::string& addr) { pair MySqlStore::getVersion(const std::string& timer_name) const { - LOG_DEBUG(legal_log_logger, DB_DBG_TRACE_DETAIL, + LOG_DEBUG(mysql_fb_logger, DB_DBG_TRACE_DETAIL, LEGAL_LOG_MYSQL_GET_VERSION); - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(mysql_legal_log_db_logger); IOServiceAccessorPtr ac(new IOServiceAccessor(&DatabaseConnection::getIOService)); DbCallback cb(&MySqlStore::dbReconnect); - return (MySqlConnection::getVersion(BackendStore::getParameters(), ac, cb, timer_name, NetworkState::DB_CONNECTION + 31)); + return (MySqlConnection::getVersion(getParameters(), ac, cb, timer_name, NetworkState::DB_CONNECTION + 31)); +} + +std::string +MySqlStore::getDBVersion() { + std::stringstream tmp; + tmp << "MySQL backend " << MYSQL_SCHEMA_VERSION_MAJOR; + tmp << "." << MYSQL_SCHEMA_VERSION_MINOR; + tmp << ", library " << mysql_get_client_info(); + return (tmp.str()); } void @@ -430,18 +441,15 @@ MySqlStore::dbReconnect(ReconnectCtlPtr db_reconnect_ctl) { // At least one connection was lost. try { - auto parameters = BackendStore::getParameters(); - // Set legal store to NULL - BackendStore::instance().reset(); - // Create temporary legal store - BackendStorePtr store(new MySqlStore(parameters)); - store->open(); - // If everything is fine, set the legal store - BackendStore::instance() = store; - BackendStore::parseExtraParameters(BackendStore::getConfig()); + auto pool = BackendStoreFactory::getPool(); + for (auto backend : pool) { + if (BackendStoreFactory::delBackend(backend.first, true)) { + BackendStoreFactory::addBackend(backend.second.first, backend.first); + } + } reopened = true; } catch (const std::exception& ex) { - LOG_ERROR(legal_log_logger, LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED) + LOG_ERROR(mysql_fb_logger, LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_FAILED) .arg(ex.what()); } @@ -458,7 +466,7 @@ MySqlStore::dbReconnect(ReconnectCtlPtr db_reconnect_ctl) { } else { if (!check) { // We're out of retries, log it and initiate shutdown. - LOG_ERROR(legal_log_logger, LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED) + LOG_ERROR(mysql_fb_logger, LEGAL_LOG_MYSQL_DB_RECONNECT_FAILED) .arg(db_reconnect_ctl->maxRetries()); // Cancel the timer. @@ -471,7 +479,7 @@ MySqlStore::dbReconnect(ReconnectCtlPtr db_reconnect_ctl) { return (false); } - LOG_INFO(legal_log_logger, LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE) + LOG_INFO(mysql_fb_logger, LEGAL_LOG_MYSQL_DB_RECONNECT_ATTEMPT_SCHEDULE) .arg(db_reconnect_ctl->maxRetries() - db_reconnect_ctl->retriesLeft() + 1) .arg(db_reconnect_ctl->maxRetries()) .arg(db_reconnect_ctl->retryInterval()); @@ -489,5 +497,17 @@ MySqlStore::dbReconnect(ReconnectCtlPtr db_reconnect_ctl) { return (true); } -} // end of isc::legal_log namespace +bool +MySqlStore::isUnusable() { + return (unusable_); +} + +BackendStorePtr +MySqlStore::factory(const isc::db::DatabaseConnection::ParameterMap& parameters) { + LOG_INFO(mysql_fb_logger, MYSQL_FB_DB) + .arg(isc::db::DatabaseConnection::redactedAccessString(parameters)); + return (BackendStorePtr(new MySqlStore(parameters))); +} + +} // end of isc::dhcp namespace } // end of isc namespace diff --git a/src/hooks/dhcp/forensic_log/mysql_legal_log.h b/src/hooks/dhcp/mysql/mysql_legal_log.h similarity index 83% rename from src/hooks/dhcp/forensic_log/mysql_legal_log.h rename to src/hooks/dhcp/mysql/mysql_legal_log.h index a2fa86f4d1..e25c0b14a4 100644 --- a/src/hooks/dhcp/forensic_log/mysql_legal_log.h +++ b/src/hooks/dhcp/mysql/mysql_legal_log.h @@ -7,7 +7,7 @@ #ifndef MYSQL_LEGAL_LOG_H #define MYSQL_LEGAL_LOG_H -#include +#include #include #include @@ -18,7 +18,7 @@ #include namespace isc { -namespace legal_log { +namespace dhcp { // Forward declaration of the exchange object. The class is defined in // the .cc file. @@ -170,6 +170,9 @@ public: /// database has failed. virtual std::pair getVersion(const std::string& timer_name = std::string()) const; + /// @brief Local version of getDBVersion() class method. + static std::string getDBVersion(); + /// @brief Statement Tags /// /// The contents of the enum are indexes into the list of SQL statements @@ -178,6 +181,13 @@ public: NUM_STATEMENTS // Number of statements }; + /// @brief Flag which indicates if the store has at least one + /// unusable connection. + /// + /// @return true if there is at least one unusable connection, false + /// otherwise + virtual bool isUnusable(); + /// @brief Context RAII Allocator. class MySqlStoreContextAlloc { public: @@ -188,7 +198,7 @@ public: /// or creates a new one. /// /// @param store A parent instance - MySqlStoreContextAlloc(const MySqlStore& store); + MySqlStoreContextAlloc(MySqlStore& store); /// @brief Destructor /// @@ -200,7 +210,7 @@ public: private: /// @brief The store - const MySqlStore& store_; + MySqlStore& store_; }; private: @@ -228,9 +238,39 @@ private: /// @brief The pool of contexts MySqlStoreContextPoolPtr pool_; + + /// @brief Indicates if there is at least one connection that can no longer + /// be used for normal operations. + bool unusable_; + +public: + /// @brief Factory class method. + /// + /// @param parameters A data structure relating keywords and values + /// concerned with the database. + /// + /// @return The MySQL Store. + static BackendStorePtr + factory(const isc::db::DatabaseConnection::ParameterMap& parameters); }; -} // end of isc::legal_log namespace +/// @brief Initialization structure used to register and deregister MySQL Forensic Backend. +struct MySqlForensicBackendInit { + // Constructor registers + MySqlForensicBackendInit() { + BackendStoreFactory::registerBackendFactory("mysql", + MySqlStore::factory, + true, + MySqlStore::getDBVersion); + } + + // Destructor deregisters + ~MySqlForensicBackendInit() { + BackendStoreFactory::unregisterBackendFactory("mysql", true); + } +}; + +} // end of isc::dhcp namespace } // end of isc namespace #endif // MYSQL_LEGAL_LOG_H diff --git a/src/hooks/dhcp/mysql/tests/Makefile.am b/src/hooks/dhcp/mysql/tests/Makefile.am index a1d4496254..ff823e538e 100644 --- a/src/hooks/dhcp/mysql/tests/Makefile.am +++ b/src/hooks/dhcp/mysql/tests/Makefile.am @@ -4,6 +4,8 @@ AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/mysql -I$(top_srcdir)/src/hooks/dhcp/mysql AM_CPPFLAGS += $(BOOST_INCLUDES) $(MYSQL_CPPFLAGS) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" +TEST_CA_DIR = $(top_srcdir)/src/lib/asiolink/testutils/ca +AM_CPPFLAGS += -DTEST_CA_DIR=\"$(TEST_CA_DIR)\" AM_CXXFLAGS = $(KEA_CXXFLAGS) @@ -27,6 +29,7 @@ mysql_unittests_SOURCES += mysql_cb_dhcp4_unittest.cc mysql_unittests_SOURCES += mysql_cb_dhcp4_mgr_unittest.cc mysql_unittests_SOURCES += mysql_cb_dhcp6_unittest.cc mysql_unittests_SOURCES += mysql_cb_dhcp6_mgr_unittest.cc +mysql_unittests_SOURCES += mysql_forensic_unittest.cc mysql_unittests_SOURCES += mysql_host_data_source_unittest.cc mysql_unittests_SOURCES += mysql_lease_mgr_unittest.cc mysql_unittests_SOURCES += mysql_lease_extended_info_unittest.cc @@ -53,6 +56,7 @@ mysql_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la mysql_unittests_LDADD += $(top_builddir)/src/lib/mysql/testutils/libmysqltest.la mysql_unittests_LDADD += $(top_builddir)/src/lib/mysql/libkea-mysql.la mysql_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la +mysql_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la mysql_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la mysql_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la mysql_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la diff --git a/src/hooks/dhcp/mysql/tests/meson.build b/src/hooks/dhcp/mysql/tests/meson.build index 84ad47a8fa..63af5686a8 100644 --- a/src/hooks/dhcp/mysql/tests/meson.build +++ b/src/hooks/dhcp/mysql/tests/meson.build @@ -15,11 +15,15 @@ dhcp_mysql_lib_tests = executable( 'mysql_cb_dhcp6_mgr_unittest.cc', 'mysql_cb_dhcp6_unittest.cc', 'mysql_cb_impl_unittest.cc', + 'mysql_forensic_unittest.cc', 'mysql_host_data_source_unittest.cc', 'mysql_lease_extended_info_unittest.cc', 'mysql_lease_mgr_unittest.cc', 'run_unittests.cc', - dependencies: [gtest, crypto, mysql], + cpp_args: [ + f'-DTEST_CA_DIR="@TEST_CA_DIR@"', + ], + dependencies: [gtest, crypto, mysql, kea_testutils_lib], include_directories: [include_directories('.'), include_directories('..')] + INCLUDES, link_with: [dhcp_mysql_archive, libs_testutils] + LIBS_BUILT_SO_FAR, ) diff --git a/src/hooks/dhcp/forensic_log/tests/mysql_unittests.cc b/src/hooks/dhcp/mysql/tests/mysql_forensic_unittest.cc similarity index 90% rename from src/hooks/dhcp/forensic_log/tests/mysql_unittests.cc rename to src/hooks/dhcp/mysql/tests/mysql_forensic_unittest.cc index 1f6f357143..65426494a4 100644 --- a/src/hooks/dhcp/forensic_log/tests/mysql_unittests.cc +++ b/src/hooks/dhcp/mysql/tests/mysql_forensic_unittest.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -29,7 +30,7 @@ using namespace isc::data; using namespace isc::db; using namespace isc::db::test; using namespace isc::dhcp::test; -using namespace isc::legal_log; +using namespace isc::dhcp; using namespace isc::test; using namespace boost::posix_time; namespace ph = std::placeholders; @@ -422,10 +423,16 @@ class MySqlLegalLogDbLostCallbackTest : public LegalLogDbLostCallbackTest { public: /// @brief Constructor. - MySqlLegalLogDbLostCallbackTest() { } + MySqlLegalLogDbLostCallbackTest() { + BackendStoreFactory::delAllBackends(); + io_service_->poll(); + } /// @brief Destructor. - virtual ~MySqlLegalLogDbLostCallbackTest() { } + virtual ~MySqlLegalLogDbLostCallbackTest() { + BackendStoreFactory::delAllBackends(); + io_service_->poll(); + } /// @brief Prepares the class for a test. /// @@ -557,6 +564,9 @@ public: /// -# The registered DbFailedCallback was invoked after two reconnect /// attempts (once failing and second triggered by timer) virtual void testDbLostAndFailedAfterTimeoutCallback(); + + /// @brief Initializer. + MySqlForensicBackendInit init_; }; void @@ -579,19 +589,19 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredCallback() { std::shared_ptr dbr(new DbConnectionInitWithRetry()); params.emplace("retry-on-startup", "true"); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenErrorWithRetry); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenErrorWithRetry); // Verify there is no instance. - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); dbr.reset(); params = db::DatabaseConnection::parse(validConnectString()); params.emplace("retry-on-startup", "true"); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); io_service_->poll(); @@ -600,7 +610,7 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredCallback() { EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); } void @@ -623,13 +633,13 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedCallback() { std::shared_ptr dbr(new DbConnectionInitWithRetry()); params.emplace("retry-on-startup", "true"); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenErrorWithRetry); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenErrorWithRetry); // Verify there is no instance. - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); dbr.reset(); @@ -640,7 +650,7 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(1, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } void @@ -667,13 +677,13 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredAfterTimeoutCall std::shared_ptr dbr(new DbConnectionInitWithRetry()); params.emplace("retry-on-startup", "true"); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenErrorWithRetry); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenErrorWithRetry); // Verify there is no instance. - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); dbr.reset(); @@ -684,13 +694,13 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredAfterTimeoutCall EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); access = validConnectString(); access += extra; params = db::DatabaseConnection::parse(access); params.emplace("retry-on-startup", "true"); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); sleep(1); @@ -701,7 +711,7 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredAfterTimeoutCall EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); sleep(1); @@ -712,7 +722,7 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredAfterTimeoutCall EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); } void @@ -739,13 +749,13 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedAfterTimeoutCallbac std::shared_ptr dbr(new DbConnectionInitWithRetry()); params.emplace("retry-on-startup", "true"); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenErrorWithRetry); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenErrorWithRetry); // Verify there is no instance. - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); dbr.reset(); @@ -756,7 +766,7 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedAfterTimeoutCallbac EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); sleep(1); @@ -767,7 +777,7 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedAfterTimeoutCallbac EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); sleep(1); @@ -778,7 +788,7 @@ MySqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedAfterTimeoutCallbac EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(1, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } void @@ -798,10 +808,10 @@ MySqlLegalLogDbLostCallbackTest::testNoCallbackOnOpenFailure() { // Verify that a MySqlStore with no database name is rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(invalidConnectString()); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenError); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenError); io_service_->poll(); @@ -809,7 +819,7 @@ MySqlLegalLogDbLostCallbackTest::testNoCallbackOnOpenFailure() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } void @@ -836,28 +846,29 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredCallback() { // Verify that a MySqlStore with database name is not rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(validConnectString()); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); + BackendStoreFactory::setParameters(params); // Check params validity is done by open(). - EXPECT_NO_THROW_LOG(BackendStore::instance()->open()); + EXPECT_NO_THROW_LOG(BackendStoreFactory::instance()->open()); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); // Find the SQL client socket. int sql_socket = findLastSocketFd(); ASSERT_TRUE(sql_socket > last_open_socket); // Verify we can execute a query. We don't care about the answer. - ASSERT_NO_THROW_LOG(BackendStore::instance()->writeln("test", "192.2.1.100")); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance()->writeln("test", "192.2.1.100")); // Now close the sql socket out from under backend client ASSERT_EQ(0, close(sql_socket)); // A query should fail with DbConnectionUnusable. - ASSERT_THROW(BackendStore::instance()->writeln("test", "192.2.1.101");, + ASSERT_THROW(BackendStoreFactory::instance()->writeln("test", "192.2.1.101");, DbConnectionUnusable); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); io_service_->poll(); @@ -866,7 +877,7 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredCallback() { EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); } void @@ -893,31 +904,31 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndFailedCallback() { // Verify that a MySqlStore with database name is not rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(validConnectString()); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); // Check params validity is done by open(). - EXPECT_NO_THROW_LOG(BackendStore::instance()->open()); + EXPECT_NO_THROW_LOG(BackendStoreFactory::instance()->open()); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); // Find the SQL client socket. int sql_socket = findLastSocketFd(); ASSERT_TRUE(sql_socket > last_open_socket); // Verify we can execute a query. We don't care about the answer. - ASSERT_NO_THROW_LOG(BackendStore::instance()->writeln("test", "192.2.1.100")); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance()->writeln("test", "192.2.1.100")); params = db::DatabaseConnection::parse(invalidConnectString()); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); // Now close the sql socket out from under backend client ASSERT_EQ(0, close(sql_socket)); // A query should fail with DbConnectionUnusable. - ASSERT_THROW(BackendStore::instance()->writeln("test", "192.2.1.101"), + ASSERT_THROW(BackendStoreFactory::instance()->writeln("test", "192.2.1.101"), DbConnectionUnusable); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); io_service_->poll(); @@ -926,7 +937,7 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndFailedCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(1, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } void @@ -957,33 +968,34 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() { // Verify that a MySqlStore with database name is not rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(access); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); + BackendStoreFactory::setParameters(params); // Check params validity is done by open(). - EXPECT_NO_THROW_LOG(BackendStore::instance()->open()); + EXPECT_NO_THROW_LOG(BackendStoreFactory::instance()->open()); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); // Find the SQL client socket. int sql_socket = findLastSocketFd(); ASSERT_TRUE(sql_socket > last_open_socket); // Verify we can execute a query. We don't care about the answer. - ASSERT_NO_THROW_LOG(BackendStore::instance()->writeln("test", "192.2.1.100")); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance()->writeln("test", "192.2.1.100")); access = invalidConnectString(); access += extra; params = db::DatabaseConnection::parse(access); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); // Now close the sql socket out from under backend client ASSERT_EQ(0, close(sql_socket)); // A query should fail with DbConnectionUnusable. - ASSERT_THROW(BackendStore::instance()->writeln("test", "192.2.1.101"), + ASSERT_THROW(BackendStoreFactory::instance()->writeln("test", "192.2.1.101"), DbConnectionUnusable); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); io_service_->poll(); @@ -992,12 +1004,12 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); access = validConnectString(); access += extra; params = db::DatabaseConnection::parse(access); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); sleep(1); @@ -1008,7 +1020,7 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() { EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); sleep(1); @@ -1019,7 +1031,7 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() { EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); } void @@ -1050,33 +1062,33 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() { // Verify that a MySqlStore with database name is not rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(access); - ASSERT_NO_THROW_LOG(BackendStore::instance().reset(new MySqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new MySqlStore(params))); // Check params validity is done by open(). - EXPECT_NO_THROW_LOG(BackendStore::instance()->open()); + EXPECT_NO_THROW_LOG(BackendStoreFactory::instance()->open()); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); // Find the SQL client socket. int sql_socket = findLastSocketFd(); ASSERT_TRUE(sql_socket > last_open_socket); // Verify we can execute a query. We don't care about the answer. - ASSERT_NO_THROW_LOG(BackendStore::instance()->writeln("test", "192.2.1.100")); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance()->writeln("test", "192.2.1.100")); access = invalidConnectString(); access += extra; params = db::DatabaseConnection::parse(access); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); // Now close the sql socket out from under backend client ASSERT_EQ(0, close(sql_socket)); // A query should fail with DbConnectionUnusable. - ASSERT_THROW(BackendStore::instance()->writeln("test", "192.2.1.101"), + ASSERT_THROW(BackendStoreFactory::instance()->writeln("test", "192.2.1.101"), DbConnectionUnusable); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); io_service_->poll(); @@ -1085,7 +1097,7 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); sleep(1); @@ -1096,7 +1108,7 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); sleep(1); @@ -1107,7 +1119,7 @@ MySqlLegalLogDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(1, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } /// @brief Verifies that loss of connectivity to MySQL is handled correctly. diff --git a/src/hooks/dhcp/mysql/tests/test_utils.h b/src/hooks/dhcp/mysql/tests/test_utils.h new file mode 100644 index 0000000000..bf1d9f589e --- /dev/null +++ b/src/hooks/dhcp/mysql/tests/test_utils.h @@ -0,0 +1,321 @@ +// Copyright (C) 2016-2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +#ifndef TEST_UTILS_H +#define TEST_UTILS_H + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace isc; +using namespace hooks; +using namespace dhcp; + +namespace isc { +namespace dhcp { + +static isc::asiolink::IOServicePtr local_io_service = isc::asiolink::IOServicePtr(new isc::asiolink::IOService()); + +/// @brief Helper class to execute a SQL tool and get results +class runSQL { +public: + /// @brief Constructor + runSQL() { + reset(); + } + + /// @brief Destructor + virtual ~runSQL() = default; + + /// @brief Reset everything + void reset() { + command_ = ""; + query_ = ""; + result_ = 0; + output_.clear(); + } + + /// @brief Get command + /// @return the command + const std::string& getCommand() const { + return (command_); + } + + /// @brief Set command + /// @param the command + void setCommand(const std::string& command) { + command_ = command; + } + + /// @brief Get query + /// @return the query + const std::string& getQuery() const { + return (query_); + } + + /// @brief Set query + /// @param the query + void setQuery(const std::string& query) { + query_ = query; + } + + /// @brief Get result + /// @return the exit code + int getResult() const { + return (result_); + } + + /// @brief Get raw output + /// @return the unprocessed output + std::vector getRawOutput() { + return (output_); + } + + /// @brief Process output + /// @param output reference to the string vector to fill with output + /// @return true if processing was successful + virtual bool getOutput(std::vector& output) = 0; + + /// @brief Execute + /// @throw std::system_error according to boost documentation + void execute() { + // Reading stream + FILE* istream; + size_t buffer_size = 1024; + char buffer[buffer_size + 1]; + + // Child process + std::string cmd = command_ + "\"" + query_ + "\""; + istream = popen(cmd.c_str(), "r"); + + // Check errors + if (!istream) { + result_ = -1; + return; + } else { + result_ = ferror(istream); + if (result_) { + return; + } + } + + // Read output + while (!feof(istream) && fgets(buffer, buffer_size, istream)) { + output_.push_back(std::string(buffer)); + } + + // Wait for children to terminate and get its exit code + result_ = pclose(istream); + } + +private: + // @brief command + std::string command_; + + // @brief query + std::string query_; + + // @brief exit code aka result + int result_; + + // @brief output + std::vector output_; +}; + +/// @brief Test fixture for testing database backend connection recovery. +class LegalLogDbLostCallbackTest : public ::testing::Test { +public: + + /// @brief Constructor. + LegalLogDbLostCallbackTest() + : db_lost_callback_called_(0), db_recovered_callback_called_(0), + db_failed_callback_called_(0), + io_service_(local_io_service) { + isc::db::DatabaseConnection::db_lost_callback_ = 0; + isc::db::DatabaseConnection::db_recovered_callback_ = 0; + isc::db::DatabaseConnection::db_failed_callback_ = 0; + isc::db::DatabaseConnection::setIOService(io_service_); + BackendStoreFactory::setIOService(io_service_); + isc::dhcp::TimerMgr::instance()->setIOService(io_service_); + BackendStoreFactory::delAllBackends(); + } + + /// @brief Destructor. + virtual ~LegalLogDbLostCallbackTest() { + isc::db::DatabaseConnection::db_lost_callback_ = 0; + isc::db::DatabaseConnection::db_recovered_callback_ = 0; + isc::db::DatabaseConnection::db_failed_callback_ = 0; + isc::db::DatabaseConnection::setIOService(isc::asiolink::IOServicePtr()); + BackendStoreFactory::setIOService(isc::asiolink::IOServicePtr()); + isc::dhcp::TimerMgr::instance()->unregisterTimers(); + BackendStoreFactory::delAllBackends(); + } + + /// @brief Prepares the class for a test. + /// + /// Invoked by gtest prior test entry, we create the + /// appropriate schema and create a basic DB manager to + /// wipe out any prior instance + virtual void SetUp() = 0; + + /// @brief Pre-text exit clean up + /// + /// Invoked by gtest upon test exit, we destroy the schema + /// we created. + virtual void TearDown() = 0; + + /// @brief Method which returns the back end specific connection + /// string + virtual std::string validConnectString() = 0; + + /// @brief Method which returns invalid back end specific connection + /// string + virtual std::string invalidConnectString() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection can not be + /// established but succeeds on retry + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies that connectivity is unavailable + /// and then recovered on retry: + /// -# The registered DbLostCallback was invoked + /// -# The registered DbRecoveredCallback was invoked + virtual void testRetryOpenDbLostAndRecoveredCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection can not be + /// established but fails on retry + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies that connectivity is unavailable + /// and then fails again on retry: + /// -# The registered DbLostCallback was invoked + /// -# The registered DbFailedCallback was invoked + virtual void testRetryOpenDbLostAndFailedCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection can not be + /// established but succeeds on retry + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies that connectivity is unavailable + /// and then recovered on retry: + /// -# The registered DbLostCallback was invoked + /// -# The registered DbRecoveredCallback was invoked after two reconnect + /// attempts (once failing and second triggered by timer) + virtual void testRetryOpenDbLostAndRecoveredAfterTimeoutCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection can not be + /// established but fails on retry + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies that connectivity is unavailable + /// and then fails again on retry: + /// -# The registered DbLostCallback was invoked + /// -# The registered DbFailedCallback was invoked after two reconnect + /// attempts (once failing and second triggered by timer) + virtual void testRetryOpenDbLostAndFailedAfterTimeoutCallback() = 0; + + /// @brief Verifies open failures do NOT invoke db lost callback + /// + /// The db lost callback should only be invoked after successfully + /// opening the DB and then subsequently losing it. Failing to + /// open should be handled directly by the application layer. + virtual void testNoCallbackOnOpenFailure() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection is lost + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies connectivity by issuing a known + /// valid query. Next it simulates connectivity lost by identifying and + /// closing the socket connection to the Backend Store. It then reissues the + /// query and verifies that: + /// -# The Query throws DbOperationError (rather than exiting) + /// -# The registered DbLostCallback was invoked + /// -# The registered DbRecoveredCallback was invoked + virtual void testDbLostAndRecoveredCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection is lost + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies connectivity by issuing a known + /// valid query. Next it simulates connectivity lost by identifying and + /// closing the socket connection to the Backend Store. It then reissues the + /// query and verifies that: + /// -# The Query throws DbOperationError (rather than exiting) + /// -# The registered DbLostCallback was invoked + /// -# The registered DbFailedCallback was invoked + virtual void testDbLostAndFailedCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection is lost + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies connectivity by issuing a known + /// valid query. Next it simulates connectivity lost by identifying and + /// closing the socket connection to the Backend Store. It then reissues the + /// query and verifies that: + /// -# The Query throws DbOperationError (rather than exiting) + /// -# The registered DbLostCallback was invoked + /// -# The registered DbRecoveredCallback was invoked after two reconnect + /// attempts (once failing and second triggered by timer) + virtual void testDbLostAndRecoveredAfterTimeoutCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection is lost + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies connectivity by issuing a known + /// valid query. Next it simulates connectivity lost by identifying and + /// closing the socket connection to the Backend Store. It then reissues the + /// query and verifies that: + /// -# The Query throws DbOperationError (rather than exiting) + /// -# The registered DbLostCallback was invoked + /// -# The registered DbFailedCallback was invoked after two reconnect + /// attempts (once failing and second triggered by timer) + virtual void testDbLostAndFailedAfterTimeoutCallback() = 0; + + /// @brief Callback function registered with the Backend Store + bool db_lost_callback(util::ReconnectCtlPtr /* not_used */) { + return (++db_lost_callback_called_); + } + + /// @brief Flag used to detect calls to db_lost_callback function + uint32_t db_lost_callback_called_; + + /// @brief Callback function registered with the Backend Store + bool db_recovered_callback(util::ReconnectCtlPtr /* not_used */) { + return (++db_recovered_callback_called_); + } + + /// @brief Flag used to detect calls to db_recovered_callback function + uint32_t db_recovered_callback_called_; + + /// @brief Callback function registered with the Backend Store + bool db_failed_callback(util::ReconnectCtlPtr /* not_used */) { + return (++db_failed_callback_called_); + } + + /// @brief Flag used to detect calls to db_failed_callback function + uint32_t db_failed_callback_called_; + + /// The IOService object, used for all ASIO operations. + isc::asiolink::IOServicePtr io_service_; +}; + +} // namespace dhcp +} // namespace isc + +#endif // TEST_UTILS_H diff --git a/src/hooks/dhcp/pgsql/.gitattributes b/src/hooks/dhcp/pgsql/.gitattributes index 13f22495b3..fcc631dabc 100644 --- a/src/hooks/dhcp/pgsql/.gitattributes +++ b/src/hooks/dhcp/pgsql/.gitattributes @@ -1,5 +1,7 @@ /pgsql_cb_messages.cc -diff merge=ours /pgsql_cb_messages.h -diff merge=ours +/pgsql_fb_messages.cc -diff merge=ours +/pgsql_fb_messages.h -diff merge=ours /pgsql_hb_messages.cc -diff merge=ours /pgsql_hb_messages.h -diff merge=ours /pgsql_lb_messages.cc -diff merge=ours diff --git a/src/hooks/dhcp/pgsql/Makefile.am b/src/hooks/dhcp/pgsql/Makefile.am index 2a4e43b442..bf4b1aaf6b 100644 --- a/src/hooks/dhcp/pgsql/Makefile.am +++ b/src/hooks/dhcp/pgsql/Makefile.am @@ -6,6 +6,7 @@ AM_CXXFLAGS = $(KEA_CXXFLAGS) # Ensure that the message file is included in the distribution EXTRA_DIST = pgsql_cb_messages.mes +EXTRA_DIST += pgsql_fb_messages.mes EXTRA_DIST += pgsql_hb_messages.mes EXTRA_DIST += pgsql_lb_messages.mes @@ -21,6 +22,8 @@ libpgsql_la_SOURCES += pgsql_cb_dhcp6.cc pgsql_cb_dhcp6.h libpgsql_la_SOURCES += pgsql_cb_impl.cc pgsql_cb_impl.h libpgsql_la_SOURCES += pgsql_cb_messages.cc pgsql_cb_messages.h libpgsql_la_SOURCES += pgsql_cb_log.cc pgsql_cb_log.h +libpgsql_la_SOURCES += pgsql_fb_messages.cc pgsql_fb_messages.h +libpgsql_la_SOURCES += pgsql_fb_log.cc pgsql_fb_log.h libpgsql_la_SOURCES += pgsql_query_macros_dhcp.h libpgsql_la_SOURCES += pgsql_hb_log.cc pgsql_hb_log.h libpgsql_la_SOURCES += pgsql_hb_messages.cc pgsql_hb_messages.h @@ -28,6 +31,7 @@ libpgsql_la_SOURCES += pgsql_host_data_source.cc pgsql_host_data_source.h libpgsql_la_SOURCES += pgsql_lb_log.cc pgsql_lb_log.h libpgsql_la_SOURCES += pgsql_lb_messages.cc pgsql_lb_messages.h libpgsql_la_SOURCES += pgsql_lease_mgr.cc pgsql_lease_mgr.h +libpgsql_la_SOURCES += pgsql_legal_log.cc pgsql_legal_log.h libpgsql_la_SOURCES += version.cc libpgsql_la_CXXFLAGS = $(AM_CXXFLAGS) @@ -72,6 +76,7 @@ libdhcp_pgsql_la_LIBADD += $(BOOST_LIBS) # reconfigure, a new target messages-clean has been added. maintainer-clean-local: rm -f pgsql_cb_messages.h pgsql_cb_messages.cc + rm -f pgsql_fb_messages.h pgsql_fb_messages.cc rm -f pgsql_hb_messages.h pgsql_hb_messages.cc rm -f pgsql_lb_messages.h pgsql_lb_messages.cc @@ -87,6 +92,7 @@ if GENERATE_MESSAGES # Define rule to build logging source files from message file messages: pgsql_cb_messages.h pgsql_cb_messages.cc \ + pgsql_fb_messages.h pgsql_fb_messages.cc \ pgsql_hb_messages.h pgsql_hb_messages.cc \ pgsql_lb_messages.h pgsql_lb_messages.cc @echo Message files regenerated @@ -95,6 +101,10 @@ pgsql_cb_messages.h pgsql_cb_messages.cc: pgsql_cb_messages.mes (cd $(top_srcdir); \ $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/pgsql/pgsql_cb_messages.mes) +pgsql_fb_messages.h pgsql_fb_messages.cc: pgsql_fb_messages.mes + (cd $(top_srcdir); \ + $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/pgsql/pgsql_fb_messages.mes) + pgsql_hb_messages.h pgsql_hb_messages.cc: pgsql_hb_messages.mes (cd $(top_srcdir); \ $(abs_top_builddir)/src/lib/log/compiler/kea-msg-compiler src/hooks/dhcp/pgsql/pgsql_hb_messages.mes) @@ -106,6 +116,7 @@ pgsql_lb_messages.h pgsql_lb_messages.cc: pgsql_lb_messages.mes else messages: pgsql_cb_messages.h pgsql_cb_messages.cc \ + pgsql_fb_messages.h pgsql_fb_messages.cc \ pgsql_hb_messages.h pgsql_hb_messages.cc \ pgsql_lb_messages.h pgsql_lb_messages.cc @echo Messages generation disabled. Configure with --enable-generate-messages to enable it. diff --git a/src/hooks/dhcp/pgsql/meson.build b/src/hooks/dhcp/pgsql/meson.build index 5da54562b7..ec3611d384 100644 --- a/src/hooks/dhcp/pgsql/meson.build +++ b/src/hooks/dhcp/pgsql/meson.build @@ -10,12 +10,15 @@ dhcp_pgsql_lib = shared_library( 'pgsql_cb_impl.cc', 'pgsql_cb_log.cc', 'pgsql_cb_messages.cc', + 'pgsql_fb_log.cc', + 'pgsql_fb_messages.cc', 'pgsql_hb_log.cc', 'pgsql_hb_messages.cc', 'pgsql_host_data_source.cc', 'pgsql_lb_log.cc', 'pgsql_lb_messages.cc', 'pgsql_lease_mgr.cc', + 'pgsql_legal_log.cc', 'version.cc', dependencies: [crypto, postgresql], include_directories: [include_directories('.')] + INCLUDES, @@ -42,6 +45,16 @@ if KEA_MSG_COMPILER.found() ], ) TARGETS_GEN_MESSAGES += [target_gen_messages] + target_gen_messages = run_target( + 'src-hooks-dhcp-pgsql-pgsql_fb_messages', + command: [ + CD_AND_RUN, + TOP_SOURCE_DIR, + KEA_MSG_COMPILER, + 'src/hooks/dhcp/pgsql/pgsql_fb_messages.mes', + ], + ) + TARGETS_GEN_MESSAGES += [target_gen_messages] target_gen_messages = run_target( 'src-hooks-dhcp-pgsql-pgsql_hb_messages', command: [ diff --git a/src/hooks/dhcp/pgsql/pgsql_callouts.cc b/src/hooks/dhcp/pgsql/pgsql_callouts.cc index 9e02d6ec08..bc40d12f4c 100644 --- a/src/hooks/dhcp/pgsql/pgsql_callouts.cc +++ b/src/hooks/dhcp/pgsql/pgsql_callouts.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -63,6 +64,12 @@ int load(LibraryHandle& /* handle */) { PgSqlConfigBackendDHCPv4::registerBackendType(); PgSqlConfigBackendDHCPv6::registerBackendType(); + // Register PostgreSQL FB factories with Backend Store managers. + BackendStoreFactory::registerBackendFactory("postgresql", + PgSqlStore::factory, + true, + PgSqlStore::getDBVersion); + // Register PostgreSQL HB factories with Host Managers HostDataSourceFactory::registerFactory("postgresql", PgSqlHostDataSource::factory, @@ -116,6 +123,9 @@ int unload() { PgSqlConfigBackendImpl::setIOService(IOServicePtr()); } + // Unregister the factories and remove PostgreSQL backends + BackendStoreFactory::unregisterBackendFactory("postgresql", true); + // Unregister the factories and remove PostgreSQL backends HostDataSourceFactory::deregisterFactory("postgresql", true); diff --git a/src/hooks/dhcp/pgsql/pgsql_fb_log.cc b/src/hooks/dhcp/pgsql/pgsql_fb_log.cc new file mode 100644 index 0000000000..7e14279bb4 --- /dev/null +++ b/src/hooks/dhcp/pgsql/pgsql_fb_log.cc @@ -0,0 +1,41 @@ +// Copyright (C) 2019-2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include + +using namespace isc::db; + +namespace isc { +namespace dhcp { + +extern const int PGSQL_FB_DBG_TRACE = isc::log::DBGLVL_TRACE_BASIC; +extern const int PGSQL_FB_DBG_RESULTS = isc::log::DBGLVL_TRACE_BASIC_DATA; +extern const int PGSQL_FB_DBG_TRACE_DETAIL = isc::log::DBGLVL_TRACE_DETAIL; +extern const int PGSQL_FB_DBG_TRACE_DETAIL_DATA = + isc::log::DBGLVL_TRACE_DETAIL_DATA; +extern const int PGSQL_FB_DBG_HOOKS = isc::log::DBGLVL_TRACE_BASIC; + +isc::log::Logger pgsql_fb_logger("pgsql-fb-hooks"); + +isc::log::Logger pgsql_legal_log_logger("pgsql-legal-log"); + +const isc::db::DbLogger::MessageMap pgsql_legal_log_db_message_map = { + { DB_INVALID_ACCESS, LEGAL_LOG_PGSQL_INVALID_ACCESS }, + + { PGSQL_DEALLOC_ERROR, LEGAL_LOG_PGSQL_DEALLOC_ERROR }, + { PGSQL_FATAL_ERROR, LEGAL_LOG_PGSQL_FATAL_ERROR }, + { PGSQL_START_TRANSACTION, LEGAL_LOG_PGSQL_START_TRANSACTION }, + { PGSQL_COMMIT, LEGAL_LOG_PGSQL_COMMIT }, + { PGSQL_ROLLBACK, LEGAL_LOG_PGSQL_ROLLBACK }, +}; + +isc::db::DbLogger pgsql_legal_log_db_logger(pgsql_legal_log_logger, pgsql_legal_log_db_message_map); + +} // namespace dhcp +} // namespace isc diff --git a/src/hooks/dhcp/pgsql/pgsql_fb_log.h b/src/hooks/dhcp/pgsql/pgsql_fb_log.h new file mode 100644 index 0000000000..14c6b07f7d --- /dev/null +++ b/src/hooks/dhcp/pgsql/pgsql_fb_log.h @@ -0,0 +1,72 @@ +// Copyright (C) 2019-2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef PGSQL_FB_LOG_H +#define PGSQL_FB_LOG_H + +#include +#include +#include +#include + +namespace isc { +namespace dhcp { + +///@{ +/// @brief PostgreSQL Forensic Mgr logging levels +/// +/// Defines the levels used to output debug messages in the PostgreSQL Forensic Mgr +/// Note that higher numbers equate to more verbose (and detailed) output. + +/// @brief Traces normal operations +/// +/// E.g. sending a query to the database etc. +extern const int PGSQL_FB_DBG_TRACE; + +/// @brief Records the results of the lookups +/// +/// Using the example of tracing queries from the backend database, this will +/// just record the summary results. +extern const int PGSQL_FB_DBG_RESULTS; + +/// @brief Additional information +/// +/// Record detailed tracing. This is generally reserved for tracing access to +/// the forensic database. +extern const int PGSQL_FB_DBG_TRACE_DETAIL; + +/// @brief Additional information +/// +/// Record detailed (and verbose) data on the server. +extern const int PGSQL_FB_DBG_TRACE_DETAIL_DATA; + +// Trace hook related operations +extern const int PGSQL_FB_DBG_HOOKS; +///@} + +/// @brief PostgreSQL Forensic Mgr Logger +/// +/// Define the logger used to log messages. We could define it in multiple +/// modules, but defining in a single module and linking to it saves time and +/// space. +extern isc::log::Logger pgsql_fb_logger; + +/// @brief Legal Log Logger +/// +/// Define the logger used to log messages. We could define it in multiple +/// modules, but defining in a single module and linking to it saves time and +/// space. +extern isc::log::Logger pgsql_legal_log_logger; + +/// @brief Legal log database Logger +/// +/// It is the default database logger. +extern isc::db::DbLogger pgsql_legal_log_db_logger; + +} // namespace dhcp +} // namespace isc + +#endif diff --git a/src/hooks/dhcp/pgsql/pgsql_fb_messages.cc b/src/hooks/dhcp/pgsql/pgsql_fb_messages.cc new file mode 100644 index 0000000000..214799d775 --- /dev/null +++ b/src/hooks/dhcp/pgsql/pgsql_fb_messages.cc @@ -0,0 +1,51 @@ +// File created from src/hooks/dhcp/pgsql/pgsql_fb_messages.mes + +#include +#include +#include + +namespace isc { +namespace dhcp { + +extern const isc::log::MessageID LEGAL_LOG_PGSQL_COMMIT = "LEGAL_LOG_PGSQL_COMMIT"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED = "LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE = "LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED = "LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_DEALLOC_ERROR = "LEGAL_LOG_PGSQL_DEALLOC_ERROR"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_FATAL_ERROR = "LEGAL_LOG_PGSQL_FATAL_ERROR"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_GET_VERSION = "LEGAL_LOG_PGSQL_GET_VERSION"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_INSERT_LOG = "LEGAL_LOG_PGSQL_INSERT_LOG"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_INVALID_ACCESS = "LEGAL_LOG_PGSQL_INVALID_ACCESS"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_NO_TLS_SUPPORT = "LEGAL_LOG_PGSQL_NO_TLS_SUPPORT"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_ROLLBACK = "LEGAL_LOG_PGSQL_ROLLBACK"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_START_TRANSACTION = "LEGAL_LOG_PGSQL_START_TRANSACTION"; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_TLS_SUPPORT = "LEGAL_LOG_PGSQL_TLS_SUPPORT"; +extern const isc::log::MessageID PGSQL_FB_DB = "PGSQL_FB_DB"; + +} // namespace dhcp +} // namespace isc + +namespace { + +const char* values[] = { + "LEGAL_LOG_PGSQL_COMMIT", "committing to PostgreSQL database", + "LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED", "database reconnect failed: %1", + "LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE", "scheduling attempt %1 of %2 in %3 milliseconds", + "LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED", "maximum number of database reconnect attempts: %1, has been exhausted without success", + "LEGAL_LOG_PGSQL_DEALLOC_ERROR", "An error occurred deallocating SQL statements while closing the PostgreSQL log database: %1", + "LEGAL_LOG_PGSQL_FATAL_ERROR", "Unrecoverable PostgreSQL error occurred: Statement: <%1>, reason: %2 (error code: %3).", + "LEGAL_LOG_PGSQL_GET_VERSION", "obtaining schema version information", + "LEGAL_LOG_PGSQL_INSERT_LOG", "Adding a log entry to the database: %1", + "LEGAL_LOG_PGSQL_INVALID_ACCESS", "invalid database access string: %1", + "LEGAL_LOG_PGSQL_NO_TLS_SUPPORT", "Attempt to configure TLS (unsupported for PostgreSQL): %1", + "LEGAL_LOG_PGSQL_ROLLBACK", "rolling back PostgreSQL database", + "LEGAL_LOG_PGSQL_START_TRANSACTION", "starting a new PostgreSQL transaction", + "LEGAL_LOG_PGSQL_TLS_SUPPORT", "Attempt to configure TLS: %1", + "PGSQL_FB_DB", "opening PostgreSQL log database: %1", + NULL +}; + +const isc::log::MessageInitializer initializer(values); + +} // Anonymous namespace + diff --git a/src/hooks/dhcp/pgsql/pgsql_fb_messages.h b/src/hooks/dhcp/pgsql/pgsql_fb_messages.h new file mode 100644 index 0000000000..13893b4191 --- /dev/null +++ b/src/hooks/dhcp/pgsql/pgsql_fb_messages.h @@ -0,0 +1,29 @@ +// File created from src/hooks/dhcp/pgsql/pgsql_fb_messages.mes + +#ifndef PGSQL_FB_MESSAGES_H +#define PGSQL_FB_MESSAGES_H + +#include + +namespace isc { +namespace dhcp { + +extern const isc::log::MessageID LEGAL_LOG_PGSQL_COMMIT; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_DEALLOC_ERROR; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_FATAL_ERROR; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_GET_VERSION; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_INSERT_LOG; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_INVALID_ACCESS; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_NO_TLS_SUPPORT; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_ROLLBACK; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_START_TRANSACTION; +extern const isc::log::MessageID LEGAL_LOG_PGSQL_TLS_SUPPORT; +extern const isc::log::MessageID PGSQL_FB_DB; + +} // namespace dhcp +} // namespace isc + +#endif // PGSQL_FB_MESSAGES_H diff --git a/src/hooks/dhcp/pgsql/pgsql_fb_messages.mes b/src/hooks/dhcp/pgsql/pgsql_fb_messages.mes new file mode 100644 index 0000000000..ec328cb025 --- /dev/null +++ b/src/hooks/dhcp/pgsql/pgsql_fb_messages.mes @@ -0,0 +1,81 @@ +# Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +$NAMESPACE isc::dhcp + +% LEGAL_LOG_PGSQL_COMMIT committing to PostgreSQL database +The code has issued a commit call. All outstanding transactions will be +committed to the database. Note that depending on the PostgreSQL settings, +the committal may not include a write to disk. + +% PGSQL_FB_DB opening PostgreSQL log database: %1 +This informational message is logged when a legal log hook library is +about to open a PostgreSQL log database. The parameters of the +connection including database name and username needed to access it +(but not the password if any) are logged. + +% LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED database reconnect failed: %1 +An error message issued when an attempt to reconnect has failed. + +% LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE scheduling attempt %1 of %2 in %3 milliseconds +An info message issued when the server is scheduling the next attempt to reconnect +to the database. This occurs when the server has lost database connectivity and +is attempting to reconnect automatically. + +% LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED maximum number of database reconnect attempts: %1, has been exhausted without success +An error message issued when the server failed to reconnect. Loss of connectivity +is typically a network or database server issue. + +% LEGAL_LOG_PGSQL_DEALLOC_ERROR An error occurred deallocating SQL statements while closing the PostgreSQL log database: %1 +This is an error message issued when a legal log hook library experienced +and error freeing database SQL resources as part of closing its connection to +the PostgreSQL database. The connection is closed as part of normal server +shutdown. This error is most likely a programmatic issue that is highly +unlikely to occur or negatively impact server operation. + +% LEGAL_LOG_PGSQL_FATAL_ERROR Unrecoverable PostgreSQL error occurred: Statement: <%1>, reason: %2 (error code: %3). +An error message indicating that communication with the PostgreSQL database server +has been lost. When this occurs the server exits immediately with a non-zero +exit code. This is most likely due to a network issue. + +% LEGAL_LOG_PGSQL_GET_VERSION obtaining schema version information +Logged at debug log level 50. +A debug message issued when the server is about to obtain schema version +information from the PostgreSQL database. + +% LEGAL_LOG_PGSQL_INSERT_LOG Adding a log entry to the database: %1 +Logged at debug log level 50. +An informational message logged when a log entry is inserted. + +% LEGAL_LOG_PGSQL_INVALID_ACCESS invalid database access string: %1 +This is logged when an attempt has been made to parse a database access string +and the attempt ended in error. The access string in question - which +should be of the form 'keyword=value keyword=value...' is included in +the message. + +% LEGAL_LOG_PGSQL_NO_TLS_SUPPORT Attempt to configure TLS (unsupported for PostgreSQL): %1 +This error message is printed when TLS support was required in the Kea +configuration: Kea was built with this feature disabled for PostgreSQL. +The parameters of the connection are logged. + +% LEGAL_LOG_PGSQL_ROLLBACK rolling back PostgreSQL database +The code has issued a rollback call. All outstanding transaction will +be rolled back and not committed to the database. + +% LEGAL_LOG_PGSQL_START_TRANSACTION starting a new PostgreSQL transaction +A debug message issued when a new PostgreSQL transaction is being started. +This message is typically not issued when inserting data into a +single table because the server doesn't explicitly start +transactions in this case. This message is issued when data is +inserted into multiple tables with multiple INSERT statements +and there may be a need to rollback the whole transaction if +any of these INSERT statements fail. + +% LEGAL_LOG_PGSQL_TLS_SUPPORT Attempt to configure TLS: %1 +This informational message is printed when TLS support was required in +the Kea configuration: The TLS support in PostgreSQL will be initialized but +its configuration is fully managed outside the C API. +The parameters of the connection are logged. diff --git a/src/hooks/dhcp/forensic_log/pgsql_legal_log.cc b/src/hooks/dhcp/pgsql/pgsql_legal_log.cc similarity index 82% rename from src/hooks/dhcp/forensic_log/pgsql_legal_log.cc rename to src/hooks/dhcp/pgsql/pgsql_legal_log.cc index d8b1316ccb..18726fcecd 100644 --- a/src/hooks/dhcp/forensic_log/pgsql_legal_log.cc +++ b/src/hooks/dhcp/pgsql/pgsql_legal_log.cc @@ -6,8 +6,10 @@ #include -#include +#include #include +#include +#include #include #include #include @@ -44,7 +46,7 @@ PgSqlTaggedStatement tagged_statements[] = { }; namespace isc { -namespace legal_log { +namespace dhcp { /// @brief Supports exchanging log entries with PostgreSQL. class PgSqlLegLExchange : public PgSqlExchange { @@ -123,7 +125,7 @@ PgSqlStoreContext::PgSqlStoreContext(const DatabaseConnection::ParameterMap& par // PgSqlStoreContextAlloc Constructor and Destructor -PgSqlStore::PgSqlStoreContextAlloc::PgSqlStoreContextAlloc(const PgSqlStore& store) +PgSqlStore::PgSqlStoreContextAlloc::PgSqlStoreContextAlloc(PgSqlStore& store) : ctx_(), store_(store) { if (MultiThreadingMgr::instance().getMode()) { @@ -153,14 +155,18 @@ PgSqlStore::PgSqlStoreContextAlloc::~PgSqlStoreContextAlloc() { // multi-threaded lock_guard lock(store_.pool_->mutex_); store_.pool_->pool_.push_back(ctx_); + if (ctx_->conn_.isUnusable()) { + store_.unusable_ = true; + } + } else if (ctx_->conn_.isUnusable()) { + store_.unusable_ = true; } - // If running in single-threaded mode, there's nothing to do here. } // PgSqlStore PgSqlStore::PgSqlStore(const DatabaseConnection::ParameterMap& parameters) - : timer_name_("") { + : BackendStore(parameters), timer_name_(""), unusable_(false) { // Store connection parameters. BackendStore::setParameters(parameters); @@ -172,7 +178,7 @@ PgSqlStore::PgSqlStore(const DatabaseConnection::ParameterMap& parameters) } void PgSqlStore::open() { - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(pgsql_legal_log_db_logger); // Check TLS support. size_t tls(0); @@ -184,13 +190,13 @@ void PgSqlStore::open() { #ifdef HAVE_PGSQL_SSL if ((tls > 0) && !PgSqlConnection::warned_about_tls) { PgSqlConnection::warned_about_tls = true; - LOG_INFO(legal_log_logger, LEGAL_LOG_PGSQL_TLS_SUPPORT) + LOG_INFO(pgsql_fb_logger, LEGAL_LOG_PGSQL_TLS_SUPPORT) .arg(DatabaseConnection::redactedAccessString(parameters)); PQinitSSL(1); } #else if (tls > 0) { - LOG_ERROR(legal_log_logger, LEGAL_LOG_PGSQL_NO_TLS_SUPPORT) + LOG_ERROR(pgsql_fb_logger, LEGAL_LOG_PGSQL_NO_TLS_SUPPORT) .arg(DatabaseConnection::redactedAccessString(parameters)); isc_throw(DbOpenError, "Attempt to configure TLS for PostgreSQL " << "backend (built with this feature disabled)"); @@ -201,8 +207,8 @@ void PgSqlStore::open() { pair code_version(PGSQL_SCHEMA_VERSION_MAJOR, PGSQL_SCHEMA_VERSION_MINOR); - BackendStorePtr store = BackendStore::instance(); - BackendStore::instance().reset(); + BackendStorePtr store = BackendStoreFactory::instance(); + BackendStoreFactory::instance().reset(); string timer_name; bool retry = false; @@ -224,7 +230,7 @@ void PgSqlStore::open() { << db_version.second); } - BackendStore::instance() = store; + BackendStoreFactory::instance() = store; // Create an initial context. pool_.reset(new PgSqlStoreContextPool()); @@ -235,8 +241,8 @@ void PgSqlStore::open() { PgSqlStoreContextPtr PgSqlStore::createContext() const { - PgSqlStoreContextPtr ctx(new PgSqlStoreContext(BackendStore::getParameters(), - IOServiceAccessorPtr(new IOServiceAccessor(&PgSqlStore::getIOService)), + PgSqlStoreContextPtr ctx(new PgSqlStoreContext(getParameters(), + IOServiceAccessorPtr(new IOServiceAccessor(&DatabaseConnection::getIOService)), &PgSqlStore::dbReconnect)); // Open the database. @@ -275,10 +281,10 @@ PgSqlStore::writeln(const string& text, const std::string& addr) { return; } - LOG_DEBUG(legal_log_logger, DB_DBG_TRACE_DETAIL, + LOG_DEBUG(pgsql_fb_logger, DB_DBG_TRACE_DETAIL, LEGAL_LOG_PGSQL_INSERT_LOG).arg(text); - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(pgsql_legal_log_db_logger); // Get a context PgSqlStoreContextAlloc get_context(*this); @@ -303,15 +309,24 @@ PgSqlStore::writeln(const string& text, const std::string& addr) { pair PgSqlStore::getVersion(const std::string& timer_name) const { - LOG_DEBUG(legal_log_logger, DB_DBG_TRACE_DETAIL, + LOG_DEBUG(pgsql_fb_logger, DB_DBG_TRACE_DETAIL, LEGAL_LOG_PGSQL_GET_VERSION); - LegalLogDbLogger pushed; + LegalLogDbLogger pushed(pgsql_legal_log_db_logger); IOServiceAccessorPtr ac(new IOServiceAccessor(&DatabaseConnection::getIOService)); DbCallback cb(&PgSqlStore::dbReconnect); - return (PgSqlConnection::getVersion(BackendStore::getParameters(), ac, cb, timer_name, NetworkState::DB_CONNECTION + 32)); + return (PgSqlConnection::getVersion(getParameters(), ac, cb, timer_name, NetworkState::DB_CONNECTION + 32)); +} + +std::string +PgSqlStore::getDBVersion() { + std::stringstream tmp; + tmp << "PostgreSQL backend " << PGSQL_SCHEMA_VERSION_MAJOR; + tmp << "." << PGSQL_SCHEMA_VERSION_MINOR; + tmp << ", library " << PQlibVersion(); + return (tmp.str()); } bool @@ -330,18 +345,15 @@ PgSqlStore::dbReconnect(ReconnectCtlPtr db_reconnect_ctl) { // At least one connection was lost. try { - auto parameters = BackendStore::getParameters(); - // Set legal store to NULL - BackendStore::instance().reset(); - // Create temporary legal store - BackendStorePtr store(new PgSqlStore(parameters)); - store->open(); - // If everything is fine, set the legal store - BackendStore::instance() = store; - BackendStore::parseExtraParameters(BackendStore::getConfig()); + auto pool = BackendStoreFactory::getPool(); + for (auto backend : pool) { + if (BackendStoreFactory::delBackend(backend.first, true)) { + BackendStoreFactory::addBackend(backend.second.first, backend.first); + } + } reopened = true; } catch (const std::exception& ex) { - LOG_ERROR(legal_log_logger, LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED) + LOG_ERROR(pgsql_fb_logger, LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_FAILED) .arg(ex.what()); } @@ -358,7 +370,7 @@ PgSqlStore::dbReconnect(ReconnectCtlPtr db_reconnect_ctl) { } else { if (!check) { // We're out of retries, log it and initiate shutdown. - LOG_ERROR(legal_log_logger, LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED) + LOG_ERROR(pgsql_fb_logger, LEGAL_LOG_PGSQL_DB_RECONNECT_FAILED) .arg(db_reconnect_ctl->maxRetries()); // Cancel the timer. @@ -371,7 +383,7 @@ PgSqlStore::dbReconnect(ReconnectCtlPtr db_reconnect_ctl) { return (false); } - LOG_INFO(legal_log_logger, LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE) + LOG_INFO(pgsql_fb_logger, LEGAL_LOG_PGSQL_DB_RECONNECT_ATTEMPT_SCHEDULE) .arg(db_reconnect_ctl->maxRetries() - db_reconnect_ctl->retriesLeft() + 1) .arg(db_reconnect_ctl->maxRetries()) .arg(db_reconnect_ctl->retryInterval()); @@ -389,5 +401,17 @@ PgSqlStore::dbReconnect(ReconnectCtlPtr db_reconnect_ctl) { return (true); } -} // end of isc::legal_log namespace +bool +PgSqlStore::isUnusable() { + return (unusable_); +} + +BackendStorePtr +PgSqlStore::factory(const isc::db::DatabaseConnection::ParameterMap& parameters) { + LOG_INFO(pgsql_fb_logger, PGSQL_FB_DB) + .arg(isc::db::DatabaseConnection::redactedAccessString(parameters)); + return (BackendStorePtr(new PgSqlStore(parameters))); +} + +} // end of isc::dhcp namespace } // end of isc namespace diff --git a/src/hooks/dhcp/forensic_log/pgsql_legal_log.h b/src/hooks/dhcp/pgsql/pgsql_legal_log.h similarity index 82% rename from src/hooks/dhcp/forensic_log/pgsql_legal_log.h rename to src/hooks/dhcp/pgsql/pgsql_legal_log.h index 7d924484cd..45b1a419f3 100644 --- a/src/hooks/dhcp/forensic_log/pgsql_legal_log.h +++ b/src/hooks/dhcp/pgsql/pgsql_legal_log.h @@ -7,7 +7,7 @@ #ifndef PGSQL_LEGAL_LOG_H #define PGSQL_LEGAL_LOG_H -#include +#include #include #include #include @@ -18,7 +18,7 @@ #include namespace isc { -namespace legal_log { +namespace dhcp { // Forward definitions (needed for shared_ptr definitions) // See pgsql_legal_log.cc file for for actual class definitions @@ -165,6 +165,9 @@ public: /// database has failed. virtual std::pair getVersion(const std::string& timer_name = std::string()) const; + /// @brief Local version of getDBVersion() class method + static std::string getDBVersion(); + /// @brief Statement Tags /// /// The contents of the enum are indexes into the list of compiled SQL @@ -174,6 +177,13 @@ public: NUM_STATEMENTS // Number of statements }; + /// @brief Flag which indicates if the store has at least one + /// unusable connection. + /// + /// @return true if there is at least one unusable connection, false + /// otherwise + virtual bool isUnusable(); + /// @brief Context RAII Allocator. class PgSqlStoreContextAlloc { public: @@ -184,7 +194,7 @@ public: /// or creates a new one. /// /// @param store A parent instance - PgSqlStoreContextAlloc(const PgSqlStore& store); + PgSqlStoreContextAlloc(PgSqlStore& store); /// @brief Destructor /// @@ -196,7 +206,7 @@ public: private: /// @brief The store - const PgSqlStore& store_; + PgSqlStore& store_; }; private: @@ -206,9 +216,39 @@ private: /// @brief The pool of contexts PgSqlStoreContextPoolPtr pool_; + + /// @brief Indicates if there is at least one connection that can no longer + /// be used for normal operations. + bool unusable_; + +public: + /// @brief Factory class method. + /// + /// @param parameters A data structure relating keywords and values + /// concerned with the database. + /// + /// @return The PostgreSQL Store. + static BackendStorePtr + factory(const isc::db::DatabaseConnection::ParameterMap& parameters); }; -} // end of isc::legal_log namespace +/// @brief Initialization structure used to register and deregister PostgreSQL Forensic Backend. +struct PgSqlForensicBackendInit { + // Constructor registers + PgSqlForensicBackendInit() { + BackendStoreFactory::registerBackendFactory("postgresql", + PgSqlStore::factory, + true, + PgSqlStore::getDBVersion); + } + + // Destructor deregisters + ~PgSqlForensicBackendInit() { + BackendStoreFactory::unregisterBackendFactory("postgresql", true); + } +}; + +} // end of isc::dhcp namespace } // end of isc namespace #endif // PGSQL_LEGAL_LOG_H diff --git a/src/hooks/dhcp/pgsql/tests/Makefile.am b/src/hooks/dhcp/pgsql/tests/Makefile.am index 7d15c41a67..2df389e57c 100644 --- a/src/hooks/dhcp/pgsql/tests/Makefile.am +++ b/src/hooks/dhcp/pgsql/tests/Makefile.am @@ -27,6 +27,7 @@ pgsql_unittests_SOURCES += pgsql_cb_dhcp4_unittest.cc pgsql_unittests_SOURCES += pgsql_cb_dhcp4_mgr_unittest.cc pgsql_unittests_SOURCES += pgsql_cb_dhcp6_unittest.cc pgsql_unittests_SOURCES += pgsql_cb_dhcp6_mgr_unittest.cc +pgsql_unittests_SOURCES += pgsql_forensic_unittest.cc pgsql_unittests_SOURCES += pgsql_host_data_source_unittest.cc pgsql_unittests_SOURCES += pgsql_lease_mgr_unittest.cc pgsql_unittests_SOURCES += pgsql_lease_extended_info_unittest.cc @@ -53,6 +54,7 @@ pgsql_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la pgsql_unittests_LDADD += $(top_builddir)/src/lib/pgsql/testutils/libpgsqltest.la pgsql_unittests_LDADD += $(top_builddir)/src/lib/pgsql/libkea-pgsql.la pgsql_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la +pgsql_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la pgsql_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la pgsql_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la pgsql_unittests_LDADD += $(top_builddir)/src/lib/dns/libkea-dns++.la diff --git a/src/hooks/dhcp/pgsql/tests/meson.build b/src/hooks/dhcp/pgsql/tests/meson.build index 46c2a55172..0fca50d1ea 100644 --- a/src/hooks/dhcp/pgsql/tests/meson.build +++ b/src/hooks/dhcp/pgsql/tests/meson.build @@ -15,11 +15,12 @@ dhcp_pgsql_lib_tests = executable( 'pgsql_cb_dhcp6_mgr_unittest.cc', 'pgsql_cb_dhcp6_unittest.cc', 'pgsql_cb_impl_unittest.cc', + 'pgsql_forensic_unittest.cc', 'pgsql_host_data_source_unittest.cc', 'pgsql_lease_extended_info_unittest.cc', 'pgsql_lease_mgr_unittest.cc', 'run_unittests.cc', - dependencies: [gtest, crypto, postgresql], + dependencies: [gtest, crypto, postgresql, kea_testutils_lib], include_directories: [include_directories('.'), include_directories('..')] + INCLUDES, link_with: [dhcp_pgsql_archive, libs_testutils] + LIBS_BUILT_SO_FAR, ) diff --git a/src/hooks/dhcp/forensic_log/tests/pgsql_unittests.cc b/src/hooks/dhcp/pgsql/tests/pgsql_forensic_unittest.cc similarity index 87% rename from src/hooks/dhcp/forensic_log/tests/pgsql_unittests.cc rename to src/hooks/dhcp/pgsql/tests/pgsql_forensic_unittest.cc index f758396500..bfaa275bf9 100644 --- a/src/hooks/dhcp/forensic_log/tests/pgsql_unittests.cc +++ b/src/hooks/dhcp/pgsql/tests/pgsql_forensic_unittest.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -29,7 +30,7 @@ using namespace isc::data; using namespace isc::db; using namespace isc::db::test; using namespace isc::dhcp::test; -using namespace isc::legal_log; +using namespace isc::dhcp; using namespace isc::test; using namespace boost::posix_time; namespace ph = std::placeholders; @@ -92,19 +93,19 @@ public: params["name"] = "keatest"; params["user"] = "keatest"; params["password"] = "keatest"; - ASSERT_NO_THROW(store_.reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(store_.reset(new PgSqlStore(params))); // Open the database - ASSERT_NO_THROW(store_->open()); + ASSERT_NO_THROW_LOG(store_->open()); } /// @brief Close the store void closeStore() { // Close does nothing - EXPECT_NO_THROW(store_->close()); + EXPECT_NO_THROW_LOG(store_->close()); // Destructor close the database - EXPECT_NO_THROW(store_.reset()); + EXPECT_NO_THROW_LOG(store_.reset()); } /// @brief Fill some log entries @@ -159,7 +160,7 @@ TEST_F(PgSqlTest, invalidConstruction) { DatabaseConnection::ParameterMap params; params["user"] = "keatest"; params["password"] = "keatest"; - ASSERT_NO_THROW(store_.reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(store_.reset(new PgSqlStore(params))); // Check params validity is done by open(). EXPECT_THROW(store_->open(), NoDatabaseName); @@ -167,7 +168,7 @@ TEST_F(PgSqlTest, invalidConstruction) { params["name"] = "keatest"; // 2^64 should be greater than INT_MAX params["connect-timeout"] = "18446744073709551616"; - ASSERT_NO_THROW(store_.reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(store_.reset(new PgSqlStore(params))); EXPECT_THROW(store_->open(), DbInvalidTimeout); } @@ -178,19 +179,19 @@ TEST_F(PgSqlTest, open) { params["name"] = "keatest"; params["user"] = "keatest"; params["password"] = "keatest"; - ASSERT_NO_THROW(store_.reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(store_.reset(new PgSqlStore(params))); // Check the type is postgresql EXPECT_EQ("postgresql", store_->getType()); // Open the database - ASSERT_NO_THROW(store_->open()); + ASSERT_NO_THROW_LOG(store_->open()); // Close does nothing - EXPECT_NO_THROW(store_->close()); + EXPECT_NO_THROW_LOG(store_->close()); // Destructor close the database - EXPECT_NO_THROW(store_.reset()); + EXPECT_NO_THROW_LOG(store_.reset()); } /// @brief Check schema version @@ -200,7 +201,7 @@ TEST_F(PgSqlTest, version) { // Check version using the API std::pair version; - EXPECT_NO_THROW(version = store_->getVersion()); + EXPECT_NO_THROW_LOG(version = store_->getVersion()); EXPECT_EQ(PGSQL_SCHEMA_VERSION_MAJOR, version.first); EXPECT_EQ(PGSQL_SCHEMA_VERSION_MINOR, version.second); @@ -212,7 +213,7 @@ TEST_F(PgSqlTest, version) { setQuery("SELECT version, minor FROM schema_version"); setCommand("psql --set ON_ERROR_STOP=1 -A -t -h localhost -q " "-U keatest -d keatest -c "); - EXPECT_NO_THROW(execute()); + EXPECT_NO_THROW_LOG(execute()); EXPECT_EQ(0, getResult()); vector output; EXPECT_TRUE(getOutput(output)); @@ -245,7 +246,7 @@ TEST_F(PgSqlTest, addresses) { setQuery("SELECT address FROM logs"); setCommand("psql --set ON_ERROR_STOP=1 -A -t -h localhost -q " "-U keatest -d keatest -c "); - EXPECT_NO_THROW(execute()); + EXPECT_NO_THROW_LOG(execute()); EXPECT_EQ(0, getResult()); vector output; EXPECT_TRUE(getOutput(output)); @@ -270,7 +271,7 @@ TEST_F(PgSqlTest, entries) { setQuery("SELECT log FROM logs"); setCommand("psql --set ON_ERROR_STOP=1 -A -t -h localhost -q " "-U keatest -d keatest -c "); - EXPECT_NO_THROW(execute()); + EXPECT_NO_THROW_LOG(execute()); EXPECT_EQ(0, getResult()); vector output; EXPECT_TRUE(getOutput(output)); @@ -302,7 +303,7 @@ TEST_F(PgSqlTest, timestamps) { setQuery("SELECT EXTRACT(epoch FROM timestamp) FROM logs"); setCommand("psql --set ON_ERROR_STOP=1 -A -t -h localhost -q " "-U keatest -d keatest -c "); - EXPECT_NO_THROW(execute()); + EXPECT_NO_THROW_LOG(execute()); EXPECT_EQ(0, getResult()); vector output; EXPECT_TRUE(getOutput(output)); @@ -311,7 +312,7 @@ TEST_F(PgSqlTest, timestamps) { for (size_t i = 0; i < output.size(); ++i) { // Get the timestamp as a double from epoch double ts_d = 0.0; - ASSERT_NO_THROW(ts_d = boost::lexical_cast(output[i])); + ASSERT_NO_THROW_LOG(ts_d = boost::lexical_cast(output[i])); // Extract seconds and microseconds; double ts_s; double ts_ms = modf(ts_d, &ts_s) * 1000000.; @@ -339,10 +340,16 @@ class PgSqlLegalLogDbLostCallbackTest : public LegalLogDbLostCallbackTest { public: /// @brief Constructor. - PgSqlLegalLogDbLostCallbackTest() { } + PgSqlLegalLogDbLostCallbackTest() { + BackendStoreFactory::delAllBackends(); + io_service_->poll(); + } /// @brief Destructor. - virtual ~PgSqlLegalLogDbLostCallbackTest() { } + virtual ~PgSqlLegalLogDbLostCallbackTest() { + BackendStoreFactory::delAllBackends(); + io_service_->poll(); + } /// @brief Prepares the class for a test. /// @@ -474,6 +481,9 @@ public: /// -# The registered DbFailedCallback was invoked after two reconnect /// attempts (once failing and second triggered by timer) virtual void testDbLostAndFailedAfterTimeoutCallback(); + + /// @brief Initializer. + PgSqlForensicBackendInit init_; }; void @@ -496,19 +506,19 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredCallback() { std::shared_ptr dbr(new DbConnectionInitWithRetry()); params.emplace("retry-on-startup", "true"); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenErrorWithRetry); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenErrorWithRetry); // Verify there is no instance. - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); dbr.reset(); params = db::DatabaseConnection::parse(validConnectString()); params.emplace("retry-on-startup", "true"); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); io_service_->poll(); @@ -517,7 +527,7 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredCallback() { EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); } void @@ -540,13 +550,13 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedCallback() { std::shared_ptr dbr(new DbConnectionInitWithRetry()); params.emplace("retry-on-startup", "true"); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenErrorWithRetry); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenErrorWithRetry); // Verify there is no instance. - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); dbr.reset(); @@ -557,7 +567,7 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(1, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } void @@ -584,13 +594,13 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredAfterTimeoutCall std::shared_ptr dbr(new DbConnectionInitWithRetry()); params.emplace("retry-on-startup", "true"); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenErrorWithRetry); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenErrorWithRetry); // Verify there is no instance. - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); dbr.reset(); @@ -601,13 +611,13 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredAfterTimeoutCall EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); access = validConnectString(); access += extra; params = db::DatabaseConnection::parse(access); params.emplace("retry-on-startup", "true"); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); sleep(1); @@ -618,7 +628,7 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredAfterTimeoutCall EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); sleep(1); @@ -629,7 +639,7 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndRecoveredAfterTimeoutCall EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); } void @@ -656,13 +666,13 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedAfterTimeoutCallbac std::shared_ptr dbr(new DbConnectionInitWithRetry()); params.emplace("retry-on-startup", "true"); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenErrorWithRetry); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenErrorWithRetry); // Verify there is no instance. - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); dbr.reset(); @@ -673,7 +683,7 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedAfterTimeoutCallbac EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); sleep(1); @@ -684,7 +694,7 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedAfterTimeoutCallbac EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); sleep(1); @@ -695,7 +705,7 @@ PgSqlLegalLogDbLostCallbackTest::testRetryOpenDbLostAndFailedAfterTimeoutCallbac EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(1, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } void @@ -715,10 +725,10 @@ PgSqlLegalLogDbLostCallbackTest::testNoCallbackOnOpenFailure() { // Verify that a PgSqlStore with no database name is rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(invalidConnectString()); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); // Check params validity is done by open(). - EXPECT_THROW(BackendStore::instance()->open(), DbOpenError); + EXPECT_THROW(BackendStoreFactory::instance()->open(), DbOpenError); io_service_->poll(); @@ -726,7 +736,7 @@ PgSqlLegalLogDbLostCallbackTest::testNoCallbackOnOpenFailure() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } void @@ -753,28 +763,29 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredCallback() { // Verify that a PgSqlStore with database name is not rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(validConnectString()); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); + BackendStoreFactory::setParameters(params); // Check params validity is done by open(). - EXPECT_NO_THROW(BackendStore::instance()->open()); + EXPECT_NO_THROW_LOG(BackendStoreFactory::instance()->open()); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); // Find the SQL client socket. int sql_socket = findLastSocketFd(); ASSERT_TRUE(sql_socket > last_open_socket); // Verify we can execute a query. We don't care about the answer. - ASSERT_NO_THROW(BackendStore::instance()->writeln("test", "192.2.1.100")); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance()->writeln("test", "192.2.1.100")); // Now close the sql socket out from under backend client ASSERT_EQ(0, close(sql_socket)); // A query should fail with DbConnectionUnusable. - ASSERT_THROW(BackendStore::instance()->writeln("test", "192.2.1.101");, + ASSERT_THROW(BackendStoreFactory::instance()->writeln("test", "192.2.1.101");, DbConnectionUnusable); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); io_service_->poll(); @@ -783,7 +794,7 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredCallback() { EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); } void @@ -810,31 +821,31 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndFailedCallback() { // Verify that a PgSqlStore with database name is not rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(validConnectString()); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); // Check params validity is done by open(). - EXPECT_NO_THROW(BackendStore::instance()->open()); + EXPECT_NO_THROW_LOG(BackendStoreFactory::instance()->open()); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); // Find the SQL client socket. int sql_socket = findLastSocketFd(); ASSERT_TRUE(sql_socket > last_open_socket); // Verify we can execute a query. We don't care about the answer. - ASSERT_NO_THROW(BackendStore::instance()->writeln("test", "192.2.1.100")); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance()->writeln("test", "192.2.1.100")); params = db::DatabaseConnection::parse(invalidConnectString()); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); // Now close the sql socket out from under backend client ASSERT_EQ(0, close(sql_socket)); // A query should fail with DbConnectionUnusable. - ASSERT_THROW(BackendStore::instance()->writeln("test", "192.2.1.101"), + ASSERT_THROW(BackendStoreFactory::instance()->writeln("test", "192.2.1.101"), DbConnectionUnusable); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); io_service_->poll(); @@ -843,7 +854,7 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndFailedCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(1, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } void @@ -874,33 +885,34 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() { // Verify that a PgSqlStore with database name is not rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(access); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); + BackendStoreFactory::setParameters(params); // Check params validity is done by open(). - EXPECT_NO_THROW(BackendStore::instance()->open()); + EXPECT_NO_THROW_LOG(BackendStoreFactory::instance()->open()); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); // Find the SQL client socket. int sql_socket = findLastSocketFd(); ASSERT_TRUE(sql_socket > last_open_socket); // Verify we can execute a query. We don't care about the answer. - ASSERT_NO_THROW(BackendStore::instance()->writeln("test", "192.2.1.100")); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance()->writeln("test", "192.2.1.100")); access = invalidConnectString(); access += extra; params = db::DatabaseConnection::parse(access); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); // Now close the sql socket out from under backend client ASSERT_EQ(0, close(sql_socket)); // A query should fail with DbConnectionUnusable. - ASSERT_THROW(BackendStore::instance()->writeln("test", "192.2.1.101"), + ASSERT_THROW(BackendStoreFactory::instance()->writeln("test", "192.2.1.101"), DbConnectionUnusable); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); io_service_->poll(); @@ -909,12 +921,12 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); access = validConnectString(); access += extra; params = db::DatabaseConnection::parse(access); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); sleep(1); @@ -925,7 +937,7 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() { EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); sleep(1); @@ -936,7 +948,7 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndRecoveredAfterTimeoutCallback() { EXPECT_EQ(1, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); } void @@ -967,33 +979,33 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() { // Verify that a PgSqlStore with database name is not rejected. DatabaseConnection::ParameterMap params = db::DatabaseConnection::parse(access); - ASSERT_NO_THROW(BackendStore::instance().reset(new PgSqlStore(params))); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance().reset(new PgSqlStore(params))); // Check params validity is done by open(). - EXPECT_NO_THROW(BackendStore::instance()->open()); + EXPECT_NO_THROW_LOG(BackendStoreFactory::instance()->open()); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); // Find the SQL client socket. int sql_socket = findLastSocketFd(); ASSERT_TRUE(sql_socket > last_open_socket); // Verify we can execute a query. We don't care about the answer. - ASSERT_NO_THROW(BackendStore::instance()->writeln("test", "192.2.1.100")); + ASSERT_NO_THROW_LOG(BackendStoreFactory::instance()->writeln("test", "192.2.1.100")); access = invalidConnectString(); access += extra; params = db::DatabaseConnection::parse(access); - BackendStore::setParameters(params); + BackendStoreFactory::setParameters(params); // Now close the sql socket out from under backend client ASSERT_EQ(0, close(sql_socket)); // A query should fail with DbConnectionUnusable. - ASSERT_THROW(BackendStore::instance()->writeln("test", "192.2.1.101"), + ASSERT_THROW(BackendStoreFactory::instance()->writeln("test", "192.2.1.101"), DbConnectionUnusable); - ASSERT_TRUE(BackendStore::instance()); + ASSERT_TRUE(BackendStoreFactory::instance()); io_service_->poll(); @@ -1002,7 +1014,7 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); sleep(1); @@ -1013,7 +1025,7 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(0, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); sleep(1); @@ -1024,7 +1036,7 @@ PgSqlLegalLogDbLostCallbackTest::testDbLostAndFailedAfterTimeoutCallback() { EXPECT_EQ(0, db_recovered_callback_called_); EXPECT_EQ(1, db_failed_callback_called_); - ASSERT_FALSE(BackendStore::instance()); + ASSERT_FALSE(BackendStoreFactory::instance()); } /// @brief Verifies that loss of connectivity to PostgreSQL is handled correctly. diff --git a/src/hooks/dhcp/pgsql/tests/test_utils.h b/src/hooks/dhcp/pgsql/tests/test_utils.h new file mode 100644 index 0000000000..fb3148bec3 --- /dev/null +++ b/src/hooks/dhcp/pgsql/tests/test_utils.h @@ -0,0 +1,321 @@ +// Copyright (C) 2016-2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +#ifndef TEST_UTILS_H +#define TEST_UTILS_H + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace isc; +using namespace hooks; +using namespace dhcp; + +namespace isc { +namespace dhcp { + +static isc::asiolink::IOServicePtr local_io_service = isc::asiolink::IOServicePtr(new isc::asiolink::IOService()); + +/// @brief Helper class to execute a SQL tool and get results +class runSQL { +public: + /// @brief Constructor + runSQL() { + reset(); + } + + /// @brief Destructor + virtual ~runSQL() = default; + + /// @brief Reset everything + void reset() { + command_ = ""; + query_ = ""; + result_ = 0; + output_.clear(); + } + + /// @brief Get command + /// @return the command + const std::string& getCommand() const { + return (command_); + } + + /// @brief Set command + /// @param the command + void setCommand(const std::string& command) { + command_ = command; + } + + /// @brief Get query + /// @return the query + const std::string& getQuery() const { + return (query_); + } + + /// @brief Set query + /// @param the query + void setQuery(const std::string& query) { + query_ = query; + } + + /// @brief Get result + /// @return the exit code + int getResult() const { + return (result_); + } + + /// @brief Get raw output + /// @return the unprocessed output + std::vector getRawOutput() { + return (output_); + } + + /// @brief Process output + /// @param output reference to the string vector to fill with output + /// @return true if processing was successful + virtual bool getOutput(std::vector& output) = 0; + + /// @brief Execute + /// @throw std::system_error according to boost documentation + void execute() { + // Reading stream + FILE* istream; + size_t buffer_size = 1024; + char buffer[buffer_size + 1]; + + // Child process + std::string cmd = command_ + "\"" + query_ + "\""; + istream = popen(cmd.c_str(), "r"); + + // Check errors + if (!istream) { + result_ = -1; + return; + } else { + result_ = ferror(istream); + if (result_) { + return; + } + } + + // Read output + while (!feof(istream) && fgets(buffer, buffer_size, istream)) { + output_.push_back(std::string(buffer)); + } + + // Wait for children to terminate and get its exit code + result_ = pclose(istream); + } + +private: + // @brief command + std::string command_; + + // @brief query + std::string query_; + + // @brief exit code aka result + int result_; + + // @brief output + std::vector output_; +}; + +/// @brief Test fixture for testing database backend connection recovery. +class LegalLogDbLostCallbackTest : public ::testing::Test { +public: + + /// @brief Constructor. + LegalLogDbLostCallbackTest() + : db_lost_callback_called_(0), db_recovered_callback_called_(0), + db_failed_callback_called_(0), + io_service_(local_io_service) { + isc::db::DatabaseConnection::db_lost_callback_ = 0; + isc::db::DatabaseConnection::db_recovered_callback_ = 0; + isc::db::DatabaseConnection::db_failed_callback_ = 0; + isc::db::DatabaseConnection::setIOService(io_service_); + BackendStoreFactory::setIOService(io_service_); + isc::dhcp::TimerMgr::instance()->setIOService(io_service_); + BackendStoreFactory::delAllBackends(); + } + + /// @brief Destructor. + virtual ~LegalLogDbLostCallbackTest() { + isc::db::DatabaseConnection::db_lost_callback_ = 0; + isc::db::DatabaseConnection::db_recovered_callback_ = 0; + isc::db::DatabaseConnection::db_failed_callback_ = 0; + isc::db::DatabaseConnection::setIOService(isc::asiolink::IOServicePtr()); + BackendStoreFactory::setIOService(isc::asiolink::IOServicePtr()); + isc::dhcp::TimerMgr::instance()->unregisterTimers(); + BackendStoreFactory::delAllBackends(); + } + + /// @brief Prepares the class for a test. + /// + /// Invoked by gtest prior test entry, we create the + /// appropriate schema and create a basic DB manager to + /// wipe out any prior instance + virtual void SetUp() = 0; + + /// @brief Pre-text exit clean up + /// + /// Invoked by gtest upon test exit, we destroy the schema + /// we created. + virtual void TearDown() = 0; + + /// @brief Method which returns the back end specific connection + /// string + virtual std::string validConnectString() = 0; + + /// @brief Method which returns invalid back end specific connection + /// string + virtual std::string invalidConnectString() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection can not be + /// established but succeeds on retry + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies that connectivity is unavailable + /// and then recovered on retry: + /// -# The registered DbLostCallback was invoked + /// -# The registered DbRecoveredCallback was invoked + virtual void testRetryOpenDbLostAndRecoveredCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection can not be + /// established but fails on retry + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies that connectivity is unavailable + /// and then fails again on retry: + /// -# The registered DbLostCallback was invoked + /// -# The registered DbFailedCallback was invoked + virtual void testRetryOpenDbLostAndFailedCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection can not be + /// established but succeeds on retry + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies that connectivity is unavailable + /// and then recovered on retry: + /// -# The registered DbLostCallback was invoked + /// -# The registered DbRecoveredCallback was invoked after two reconnect + /// attempts (once failing and second triggered by timer) + virtual void testRetryOpenDbLostAndRecoveredAfterTimeoutCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection can not be + /// established but fails on retry + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies that connectivity is unavailable + /// and then fails again on retry: + /// -# The registered DbLostCallback was invoked + /// -# The registered DbFailedCallback was invoked after two reconnect + /// attempts (once failing and second triggered by timer) + virtual void testRetryOpenDbLostAndFailedAfterTimeoutCallback() = 0; + + /// @brief Verifies open failures do NOT invoke db lost callback + /// + /// The db lost callback should only be invoked after successfully + /// opening the DB and then subsequently losing it. Failing to + /// open should be handled directly by the application layer. + virtual void testNoCallbackOnOpenFailure() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection is lost + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies connectivity by issuing a known + /// valid query. Next it simulates connectivity lost by identifying and + /// closing the socket connection to the Backend Store. It then reissues the + /// query and verifies that: + /// -# The Query throws DbOperationError (rather than exiting) + /// -# The registered DbLostCallback was invoked + /// -# The registered DbRecoveredCallback was invoked + virtual void testDbLostAndRecoveredCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection is lost + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies connectivity by issuing a known + /// valid query. Next it simulates connectivity lost by identifying and + /// closing the socket connection to the Backend Store. It then reissues the + /// query and verifies that: + /// -# The Query throws DbOperationError (rather than exiting) + /// -# The registered DbLostCallback was invoked + /// -# The registered DbFailedCallback was invoked + virtual void testDbLostAndFailedCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection is lost + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies connectivity by issuing a known + /// valid query. Next it simulates connectivity lost by identifying and + /// closing the socket connection to the Backend Store. It then reissues the + /// query and verifies that: + /// -# The Query throws DbOperationError (rather than exiting) + /// -# The registered DbLostCallback was invoked + /// -# The registered DbRecoveredCallback was invoked after two reconnect + /// attempts (once failing and second triggered by timer) + virtual void testDbLostAndRecoveredAfterTimeoutCallback() = 0; + + /// @brief Verifies the Backend Store behavior if DB connection is lost + /// + /// This function creates a Backend Store with a back end that supports + /// connectivity lost callback. It verifies connectivity by issuing a known + /// valid query. Next it simulates connectivity lost by identifying and + /// closing the socket connection to the Backend Store. It then reissues the + /// query and verifies that: + /// -# The Query throws DbOperationError (rather than exiting) + /// -# The registered DbLostCallback was invoked + /// -# The registered DbFailedCallback was invoked after two reconnect + /// attempts (once failing and second triggered by timer) + virtual void testDbLostAndFailedAfterTimeoutCallback() = 0; + + /// @brief Callback function registered with the Backend Store + bool db_lost_callback(util::ReconnectCtlPtr /* not_used */) { + return (++db_lost_callback_called_); + } + + /// @brief Flag used to detect calls to db_lost_callback function + uint32_t db_lost_callback_called_; + + /// @brief Callback function registered with the Backend Store + bool db_recovered_callback(util::ReconnectCtlPtr /* not_used */) { + return (++db_recovered_callback_called_); + } + + /// @brief Flag used to detect calls to db_recovered_callback function + uint32_t db_recovered_callback_called_; + + /// @brief Callback function registered with the Backend Store + bool db_failed_callback(util::ReconnectCtlPtr /* not_used */) { + return (++db_failed_callback_called_); + } + + /// @brief Flag used to detect calls to db_failed_callback function + uint32_t db_failed_callback_called_; + + /// The IOService object, used for all ASIO operations. + isc::asiolink::IOServicePtr io_service_; +}; + +} // namespace dhcp +} // namespace isc + +#endif // TEST_UTILS_H diff --git a/src/lib/cryptolink/Makefile.am b/src/lib/cryptolink/Makefile.am index c25bd54bfb..1abc669308 100644 --- a/src/lib/cryptolink/Makefile.am +++ b/src/lib/cryptolink/Makefile.am @@ -22,7 +22,6 @@ if HAVE_OPENSSL libkea_cryptolink_la_SOURCES += openssl_link.cc libkea_cryptolink_la_SOURCES += openssl_common.h libkea_cryptolink_la_SOURCES += openssl_hash.cc -libkea_cryptolink_la_SOURCES += openssl_compat.h libkea_cryptolink_la_SOURCES += openssl_hmac.cc endif @@ -47,6 +46,5 @@ endif if HAVE_OPENSSL libkea_cryptolink_include_HEADERS += \ - openssl_common.h \ - openssl_compat.h + openssl_common.h endif diff --git a/src/lib/cryptolink/meson.build b/src/lib/cryptolink/meson.build index 4ee51b98b6..969ee365e5 100644 --- a/src/lib/cryptolink/meson.build +++ b/src/lib/cryptolink/meson.build @@ -28,7 +28,6 @@ kea_cryptolink_headers = [ 'cryptolink.h', 'crypto_rng.h', 'openssl_common.h', - 'openssl_compat.h', ] install_headers( kea_cryptolink_headers, diff --git a/src/lib/cryptolink/openssl_compat.h b/src/lib/cryptolink/openssl_compat.h deleted file mode 100644 index 33b85e1a22..0000000000 --- a/src/lib/cryptolink/openssl_compat.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (C) 2016-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/. - -// This file is included by hash and hmac codes so KEA_H* macros -// avoid to define unused inlines. - -#ifdef KEA_HASH - -#ifndef HAVE_EVP_MD_CTX_NEW -#ifdef HAVE_EVP_MD_CTX_CREATE - -// EVP_MD_CTX_new() is EVP_MD_CTX_create() in old OpenSSL - -inline EVP_MD_CTX* EVP_MD_CTX_new() { - return (EVP_MD_CTX_create()); -} - -#else -#error have no EVP_MD_CTX_new() nor EVP_MD_CTX_create() -#endif -#endif - -#ifndef HAVE_EVP_MD_CTX_FREE -#ifdef HAVE_EVP_MD_CTX_DESTROY - -// EVP_MD_CTX_free(ctx) is EVP_MD_CTX_destroy(ctx) in old OpenSSL - -inline void EVP_MD_CTX_free(EVP_MD_CTX* ctx) { - EVP_MD_CTX_destroy(ctx); -} - -#else -#error have no EVP_MD_CTX_free() nor EVP_MD_CTX_destroy() -#endif -#endif - -#endif - -#ifdef KEA_HMAC - -#ifndef HAVE_EVP_PKEY_NEW_RAW_PRIVATE_KEY -#ifdef HAVE_EVP_PKEY_NEW_MAC_KEY - -// EVP_PKEY_new_raw_private_key(type, e, key, keylen) is -// EVP_PKEY_new_mac_key(type, e, key, (int)keylen) in old OpenSSL - -inline EVP_PKEY* EVP_PKEY_new_raw_private_key(int type, ENGINE* e, - const unsigned char *key, - size_t keylen) { - return (EVP_PKEY_new_mac_key(type, e, key, static_cast(keylen))); -} - -#else -#error have no EVP_PKEY_new_raw_private_key() nor EVP_PKEY_new_mac_key() -#endif -#endif - -#endif diff --git a/src/lib/dhcpsrv/Makefile.am b/src/lib/dhcpsrv/Makefile.am index ff74e78f0e..6ea912f594 100644 --- a/src/lib/dhcpsrv/Makefile.am +++ b/src/lib/dhcpsrv/Makefile.am @@ -13,6 +13,8 @@ AM_CPPFLAGS += -DTOP_BUILDDIR="\"$(top_builddir)\"" # Set location of the kea-lfc binary. AM_CPPFLAGS += -DKEA_LFC_EXECUTABLE="\"$(kea_lfc_location)\"" AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) +legal_log_dir = @localstatedir@/lib/@PACKAGE@ +AM_CPPFLAGS += -DLEGAL_LOG_DIR="\"$(legal_log_dir)\"" AM_CXXFLAGS = $(KEA_CXXFLAGS) @@ -60,6 +62,8 @@ libkea_dhcpsrv_la_SOURCES += alloc_engine.cc alloc_engine.h libkea_dhcpsrv_la_SOURCES += alloc_engine_log.cc alloc_engine_log.h libkea_dhcpsrv_la_SOURCES += alloc_engine_messages.h alloc_engine_messages.cc libkea_dhcpsrv_la_SOURCES += allocator.h allocator.cc +libkea_dhcpsrv_la_SOURCES += backend_store_factory.h backend_store_factory.cc +libkea_dhcpsrv_la_SOURCES += backend_store.h backend_store.cc libkea_dhcpsrv_la_SOURCES += base_host_data_source.h libkea_dhcpsrv_la_SOURCES += cache_host_data_source.h libkea_dhcpsrv_la_SOURCES += callout_handle_store.h @@ -119,6 +123,7 @@ libkea_dhcpsrv_la_SOURCES += lease_file_loader.h libkea_dhcpsrv_la_SOURCES += lease_file_stats.h libkea_dhcpsrv_la_SOURCES += lease_mgr.cc lease_mgr.h libkea_dhcpsrv_la_SOURCES += lease_mgr_factory.cc lease_mgr_factory.h +libkea_dhcpsrv_la_SOURCES += legal_log_db_log.cc legal_log_db_log.h libkea_dhcpsrv_la_SOURCES += memfile_lease_limits.cc memfile_lease_limits.h libkea_dhcpsrv_la_SOURCES += memfile_lease_mgr.cc memfile_lease_mgr.h libkea_dhcpsrv_la_SOURCES += memfile_lease_storage.h @@ -275,6 +280,8 @@ libkea_dhcpsrv_include_HEADERS = \ alloc_engine_log.h \ alloc_engine_messages.h \ allocator.h \ + backend_store_factory.h \ + backend_store.h \ base_host_data_source.h \ cache_host_data_source.h \ callout_handle_store.h \ @@ -334,6 +341,7 @@ libkea_dhcpsrv_include_HEADERS = \ lease_file_stats.h \ lease_mgr.h \ lease_mgr_factory.h \ + legal_log_db_log.h \ memfile_lease_limits.h \ memfile_lease_mgr.h \ memfile_lease_storage.h \ diff --git a/src/hooks/dhcp/forensic_log/backend_store.cc b/src/lib/dhcpsrv/backend_store.cc similarity index 55% rename from src/hooks/dhcp/forensic_log/backend_store.cc rename to src/lib/dhcpsrv/backend_store.cc index bb533ccdcc..ad10e1cb60 100644 --- a/src/hooks/dhcp/forensic_log/backend_store.cc +++ b/src/lib/dhcpsrv/backend_store.cc @@ -6,16 +6,8 @@ #include -#include #include - -#include -#ifdef HAVE_MYSQL -#include -#endif -#ifdef HAVE_PGSQL -#include -#endif +#include #include #include @@ -30,7 +22,7 @@ #include namespace isc { -namespace legal_log { +namespace dhcp { using namespace isc::asiolink; using namespace isc::data; @@ -39,69 +31,21 @@ using namespace isc::dhcp; using namespace isc::eval; using namespace isc::hooks; using namespace isc::util; - +using namespace std; void -BackendStore::parseFile(const ConstElementPtr& parameters) { - // Defaults - std::string path(LEGAL_LOG_DIR); - std::string base("kea-legal"); - RotatingFile::TimeUnit unit(RotatingFile::TimeUnit::Day); - int64_t count(1); - std::string prerotate; - std::string postrotate; - - // Prioritize parameters. - if (parameters) { - if (parameters->get("path")) { - path = parameters->get("path")->stringValue(); - } - if (parameters->get("base-name")) { - base = parameters->get("base-name")->stringValue(); - } - if (parameters->get("time-unit")) { - std::string time_unit(parameters->get("time-unit")->stringValue()); - if (time_unit == "second") { - unit = RotatingFile::TimeUnit::Second; - } else if (time_unit == "day") { - unit = RotatingFile::TimeUnit::Day; - } else if (time_unit == "month") { - unit = RotatingFile::TimeUnit::Month; - } else if (time_unit == "year") { - unit = RotatingFile::TimeUnit::Year; - } else { - isc_throw(BadValue, "unknown time unit type: " << time_unit - << ", expected one of: second, day, month, year"); - } - } - if (parameters->get("count")) { - count = parameters->get("count")->intValue(); - if ((count < 0) || - (count > std::numeric_limits::max())) { - isc_throw(OutOfRange, "count value: " << count - << " is out of range, expected value: 0.." - << std::numeric_limits::max()); - } - } - if (parameters->get("prerotate")) { - prerotate = parameters->get("prerotate")->stringValue(); - } - if (parameters->get("postrotate")) { - postrotate = parameters->get("postrotate")->stringValue(); - } +BackendStore::parseConfig(const ConstElementPtr& parameters, DatabaseConnection::ParameterMap& map) { + if (!parameters || !parameters->get("type") || + parameters->get("type")->stringValue() == "logfile") { + parseFile(parameters, map); + } else { + parseDatabase(parameters, map); } - - BackendStore::instance() = boost::make_shared( - path, base, unit, count, prerotate, postrotate); - - DatabaseConnection::ParameterMap db_parameters; - db_parameters.emplace("path", path); - db_parameters.emplace("base-name", base); - BackendStore::setParameters(db_parameters); + parseExtraParameters(parameters, map); } void -BackendStore::parseDatabase(const ConstElementPtr& parameters) { +BackendStore::parseDatabase(const ConstElementPtr& parameters, DatabaseConnection::ParameterMap& map) { // Should never happen with the code flow at the time of writing, but // let's get this check out of the way. if (!parameters) { @@ -126,7 +70,7 @@ BackendStore::parseDatabase(const ConstElementPtr& parameters) { ConstElementPtr const value(parameters->get(key)); if (value) { int64_t integer_value(value->intValue()); - auto const max(std::numeric_limits::max()); + auto const max(numeric_limits::max()); if (integer_value < 0 || max < integer_value) { isc_throw(OutOfRange, key << " value: " << integer_value @@ -134,11 +78,11 @@ BackendStore::parseDatabase(const ConstElementPtr& parameters) { << max); } db_parameters[key] = - boost::lexical_cast(integer_value); + boost::lexical_cast(integer_value); } } - std::string param_name = "tcp-nodelay"; + string param_name = "tcp-nodelay"; ConstElementPtr param = parameters->get(param_name); if (param) { db_parameters.emplace(param_name, @@ -147,7 +91,7 @@ BackendStore::parseDatabase(const ConstElementPtr& parameters) { // Always set "on-fail" to "serve-retry-continue" if not explicitly // configured. - std::string on_fail_action = ReconnectCtl::onFailActionToText(OnFailAction::SERVE_RETRY_CONTINUE); + string on_fail_action = ReconnectCtl::onFailActionToText(OnFailAction::SERVE_RETRY_CONTINUE); param_name = "on-fail"; param = parameters->get(param_name); if (param) { @@ -168,72 +112,73 @@ BackendStore::parseDatabase(const ConstElementPtr& parameters) { param = parameters->get(param_name); if (param) { port = param->intValue(); - if ((port < 0) || (port > std::numeric_limits::max())) { + if ((port < 0) || (port > numeric_limits::max())) { isc_throw(OutOfRange, param_name << " value: " << port << " is out of range, expected value: 0.." - << std::numeric_limits::max()); + << numeric_limits::max()); } db_parameters.emplace(param_name, - boost::lexical_cast(port)); + boost::lexical_cast(port)); } - std::string redacted = + string redacted = DatabaseConnection::redactedAccessString(db_parameters); - std::string const dbtype(db_parameters["type"]); -#ifdef HAVE_MYSQL - if (dbtype == "mysql") { - LOG_INFO(legal_log_logger, LEGAL_LOG_MYSQL_DB).arg(redacted); - BackendStore::instance().reset(new MySqlStore(db_parameters)); - return; - } -#endif -#ifdef HAVE_PGSQL - if (dbtype == "postgresql") { - LOG_INFO(legal_log_logger, LEGAL_LOG_PGSQL_DB).arg(redacted); - BackendStore::instance().reset(new PgSqlStore(db_parameters)); - return; - } -#endif - - if ((dbtype == "mysql") || (dbtype == "postgresql")) { - std::string with = (dbtype == "postgresql" ? "pgsql" : dbtype); - isc_throw(InvalidType, "The Kea server has not been compiled with " - "support for database type: " << dbtype - << ". Did you forget to use --with-" - << with << " during compilation?"); - } - - isc_throw(InvalidType, "Database access parameter 'type' does " - "not specify a supported database backend: " << dbtype); + string const db_type(db_parameters["type"]); + map = db_parameters; } void -BackendStore::parseExtraParameters(const ConstElementPtr& parameters) { +BackendStore::parseFile(const ConstElementPtr& parameters, DatabaseConnection::ParameterMap& map) { + DatabaseConnection::ParameterMap file_parameters; + file_parameters["type"] = "logfile"; + + if (!parameters) { + map = file_parameters; + return; + } + + // Strings + for (char const* const& key : { "path", "base-name", "time-unit", "prerotate", "postrotate" }) { + ConstElementPtr const value(parameters->get(key)); + if (value) { + file_parameters.emplace(key, value->stringValue()); + } + } + + // uint32_t + for (char const* const& key : { "count" }) { + ConstElementPtr const value(parameters->get(key)); + if (value) { + int64_t integer_value(value->intValue()); + auto const max(numeric_limits::max()); + if (integer_value < 0 || max < integer_value) { + isc_throw(OutOfRange, + key << " value: " << integer_value + << " is out of range, expected value: 0.." + << max); + } + file_parameters[key] = boost::lexical_cast(integer_value); + } + } + map = file_parameters; +} + +void +BackendStore::parseExtraParameters(const ConstElementPtr& parameters, DatabaseConnection::ParameterMap& map) { if (!parameters) { return; } - ConstElementPtr param = parameters->get("request-parser-format"); - if (param && !param->stringValue().empty()) { - BackendStore::instance()->setRequestFormatExpression(param->stringValue()); - } - - param = parameters->get("response-parser-format"); - if (param && !param->stringValue().empty()) { - BackendStore::instance()->setResponseFormatExpression(param->stringValue()); - } - - param = parameters->get("timestamp-format"); - if (param && !param->stringValue().empty()) { - BackendStore::instance()->setTimestampFormat(param->stringValue()); + // Strings + for (char const* const& key : { "request-parser-format", "response-parser-format", "timestamp-format" }) { + ConstElementPtr const value(parameters->get(key)); + if (value && !value->stringValue().empty()) { + map.emplace(key, value->stringValue()); + } } } -IOServicePtr BackendStore::io_service_; -DatabaseConnection::ParameterMap BackendStore::parameters_; -ConstElementPtr BackendStore::config_; - struct tm BackendStore::currentTimeInfo() const { struct tm time_info; @@ -249,35 +194,35 @@ BackendStore::now() const { return (now); } -std::string +string BackendStore::getNowString() const { // Get a text representation of the current time. return (getNowString(timestamp_format_)); } -std::string -BackendStore::getNowString(const std::string& format) const { +string +BackendStore::getNowString(const string& format) const { // Get a text representation of the current time. return (getTimeString(now(), format)); } -std::string -BackendStore::getTimeString(const struct timespec& time, const std::string& format) { +string +BackendStore::getTimeString(const struct timespec& time, const string& format) { // Get a text representation of the requested time. // First a quick and dirty support for fractional seconds: Replace any "%Q" // tokens in the format string with the microsecond count from the timespec, // before handing it off to strftime(). - std::string tmp_format = format; + string tmp_format = format; for (auto it = tmp_format.begin(); it < tmp_format.end(); ++it) { if (*it == '%' && ((it + 1) < tmp_format.end())) { if (*(it + 1) == 'Q') { // Save the current position. - std::string::size_type pos = it - tmp_format.begin(); + string::size_type pos = it - tmp_format.begin(); // Render the microsecond count. - std::ostringstream usec; - usec << std::setw(6) << std::setfill('0') << (time.tv_nsec / 1000); - std::string microseconds = usec.str(); + ostringstream usec; + usec << setw(6) << setfill('0') << (time.tv_nsec / 1000); + string microseconds = usec.str(); microseconds.insert(3, 1, '.'); tmp_format.replace(it, it + 2, microseconds); // Reinitialize the iterator after manipulating the string. @@ -299,10 +244,10 @@ BackendStore::getTimeString(const struct timespec& time, const std::string& form << "' result is too long, maximum length allowed: " << sizeof(buffer)); } - return (std::string(buffer)); + return (string(buffer)); } -std::string +string BackendStore::genDurationString(const uint32_t secs) { // Because Kea handles lease lifetimes as uint32_t and supports // a value of 0xFFFFFFFF (infinite lifetime), we don't use things like @@ -319,7 +264,7 @@ BackendStore::genDurationString(const uint32_t secs) { uint32_t hours = remainder % 24; uint32_t days = remainder / 24; - std::ostringstream os; + ostringstream os; // Only spit out days if we have em. if (days) { os << days << " days "; @@ -332,32 +277,32 @@ BackendStore::genDurationString(const uint32_t secs) { return (os.str()); } -std::string -BackendStore::vectorHexDump(const std::vector& bytes, - const std::string& delimiter) { - std::stringstream tmp; - tmp << std::hex; +string +BackendStore::vectorHexDump(const vector& bytes, + const string& delimiter) { + stringstream tmp; + tmp << hex; bool delim = false; for (auto const& it : bytes) { if (delim) { tmp << delimiter; } - tmp << std::setw(2) << std::setfill('0') << static_cast(it); + tmp << setw(2) << setfill('0') << static_cast(it); delim = true; } return (tmp.str()); } -std::string -BackendStore::vectorDump(const std::vector& bytes) { +string +BackendStore::vectorDump(const vector& bytes) { if (bytes.empty()) { - return (std::string()); + return (string()); } - return (std::string(bytes.cbegin(), bytes.cend())); + return (string(bytes.cbegin(), bytes.cend())); } void -BackendStore::setRequestFormatExpression(const std::string& extended_format) { +BackendStore::setRequestFormatExpression(const string& extended_format) { Option::Universe universe; if (CfgMgr::instance().getFamily() == AF_INET) { universe = Option::V4; @@ -370,7 +315,7 @@ BackendStore::setRequestFormatExpression(const std::string& extended_format) { } void -BackendStore::setResponseFormatExpression(const std::string& extended_format) { +BackendStore::setResponseFormatExpression(const string& extended_format) { Option::Universe universe; if (CfgMgr::instance().getFamily() == AF_INET) { universe = Option::V4; @@ -383,11 +328,11 @@ BackendStore::setResponseFormatExpression(const std::string& extended_format) { } void -BackendStore::setTimestampFormat(const std::string& timestamp_format) { +BackendStore::setTimestampFormat(const string& timestamp_format) { timestamp_format_ = timestamp_format; } -const std::string +const string actionToVerb(Action action) { switch (action) { case Action::ASSIGN: @@ -399,5 +344,5 @@ actionToVerb(Action action) { } } -} // namespace legal_log +} // namespace dhcp } // namespace isc diff --git a/src/hooks/dhcp/forensic_log/backend_store.h b/src/lib/dhcpsrv/backend_store.h similarity index 77% rename from src/hooks/dhcp/forensic_log/backend_store.h rename to src/lib/dhcpsrv/backend_store.h index 5bcb135168..95dd4bcaac 100644 --- a/src/hooks/dhcp/forensic_log/backend_store.h +++ b/src/lib/dhcpsrv/backend_store.h @@ -20,7 +20,7 @@ #include namespace isc { -namespace legal_log { +namespace dhcp { /// @brief Thrown if a BackendStore encounters an error. class BackendStoreError : public isc::Exception { @@ -39,7 +39,9 @@ typedef boost::shared_ptr BackendStorePtr; class BackendStore { public: /// @brief Constructor. - BackendStore() : timestamp_format_("%Y-%m-%d %H:%M:%S %Z") { + BackendStore(const isc::db::DatabaseConnection::ParameterMap parameters) : + timestamp_format_("%Y-%m-%d %H:%M:%S %Z"), + parameters_(parameters) { } /// @brief Destructor. @@ -47,25 +49,14 @@ public: /// Derived destructors do call the close method. virtual ~BackendStore() = default; - /// @brief Parse file specification. + /// @brief Parse database specification. /// - /// It supports the following parameters via the Hook Library Parameter - /// mechanism: - /// - /// @b path - Directory in which the legal file(s) will be written. - /// The default value is "/var/lib/kea". The directory must exist. - /// - /// @b base-name - An arbitrary value which is used in conjunction - /// with current system date to form the current legal file name. - /// It defaults to "kea-legal". - /// - /// Legal file names will have the form: - /// - /// /..txt - /// /..txt + /// Parse the configuration and check that the various keywords are + /// consistent. /// /// @param parameters The library parameters. - static void parseFile(const isc::data::ConstElementPtr& parameters); + /// @param map The parameter map used by BackendStore objects. + static void parseConfig(const isc::data::ConstElementPtr& parameters, isc::db::DatabaseConnection::ParameterMap& map); /// @brief Parse database specification. /// @@ -75,13 +66,22 @@ public: /// consistent. /// /// @param parameters The library parameters. - static void parseDatabase(const isc::data::ConstElementPtr& parameters); + /// @param map The parameter map used by BackendStore objects. + static void parseDatabase(const isc::data::ConstElementPtr& parameters, isc::db::DatabaseConnection::ParameterMap& map); + + /// @brief Parse file specification. + /// + /// Parse the configuration and check that the various keywords are + /// consistent. + /// + /// @param parameters The library parameters. + static void parseFile(const isc::data::ConstElementPtr& parameters, isc::db::DatabaseConnection::ParameterMap& map); /// @brief Parse extra parameters which are not related to backend /// connection. /// /// @param parameters The library parameters. - static void parseExtraParameters(const isc::data::ConstElementPtr& parameters); + static void parseExtraParameters(const isc::data::ConstElementPtr& parameters, isc::db::DatabaseConnection::ParameterMap& map); /// @brief Opens the store. virtual void open() = 0; @@ -185,56 +185,6 @@ public: /// @param bytes Vector of bytes to convert static std::string vectorDump(const std::vector& bytes); - /// @brief Get the hook I/O service. - /// - /// @return the hook I/O service. - static isc::asiolink::IOServicePtr getIOService() { - return (io_service_); - } - - /// @brief Set the hook I/O service. - /// - /// @param io_service the hook I/O service. - static void setIOService(isc::asiolink::IOServicePtr io_service) { - io_service_ = io_service; - } - - /// @brief Set backend parameters. - /// - /// Set the backend parameters. - /// - /// @param parameters The backend parameters. - static void setParameters(const isc::db::DatabaseConnection::ParameterMap& parameters) { - parameters_ = parameters; - } - - /// @brief Get backend parameters. - /// - /// Get the backend parameters. - /// - /// @return The backend parameters. - static isc::db::DatabaseConnection::ParameterMap getParameters() { - return (parameters_); - } - - /// @brief Set hook configuration. - /// - /// Set the hook configuration. - /// - /// @param config The hook configuration. - static void setConfig(const isc::data::ConstElementPtr& config) { - config_ = config; - } - - /// @brief Get hook configuration. - /// - /// Get the hook configuration. - /// - /// @return The hook configuration. - static isc::data::ConstElementPtr getConfig() { - return (config_); - } - /// @brief Sets request extended format expression for custom logging. /// /// @param extended_format The request extended format expression. @@ -275,24 +225,32 @@ public: return (timestamp_format_); } - /// @brief Return the BackendStore instance. + /// @brief Return backend parameters /// - /// @return The BackendStore instance. - static BackendStorePtr& instance() { - static BackendStorePtr backend_store; - return (backend_store); + /// Returns the backend parameters + /// + /// @return Parameters of the backend. + virtual isc::db::DatabaseConnection::ParameterMap getParameters() const { + return (parameters_); } -protected: + /// @brief Sets backend parameters + /// + /// Sets the backend parameters + /// + /// @param parameter Parameters of the backend. + virtual void setParameters(isc::db::DatabaseConnection::ParameterMap parameters) { + parameters_ = parameters; + } - /// @brief The hook I/O service. - static isc::asiolink::IOServicePtr io_service_; - - /// @brief The parameters used to connect to the backend. - static isc::db::DatabaseConnection::ParameterMap parameters_; - - /// @brief The hook configuration. - static isc::data::ConstElementPtr config_; + /// @brief Flag which indicates if the forensic store backed has at least one + /// unusable connection. + /// + /// @return true if there is at least one unusable connection, false + /// otherwise + virtual bool isUnusable() { + return (false); + } private: @@ -304,8 +262,17 @@ private: /// @brief The strftime format string for timestamps in the log file. std::string timestamp_format_; + + /// @brief The configuration parameters. + isc::db::DatabaseConnection::ParameterMap parameters_; }; +/// @brief Manger ID used by hook libraries to retrieve respective BackendStore instance. +typedef uint64_t ManagerID; + +/// @brief BackendStore pool +typedef std::map> BackendStorePool; + /// @brief Describe what kind of event is being logged. enum class Action { ASSIGN, RELEASE }; @@ -315,7 +282,7 @@ enum class Action { ASSIGN, RELEASE }; /// @result the verb corresponding to the action. const std::string actionToVerb(Action action); -} // namespace legal_log +} // namespace dhcp } // namespace isc #endif diff --git a/src/lib/dhcpsrv/backend_store_factory.cc b/src/lib/dhcpsrv/backend_store_factory.cc new file mode 100644 index 0000000000..d5dc017bb3 --- /dev/null +++ b/src/lib/dhcpsrv/backend_store_factory.cc @@ -0,0 +1,236 @@ +// Copyright (C) 2025 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 + +using namespace isc::db; +using namespace std; + +namespace isc { +namespace dhcp { + +isc::asiolink::IOServicePtr BackendStoreFactory::io_service_; +map> BackendStoreFactory::map_; +BackendStorePool BackendStoreFactory::pool_; + +bool +BackendStoreFactory::registerBackendFactory(const string& db_type, + const Factory& factory, + bool no_log, + DBVersion db_version) { + if (map_.count(db_type)) { + return (false); + } + + static auto default_db_version = []() -> string { + return (string()); + }; + + if (!db_version) { + db_version = default_db_version; + } + + map_.insert(pair>(db_type, pair(factory, db_version))); + + // We are dealing here with static logger initialization fiasco. + // registerFactory may be called from constructors of static global + // objects for built in backends. The logging is not initialized yet, + // so the LOG_DEBUG would throw. + if (!no_log) { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_FORENSIC_BACKEND_REGISTER) + .arg(db_type); + } + return (true); +} + +bool +BackendStoreFactory::unregisterBackendFactory(const string& db_type, bool no_log) { + // Look for it. + auto index = map_.find(db_type); + + // If it's there remove it + if (index != map_.end()) { + map_.erase(index); + delAllBackends(db_type); + if (!no_log) { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, + DHCPSRV_FORENSIC_BACKEND_DEREGISTER) + .arg(db_type); + } + return (true); + } + return (false); +} + +void +BackendStoreFactory::addBackend(DatabaseConnection::ParameterMap& parameters, ManagerID id) { + // Get the database type to locate a factory function. + auto it = parameters.find("type"); + if (it == parameters.end()) { + isc_throw(InvalidParameter, "Forensic store backend specification lacks the " + "'type' keyword"); + } + + string db_type = it->second; + auto index = map_.find(db_type); + + // No match? + if (index == map_.end()) { + if ((db_type == "mysql") || (db_type == "postgresql")) { + string with = (db_type == "postgresql" ? "pgsql" : db_type); + isc_throw(InvalidType, "The Kea server has not been compiled with " + "support for configuration database type: " << db_type + << ". Did you forget to use --with-" + << with << " during compilation or to load libdhcp_" + << with << " hook library?"); + } + isc_throw(InvalidType, "The type of the forensic store backend: '" << + db_type << "' is not supported"); + } + + // Call the factory and push the pointer on sources. + auto backend = index->second.first(parameters); + if (!backend) { + isc_throw(Unexpected, "Forensic store database " << db_type << + " factory returned NULL"); + } + + backend->open(); + + // Apply extra parameters. + if (parameters.find("request-parser-format") != parameters.end()) { + backend->setRequestFormatExpression(parameters["request-parser-format"]); + } + if (parameters.find("response-parser-format") != parameters.end()) { + backend->setResponseFormatExpression(parameters["response-parser-format"]); + } + if (parameters.find("timestamp-format") != parameters.end()) { + backend->setTimestampFormat(parameters["timestamp-format"]); + } + + // Backend instance created successfully. + pool_[id] = pair(parameters, backend); +} + +BackendStorePtr& +BackendStoreFactory::instance(ManagerID id) { + auto it = pool_.find(id); + if (it != pool_.end()) { + return (it->second.second); + } + static BackendStorePtr backend; + if (!id) { + DatabaseConnection::ParameterMap parameters; + pool_[id] = pair(parameters, backend); + return (pool_[id].second); + } + return (backend); +} + +void +BackendStoreFactory::setParameters(DatabaseConnection::ParameterMap parameters, ManagerID id) { + auto it = pool_.find(id); + if (it != pool_.end()) { + it->second.first = parameters; + return; + } + if (!id) { + BackendStorePtr backend; + pool_[id] = pair(parameters, backend); + } +} + +DatabaseConnection::ParameterMap +BackendStoreFactory::getParameters(ManagerID id) { + auto it = pool_.find(id); + if (it != pool_.end()) { + return (it->second.first); + } + return (DatabaseConnection::ParameterMap()); +} + +bool +BackendStoreFactory::delBackend(const string& db_type, + DatabaseConnection::ParameterMap& parameters, + bool if_unusable) { + bool deleted = false; + if (if_unusable) { + deleted = true; + } + + for (auto it = pool_.begin(); it != pool_.end(); ++it) { + if (!it->second.second || it->second.second->getType() != db_type || it->second.first != parameters) { + continue; + } + if (if_unusable && (!(it->second.second->isUnusable()))) { + deleted = false; + continue; + } + it->second.second = BackendStorePtr(); + return (true); + } + return (deleted); +} + +bool +BackendStoreFactory::delBackend(ManagerID id, + bool if_unusable) { + bool deleted = false; + if (if_unusable) { + deleted = true; + } + + auto it = pool_.find(id); + if (it != pool_.end()) { + if (if_unusable && it->second.second && !it->second.second->isUnusable()) { + return (false); + } + it->second.second = BackendStorePtr(); + return (true); + } + return (deleted); +} + +bool +BackendStoreFactory::registeredFactory(const string& db_type) { + auto index = map_.find(db_type); + return (index != map_.end()); +} + +void +BackendStoreFactory::logRegistered() { + stringstream txt; + + for (auto const& x : map_) { + if (!txt.str().empty()) { + txt << " "; + } + txt << x.first; + } + + LOG_INFO(dhcpsrv_logger, DHCPSRV_FORENSIC_BACKENDS_REGISTERED) + .arg(txt.str()); +} + +list +BackendStoreFactory::getDBVersions() { + list result; + for (auto const& x : map_) { + auto version = x.second.second(); + if (!version.empty()) { + result.push_back(version); + } + } + + return (result); +} + +} // end of namespace isc::dhcp +} // end of namespace isc + diff --git a/src/lib/dhcpsrv/backend_store_factory.h b/src/lib/dhcpsrv/backend_store_factory.h new file mode 100644 index 0000000000..d67a599e1b --- /dev/null +++ b/src/lib/dhcpsrv/backend_store_factory.h @@ -0,0 +1,223 @@ +// Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef BACKEND_STORE_FACTORY_H +#define BACKEND_STORE_FACTORY_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace isc { +namespace dhcp { + +/// @brief Backend Store Factory +/// +/// This class comprises nothing but static methods used to create a forensic +/// backend store. It analyzes the database information passed to the creation +/// function and instantiates an appropriate forensic backend store based on the +/// type requested. +/// +/// Strictly speaking these functions could be stand-alone functions. However, +/// it is convenient to encapsulate them in a class for naming purposes. +class BackendStoreFactory : public boost::noncopyable { +private: + + /// @brief Type of the backend factory function. + /// + /// Factory function returns a pointer to the instance of the configuration + /// backend created. + typedef std::function Factory; + + /// @brief Type of backend store version. + typedef std::function DBVersion; + +public: + + /// @brief Registers new backend factory function for a given backend type. + /// + /// If the backend of the given type has already been registered, perhaps + /// by another hooks library, the CBM will refuse to register another + /// backend of the same type. + /// + /// @param db_type backend type, e.g. "mysql". + /// @param factory pointer to the backend factory function. + /// @param no_log do not log (default false) + /// @param db_version forensic backend version + /// @return true if the factory was successfully added to the map, false + /// if it already exists. + bool static registerBackendFactory(const std::string& db_type, + const Factory& factory, + bool no_log = false, + DBVersion db_version = DBVersion()); + + /// @brief Unregisters the backend factory function for a given backend type. + /// + /// @param db_type backend type, e.g. "mysql". + /// @param no_log do not log (default false) + /// @return false if no factory for the given type was unregistered, true + /// if the factory was removed. + bool static unregisterBackendFactory(const std::string& db_type, + bool no_log = false); + + /// @brief Create an instance of a forensic store backend. + /// + /// This method uses provided @c parameters map representing database + /// connection information to create an instance of the forensic store + /// backend. If the specified backend type is not supported, i.e. there + /// is no relevant factory function registered, an exception is thrown. + /// + /// @param parameters database parameters. + /// @param id the forensic backend manager ID + /// (default value is 0 and it is used only in unit tests). + /// + /// @throw InvalidParameter if access string lacks database type value. + /// @throw db::InvalidType if the type of the database backend is not + /// supported. + /// @throw Unexpected if the backend factory function returned NULL. + static void addBackend(db::DatabaseConnection::ParameterMap& parameters, ManagerID id = 0); + + /// @brief Removes all backends from the pool. + static void delAllBackends() { + pool_.clear(); + } + + /// @brief Returns the forensic backend manager with specified ID. + /// + /// @param id the forensic backend manager ID + /// (default value is 0 and it is used only in unit tests). + /// @return the forensic backend manager instance or null pointer. + static BackendStorePtr& instance(ManagerID id = 0); + + /// @brief Deletes all backends of the given type from the pool. + /// + /// @param db_type backend to remove. + static void delAllBackends(const std::string& db_type) { + auto it = pool_.begin(); + + while (it != pool_.end()) { + if (it->second.second && it->second.second->getType() == db_type) { + it = pool_.erase(it); + } else { + ++it; + } + } + } + + /// @brief Sets the forensic backend manager parameters. + /// + /// @param parameters database parameters. + /// @param id the forensic backend manager ID + /// (default value is 0 and it is used only in unit tests). + static void setParameters(isc::db::DatabaseConnection::ParameterMap parameters, + ManagerID id = 0); + + /// @brief Gets the forensic backend manager parameters. + /// + /// @param parameters database parameters. + /// @param id the forensic backend manager ID + /// (default value is 0 and it is used only in unit tests). + static isc::db::DatabaseConnection::ParameterMap getParameters(ManagerID id = 0); + + /// @brief Delete a forensic backend manager. + /// + /// Delete the first instance of a config database manager which matches + /// specific parameters. + /// This should have the effect of closing the database connection. + /// + /// @param db_type backend to remove. + /// @param parameters database parameters. + /// @param if_unusable Flag which indicates if the config backend should be + /// deleted only if it is unusable. + /// @return false when not removed because it is not found or because it is + /// still usable (if_unusable is true), true otherwise. + static bool delBackend(const std::string& db_type, + db::DatabaseConnection::ParameterMap& parameters, + bool if_unusable = false); + + /// @brief Delete a forensic backend manager. + /// + /// Delete the first instance of a config database manager which matches + /// specific parameters. + /// This should have the effect of closing the database connection. + /// + /// @param id the forensic backend manager ID. + /// @param if_unusable Flag which indicates if the forensic backend should be + /// deleted only if it is unusable. + /// @return false when not removed because it is not found or because it is + /// still usable (if_unusable is true), true otherwise. + static bool delBackend(ManagerID id, + bool if_unusable = false); + + /// @brief Check if a backend store factory was registered + /// + /// @param db_type database type + /// @return true if a factory was registered for db_type, false if not. + static bool registeredFactory(const std::string& db_type); + + /// @brief Logs out all registered backends. + /// + /// We need a dedicated method for this, because we sometimes can't log + /// the backend type when doing early initialization for backends + /// initialized statically. + static void logRegistered(); + + /// @brief Return extended version info for registered backends. + static std::list getDBVersions(); + + /// @brief Returns underlying forensic store backend pool. + static BackendStorePool getPool() { + return (pool_); + } + + /// @brief Returns true is respective backend store is present, false otherwise. + /// + /// @param type the backend store type to check if it exists. + static bool haveInstance(std::string type) { + for (auto const& backend : pool_) { + if (backend.second.second && backend.second.second->getType() == type) { + return (true); + } + } + return (false); + } + + /// @brief Get the hook I/O service. + /// + /// @return the hook I/O service. + static isc::asiolink::IOServicePtr getIOService() { + return (io_service_); + } + + /// @brief Set the hook I/O service. + /// + /// @param io_service the hook I/O service. + static void setIOService(isc::asiolink::IOServicePtr io_service) { + io_service_ = io_service; + } + +protected: + + /// @brief The hook I/O service. + static isc::asiolink::IOServicePtr io_service_; + + /// @brief A map holding registered backend factory functions. + static std::map> map_; + + /// @brief Pointer to the forensic store backends pool. + static BackendStorePool pool_; +}; + +} // end of namespace isc::dhcp +} // end of namespace isc + +#endif // BASE_CONFIG_BACKEND_MGR_H diff --git a/src/lib/dhcpsrv/dhcpsrv_messages.cc b/src/lib/dhcpsrv/dhcpsrv_messages.cc index 7d51603726..6ab28a4192 100644 --- a/src/lib/dhcpsrv/dhcpsrv_messages.cc +++ b/src/lib/dhcpsrv/dhcpsrv_messages.cc @@ -61,6 +61,9 @@ extern const isc::log::MessageID DHCPSRV_DHCP_DDNS_SENDER_STOPPED = "DHCPSRV_DHC extern const isc::log::MessageID DHCPSRV_DHCP_DDNS_SUSPEND_UPDATES = "DHCPSRV_DHCP_DDNS_SUSPEND_UPDATES"; extern const isc::log::MessageID DHCPSRV_EVAL_ERROR = "DHCPSRV_EVAL_ERROR"; extern const isc::log::MessageID DHCPSRV_EVAL_RESULT = "DHCPSRV_EVAL_RESULT"; +extern const isc::log::MessageID DHCPSRV_FORENSIC_BACKENDS_REGISTERED = "DHCPSRV_FORENSIC_BACKENDS_REGISTERED"; +extern const isc::log::MessageID DHCPSRV_FORENSIC_BACKEND_DEREGISTER = "DHCPSRV_FORENSIC_BACKEND_DEREGISTER"; +extern const isc::log::MessageID DHCPSRV_FORENSIC_BACKEND_REGISTER = "DHCPSRV_FORENSIC_BACKEND_REGISTER"; extern const isc::log::MessageID DHCPSRV_HOOK_LEASE4_RECOVER_SKIP = "DHCPSRV_HOOK_LEASE4_RECOVER_SKIP"; extern const isc::log::MessageID DHCPSRV_HOOK_LEASE4_RENEW_SKIP = "DHCPSRV_HOOK_LEASE4_RENEW_SKIP"; extern const isc::log::MessageID DHCPSRV_HOOK_LEASE4_SELECT_SKIP = "DHCPSRV_HOOK_LEASE4_SELECT_SKIP"; @@ -233,6 +236,9 @@ const char* values[] = { "DHCPSRV_DHCP_DDNS_SUSPEND_UPDATES", "DHCP_DDNS updates are being suspended.", "DHCPSRV_EVAL_ERROR", "%1: Expression '%2' evaluated to %3", "DHCPSRV_EVAL_RESULT", "%1: Expression '%2' evaluated to %3", + "DHCPSRV_FORENSIC_BACKENDS_REGISTERED", "the following forensic backend types are available: %1", + "DHCPSRV_FORENSIC_BACKEND_DEREGISTER", "deregistered forensic backend type: %1", + "DHCPSRV_FORENSIC_BACKEND_REGISTER", "registered forensic backend type: %1", "DHCPSRV_HOOK_LEASE4_RECOVER_SKIP", "DHCPv4 lease %1 was not recovered from the declined state because a callout set the skip status.", "DHCPSRV_HOOK_LEASE4_RENEW_SKIP", "DHCPv4 lease was not renewed because a callout set the skip flag.", "DHCPSRV_HOOK_LEASE4_SELECT_SKIP", "Lease4 creation was skipped, because of callout skip flag.", diff --git a/src/lib/dhcpsrv/dhcpsrv_messages.h b/src/lib/dhcpsrv/dhcpsrv_messages.h index c130961fa4..6a2c7e6ed1 100644 --- a/src/lib/dhcpsrv/dhcpsrv_messages.h +++ b/src/lib/dhcpsrv/dhcpsrv_messages.h @@ -62,6 +62,9 @@ extern const isc::log::MessageID DHCPSRV_DHCP_DDNS_SENDER_STOPPED; extern const isc::log::MessageID DHCPSRV_DHCP_DDNS_SUSPEND_UPDATES; extern const isc::log::MessageID DHCPSRV_EVAL_ERROR; extern const isc::log::MessageID DHCPSRV_EVAL_RESULT; +extern const isc::log::MessageID DHCPSRV_FORENSIC_BACKENDS_REGISTERED; +extern const isc::log::MessageID DHCPSRV_FORENSIC_BACKEND_DEREGISTER; +extern const isc::log::MessageID DHCPSRV_FORENSIC_BACKEND_REGISTER; extern const isc::log::MessageID DHCPSRV_HOOK_LEASE4_RECOVER_SKIP; extern const isc::log::MessageID DHCPSRV_HOOK_LEASE4_RENEW_SKIP; extern const isc::log::MessageID DHCPSRV_HOOK_LEASE4_SELECT_SKIP; diff --git a/src/lib/dhcpsrv/dhcpsrv_messages.mes b/src/lib/dhcpsrv/dhcpsrv_messages.mes index 128e1e9ff9..e1c61f8623 100644 --- a/src/lib/dhcpsrv/dhcpsrv_messages.mes +++ b/src/lib/dhcpsrv/dhcpsrv_messages.mes @@ -336,6 +336,20 @@ This debug message indicates that the expression of a client class has been successfully evaluated. The client class name and the result value of the evaluation are printed. +% DHCPSRV_FORENSIC_BACKENDS_REGISTERED the following forensic backend types are available: %1 +This informational message lists all possible forensic backends that could +be used in forensic logging. + +% DHCPSRV_FORENSIC_BACKEND_DEREGISTER deregistered forensic backend type: %1 +Logged at debug log level 40. +This debug message is issued when a backend factory was deregistered. +It is no longer possible to use forensic backend of this type. + +% DHCPSRV_FORENSIC_BACKEND_REGISTER registered forensic backend type: %1 +Logged at debug log level 40. +This debug message is issued when a backend factory was successfully +registered. It is now possible to use forensic backend of this type. + % DHCPSRV_HOOK_LEASE4_RECOVER_SKIP DHCPv4 lease %1 was not recovered from the declined state because a callout set the skip status. Logged at debug log level 40. This debug message is printed when a callout installed on lease4_recover diff --git a/src/lib/dhcpsrv/host_data_source_factory.cc b/src/lib/dhcpsrv/host_data_source_factory.cc index 47f1301a32..417551fae4 100644 --- a/src/lib/dhcpsrv/host_data_source_factory.cc +++ b/src/lib/dhcpsrv/host_data_source_factory.cc @@ -154,9 +154,8 @@ HostDataSourceFactory::deregisterFactory(const string& db_type, bool no_log) { .arg(db_type); } return (true); - } else { - return (false); } + return (false); } bool diff --git a/src/lib/dhcpsrv/lease_mgr_factory.h b/src/lib/dhcpsrv/lease_mgr_factory.h index d71707700b..bf514d2788 100644 --- a/src/lib/dhcpsrv/lease_mgr_factory.h +++ b/src/lib/dhcpsrv/lease_mgr_factory.h @@ -37,9 +37,6 @@ public: /// /// Strictly speaking these functions could be stand-alone functions. However, /// it is convenient to encapsulate them in a class for naming purposes. -/// -/// @todo: Will need to develop some form of registration mechanism for -/// user-supplied backends (so that there is no need to modify the code). class LeaseMgrFactory { public: ~LeaseMgrFactory() { diff --git a/src/lib/dhcpsrv/legal_log_db_log.cc b/src/lib/dhcpsrv/legal_log_db_log.cc new file mode 100644 index 0000000000..4b1e28a289 --- /dev/null +++ b/src/lib/dhcpsrv/legal_log_db_log.cc @@ -0,0 +1,38 @@ +// Copyright (C) 2018-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/. + +/// Defines the logger used by the NSAS + +#include + +#include +#include + +using namespace isc::db; +using namespace std; + +namespace isc { + +namespace db { + +extern mutex db_logger_mutex; + +} // namespace db + +namespace dhcp { + +LegalLogDbLogger::LegalLogDbLogger(DbLogger& legal_log_db_logger) { + lock_guard lk(db_logger_mutex); + db_logger_stack.push_back(legal_log_db_logger); +} + +LegalLogDbLogger::~LegalLogDbLogger() { + lock_guard lk(db_logger_mutex); + db_logger_stack.pop_back(); +} + +} // namespace dhcp +} // namespace isc diff --git a/src/hooks/dhcp/forensic_log/legal_log_db_log.h b/src/lib/dhcpsrv/legal_log_db_log.h similarity index 71% rename from src/hooks/dhcp/forensic_log/legal_log_db_log.h rename to src/lib/dhcpsrv/legal_log_db_log.h index a06b7fed50..dad4342c53 100644 --- a/src/hooks/dhcp/forensic_log/legal_log_db_log.h +++ b/src/lib/dhcpsrv/legal_log_db_log.h @@ -8,20 +8,11 @@ #define LEGAL_LOG_DB_LOG_H #include -#include #include namespace isc { -namespace legal_log { - -/// @brief Legal log database message map -extern const isc::db::DbLogger::MessageMap legal_log_db_message_map; - -/// @brief Legal log database Logger -/// -/// It is the default database logger. -extern isc::db::DbLogger legal_log_db_logger; +namespace dhcp { /// @brief Legal log database logger class in RAII style class LegalLogDbLogger : boost::noncopyable { @@ -30,7 +21,7 @@ public: /// /// Push the legal log database logger on the database logger stack. /// - LegalLogDbLogger(); + LegalLogDbLogger(isc::db::DbLogger& legal_log_db_logger); /// @brief Destructor /// @@ -39,7 +30,7 @@ public: ~LegalLogDbLogger(); }; -} // namespace legal_log +} // namespace dhcp } // namespace isc #endif // LEGAL_LOG_DB_LOG_H diff --git a/src/lib/dhcpsrv/meson.build b/src/lib/dhcpsrv/meson.build index c076e3c874..ba441bb00a 100644 --- a/src/lib/dhcpsrv/meson.build +++ b/src/lib/dhcpsrv/meson.build @@ -5,6 +5,8 @@ kea_dhcpsrv_lib = shared_library( 'alloc_engine.cc', 'alloc_engine_log.cc', 'alloc_engine_messages.cc', + 'backend_store_factory.cc', + 'backend_store.cc', 'cb_ctl_dhcp4.cc', 'cb_ctl_dhcp6.cc', 'cfgmgr.cc', @@ -55,6 +57,7 @@ kea_dhcpsrv_lib = shared_library( 'lease.cc', 'lease_mgr.cc', 'lease_mgr_factory.cc', + 'legal_log_db_log.cc', 'memfile_lease_limits.cc', 'memfile_lease_mgr.cc', 'ncr_generator.cc', @@ -105,6 +108,8 @@ kea_dhcpsrv_headers = [ 'alloc_engine_messages.h', 'allocation_state.h', 'allocator.h', + 'backend_store_factory.h', + 'backend_store.h', 'base_host_data_source.h', 'cache_host_data_source.h', 'callout_handle_store.h', @@ -167,6 +172,7 @@ kea_dhcpsrv_headers = [ 'lease_file_stats.h', 'lease_mgr.h', 'lease_mgr_factory.h', + 'legal_log_db_log.h', 'memfile_lease_limits.h', 'memfile_lease_mgr.h', 'memfile_lease_storage.h', diff --git a/src/lib/dhcpsrv/tests/Makefile.am b/src/lib/dhcpsrv/tests/Makefile.am index f834f9b90b..4b7dde18d0 100644 --- a/src/lib/dhcpsrv/tests/Makefile.am +++ b/src/lib/dhcpsrv/tests/Makefile.am @@ -65,6 +65,7 @@ libdhcpsrv_unittests_SOURCES += alloc_engine_hooks_unittest.cc libdhcpsrv_unittests_SOURCES += alloc_engine4_unittest.cc libdhcpsrv_unittests_SOURCES += alloc_engine6_unittest.cc libdhcpsrv_unittests_SOURCES += allocation_state_unittest.cc +libdhcpsrv_unittests_SOURCES += backend_store_factory_unittest.cc libdhcpsrv_unittests_SOURCES += callout_handle_store_unittest.cc libdhcpsrv_unittests_SOURCES += cb_ctl_dhcp_unittest.cc libdhcpsrv_unittests_SOURCES += cfg_db_access_unittest.cc diff --git a/src/lib/dhcpsrv/tests/backend_store_factory_unittest.cc b/src/lib/dhcpsrv/tests/backend_store_factory_unittest.cc new file mode 100644 index 0000000000..bac79b0bc6 --- /dev/null +++ b/src/lib/dhcpsrv/tests/backend_store_factory_unittest.cc @@ -0,0 +1,275 @@ +// Copyright (C) 2012-2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +using namespace std; +using namespace isc; +using namespace isc::db; +using namespace isc::dhcp; + +// This set of tests only check the parsing functions of BackendStoreFactory. +// Tests of the BackendStore create/instance/destroy are implicitly carried out +// in the tests for the BackendStore. + +namespace { + +class DummyBackendStore : public BackendStore { +public: + + /// @brief Constructor. + DummyBackendStore(const string& type, DatabaseConnection::ParameterMap parameters) : + BackendStore(parameters), type_(type) { + } + + /// @brief Opens the store. + virtual void open() { + } + + /// @brief Closes the store. + virtual void close() { + } + + /// @brief Return backend type + /// + /// Returns the type of the backend (e.g. "mysql", "logfile" etc.) + /// + /// @return Type of the backend. + virtual string getType() const { + return (type_); + } + + /// @brief Appends a string to the store with a timestamp and address. + /// + /// @param text String to append + /// @param addr Address or prefix + /// @throw BackendStoreError if the write fails + virtual void writeln(const string& /* text */, const string& /* addr */) { + } + +private: + + /// @brief The BackendStore type. + string type_; +}; + +BackendStorePtr +dummyFactory(const DatabaseConnection::ParameterMap& parameters) { + return (BackendStorePtr(new DummyBackendStore(string("dummy"), parameters))); +} + +// @brief Register dummyFactory +bool registerFactory() { + static auto db_version = []() -> string { + return (string("version 1")); + }; + return (BackendStoreFactory::registerBackendFactory(string("dummy"), dummyFactory, false, db_version)); +} + +// @brief Derive dummy1 class +class Dummy1BackendStore : public DummyBackendStore { +public: + Dummy1BackendStore(const DatabaseConnection::ParameterMap& parameters) : + DummyBackendStore(string("dummy1"), parameters) { + } + virtual ~Dummy1BackendStore() = default; +}; + +// @brief Factory of dummy1 +BackendStorePtr +dummy1Factory(const DatabaseConnection::ParameterMap& parameters) { + return (BackendStorePtr(new Dummy1BackendStore(parameters))); +} + +// @brief Register dummy1Factory +bool registerFactory1() { + return (BackendStoreFactory::registerBackendFactory(string("dummy1"), dummy1Factory)); +} + +// @brief Derive dummy2 class +class Dummy2BackendStore : public DummyBackendStore { +public: + Dummy2BackendStore(const DatabaseConnection::ParameterMap& parameters) : + DummyBackendStore(string("dummy2"), parameters) { + } + virtual ~Dummy2BackendStore() = default; +}; + +// @brief Factory of dummy2 +BackendStorePtr +dummy2Factory(const DatabaseConnection::ParameterMap& parameters) { + return (BackendStorePtr(new Dummy2BackendStore(parameters))); +} + +// @brief Register dummy2Factory +bool registerFactory2() { + static auto db_version = []() -> string { + return (string("version 2")); + }; + return (BackendStoreFactory::registerBackendFactory(string("dummy2"), dummy2Factory, false, db_version)); +} + +// @brief Factory function returning 0 +BackendStorePtr +factory0(const DatabaseConnection::ParameterMap&) { + return (BackendStorePtr()); +} + +// @brief Test fixture class +class BackendStoreFactoryTest : public ::testing::Test { +public: + /// @brief Constructor + BackendStoreFactoryTest() = default; + + /// @brief Destructor + virtual ~BackendStoreFactoryTest() = default; + +private: + // @brief Prepares the class for a test. + virtual void SetUp() { + BackendStoreFactory::delAllBackends(); + } + + // @brief Cleans up after the test. + virtual void TearDown() { + BackendStoreFactory::delAllBackends(); + BackendStoreFactory::unregisterBackendFactory(string("dummy")); + BackendStoreFactory::unregisterBackendFactory(string("dummy1")); + BackendStoreFactory::unregisterBackendFactory(string("dummy2")); + } +}; + +// Verify a factory can be registered and only once. +TEST_F(BackendStoreFactoryTest, registerFactory) { + EXPECT_TRUE(registerFactory()); + + // Only once + EXPECT_FALSE(registerFactory()); +} + +// Verify a factory registration can be checked. +TEST_F(BackendStoreFactoryTest, registeredFactory) { + // Not yet registered + EXPECT_FALSE(BackendStoreFactory::registeredFactory(string("dummy"))); + EXPECT_FALSE(BackendStoreFactory::registeredFactory(string("dummy1"))); + + // Register dummy + EXPECT_TRUE(registerFactory()); + + // Now dummy is registered but not dummy1 + EXPECT_TRUE(BackendStoreFactory::registeredFactory(string("dummy"))); + EXPECT_FALSE(BackendStoreFactory::registeredFactory(string("dummy1"))); +} + +// Verify a factory can be registered and deregistered +TEST_F(BackendStoreFactoryTest, deregisterFactory) { + // Does not exist at the beginning + EXPECT_FALSE(BackendStoreFactory::unregisterBackendFactory(string("dummy"))); + + // Register and deregister + EXPECT_TRUE(registerFactory()); + EXPECT_TRUE(BackendStoreFactory::unregisterBackendFactory(string("dummy"))); + EXPECT_FALSE(BackendStoreFactory::registeredFactory(string("dummy"))); + + // No longer exists + EXPECT_FALSE(BackendStoreFactory::unregisterBackendFactory(string("dummy"))); +} + +// Verify a registered factory can be called +TEST_F(BackendStoreFactoryTest, create) { + EXPECT_TRUE(registerFactory()); + DatabaseConnection::ParameterMap parameters; + parameters["type"] = "dummy"; + EXPECT_NO_THROW(BackendStoreFactory::addBackend(parameters)); + EXPECT_TRUE(BackendStoreFactory::haveInstance(string("dummy"))); +} + +// Verify that type is required +TEST_F(BackendStoreFactoryTest, notype) { + DatabaseConnection::ParameterMap parameters; + EXPECT_THROW(BackendStoreFactory::addBackend(parameters), + InvalidParameter); + EXPECT_FALSE(BackendStoreFactory::haveInstance(string("dummy"))); + parameters["type"] = "dummy"; + EXPECT_THROW(BackendStoreFactory::addBackend(parameters), + InvalidType); + EXPECT_FALSE(BackendStoreFactory::haveInstance(string("dummy"))); +} + +// Verify that factory must not return null +TEST_F(BackendStoreFactoryTest, null) { + EXPECT_TRUE(BackendStoreFactory::registerBackendFactory(string("dummy"), factory0)); + DatabaseConnection::ParameterMap parameters; + parameters["type"] = "dummy"; + EXPECT_THROW(BackendStoreFactory::addBackend(parameters), + Unexpected); + EXPECT_FALSE(BackendStoreFactory::haveInstance(string("dummy"))); +} + +// Verify destroy class method +TEST_F(BackendStoreFactoryTest, destroy) { + DatabaseConnection::ParameterMap parameters; + parameters["type"] = "dummy"; + // No sources at the beginning + EXPECT_FALSE(BackendStoreFactory::haveInstance(string("dummy"))); + EXPECT_NO_THROW(BackendStoreFactory::delBackend(string("dummy"), parameters)); + EXPECT_FALSE(BackendStoreFactory::haveInstance(string("dummy"))); + + // Add dummy + EXPECT_TRUE(registerFactory()); + EXPECT_NO_THROW(BackendStoreFactory::addBackend(parameters)); + EXPECT_TRUE(BackendStoreFactory::haveInstance(string("dummy"))); + + EXPECT_NO_THROW(BackendStoreFactory::delBackend(string("dummy"), parameters)); + EXPECT_FALSE(BackendStoreFactory::haveInstance(string("dummy"))); +} + +// Verify create and destroy class method on multiple backends +TEST_F(BackendStoreFactoryTest, multiple) { + CfgMgr::instance().setFamily(AF_INET); + // Add foo twice + EXPECT_TRUE(registerFactory1()); + DatabaseConnection::ParameterMap parameters; + parameters["type"] = "dummy1"; + EXPECT_NO_THROW(BackendStoreFactory::addBackend(parameters)); + EXPECT_NO_THROW(BackendStoreFactory::addBackend(parameters)); + EXPECT_TRUE(BackendStoreFactory::haveInstance(string("dummy1"))); + + // Add dummy2 once + EXPECT_TRUE(registerFactory2()); + parameters["type"] = "dummy2"; + EXPECT_NO_THROW(BackendStoreFactory::addBackend(parameters, 1)); + EXPECT_TRUE(BackendStoreFactory::haveInstance(string("dummy2"))); + + list expected; + expected.push_back("version 2"); + EXPECT_EQ(expected, BackendStoreFactory::getDBVersions()); + + EXPECT_TRUE(registerFactory()); + + expected.clear(); + expected.push_back("version 1"); + expected.push_back("version 2"); + EXPECT_EQ(expected, BackendStoreFactory::getDBVersions()); + + // Delete them + EXPECT_NO_THROW(BackendStoreFactory::delAllBackends()); + EXPECT_FALSE(BackendStoreFactory::haveInstance(string("dummy"))); +} + +} // end of anonymous namespace diff --git a/src/lib/dhcpsrv/tests/lease_mgr_factory_unittest.cc b/src/lib/dhcpsrv/tests/lease_mgr_factory_unittest.cc index bb22484cf0..fbaa535d64 100644 --- a/src/lib/dhcpsrv/tests/lease_mgr_factory_unittest.cc +++ b/src/lib/dhcpsrv/tests/lease_mgr_factory_unittest.cc @@ -28,10 +28,6 @@ using namespace isc::dhcp; // Tests of the LeaseMgr create/instance/destroy are implicitly carried out // in the tests for the different concrete lease managers (e.g. MySqlLeaseMgr). -// Currently there are no unit-tests as the sole testable method (parse) -// was moved to its own class (DataSource). All existing unit-tests were -// moved there. - namespace { TrackingLeaseMgrPtr diff --git a/src/lib/dhcpsrv/tests/meson.build b/src/lib/dhcpsrv/tests/meson.build index 9ccd4bf478..3d5b690f83 100644 --- a/src/lib/dhcpsrv/tests/meson.build +++ b/src/lib/dhcpsrv/tests/meson.build @@ -54,6 +54,7 @@ kea_dhcpsrv_tests = executable( 'alloc_engine_expiration_unittest.cc', 'alloc_engine_hooks_unittest.cc', 'allocation_state_unittest.cc', + 'backend_store_factory_unittest.cc', 'callout_handle_store_unittest.cc', 'cb_ctl_dhcp_unittest.cc', 'cfg_db_access_unittest.cc', diff --git a/src/lib/mysql/mysql_connection.cc b/src/lib/mysql/mysql_connection.cc index ae1303bbad..59f7088b56 100644 --- a/src/lib/mysql/mysql_connection.cc +++ b/src/lib/mysql/mysql_connection.cc @@ -286,6 +286,9 @@ MySqlConnection::openDatabase() { auto const& rec = reconnectCtl(); if (rec && DatabaseConnection::retry_) { + // Mark this connection as no longer usable. + markUnusable(); + // Start the connection recovery. startRecoverDbConnection(); diff --git a/src/lib/pgsql/pgsql_connection.cc b/src/lib/pgsql/pgsql_connection.cc index 2424b5f999..c76c3ef7d0 100644 --- a/src/lib/pgsql/pgsql_connection.cc +++ b/src/lib/pgsql/pgsql_connection.cc @@ -119,7 +119,7 @@ PgSqlTransaction::commit() { } PgSqlConnection::~PgSqlConnection() { - if (conn_) { + if (conn_ && !isUnusable()) { // Deallocate the prepared queries. if (PQstatus(conn_) == CONNECTION_OK) { PgSqlResult r(PQexec(conn_, "DEALLOCATE all")); @@ -421,6 +421,9 @@ PgSqlConnection::openDatabaseInternal(bool logging) { auto const& rec = reconnectCtl(); if (rec && DatabaseConnection::retry_) { + // Mark this connection as no longer usable. + markUnusable(); + // Start the connection recovery. startRecoverDbConnection();