icinga2/lib/remote/apilistener-filesync.cpp

888 lines
27 KiB
C++

/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
#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"
#include "base/application.hpp"
#include "base/exception.hpp"
#include "base/shared.hpp"
#include "base/utility.hpp"
#include <fstream>
#include <iomanip>
#include <thread>
using namespace icinga;
REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
std::mutex ApiListener::m_ConfigSyncStageLock;
/**
* Entrypoint for updating all authoritative configs from /etc/zones.d, packages, etc.
* into var/lib/icinga2/api/zones
*/
void ApiListener::SyncLocalZoneDirs() const
{
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
try {
SyncLocalZoneDir(zone);
} catch (const std::exception&) {
continue;
}
}
}
/**
* Sync a zone directory where we have an authoritative copy (zones.d, packages, etc.)
*
* This function collects the registered zone config dirs from
* the config compiler and reads the file content into the config
* information structure.
*
* Returns early when there are no updates.
*
* @param zone Pointer to the zone object being synced.
*/
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) {
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) {
String path = "/" + zf.Tag + kv.first;
newConfigInfo.UpdateV2->Set(path, kv.second);
newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
}
}
}
size_t sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
// Return early if there are no updates.
if (sumUpdates == 0)
return;
String productionZonesDir = GetApiZonesDir() + zoneName;
Log(LogInformation, "ApiListener")
<< "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << productionZonesDir << "'.";
// Purge files to allow deletion via zones.d.
if (Utility::PathExists(productionZonesDir))
Utility::RemoveDirRecursive(productionZonesDir);
Utility::MkDirP(productionZonesDir, 0700);
// 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);
fp.close();
}
// Checksums.
String checksumsPath = productionZonesDir + "/.checksums";
if (Utility::PathExists(checksumsPath))
Utility::Remove(checksumsPath);
std::ofstream fp(checksumsPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp << std::fixed << JsonEncode(newConfigInfo.Checksums);
fp.close();
Log(LogNotice, "ApiListener")
<< "Updated meta data for cluster config sync. Checksum: '" << checksumsPath
<< "', timestamp: '" << tsPath << "', auth: '" << authPath << "'.";
}
/**
* Entrypoint for sending a file based config update to a cluster client.
* This includes security checks for zone relations.
* Loads the zone config files where this client belongs to
* and sends the 'config::Update' JSON-RPC message.
*
* @param aclient Connected JSON-RPC client.
*/
void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
{
Endpoint::Ptr endpoint = aclient->GetEndpoint();
ASSERT(endpoint);
Zone::Ptr clientZone = endpoint->GetZone();
Zone::Ptr localZone = Zone::GetLocalZone();
// Don't send config updates to parent zones
if (!clientZone->IsChildOf(localZone))
return;
Dictionary::Ptr configUpdateV1 = new Dictionary();
Dictionary::Ptr configUpdateV2 = new Dictionary();
Dictionary::Ptr configUpdateChecksums = new Dictionary(); // new since 2.11
String zonesDir = GetApiZonesDir();
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
String zoneName = zone->GetName();
String zoneDir = zonesDir + zoneName;
// Only sync child and global zones.
if (!zone->IsChildOf(clientZone) && !zone->IsGlobal())
continue;
// Zone was configured, but there's no configuration directory.
if (!Utility::PathExists(zoneDir))
continue;
Log(LogInformation, "ApiListener")
<< "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
<< "zone '" << zoneName << "' to endpoint '" << endpoint->GetName() << "'.";
ConfigDirInformation config = LoadConfigDir(zoneDir);
configUpdateV1->Set(zoneName, config.UpdateV1);
configUpdateV2->Set(zoneName, config.UpdateV2);
configUpdateChecksums->Set(zoneName, config.Checksums); // new since 2.11
}
Dictionary::Ptr message = new Dictionary({
{ "jsonrpc", "2.0" },
{ "method", "config::Update" },
{ "params", new Dictionary({
{ "update", configUpdateV1 },
{ "update_v2", configUpdateV2 }, // Since 2.4.2.
{ "checksums", configUpdateChecksums } // Since 2.11.0.
}) }
});
aclient->SendMessage(message);
}
static bool CompareTimestampsConfigChange(const Dictionary::Ptr& productionConfig, const Dictionary::Ptr& receivedConfig,
const String& stageConfigZoneDir)
{
double productionTimestamp;
double receivedTimestamp;
// Missing production timestamp means that something really broke. Always trigger a config change then.
if (!productionConfig->Contains("/.timestamp"))
productionTimestamp = 0;
else
productionTimestamp = productionConfig->Get("/.timestamp");
// Missing received config timestamp means that something really broke. Always trigger a config change then.
if (!receivedConfig->Contains("/.timestamp"))
receivedTimestamp = Utility::GetTime() + 10;
else
receivedTimestamp = receivedConfig->Get("/.timestamp");
bool configChange;
// Skip update if our configuration files are more recent.
if (productionTimestamp >= receivedTimestamp) {
Log(LogInformation, "ApiListener")
<< "Our production configuration is more recent than the received configuration update."
<< " Ignoring configuration file update for path '" << stageConfigZoneDir << "'. Current timestamp '"
<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", productionTimestamp) << "' ("
<< std::fixed << std::setprecision(6) << productionTimestamp
<< ") >= received timestamp '"
<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", receivedTimestamp) << "' ("
<< receivedTimestamp << ").";
configChange = false;
} else {
configChange = true;
}
// Update the .timestamp file inside the staging directory.
String tsPath = stageConfigZoneDir + "/.timestamp";
if (!Utility::PathExists(tsPath)) {
std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp << std::fixed << receivedTimestamp;
fp.close();
}
return configChange;
}
/**
* Registered handler when a new config::Update message is received.
*
* Checks destination and permissions first, locks the transaction and analyses the update.
* The newly received configuration is not copied to production immediately,
* but into the staging directory first.
* Last, the async validation and restart is triggered.
*
* @param origin Where this message came from.
* @param params Message parameters including the config updates.
* @returns Empty, required by the interface.
*/
Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
// Verify permissions and trust relationship.
if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
return Empty;
ApiListener::Ptr listener = ApiListener::GetInstance();
if (!listener) {
Log(LogCritical, "ApiListener", "No instance available.");
return Empty;
}
if (!listener->GetAcceptConfig()) {
Log(LogWarning, "ApiListener")
<< "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
return Empty;
}
std::thread([origin, params, listener]() {
try {
listener->HandleConfigUpdate(origin, params);
} catch (const std::exception& ex) {
auto msg ("Exception during config sync: " + DiagnosticInformation(ex));
Log(LogCritical, "ApiListener") << msg;
listener->UpdateLastFailedZonesStageValidation(msg);
}
}).detach();
return Empty;
}
void ApiListener::HandleConfigUpdate(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
/* Only one transaction is allowed, concurrent message handlers need to wait.
* This affects two parent endpoints sending the config in the same moment.
*/
std::lock_guard<std::mutex> lock(m_ConfigSyncStageLock);
String apiZonesStageDir = GetApiZonesStageDir();
String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
String fromZoneName = GetFromZoneName(origin->FromZone);
Log(LogInformation, "ApiListener")
<< "Applying config update from endpoint '" << fromEndpointName
<< "' of zone '" << fromZoneName << "'.";
// Config files.
Dictionary::Ptr updateV1 = params->Get("update");
// Meta data files: .timestamp, etc.
Dictionary::Ptr updateV2 = params->Get("update_v2");
// New since 2.11.0.
Dictionary::Ptr checksums;
if (params->Contains("checksums"))
checksums = params->Get("checksums");
bool configChange = false;
// Keep track of the relative config paths for later validation and copying. TODO: Find a better algorithm.
std::vector<String> relativePaths;
/*
* We can and must safely purge the staging directory, as the difference is taken between
* runtime production config and newly received configuration.
* This is needed to not mix deleted/changed content between received and stage
* config.
*/
if (Utility::PathExists(apiZonesStageDir))
Utility::RemoveDirRecursive(apiZonesStageDir);
Utility::MkDirP(apiZonesStageDir, 0700);
// Analyse and process the update.
size_t count = 0;
ObjectLock olock(updateV1);
for (const Dictionary::Pair& kv : updateV1) {
// Check for the configured zones.
String zoneName = kv.first;
Zone::Ptr zone = Zone::GetByName(zoneName);
if (!zone) {
Log(LogWarning, "ApiListener")
<< "Ignoring config update from endpoint '" << fromEndpointName
<< "' for unknown zone '" << zoneName << "'.";
continue;
}
// Ignore updates where we have an authoritive copy in etc/zones.d, packages, etc.
if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
Log(LogInformation, "ApiListener")
<< "Ignoring config update from endpoint '" << fromEndpointName
<< "' for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";
continue;
}
// Put the received configuration into our stage directory.
String productionConfigZoneDir = GetApiZonesDir() + zoneName;
String stageConfigZoneDir = GetApiZonesStageDir() + zoneName;
Utility::MkDirP(productionConfigZoneDir, 0700);
Utility::MkDirP(stageConfigZoneDir, 0700);
// Merge the config information.
ConfigDirInformation newConfigInfo;
newConfigInfo.UpdateV1 = kv.second;
// Load metadata.
if (updateV2)
newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
// Load checksums. New since 2.11.
if (checksums)
newConfigInfo.Checksums = checksums->Get(kv.first);
// Load the current production config details.
ConfigDirInformation productionConfigInfo = LoadConfigDir(productionConfigZoneDir);
// Merge updateV1 and updateV2
Dictionary::Ptr productionConfig = MergeConfigUpdate(productionConfigInfo);
Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
bool timestampChanged = false;
if (CompareTimestampsConfigChange(productionConfig, newConfig, stageConfigZoneDir)) {
timestampChanged = true;
}
/* If we have received 'checksums' via cluster message, go for it.
* Otherwise do the old timestamp dance for versions < 2.11.
*/
if (checksums) {
Log(LogInformation, "ApiListener")
<< "Received configuration for zone '" << zoneName << "' from endpoint '"
<< fromEndpointName << "'. Comparing the timestamp and checksums.";
if (timestampChanged) {
if (CheckConfigChange(productionConfigInfo, newConfigInfo))
configChange = true;
}
} else {
/* Fallback to timestamp handling when the parent endpoint didn't send checks.
* This can happen when the satellite is 2.11 and the master is 2.10.
*
* TODO: Deprecate and remove this behaviour in 2.13+.
*/
Log(LogWarning, "ApiListener")
<< "Received configuration update without checksums from parent endpoint "
<< fromEndpointName << ". This behaviour is deprecated. Please upgrade the parent endpoint to 2.11+";
if (timestampChanged) {
configChange = true;
}
// Keep another hack when there's a timestamp file missing.
{
ObjectLock olock(newConfig);
for (const Dictionary::Pair &kv : newConfig) {
// This is super expensive with a string content comparison.
if (productionConfig->Get(kv.first) != kv.second) {
if (!Utility::Match("*/.timestamp", kv.first))
configChange = true;
}
}
}
}
// Dump the received configuration for this zone into the stage directory.
size_t numBytes = 0;
{
ObjectLock olock(newConfig);
for (const Dictionary::Pair& kv : newConfig) {
/* Store the relative config file path for later validation and activation.
* IMPORTANT: Store this prior to any filters.
* */
relativePaths.push_back(zoneName + "/" + kv.first);
String path = stageConfigZoneDir + "/" + kv.first;
if (Utility::Match("*.conf", path)) {
Log(LogInformation, "ApiListener")
<< "Stage: Updating received configuration file '" << path << "' for zone '" << zoneName << "'.";
}
// Parent nodes < 2.11 always send this, avoid this bug and deny its receival prior to writing it on disk.
if (Utility::BaseName(path) == ".authoritative")
continue;
// 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);
// Write the content to file.
std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fp << content;
fp.close();
numBytes += content.GetLength();
}
}
Log(LogInformation, "ApiListener")
<< "Applying configuration file update for path '" << stageConfigZoneDir << "' ("
<< numBytes << " Bytes).";
if (timestampChanged) {
// If the update removes a path, delete it on disk and signal a config change.
ObjectLock xlock(productionConfig);
for (const Dictionary::Pair& kv : productionConfig) {
if (!newConfig->Contains(kv.first)) {
configChange = true;
String path = stageConfigZoneDir + "/" + kv.first;
Utility::Remove(path);
}
}
}
count++;
}
/*
* We have processed all configuration files and stored them in the staging directory.
*
* We need to store them locally for later analysis. A config change means
* that we will validate the configuration in a separate process sandbox,
* and only copy the configuration to production when everything is ok.
*
* A successful validation also triggers the final restart.
*/
if (configChange) {
Log(LogInformation, "ApiListener")
<< "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
<< "' are different to production, triggering validation and reload.";
TryActivateZonesStage(relativePaths);
} else {
Log(LogInformation, "ApiListener")
<< "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
<< "' are equal to production, skipping validation and reload.";
ClearLastFailedZonesStageValidation();
}
}
/**
* Spawns a new validation process with 'Internal.ZonesStageVarDir' set to override the config validation zone dirs with
* our current stage. Then waits for the validation result and if it was successful, the configuration is copied from
* stage to production and a restart is triggered. On validation failure, there is no restart and this is logged.
*
* The caller of this function must hold m_ConfigSyncStageLock.
*
* @param relativePaths Collected paths including the zone name, which are copied from stage to current directories.
*/
void ApiListener::TryActivateZonesStage(const std::vector<String>& relativePaths)
{
VERIFY(Application::GetArgC() >= 1);
/* Inherit parent process args. */
Array::Ptr args = new Array({
Application::GetExePath(Application::GetArgV()[0]),
});
for (int i = 1; i < Application::GetArgC(); i++) {
String argV = Application::GetArgV()[i];
if (argV == "-d" || argV == "--daemonize")
continue;
args->Add(argV);
}
args->Add("--validate");
// Set the ZonesStageDir. This creates our own local chroot without any additional automated zone includes.
args->Add("--define");
args->Add("Internal.ZonesStageVarDir=" + GetApiZonesStageDir());
Process::Ptr process = new Process(Process::PrepareCommand(args));
process->SetTimeout(Application::GetReloadTimeout());
process->Run();
const ProcessResult& pr = process->WaitForResult();
String apiDir = GetApiDir();
String apiZonesDir = GetApiZonesDir();
String apiZonesStageDir = GetApiZonesStageDir();
String logFile = apiDir + "/zones-stage-startup.log";
std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fpLog << pr.Output;
fpLog.close();
String statusFile = apiDir + "/zones-stage-status";
std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fpStatus << pr.ExitStatus;
fpStatus.close();
// Validation went fine, copy stage and reload.
if (pr.ExitStatus == 0) {
Log(LogInformation, "ApiListener")
<< "Config validation for stage '" << apiZonesStageDir << "' was OK, replacing into '" << apiZonesDir << "' and triggering reload.";
// Purge production before copying stage.
if (Utility::PathExists(apiZonesDir))
Utility::RemoveDirRecursive(apiZonesDir);
Utility::MkDirP(apiZonesDir, 0700);
// Copy all synced configuration files from stage to production.
for (const String& path : relativePaths) {
if (!Utility::PathExists(apiZonesStageDir + path))
continue;
Log(LogInformation, "ApiListener")
<< "Copying file '" << path << "' from config sync staging to production zones directory.";
String stagePath = apiZonesStageDir + path;
String currentPath = apiZonesDir + path;
Utility::MkDirP(Utility::DirName(currentPath), 0700);
Utility::CopyFile(stagePath, currentPath);
}
// Clear any failed deployment before
ApiListener::Ptr listener = ApiListener::GetInstance();
if (listener)
listener->ClearLastFailedZonesStageValidation();
Application::RequestRestart();
// All good, return early.
return;
}
String failedLogFile = apiDir + "/zones-stage-startup-last-failed.log";
std::ofstream fpFailedLog(failedLogFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fpFailedLog << pr.Output;
fpFailedLog.close();
// Error case.
Log(LogCritical, "ApiListener")
<< "Config validation failed for staged cluster config sync in '" << apiZonesStageDir
<< "'. Aborting. Logs: '" << failedLogFile << "'";
ApiListener::Ptr listener = ApiListener::GetInstance();
if (listener)
listener->UpdateLastFailedZonesStageValidation(pr.Output);
}
/**
* Update the structure from the last failed validation output.
* Uses the current timestamp.
*
* @param log The process output from the config validation.
*/
void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
{
Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
{ "log", log },
{ "ts", Utility::GetTime() }
});
SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
}
/**
* Clear the structure for the last failed reload.
*
*/
void ApiListener::ClearLastFailedZonesStageValidation()
{
SetLastFailedZonesStageValidation(Dictionary::Ptr());
}
/**
* Generate a config checksum.
*
* @param content String content used for generating the checksum.
* @returns The checksum as string.
*/
String ApiListener::GetChecksum(const String& content)
{
return SHA256(content);
}
bool ApiListener::CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig)
{
Dictionary::Ptr oldChecksums = oldConfig.Checksums;
Dictionary::Ptr newChecksums = newConfig.Checksums;
// TODO: Figure out whether normal users need this for debugging.
Log(LogDebug, "ApiListener")
<< "Checking for config change between stage and production. Old (" << oldChecksums->GetLength() << "): '"
<< JsonEncode(oldChecksums)
<< "' vs. new (" << newChecksums->GetLength() << "): '"
<< JsonEncode(newChecksums) << "'.";
/* Since internal files are synced here too, we can not depend on length.
* So we need to go through both checksum sets to cover the cases"everything is new" and "everything was deleted".
*/
{
ObjectLock olock(oldChecksums);
for (const Dictionary::Pair& kv : oldChecksums) {
String path = kv.first;
String oldChecksum = kv.second;
/* Ignore internal files, especially .timestamp and .checksums.
*
* If we don't, this results in "always change" restart loops.
*/
if (Utility::Match("/.*", path)) {
Log(LogDebug, "ApiListener")
<< "Ignoring old internal file '" << path << "'.";
continue;
}
Log(LogDebug, "ApiListener")
<< "Checking " << path << " for old checksum: " << oldChecksum << ".";
// Check if key exists first for more verbose logging.
// Note: Don't do this later on.
if (!newChecksums->Contains(path)) {
Log(LogDebug, "ApiListener")
<< "File '" << path << "' was deleted by remote.";
return true;
}
String newChecksum = newChecksums->Get(path);
if (newChecksum != kv.second) {
Log(LogDebug, "ApiListener")
<< "Path '" << path << "' doesn't match old checksum '"
<< oldChecksum << "' with new checksum '" << newChecksum << "'.";
return true;
}
}
}
{
ObjectLock olock(newChecksums);
for (const Dictionary::Pair& kv : newChecksums) {
String path = kv.first;
String newChecksum = kv.second;
/* Ignore internal files, especially .timestamp and .checksums.
*
* If we don't, this results in "always change" restart loops.
*/
if (Utility::Match("/.*", path)) {
Log(LogDebug, "ApiListener")
<< "Ignoring new internal file '" << path << "'.";
continue;
}
Log(LogDebug, "ApiListener")
<< "Checking " << path << " for new checksum: " << newChecksum << ".";
// Check if the checksum exists, checksums in both sets have already been compared
if (!oldChecksums->Contains(path)) {
Log(LogDebug, "ApiListener")
<< "File '" << path << "' was added by remote.";
return true;
}
}
}
return false;
}
/**
* Load the given config dir and read their file content into the config structure.
*
* @param dir Path to the config directory.
* @returns ConfigDirInformation structure.
*/
ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
{
ConfigDirInformation config;
config.UpdateV1 = new Dictionary();
config.UpdateV2 = new Dictionary();
config.Checksums = new Dictionary();
Utility::GlobRecursive(dir, "*", [&config, dir](const String& file) { ConfigGlobHandler(config, dir, file); }, 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)
{
// Avoid loading the authoritative marker for syncs at all cost.
if (Utility::BaseName(file) == ".authoritative")
return;
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.**
*/
String sanitizedContent = Utility::ValidateUTF8(content);
if (Utility::Match("*.conf", file)) {
update = config.UpdateV1;
// Configuration files should be automatically sanitized with UTF8.
update->Set(relativePath, sanitizedContent);
} else {
update = config.UpdateV2;
/*
* Ensure that only valid UTF8 content is being read for the cluster config sync.
* Binary files are not supported when wrapped into JSON encoded messages.
* Rationale: https://github.com/Icinga/icinga2/issues/7382
*/
if (content != sanitizedContent) {
Log(LogCritical, "ApiListener")
<< "Ignoring file '" << file << "' for cluster config sync: Does not contain valid UTF8. Binary files are not supported.";
return;
}
update->Set(relativePath, content);
}
/* Calculate a checksum for each file (and a global one later).
*
* IMPORTANT: Ignore the .authoritative file above, this must not be synced.
* */
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;
}