Refactor the client sync, part II (WIP, currently checksums generate an endless loop)

This commit is contained in:
Michael Friedrich 2018-10-25 17:47:38 +02:00
parent 6105ace50f
commit 7a02990ef8
2 changed files with 122 additions and 112 deletions

View File

@ -249,6 +249,7 @@ Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const D
*/ */
boost::mutex::scoped_lock lock(m_ConfigSyncStageLock); boost::mutex::scoped_lock lock(m_ConfigSyncStageLock);
String apiZonesStageDir = GetApiZonesStageDir();
String fromEndpointName = origin->FromClient->GetEndpoint()->GetName(); String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
String fromZoneName = GetFromZoneName(origin->FromZone); String fromZoneName = GetFromZoneName(origin->FromZone);
@ -259,6 +260,12 @@ Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const D
Dictionary::Ptr updateV1 = params->Get("update"); Dictionary::Ptr updateV1 = params->Get("update");
Dictionary::Ptr updateV2 = params->Get("update_v2"); 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; bool configChange = false;
std::vector<String> relativePaths; std::vector<String> relativePaths;
@ -266,8 +273,6 @@ Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const D
* We can and must safely purge the staging directory, as the difference is taken between * We can and must safely purge the staging directory, as the difference is taken between
* runtime production config and newly received configuration. * runtime production config and newly received configuration.
*/ */
String apiZonesStageDir = GetApiZonesStageDir();
if (Utility::PathExists(apiZonesStageDir)) if (Utility::PathExists(apiZonesStageDir))
Utility::RemoveDirRecursive(apiZonesStageDir); Utility::RemoveDirRecursive(apiZonesStageDir);
@ -297,106 +302,112 @@ Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const D
} }
/* Put the received configuration into our stage directory. */ /* Put the received configuration into our stage directory. */
String currentConfigDir = GetApiZonesDir() + zoneName; String productionConfigZoneDir = GetApiZonesDir() + zoneName;
String stageConfigDir = GetApiZonesStageDir() + zoneName; String stageConfigZoneDir = GetApiZonesStageDir() + zoneName;
Utility::MkDirP(currentConfigDir, 0700); Utility::MkDirP(productionConfigZoneDir, 0700);
Utility::MkDirP(stageConfigDir, 0700); Utility::MkDirP(stageConfigZoneDir, 0700);
/* Merge the config information. */ /* Merge the config information. */
ConfigDirInformation newConfigInfo; ConfigDirInformation newConfigInfo;
newConfigInfo.UpdateV1 = kv.second; newConfigInfo.UpdateV1 = kv.second;
/* Load metadata. */
if (updateV2) if (updateV2)
newConfigInfo.UpdateV2 = updateV2->Get(kv.first); newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
/* Load checksums. */
if (checksums)
newConfigInfo.Checksums = checksums->Get(kv.first);
/* Load the current production config details. */ /* Load the current production config details. */
ConfigDirInformation currentConfigInfo = LoadConfigDir(currentConfigDir); ConfigDirInformation productionConfigInfo = LoadConfigDir(productionConfigZoneDir);
/* Diff the current production configuration with the received configuration. Dictionary::Ptr productionConfig = MergeConfigUpdate(productionConfigInfo);
* If there was a change, collect a signal for later stage validation. Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
/* If we have received 'checksums' via cluster message, go for it.
* Otherwise do the old timestamp dance.
*/ */
if (UpdateConfigDir(currentConfigInfo, newConfigInfo, stageConfigDir, zoneName, relativePaths, false)) if (checksums) {
configChange = true; /* Calculate and compare the checksums. */
} String productionConfigChecksum = GetGlobalChecksum(productionConfigInfo);
String newConfigChecksum = GetGlobalChecksum(newConfigInfo);
if (configChange) { Log(LogWarning, "ApiListener")
/* Spawn a validation process. On success, move the staged configuration << "Received configuration for zone '" << zoneName << "' from endpoint '"
* into production and restart. << fromEndpointName << "' with checksum '" << newConfigChecksum << "'."
*/ << "Our production configuration has checksum '" << productionConfigChecksum << "'.";
AsyncTryActivateZonesStage(relativePaths);
}
return Empty; /* TODO: Do this earlier in hello-handshakes. */
} if (newConfigChecksum != productionConfigChecksum)
configChange = true;
} else {
/* TODO: Figure out whether we always need to rely on the timestamp flags. */
double productionTimestamp;
/** if (!productionConfig->Contains("/.timestamp"))
* Diffs the old current configuration with the new configuration productionTimestamp = 0;
* and copies the collected content. Detects whether a change else
* happened, this is used for later restarts. productionTimestamp = productionConfig->Get("/.timestamp");
*
* 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); double newTimestamp;
Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
double oldTimestamp; if (!newConfig->Contains("/.timestamp"))
newTimestamp = Utility::GetTime();
else
newTimestamp = newConfig->Get("/.timestamp");
if (!oldConfig->Contains("/.timestamp")) /* skip update if our configuration files are more recent */
oldTimestamp = 0; if (productionTimestamp >= newTimestamp) {
else Log(LogInformation, "ApiListener")
oldTimestamp = oldConfig->Get("/.timestamp"); << "Our 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", newTimestamp) << "' ("
<< newTimestamp << ").";
} else {
configChange = true;
}
double newTimestamp; /* 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;
}
}
if (!newConfig->Contains("/.timestamp")) /* Update the .timestamp file. */
newTimestamp = Utility::GetTime(); String tsPath = stageConfigZoneDir + "/.timestamp";
else if (!Utility::PathExists(tsPath)) {
newTimestamp = newConfig->Get("/.timestamp"); std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp << std::fixed << newTimestamp;
fp.close();
}
}
/* skip update if our configuration files are more recent */ /* Dump the received configuration for this zone into the stage directory. */
if (oldTimestamp >= newTimestamp) { size_t numBytes = 0;
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) {
/* Ignore same config content. This is an expensive comparison. */
if (productionConfig->Get(kv.first) == kv.second)
continue;
{ /* Store the relative config file path for later validation and activation. */
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); relativePaths.push_back(zoneName + "/" + kv.first);
String path = configDir + "/" + kv.first; String path = stageConfigZoneDir + "/" + kv.first;
Log(LogInformation, "ApiListener") Log(LogInformation, "ApiListener")
<< "Updating configuration file: " << path; << "Stage: Updating received configuration file '" << path << "' for zone '" << zoneName << "'.";
/* Sync string content only. */ /* Sync string content only. */
String content = kv.second; String content = kv.second;
@ -410,47 +421,33 @@ bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, con
numBytes += content.GetLength(); numBytes += content.GetLength();
} }
} }
}
/* Log something whether we're authoritative or receing a staged config. */ Log(LogInformation, "ApiListener")
Log(LogInformation, "ApiListener") << "Applying configuration file update for path '" << stageConfigZoneDir << "' ("
<< "Applying configuration file update for " << (authoritative ? "" : "stage ") << numBytes << " Bytes).";
<< "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. */ /* If the update removes a path, delete it on disk and signal a config change. */
ObjectLock xlock(oldConfig); {
for (const Dictionary::Pair& kv : oldConfig) { ObjectLock xlock(productionConfig);
if (!newConfig->Contains(kv.first)) { for (const Dictionary::Pair& kv : productionConfig) {
configChange = true; if (!newConfig->Contains(kv.first)) {
configChange = true;
String path = configDir + "/" + kv.first; String path = stageConfigZoneDir + "/" + kv.first;
(void) unlink(path.CStr()); (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. */ if (configChange) {
/* Spawn a validation process. On success, move the staged configuration
String tsPath = configDir + "/.timestamp"; * into production and restart.
if (!Utility::PathExists(tsPath)) { */
std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc); AsyncTryActivateZonesStage(relativePaths);
fp << std::fixed << newTimestamp;
fp.close();
} }
if (authoritative) { return Empty;
String authPath = configDir + "/.authoritative";
if (!Utility::PathExists(authPath)) {
std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp.close();
}
}
return configChange;
} }
/** /**
@ -594,11 +591,25 @@ String ApiListener::GetChecksum(const String& content)
return SHA256(content); return SHA256(content);
} }
String ApiListener::GetGlobalChecksum(const ConfigDirInformation& config)
{
Dictionary::Ptr checksums = config.Checksums;
String result;
ObjectLock olock(checksums);
for (const Dictionary::Pair& kv : checksums) {
result += GetChecksum(kv.second);
}
return GetChecksum(result);
}
/** /**
* Load the given config dir and read their file content into the config structure. * Load the given config dir and read their file content into the config structure.
* *
* @param dir Path to the config directory. * @param dir Path to the config directory.
* @returns ConfigInformation structure. * @returns ConfigDirInformation structure.
*/ */
ConfigDirInformation ApiListener::LoadConfigDir(const String& dir) ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
{ {

View File

@ -177,8 +177,6 @@ private:
void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient); void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient);
static Dictionary::Ptr MergeConfigUpdate(const ConfigDirInformation& config); 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);
static ConfigDirInformation LoadConfigDir(const String& dir); static ConfigDirInformation LoadConfigDir(const String& dir);
static void ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file); static void ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file);
@ -188,6 +186,7 @@ private:
static void AsyncTryActivateZonesStage(const std::vector<String>& relativePaths); static void AsyncTryActivateZonesStage(const std::vector<String>& relativePaths);
static String GetChecksum(const String& content); static String GetChecksum(const String& content);
static String GetGlobalChecksum(const ConfigDirInformation& config);
void UpdateLastFailedZonesStageValidation(const String& log); void UpdateLastFailedZonesStageValidation(const String& log);
void ClearLastFailedZonesStageValidation(); void ClearLastFailedZonesStageValidation();