]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3025] automatic init of mysql schema
authorAndrei Pavel <andrei@isc.org>
Mon, 20 Nov 2023 10:21:45 +0000 (12:21 +0200)
committerAndrei Pavel <andrei@isc.org>
Thu, 22 Feb 2024 07:57:35 +0000 (09:57 +0200)
src/lib/database/database_connection.h
src/lib/dhcpsrv/mysql_lease_mgr.cc
src/lib/dhcpsrv/mysql_lease_mgr.h
src/lib/mysql/Makefile.am
src/lib/mysql/mysql_connection.cc
src/lib/mysql/mysql_connection.h

index 3f36e963e4d264ad9cd40c21028c1b7ceb4789be..f85d8a0b567e967130bf9d01b23434d1738f62bd 100644 (file)
@@ -93,6 +93,14 @@ public:
         isc::Exception(file, line, what) {}
 };
 
+/// @brief Thrown when an initialization of the schema failed.
+class SchemaInitializationFailed: public Exception {
+public:
+    SchemaInitializationFailed(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+
 /// @brief Defines a callback prototype for propagating events upward
 typedef std::function<bool (util::ReconnectCtlPtr db_reconnect_ctl)> DbCallback;
 
index 1f95e41940f34b9af8af3a2a4c66aea3fce764e6..23ad6f2d768adb4586e5b20826b2729dc8f3f618 100644 (file)
@@ -2185,38 +2185,28 @@ MySqlLeaseMgr::MySqlLeaseTrackingContextAlloc::~MySqlLeaseTrackingContextAlloc()
 // MySqlLeaseMgr Constructor and Destructor
 
 MySqlLeaseMgr::MySqlLeaseMgr(const DatabaseConnection::ParameterMap& parameters)
-    : TrackingLeaseMgr(), parameters_(parameters), timer_name_("") {
+    : TrackingLeaseMgr(), parameters_(parameters) {
 
     // Check if the extended info tables are enabled.
     setExtendedInfoTablesEnabled(parameters);
 
+    // retry-on-startup?
+    bool const retry(parameters.count("retry-on-startup") &&
+                     parameters.at("retry-on-startup") == "true");
+
+    // retry-on-startup disabled. Ensure schema version with empty timer name / no retry.
+    if (!retry) {
+        ensureSchemaVersion();
+    }
+
     // Create unique timer name per instance.
     timer_name_ = "MySqlLeaseMgr[";
     timer_name_ += boost::lexical_cast<std::string>(reinterpret_cast<uint64_t>(this));
     timer_name_ += "]DbReconnectTimer";
 
-    // Validate schema version first.
-    std::pair<uint32_t, uint32_t> code_version(MYSQL_SCHEMA_VERSION_MAJOR,
-                                               MYSQL_SCHEMA_VERSION_MINOR);
-
-    std::string timer_name;
-    bool retry = false;
-    if (parameters.count("retry-on-startup")) {
-        if (parameters.at("retry-on-startup") == "true") {
-            retry = true;
-        }
-    }
+    // retry-on-startup enabled. Ensure schema version with timer name set / retries.
     if (retry) {
-        timer_name = timer_name_;
-    }
-
-    std::pair<uint32_t, uint32_t> db_version = getVersion(timer_name);
-    if (code_version != db_version) {
-        isc_throw(DbOpenError,
-                  "MySQL schema version mismatch: need version: "
-                      << code_version.first << "." << code_version.second
-                      << " found version: " << db_version.first << "."
-                      << db_version.second);
+        ensureSchemaVersion();
     }
 
     // Create an initial context.
@@ -3889,6 +3879,16 @@ MySqlLeaseMgr::getDescription() const {
     return (std::string("MySQL Database"));
 }
 
+void
+MySqlLeaseMgr::ensureSchemaVersion() const {
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_GET_VERSION);
+
+    IOServiceAccessorPtr ac(new IOServiceAccessor(&DatabaseConnection::getIOService));
+    DbCallback cb(&MySqlLeaseMgr::dbReconnect);
+
+    return (MySqlConnection::ensureSchemaVersion(parameters_, ac, cb, timer_name_));
+}
+
 std::pair<uint32_t, uint32_t>
 MySqlLeaseMgr::getVersion(const string& timer_name) const {
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_GET_VERSION);
index af1c964eafe4ba6896c7869668b0d5b2ff6af61f..7f6cb14130269bd1b0cce1802ea6e1786186c679 100644 (file)
@@ -700,6 +700,8 @@ public:
     /// @return Description of the backend.
     virtual std::string getDescription() const override;
 
+    void ensureSchemaVersion() const;
+
     /// @brief Returns backend version.
     ///
     /// @param timer_name The DB reconnect timer name.
index d3ff255fdbc40a34990ba72ab4e2ccefaac8cf08..f8a41b80093a70f44d9080189403ede6eb29e7bd 100644 (file)
@@ -1,6 +1,8 @@
 SUBDIRS = . testutils tests
 
-AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
+AM_CPPFLAGS  =
+AM_CPPFLAGS += -DKEA_ADMIN=\"@prefix@/sbin/kea-admin\"
+AM_CPPFLAGS += -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += $(BOOST_INCLUDES) $(MYSQL_CPPFLAGS)
 
 AM_CXXFLAGS = $(KEA_CXXFLAGS)
index 12234774b9776479f068a801eff0cb2f135a8bf8..d3a13aec43ff3a39ec8ce541e24ce42a1872a928 100644 (file)
@@ -6,6 +6,8 @@
 
 #include <config.h>
 
+#include <asiolink/io_service.h>
+#include <asiolink/process_spawn.h>
 #include <database/database_connection.h>
 #include <database/db_log.h>
 #include <exceptions/exceptions.h>
 #include <boost/lexical_cast.hpp>
 
 #include <algorithm>
-#include <stdint.h>
-#include <string>
+#include <cstdint>
 #include <limits>
+#include <string>
 
 using namespace isc;
+using namespace isc::asiolink;
 using namespace std;
 
 namespace isc {
@@ -366,6 +369,101 @@ MySqlConnection::getVersion(const ParameterMap& parameters,
     }
 }
 
+void
+MySqlConnection::ensureSchemaVersion(const ParameterMap& parameters,
+                                     const IOServiceAccessorPtr& ac,
+                                     const DbCallback& cb,
+                                     const string& timer_name) {
+    pair<uint32_t, uint32_t> schema_version;
+    try {
+        schema_version = getVersion(parameters, ac, cb, timer_name);
+    } catch (DbOpenError const& exception) {
+        // Do nothing for open errors. We first need to establish a connection,
+        // and only afterwards can we initialize the schema if it still fails.
+        // Let it fail, or retry if retry is configured.
+    } catch (exception const& exception) {
+        // This may fail for a variety of reasons. We don't have to necessarily
+        // check for the error that is most common in situations where the
+        // database is not initialized which would sound something like
+        // "table schema_version does not exist". If the error had another
+        // cause, it will fail again during initialization or during the
+        // subsequent version retrieval and that is fine.
+        initializeSchema(parameters);
+
+        // Retrieve again because the initial retrieval failed.
+        schema_version = getVersion(parameters, ac, cb, timer_name);
+    }
+
+    // Check that the versions match.
+    pair<uint32_t, uint32_t> const expected_version(MYSQL_SCHEMA_VERSION_MAJOR,
+                                                    MYSQL_SCHEMA_VERSION_MINOR);
+    if (schema_version != expected_version) {
+        isc_throw(DbOpenError, "MySQL schema version mismatch: expected version: "
+                                   << expected_version.first << "." << expected_version.second
+                                   << ", found version: " << schema_version.first << "."
+                                   << schema_version.second);
+    }
+}
+
+void
+MySqlConnection::initializeSchema(const ParameterMap& parameters) {
+    IOServicePtr io_service(new IOService());
+    ProcessSpawn kea_admin(io_service, KEA_ADMIN, kea_admin_parameters);
+    DB_LOG_INFO(MYSQL_INITIALIZE_SCHEMA).arg(kea_admin.getCommandLine());
+    pid_t const pid(kea_admin.spawn());
+    io_service->runOne();
+    if (kea_admin.isRunning(pid)) {
+        // TODO: implement synchronous process spawning. Otherwise kea-admin is not waited by the
+        // parent process, and it becomes a zombie, even though its work is finished. Uncomment the
+        // following throw when that is done.
+        // isc_throw(SchemaInitializationFailed, "kea-admin still running");
+    }
+    int const exit_code(kea_admin.getExitStatus(pid));
+    if (exit_code != 0) {
+        isc_throw(SchemaInitializationFailed, "Expected exit code 0. Got " << exit_code);
+    }
+}
+
+vector<string> MySqlConnection::toKeaAdminParameters(ParameterMap const& params) {
+    vector<string> result{"mysql"};
+    for (auto const& p : params) {
+        string const& keyword(p.first);
+        string const& value(p.second);
+
+        // These Kea parameters are the same as the kea-admin parameters.
+        if (keyword == "user" ||
+            keyword == "password" ||
+            keyword == "host" ||
+            keyword == "port" ||
+            keyword == "name"||
+            keyword == "connect-timeout") {
+            result.push_back("--" + keyword);
+            result.push_back(value);
+            continue;
+        }
+
+        // These Kea parameters do not have a direct kea-admin equivalent.
+        // But they do have a mariadb client flag equivalent.
+        // We pass them to kea-admin using the --extra flag.
+        static unordered_map<string, string> conversions{
+            {"cihper-list", "ssl-cipher"},
+            {"cert-file", "ssl-cert"},
+            {"key-file", "ssl-key"},
+            {"trust-anchor", "ssl-ca"},
+        };
+        bool extra_flag_added(false);
+        if (conversions.count(keyword)) {
+            if (!extra_flag_added) {
+                result.push_back("--extra");
+                extra_flag_added = true;
+            }
+            result.push_back("--" + conversions.at(keyword));
+            result.push_back(value);
+        }
+    }
+    return result;
+}
+
 // Prepared statement setup.  The textual form of an SQL statement is stored
 // in a vector of strings (text_statements_) and is used in the output of
 // error messages.  The SQL statement is also compiled into a "prepared
index bd70f411ea3732a481d3fb06c53cd2da7fe38da2..f3188c5fe5666655b47660383746216c16f33088 100644 (file)
@@ -275,6 +275,29 @@ public:
                const DbCallback& cb = DbCallback(),
                const std::string& timer_name = std::string());
 
+    /// @brief Retrieve schema version, validate it against the hardcoded
+    ///     version, and attempt to initialize the schema if there is an
+    ///     error during retrieval.
+    ///
+    /// @param parameters A data structure relating keywords and values
+    ///     concerned with the database.
+    ///
+    /// @throw isc::db::ScehamInitializationFailed if the initialization fails
+    static void
+    ensureSchemaVersion(const ParameterMap& parameters,
+                        const IOServiceAccessorPtr& ac = IOServiceAccessorPtr(),
+                        const DbCallback& cb = DbCallback(),
+                        const std::string& timer_name = std::string());
+
+    /// @brief Initialize schema.
+    ///
+    /// @param parameters A data structure relating keywords and values
+    ///     concerned with the database.
+    ///
+    /// @throw isc::db::ScehamInitializationFailed if the initialization fails
+    static void
+    initializeSchema(const ParameterMap& parameters);
+
     /// @brief Prepare Single Statement
     ///
     /// Creates a prepared statement from the text given and adds it to the