#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"
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<char>(fp)), std::istreambuf_iterator<char>());
-
- 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<String>& 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<Zone>()) {
+ try {
+ SyncLocalZoneDir(zone);
+ } catch (const std::exception&) {
+ continue;
}
}
-
- return configChange;
}
/**
*
* @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;
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));
}
}
}
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<String> 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<Zone>()) {
- 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();
}
/**
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<Zone>()) {
- String zoneDir = zonesDir + "/" + zone->GetName();
+ String zoneName = zone->GetName();
+ String zoneDir = zonesDir + zoneName;
if (!zone->IsChildOf(azone) && !zone->IsGlobal())
continue;
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({
{ "method", "config::Update" },
{ "params", new Dictionary({
{ "update", configUpdateV1 },
- { "update_v2", configUpdateV2 }
+ { "update_v2", configUpdateV2 }, /* Since 2.4.2. */
+ { "checksums", configUpdateChecksums } /* Since 2.11.0. */
}) }
});
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<String>& 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
{
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<char>(fp)), std::istreambuf_iterator<char>());
+
+ 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;
+}
+