From fcc1799a5de532ffb183aae382b7104fab677b26 Mon Sep 17 00:00:00 2001 From: Michael Friedrich Date: Thu, 25 Oct 2018 14:10:30 +0200 Subject: [PATCH] Split config file sync updates, part I This commit also introduces a playground for checksums, whilst refactoring the code in large parts. --- lib/remote/apilistener-filesync.cpp | 511 ++++++++++++++++------------ lib/remote/apilistener.cpp | 2 +- lib/remote/apilistener.hpp | 14 +- 3 files changed, 306 insertions(+), 221 deletions(-) diff --git a/lib/remote/apilistener-filesync.cpp b/lib/remote/apilistener-filesync.cpp index a65617318..cec19e580 100644 --- a/lib/remote/apilistener-filesync.cpp +++ b/lib/remote/apilistener-filesync.cpp @@ -3,6 +3,8 @@ #include "remote/apilistener.hpp" #include "remote/apifunction.hpp" #include "config/configcompiler.hpp" +#include "base/tlsutility.hpp" +#include "base/json.hpp" #include "base/configtype.hpp" #include "base/logger.hpp" #include "base/convert.hpp" @@ -18,196 +20,18 @@ REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler); boost::mutex ApiListener::m_ConfigSyncStageLock; /** - * Read the given file and store it in the config information structure. - * Callback function for Glob(). - * - * @param config Reference to the config information object. - * @param path File path. - * @param file Full file name. - */ -void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file) -{ - CONTEXT("Creating config update for file '" + file + "'"); - - Log(LogNotice, "ApiListener") - << "Creating config update for file '" << file << "'."; - - std::ifstream fp(file.CStr(), std::ifstream::binary); - if (!fp) - return; - - String content((std::istreambuf_iterator(fp)), std::istreambuf_iterator()); - - Dictionary::Ptr update; - - /* - * 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp). - * - * **Keep this intact to stay compatible with older clients.** - */ - if (Utility::Match("*.conf", file)) - update = config.UpdateV1; - else - update = config.UpdateV2; - - update->Set(file.SubStr(path.GetLength()), content); -} - -/** - * Compatibility helper for merging config update v1 and v2 into a global result. - * - * @param config Config information structure. - * @returns Dictionary which holds the merged information. - */ -Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config) -{ - Dictionary::Ptr result = new Dictionary(); - - if (config.UpdateV1) - config.UpdateV1->CopyTo(result); - - if (config.UpdateV2) - config.UpdateV2->CopyTo(result); - - return result; -} - -/** - * Load the given config dir and read their file content into the config structure. - * - * @param dir Path to the config directory. - * @returns ConfigInformation structure. - */ -ConfigDirInformation ApiListener::LoadConfigDir(const String& dir) -{ - ConfigDirInformation config; - config.UpdateV1 = new Dictionary(); - config.UpdateV2 = new Dictionary(); - Utility::GlobRecursive(dir, "*", std::bind(&ApiListener::ConfigGlobHandler, std::ref(config), dir, _1), GlobFile); - return config; -} - -/** - * Diffs the old current configuration with the new configuration - * and copies the collected content. Detects whether a change - * happened, this is used for later restarts. - * - * This generic function is called in two situations: - * - Local zones.d to var/lib/api/zones copy on the master (authoritative: true) - * - Received config update on a cluster node (authoritative: false) + * Entrypoint for updating all authoritative configs into var/lib/icinga2/api/zones * - * @param oldConfigInfo Config information struct for the current old deployed config. - * @param newConfigInfo Config information struct for the received synced config. - * @param configDir Destination for copying new files (production, or stage dir). - * @param zoneName Currently processed zone, for storing the relative paths for later. - * @param relativePaths Reference which stores all updated config path destinations. - * @param Whether we're authoritative for this config. - * @returns Whether a config change happened. */ -bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo, - const String& configDir, const String& zoneName, std::vector& relativePaths, bool authoritative) +void ApiListener::SyncLocalZoneDirs() const { - bool configChange = false; - - Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo); - Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo); - - double oldTimestamp; - - if (!oldConfig->Contains("/.timestamp")) - oldTimestamp = 0; - else - oldTimestamp = oldConfig->Get("/.timestamp"); - - double newTimestamp; - - if (!newConfig->Contains("/.timestamp")) - newTimestamp = Utility::GetTime(); - else - newTimestamp = newConfig->Get("/.timestamp"); - - /* skip update if our configuration files are more recent */ - if (oldTimestamp >= newTimestamp) { - Log(LogNotice, "ApiListener") - << "Our configuration is more recent than the received configuration update." - << " Ignoring configuration file update for path '" << configDir << "'. Current timestamp '" - << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' (" - << std::fixed << std::setprecision(6) << oldTimestamp - << ") >= received timestamp '" - << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' (" - << newTimestamp << ")."; - return false; - } - - size_t numBytes = 0; - - { - ObjectLock olock(newConfig); - for (const Dictionary::Pair& kv : newConfig) { - if (oldConfig->Get(kv.first) != kv.second) { - if (!Utility::Match("*/.timestamp", kv.first)) - configChange = true; - - /* Store the relative config file path for later. */ - relativePaths.push_back(zoneName + "/" + kv.first); - - String path = configDir + "/" + kv.first; - Log(LogInformation, "ApiListener") - << "Updating configuration file: " << path; - - /* Sync string content only. */ - String content = kv.second; - - /* Generate a directory tree (zones/1/2/3 might not exist yet). */ - Utility::MkDirP(Utility::DirName(path), 0755); - std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); - fp << content; - fp.close(); - - numBytes += content.GetLength(); - } - } - } - - /* Log something whether we're authoritative or receing a staged config. */ - Log(LogInformation, "ApiListener") - << "Applying configuration file update for " << (authoritative ? "" : "stage ") - << "path '" << configDir << "' (" << numBytes << " Bytes). Received timestamp '" - << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' (" - << std::fixed << std::setprecision(6) << newTimestamp - << "), Current timestamp '" - << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' (" - << oldTimestamp << ")."; - - /* If the update removes a path, delete it on disk. */ - ObjectLock xlock(oldConfig); - for (const Dictionary::Pair& kv : oldConfig) { - if (!newConfig->Contains(kv.first)) { - configChange = true; - - String path = configDir + "/" + kv.first; - (void) unlink(path.CStr()); - } - } - - /* Consider that one of the paths leaves an empty directory here. Such is not copied from stage to prod and purged then automtically. */ - - String tsPath = configDir + "/.timestamp"; - if (!Utility::PathExists(tsPath)) { - std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc); - fp << std::fixed << newTimestamp; - fp.close(); - } - - if (authoritative) { - String authPath = configDir + "/.authoritative"; - if (!Utility::PathExists(authPath)) { - std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc); - fp.close(); + for (const Zone::Ptr& zone : ConfigType::GetObjectsByType()) { + try { + SyncLocalZoneDir(zone); + } catch (const std::exception&) { + continue; } } - - return configChange; } /** @@ -221,7 +45,7 @@ bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, con * * @param zone Pointer to the zone object being synced. */ -void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const +void ApiListener::SyncLocalZoneDir(const Zone::Ptr& zone) const { if (!zone) return; @@ -229,23 +53,31 @@ void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const ConfigDirInformation newConfigInfo; newConfigInfo.UpdateV1 = new Dictionary(); newConfigInfo.UpdateV2 = new Dictionary(); + newConfigInfo.Checksums = new Dictionary(); String zoneName = zone->GetName(); + /* Load registered zone paths, e.g. '_etc', '_api' and user packages. */ for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) { ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path); + /* Config files '*.conf'. */ { ObjectLock olock(newConfigPart.UpdateV1); for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) { - newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second); + String path = "/" + zf.Tag + kv.first; + newConfigInfo.UpdateV1->Set(path, kv.second); + newConfigInfo.Checksums->Set(path, GetChecksum(kv.second)); } } + /* Meta files. */ { ObjectLock olock(newConfigPart.UpdateV2); for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) { - newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second); + String path = "/" + zf.Tag + kv.first; + newConfigInfo.UpdateV2->Set(path, kv.second); + newConfigInfo.Checksums->Set(path, GetChecksum(kv.second)); } } } @@ -255,34 +87,68 @@ void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const if (sumUpdates == 0) return; - String currentDir = Configuration::DataDir + "/api/zones/" + zoneName; + String productionZonesDir = GetApiZonesDir() + zoneName; Log(LogInformation, "ApiListener") - << "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << currentDir << "'."; - - ConfigDirInformation oldConfigInfo = LoadConfigDir(currentDir); + << "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << productionZonesDir << "'."; /* Purge files to allow deletion via zones.d. */ - Utility::RemoveDirRecursive(currentDir); - Utility::MkDirP(currentDir, 0700); + if (Utility::PathExists(productionZonesDir)) + Utility::RemoveDirRecursive(productionZonesDir); - std::vector relativePaths; - UpdateConfigDir(oldConfigInfo, newConfigInfo, currentDir, zoneName, relativePaths, true); -} + Utility::MkDirP(productionZonesDir, 0700); -/** - * Entrypoint for updating all authoritative configs into var/lib/icinga2/api/zones - * - */ -void ApiListener::SyncZoneDirs() const -{ - for (const Zone::Ptr& zone : ConfigType::GetObjectsByType()) { - try { - SyncZoneDir(zone); - } catch (const std::exception&) { - continue; + /* Copy content and add additional meta data. */ + size_t numBytes = 0; + + /* Note: We cannot simply copy directories here. + * + * Zone directories are registered from everywhere and we already + * have read their content into memory with LoadConfigDir(). + */ + Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo); + + { + ObjectLock olock(newConfig); + for (const Dictionary::Pair& kv : newConfig) { + String dst = productionZonesDir + "/" + kv.first; + Utility::MkDirP(Utility::DirName(dst), 0755); + + Log(LogInformation, "ApiListener") + << "Updating configuration file: " << dst; + + String content = kv.second; + std::ofstream fp(dst.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); + fp << content; + fp.close(); + + numBytes += content.GetLength(); } } + + /* Additional metadata. */ + String tsPath = productionZonesDir + "/.timestamp"; + + if (!Utility::PathExists(tsPath)) { + std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc); + fp << std::fixed << Utility::GetTime(); + fp.close(); + } + + String authPath = productionZonesDir + "/.authoritative"; + + if (!Utility::PathExists(authPath)) { + std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc); + } + + String checksumsPath = productionZonesDir + "/.checksums"; + + if (Utility::PathExists(checksumsPath)) + (void) unlink(checksumsPath.CStr()); + + std::ofstream fp(checksumsPath.CStr(), std::ofstream::out | std::ostream::trunc); + fp << std::fixed << JsonEncode(newConfigInfo.Checksums); + fp.close(); } /** @@ -307,11 +173,13 @@ void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient) Dictionary::Ptr configUpdateV1 = new Dictionary(); Dictionary::Ptr configUpdateV2 = new Dictionary(); + Dictionary::Ptr configUpdateChecksums = new Dictionary(); - String zonesDir = Configuration::DataDir + "/api/zones"; + String zonesDir = GetApiZonesDir(); for (const Zone::Ptr& zone : ConfigType::GetObjectsByType()) { - String zoneDir = zonesDir + "/" + zone->GetName(); + String zoneName = zone->GetName(); + String zoneDir = zonesDir + zoneName; if (!zone->IsChildOf(azone) && !zone->IsGlobal()) continue; @@ -321,11 +189,13 @@ void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient) Log(LogInformation, "ApiListener") << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "") - << "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'."; + << "zone '" << zoneName << "' to endpoint '" << endpoint->GetName() << "'."; + + ConfigDirInformation config = LoadConfigDir(zoneDir); - ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName()); - configUpdateV1->Set(zone->GetName(), config.UpdateV1); - configUpdateV2->Set(zone->GetName(), config.UpdateV2); + configUpdateV1->Set(zoneName, config.UpdateV1); + configUpdateV2->Set(zoneName, config.UpdateV2); + configUpdateChecksums->Set(zoneName, config.Checksums); } Dictionary::Ptr message = new Dictionary({ @@ -333,7 +203,8 @@ void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient) { "method", "config::Update" }, { "params", new Dictionary({ { "update", configUpdateV1 }, - { "update_v2", configUpdateV2 } + { "update_v2", configUpdateV2 }, /* Since 2.4.2. */ + { "checksums", configUpdateChecksums } /* Since 2.11.0. */ }) } }); @@ -457,6 +328,129 @@ Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const D return Empty; } +/** + * Diffs the old current configuration with the new configuration + * and copies the collected content. Detects whether a change + * happened, this is used for later restarts. + * + * This generic function is called in two situations: + * - Local zones.d to var/lib/api/zones copy on the master (authoritative: true) + * - Received config update on a cluster node (authoritative: false) + * + * @param oldConfigInfo Config information struct for the current old deployed config. + * @param newConfigInfo Config information struct for the received synced config. + * @param configDir Destination for copying new files (production, or stage dir). + * @param zoneName Currently processed zone, for storing the relative paths for later. + * @param relativePaths Reference which stores all updated config path destinations. + * @param Whether we're authoritative for this config. + * @returns Whether a config change happened. + */ +bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo, + const String& configDir, const String& zoneName, std::vector& relativePaths, bool authoritative) +{ + bool configChange = false; + + Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo); + Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo); + + double oldTimestamp; + + if (!oldConfig->Contains("/.timestamp")) + oldTimestamp = 0; + else + oldTimestamp = oldConfig->Get("/.timestamp"); + + double newTimestamp; + + if (!newConfig->Contains("/.timestamp")) + newTimestamp = Utility::GetTime(); + else + newTimestamp = newConfig->Get("/.timestamp"); + + /* skip update if our configuration files are more recent */ + if (oldTimestamp >= newTimestamp) { + Log(LogNotice, "ApiListener") + << "Our configuration is more recent than the received configuration update." + << " Ignoring configuration file update for path '" << configDir << "'. Current timestamp '" + << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' (" + << std::fixed << std::setprecision(6) << oldTimestamp + << ") >= received timestamp '" + << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' (" + << newTimestamp << ")."; + return false; + } + + size_t numBytes = 0; + + { + ObjectLock olock(newConfig); + for (const Dictionary::Pair& kv : newConfig) { + if (oldConfig->Get(kv.first) != kv.second) { + if (!Utility::Match("*/.timestamp", kv.first)) + configChange = true; + + /* Store the relative config file path for later. */ + relativePaths.push_back(zoneName + "/" + kv.first); + + String path = configDir + "/" + kv.first; + Log(LogInformation, "ApiListener") + << "Updating configuration file: " << path; + + /* Sync string content only. */ + String content = kv.second; + + /* Generate a directory tree (zones/1/2/3 might not exist yet). */ + Utility::MkDirP(Utility::DirName(path), 0755); + std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); + fp << content; + fp.close(); + + numBytes += content.GetLength(); + } + } + } + + /* Log something whether we're authoritative or receing a staged config. */ + Log(LogInformation, "ApiListener") + << "Applying configuration file update for " << (authoritative ? "" : "stage ") + << "path '" << configDir << "' (" << numBytes << " Bytes). Received timestamp '" + << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' (" + << std::fixed << std::setprecision(6) << newTimestamp + << "), Current timestamp '" + << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' (" + << oldTimestamp << ")."; + + /* If the update removes a path, delete it on disk. */ + ObjectLock xlock(oldConfig); + for (const Dictionary::Pair& kv : oldConfig) { + if (!newConfig->Contains(kv.first)) { + configChange = true; + + String path = configDir + "/" + kv.first; + (void) unlink(path.CStr()); + } + } + + /* Consider that one of the paths leaves an empty directory here. Such is not copied from stage to prod and purged then automtically. */ + + String tsPath = configDir + "/.timestamp"; + if (!Utility::PathExists(tsPath)) { + std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc); + fp << std::fixed << newTimestamp; + fp.close(); + } + + if (authoritative) { + String authPath = configDir + "/.authoritative"; + if (!Utility::PathExists(authPath)) { + std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc); + fp.close(); + } + } + + return configChange; +} + /** * Callback for stage config validation. * When validation was successful, the configuration is copied from @@ -586,3 +580,90 @@ void ApiListener::ClearLastFailedZonesStageValidation() { SetLastFailedZonesStageValidation(Dictionary::Ptr()); } + +/** + * Generate a config checksum. + * + * @param + */ +String ApiListener::GetChecksum(const String& content) +{ + return SHA256(content); +} + +/** + * Load the given config dir and read their file content into the config structure. + * + * @param dir Path to the config directory. + * @returns ConfigInformation structure. + */ +ConfigDirInformation ApiListener::LoadConfigDir(const String& dir) +{ + ConfigDirInformation config; + config.UpdateV1 = new Dictionary(); + config.UpdateV2 = new Dictionary(); + config.Checksums = new Dictionary(); + + Utility::GlobRecursive(dir, "*", std::bind(&ApiListener::ConfigGlobHandler, std::ref(config), dir, _1), GlobFile); + return config; +} + +/** + * Read the given file and store it in the config information structure. + * Callback function for Glob(). + * + * @param config Reference to the config information object. + * @param path File path. + * @param file Full file name. + */ +void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file) +{ + CONTEXT("Creating config update for file '" + file + "'"); + + Log(LogNotice, "ApiListener") + << "Creating config update for file '" << file << "'."; + + std::ifstream fp(file.CStr(), std::ifstream::binary); + if (!fp) + return; + + String content((std::istreambuf_iterator(fp)), std::istreambuf_iterator()); + + Dictionary::Ptr update; + String relativePath = file.SubStr(path.GetLength()); + + /* + * 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp). + * + * **Keep this intact to stay compatible with older clients.** + */ + if (Utility::Match("*.conf", file)) + update = config.UpdateV1; + else + update = config.UpdateV2; + + update->Set(relativePath, content); + + /* Calculate a checksum for each file (and a global one later). */ + config.Checksums->Set(relativePath, GetChecksum(content)); +} + +/** + * Compatibility helper for merging config update v1 and v2 into a global result. + * + * @param config Config information structure. + * @returns Dictionary which holds the merged information. + */ +Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config) +{ + Dictionary::Ptr result = new Dictionary(); + + if (config.UpdateV1) + config.UpdateV1->CopyTo(result); + + if (config.UpdateV2) + config.UpdateV2->CopyTo(result); + + return result; +} + diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index b9f8b9780..661a4eaaf 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -242,7 +242,7 @@ void ApiListener::Start(bool runtimeCreated) Log(LogInformation, "ApiListener") << "'" << GetName() << "' started."; - SyncZoneDirs(); + SyncLocalZoneDirs(); ObjectImpl::Start(runtimeCreated); diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp index 3d1082444..f9d92848f 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -32,6 +32,7 @@ struct ConfigDirInformation { Dictionary::Ptr UpdateV1; Dictionary::Ptr UpdateV2; + Dictionary::Ptr Checksums; }; /** @@ -170,21 +171,24 @@ private: /* filesync */ static boost::mutex m_ConfigSyncStageLock; - static ConfigDirInformation LoadConfigDir(const String& dir); + void SyncLocalZoneDirs() const; + void SyncLocalZoneDir(const Zone::Ptr& zone) const; + + void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient); + static Dictionary::Ptr MergeConfigUpdate(const ConfigDirInformation& config); static bool UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo, const String& configDir, const String& zoneName, std::vector& relativePaths, bool authoritative); - void SyncZoneDirs() const; - void SyncZoneDir(const Zone::Ptr& zone) const; - + static ConfigDirInformation LoadConfigDir(const String& dir); static void ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file); - void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient); static void TryActivateZonesStageCallback(const ProcessResult& pr, const std::vector& relativePaths); static void AsyncTryActivateZonesStage(const std::vector& relativePaths); + static String GetChecksum(const String& content); + void UpdateLastFailedZonesStageValidation(const String& log); void ClearLastFailedZonesStageValidation(); -- 2.40.0