mirror of https://github.com/Icinga/icinga2.git
Split config file sync updates, part I
This commit also introduces a playground for checksums, whilst refactoring the code in large parts.
This commit is contained in:
parent
9df389a843
commit
fcc1799a5d
|
@ -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,73 +20,312 @@ 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().
|
||||
* Entrypoint for updating all authoritative configs into var/lib/icinga2/api/zones
|
||||
*
|
||||
* @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)
|
||||
void ApiListener::SyncLocalZoneDirs() const
|
||||
{
|
||||
CONTEXT("Creating config update for file '" + file + "'");
|
||||
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
|
||||
try {
|
||||
SyncLocalZoneDir(zone);
|
||||
} catch (const std::exception&) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log(LogNotice, "ApiListener")
|
||||
<< "Creating config update for file '" << file << "'.";
|
||||
|
||||
std::ifstream fp(file.CStr(), std::ifstream::binary);
|
||||
if (!fp)
|
||||
/**
|
||||
* Sync a zone directory where we have an authoritative copy (zones.d, 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;
|
||||
|
||||
String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
|
||||
ConfigDirInformation newConfigInfo;
|
||||
newConfigInfo.UpdateV1 = new Dictionary();
|
||||
newConfigInfo.UpdateV2 = new Dictionary();
|
||||
newConfigInfo.Checksums = new Dictionary();
|
||||
|
||||
Dictionary::Ptr update;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 azone = endpoint->GetZone();
|
||||
Zone::Ptr lzone = Zone::GetLocalZone();
|
||||
|
||||
/* don't try to send config updates to our master */
|
||||
if (!azone->IsChildOf(lzone))
|
||||
return;
|
||||
|
||||
Dictionary::Ptr configUpdateV1 = new Dictionary();
|
||||
Dictionary::Ptr configUpdateV2 = new Dictionary();
|
||||
Dictionary::Ptr configUpdateChecksums = new Dictionary();
|
||||
|
||||
String zonesDir = GetApiZonesDir();
|
||||
|
||||
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
|
||||
String zoneName = zone->GetName();
|
||||
String zoneDir = zonesDir + zoneName;
|
||||
|
||||
if (!zone->IsChildOf(azone) && !zone->IsGlobal())
|
||||
continue;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered handler when a new config::Update message is received.
|
||||
*
|
||||
* Checks destination and permissions first, then 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;
|
||||
}
|
||||
|
||||
/* Only one transaction is allowed, concurrent message handlers need to wait.
|
||||
* This affects two parent endpoints sending the config in the same moment.
|
||||
*/
|
||||
boost::mutex::scoped_lock lock(m_ConfigSyncStageLock);
|
||||
|
||||
String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
|
||||
String fromZoneName = GetFromZoneName(origin->FromZone);
|
||||
|
||||
Log(LogInformation, "ApiListener")
|
||||
<< "Applying config update from endpoint '" << fromEndpointName
|
||||
<< "' of zone '" << fromZoneName << "'.";
|
||||
|
||||
Dictionary::Ptr updateV1 = params->Get("update");
|
||||
Dictionary::Ptr updateV2 = params->Get("update_v2");
|
||||
|
||||
bool configChange = false;
|
||||
std::vector<String> relativePaths;
|
||||
|
||||
/*
|
||||
* 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp).
|
||||
*
|
||||
* **Keep this intact to stay compatible with older clients.**
|
||||
* We can and must safely purge the staging directory, as the difference is taken between
|
||||
* runtime production config and newly received configuration.
|
||||
*/
|
||||
if (Utility::Match("*.conf", file))
|
||||
update = config.UpdateV1;
|
||||
else
|
||||
update = config.UpdateV2;
|
||||
String apiZonesStageDir = GetApiZonesStageDir();
|
||||
|
||||
update->Set(file.SubStr(path.GetLength()), content);
|
||||
}
|
||||
if (Utility::PathExists(apiZonesStageDir))
|
||||
Utility::RemoveDirRecursive(apiZonesStageDir);
|
||||
|
||||
/**
|
||||
* 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();
|
||||
Utility::MkDirP(apiZonesStageDir, 0700);
|
||||
|
||||
if (config.UpdateV1)
|
||||
config.UpdateV1->CopyTo(result);
|
||||
/* Analyse and process the update. */
|
||||
ObjectLock olock(updateV1);
|
||||
for (const Dictionary::Pair& kv : updateV1) {
|
||||
|
||||
if (config.UpdateV2)
|
||||
config.UpdateV2->CopyTo(result);
|
||||
/* Check for the configured zones. */
|
||||
String zoneName = kv.first;
|
||||
Zone::Ptr zone = Zone::GetByName(zoneName);
|
||||
|
||||
return result;
|
||||
}
|
||||
if (!zone) {
|
||||
Log(LogWarning, "ApiListener")
|
||||
<< "Ignoring config update from endpoint '" << fromEndpointName
|
||||
<< "' for unknown zone '" << zoneName << "'.";
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/* Whether we already have configuration in zones.d. */
|
||||
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 currentConfigDir = GetApiZonesDir() + zoneName;
|
||||
String stageConfigDir = GetApiZonesStageDir() + zoneName;
|
||||
|
||||
Utility::MkDirP(currentConfigDir, 0700);
|
||||
Utility::MkDirP(stageConfigDir, 0700);
|
||||
|
||||
/* Merge the config information. */
|
||||
ConfigDirInformation newConfigInfo;
|
||||
newConfigInfo.UpdateV1 = kv.second;
|
||||
|
||||
if (updateV2)
|
||||
newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
|
||||
|
||||
/* Load the current production config details. */
|
||||
ConfigDirInformation currentConfigInfo = LoadConfigDir(currentConfigDir);
|
||||
|
||||
/* Diff the current production configuration with the received configuration.
|
||||
* If there was a change, collect a signal for later stage validation.
|
||||
*/
|
||||
if (UpdateConfigDir(currentConfigInfo, newConfigInfo, stageConfigDir, zoneName, relativePaths, false))
|
||||
configChange = true;
|
||||
}
|
||||
|
||||
if (configChange) {
|
||||
/* Spawn a validation process. On success, move the staged configuration
|
||||
* into production and restart.
|
||||
*/
|
||||
AsyncTryActivateZonesStage(relativePaths);
|
||||
}
|
||||
|
||||
return Empty;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -210,253 +451,6 @@ bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, con
|
|||
return configChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a zone directory where we have an authoritative copy (zones.d, 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::SyncZoneDir(const Zone::Ptr& zone) const
|
||||
{
|
||||
if (!zone)
|
||||
return;
|
||||
|
||||
ConfigDirInformation newConfigInfo;
|
||||
newConfigInfo.UpdateV1 = new Dictionary();
|
||||
newConfigInfo.UpdateV2 = new Dictionary();
|
||||
|
||||
String zoneName = zone->GetName();
|
||||
|
||||
for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
|
||||
ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
|
||||
|
||||
{
|
||||
ObjectLock olock(newConfigPart.UpdateV1);
|
||||
for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
|
||||
newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ObjectLock olock(newConfigPart.UpdateV2);
|
||||
for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
|
||||
newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
|
||||
|
||||
if (sumUpdates == 0)
|
||||
return;
|
||||
|
||||
String currentDir = Configuration::DataDir + "/api/zones/" + zoneName;
|
||||
|
||||
Log(LogInformation, "ApiListener")
|
||||
<< "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << currentDir << "'.";
|
||||
|
||||
ConfigDirInformation oldConfigInfo = LoadConfigDir(currentDir);
|
||||
|
||||
/* Purge files to allow deletion via zones.d. */
|
||||
Utility::RemoveDirRecursive(currentDir);
|
||||
Utility::MkDirP(currentDir, 0700);
|
||||
|
||||
std::vector<String> relativePaths;
|
||||
UpdateConfigDir(oldConfigInfo, newConfigInfo, currentDir, zoneName, relativePaths, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 azone = endpoint->GetZone();
|
||||
Zone::Ptr lzone = Zone::GetLocalZone();
|
||||
|
||||
/* don't try to send config updates to our master */
|
||||
if (!azone->IsChildOf(lzone))
|
||||
return;
|
||||
|
||||
Dictionary::Ptr configUpdateV1 = new Dictionary();
|
||||
Dictionary::Ptr configUpdateV2 = new Dictionary();
|
||||
|
||||
String zonesDir = Configuration::DataDir + "/api/zones";
|
||||
|
||||
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
|
||||
String zoneDir = zonesDir + "/" + zone->GetName();
|
||||
|
||||
if (!zone->IsChildOf(azone) && !zone->IsGlobal())
|
||||
continue;
|
||||
|
||||
if (!Utility::PathExists(zoneDir))
|
||||
continue;
|
||||
|
||||
Log(LogInformation, "ApiListener")
|
||||
<< "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
|
||||
<< "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'.";
|
||||
|
||||
ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName());
|
||||
configUpdateV1->Set(zone->GetName(), config.UpdateV1);
|
||||
configUpdateV2->Set(zone->GetName(), config.UpdateV2);
|
||||
}
|
||||
|
||||
Dictionary::Ptr message = new Dictionary({
|
||||
{ "jsonrpc", "2.0" },
|
||||
{ "method", "config::Update" },
|
||||
{ "params", new Dictionary({
|
||||
{ "update", configUpdateV1 },
|
||||
{ "update_v2", configUpdateV2 }
|
||||
}) }
|
||||
});
|
||||
|
||||
aclient->SendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered handler when a new config::Update message is received.
|
||||
*
|
||||
* Checks destination and permissions first, then 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;
|
||||
}
|
||||
|
||||
/* Only one transaction is allowed, concurrent message handlers need to wait.
|
||||
* This affects two parent endpoints sending the config in the same moment.
|
||||
*/
|
||||
boost::mutex::scoped_lock lock(m_ConfigSyncStageLock);
|
||||
|
||||
String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
|
||||
String fromZoneName = GetFromZoneName(origin->FromZone);
|
||||
|
||||
Log(LogInformation, "ApiListener")
|
||||
<< "Applying config update from endpoint '" << fromEndpointName
|
||||
<< "' of zone '" << fromZoneName << "'.";
|
||||
|
||||
Dictionary::Ptr updateV1 = params->Get("update");
|
||||
Dictionary::Ptr updateV2 = params->Get("update_v2");
|
||||
|
||||
bool configChange = false;
|
||||
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.
|
||||
*/
|
||||
String apiZonesStageDir = GetApiZonesStageDir();
|
||||
|
||||
if (Utility::PathExists(apiZonesStageDir))
|
||||
Utility::RemoveDirRecursive(apiZonesStageDir);
|
||||
|
||||
Utility::MkDirP(apiZonesStageDir, 0700);
|
||||
|
||||
/* Analyse and process the update. */
|
||||
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;
|
||||
}
|
||||
|
||||
/* Whether we already have configuration in zones.d. */
|
||||
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 currentConfigDir = GetApiZonesDir() + zoneName;
|
||||
String stageConfigDir = GetApiZonesStageDir() + zoneName;
|
||||
|
||||
Utility::MkDirP(currentConfigDir, 0700);
|
||||
Utility::MkDirP(stageConfigDir, 0700);
|
||||
|
||||
/* Merge the config information. */
|
||||
ConfigDirInformation newConfigInfo;
|
||||
newConfigInfo.UpdateV1 = kv.second;
|
||||
|
||||
if (updateV2)
|
||||
newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
|
||||
|
||||
/* Load the current production config details. */
|
||||
ConfigDirInformation currentConfigInfo = LoadConfigDir(currentConfigDir);
|
||||
|
||||
/* Diff the current production configuration with the received configuration.
|
||||
* If there was a change, collect a signal for later stage validation.
|
||||
*/
|
||||
if (UpdateConfigDir(currentConfigInfo, newConfigInfo, stageConfigDir, zoneName, relativePaths, false))
|
||||
configChange = true;
|
||||
}
|
||||
|
||||
if (configChange) {
|
||||
/* Spawn a validation process. On success, move the staged configuration
|
||||
* into production and restart.
|
||||
*/
|
||||
AsyncTryActivateZonesStage(relativePaths);
|
||||
}
|
||||
|
||||
return Empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<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;
|
||||
}
|
||||
|
||||
|
|
|
@ -242,7 +242,7 @@ void ApiListener::Start(bool runtimeCreated)
|
|||
Log(LogInformation, "ApiListener")
|
||||
<< "'" << GetName() << "' started.";
|
||||
|
||||
SyncZoneDirs();
|
||||
SyncLocalZoneDirs();
|
||||
|
||||
ObjectImpl<ApiListener>::Start(runtimeCreated);
|
||||
|
||||
|
|
|
@ -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<String>& 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<String>& relativePaths);
|
||||
static void AsyncTryActivateZonesStage(const std::vector<String>& relativePaths);
|
||||
|
||||
static String GetChecksum(const String& content);
|
||||
|
||||
void UpdateLastFailedZonesStageValidation(const String& log);
|
||||
void ClearLastFailedZonesStageValidation();
|
||||
|
||||
|
|
Loading…
Reference in New Issue