2019-02-25 14:48:22 +01:00
|
|
|
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
|
2014-05-13 13:18:27 +02:00
|
|
|
|
2014-05-25 16:23:35 +02:00
|
|
|
#include "remote/apilistener.hpp"
|
|
|
|
#include "remote/apifunction.hpp"
|
2015-07-21 09:32:17 +02:00
|
|
|
#include "config/configcompiler.hpp"
|
2018-10-25 14:10:30 +02:00
|
|
|
#include "base/tlsutility.hpp"
|
|
|
|
#include "base/json.hpp"
|
2015-08-15 20:28:05 +02:00
|
|
|
#include "base/configtype.hpp"
|
2014-10-19 14:21:12 +02:00
|
|
|
#include "base/logger.hpp"
|
2014-05-25 16:23:35 +02:00
|
|
|
#include "base/convert.hpp"
|
2014-12-15 10:16:06 +01:00
|
|
|
#include "base/exception.hpp"
|
2019-04-01 18:54:13 +02:00
|
|
|
#include "base/utility.hpp"
|
2014-05-13 13:18:27 +02:00
|
|
|
#include <fstream>
|
2015-02-09 15:22:55 +01:00
|
|
|
#include <iomanip>
|
2014-05-13 13:18:27 +02:00
|
|
|
|
|
|
|
using namespace icinga;
|
|
|
|
|
2014-05-13 15:57:02 +02:00
|
|
|
REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
|
|
|
|
|
2019-05-13 11:26:39 +02:00
|
|
|
boost::mutex ApiListener::m_ConfigSyncStageLock;
|
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/**
|
2018-10-25 14:10:30 +02:00
|
|
|
* Entrypoint for updating all authoritative configs into var/lib/icinga2/api/zones
|
2018-10-23 18:30:19 +02:00
|
|
|
*
|
|
|
|
*/
|
2018-10-25 14:10:30 +02:00
|
|
|
void ApiListener::SyncLocalZoneDirs() const
|
2014-05-13 15:57:02 +02:00
|
|
|
{
|
2018-10-25 14:10:30 +02:00
|
|
|
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
|
|
|
|
try {
|
|
|
|
SyncLocalZoneDir(zone);
|
|
|
|
} catch (const std::exception&) {
|
|
|
|
continue;
|
2015-01-20 13:18:40 +01:00
|
|
|
}
|
|
|
|
}
|
2014-05-13 15:57:02 +02:00
|
|
|
}
|
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2018-10-25 14:10:30 +02:00
|
|
|
void ApiListener::SyncLocalZoneDir(const Zone::Ptr& zone) const
|
2014-05-13 15:57:02 +02:00
|
|
|
{
|
2018-09-28 16:51:20 +02:00
|
|
|
if (!zone)
|
|
|
|
return;
|
|
|
|
|
2016-01-26 10:46:27 +01:00
|
|
|
ConfigDirInformation newConfigInfo;
|
|
|
|
newConfigInfo.UpdateV1 = new Dictionary();
|
|
|
|
newConfigInfo.UpdateV2 = new Dictionary();
|
2018-10-25 14:10:30 +02:00
|
|
|
newConfigInfo.Checksums = new Dictionary();
|
2016-01-26 10:46:27 +01:00
|
|
|
|
2018-09-28 16:51:20 +02:00
|
|
|
String zoneName = zone->GetName();
|
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
/* Load registered zone paths, e.g. '_etc', '_api' and user packages. */
|
2018-09-28 16:51:20 +02:00
|
|
|
for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
|
2016-01-26 10:46:27 +01:00
|
|
|
ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
|
2015-07-21 09:32:17 +02:00
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
/* Config files '*.conf'. */
|
2016-01-26 10:46:27 +01:00
|
|
|
{
|
|
|
|
ObjectLock olock(newConfigPart.UpdateV1);
|
2016-08-25 06:19:44 +02:00
|
|
|
for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
|
2018-10-25 14:10:30 +02:00
|
|
|
String path = "/" + zf.Tag + kv.first;
|
|
|
|
newConfigInfo.UpdateV1->Set(path, kv.second);
|
|
|
|
newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
|
2016-01-26 10:46:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
/* Meta files. */
|
2016-01-26 10:46:27 +01:00
|
|
|
{
|
|
|
|
ObjectLock olock(newConfigPart.UpdateV2);
|
2016-08-25 06:19:44 +02:00
|
|
|
for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
|
2018-10-25 14:10:30 +02:00
|
|
|
String path = "/" + zf.Tag + kv.first;
|
|
|
|
newConfigInfo.UpdateV2->Set(path, kv.second);
|
|
|
|
newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
|
2016-01-26 10:46:27 +01:00
|
|
|
}
|
2015-07-21 09:32:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-26 10:46:27 +01:00
|
|
|
int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
|
|
|
|
|
|
|
|
if (sumUpdates == 0)
|
2015-12-11 19:54:17 +01:00
|
|
|
return;
|
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
String productionZonesDir = GetApiZonesDir() + zoneName;
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2014-10-20 10:09:57 +02:00
|
|
|
Log(LogInformation, "ApiListener")
|
2018-10-25 14:10:30 +02:00
|
|
|
<< "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << productionZonesDir << "'.";
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-09-28 16:51:20 +02:00
|
|
|
/* Purge files to allow deletion via zones.d. */
|
2018-10-25 14:10:30 +02:00
|
|
|
if (Utility::PathExists(productionZonesDir))
|
|
|
|
Utility::RemoveDirRecursive(productionZonesDir);
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
Utility::MkDirP(productionZonesDir, 0700);
|
2014-05-13 13:18:27 +02:00
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
/* 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();
|
2014-06-13 09:23:05 +02:00
|
|
|
}
|
2014-05-13 13:18:27 +02:00
|
|
|
}
|
2018-10-25 14:10:30 +02:00
|
|
|
|
|
|
|
/* 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();
|
2014-05-13 15:57:02 +02:00
|
|
|
}
|
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2015-06-22 11:11:21 +02:00
|
|
|
void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
|
2014-05-13 15:57:02 +02:00
|
|
|
{
|
|
|
|
Endpoint::Ptr endpoint = aclient->GetEndpoint();
|
|
|
|
ASSERT(endpoint);
|
|
|
|
|
2018-10-25 14:30:34 +02:00
|
|
|
Zone::Ptr clientZone = endpoint->GetZone();
|
|
|
|
Zone::Ptr localZone = Zone::GetLocalZone();
|
2014-05-13 15:57:02 +02:00
|
|
|
|
|
|
|
/* don't try to send config updates to our master */
|
2018-10-25 14:30:34 +02:00
|
|
|
if (!clientZone->IsChildOf(localZone))
|
2014-05-13 15:57:02 +02:00
|
|
|
return;
|
|
|
|
|
2016-01-26 10:46:27 +01:00
|
|
|
Dictionary::Ptr configUpdateV1 = new Dictionary();
|
|
|
|
Dictionary::Ptr configUpdateV2 = new Dictionary();
|
2018-10-25 14:10:30 +02:00
|
|
|
Dictionary::Ptr configUpdateChecksums = new Dictionary();
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
String zonesDir = GetApiZonesDir();
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2016-08-25 06:19:44 +02:00
|
|
|
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
|
2018-10-25 14:10:30 +02:00
|
|
|
String zoneName = zone->GetName();
|
|
|
|
String zoneDir = zonesDir + zoneName;
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-10-25 14:30:34 +02:00
|
|
|
/* Only sync child and global zones. */
|
|
|
|
if (!zone->IsChildOf(clientZone) && !zone->IsGlobal())
|
2014-05-13 15:57:02 +02:00
|
|
|
continue;
|
2016-01-21 13:02:53 +01:00
|
|
|
|
2018-10-25 14:30:34 +02:00
|
|
|
/* Zone was configured, but there's no configuration directory. */
|
2016-01-21 13:02:53 +01:00
|
|
|
if (!Utility::PathExists(zoneDir))
|
2014-06-12 14:31:07 +02:00
|
|
|
continue;
|
|
|
|
|
2015-09-15 16:09:56 +02:00
|
|
|
Log(LogInformation, "ApiListener")
|
2017-12-19 15:50:05 +01:00
|
|
|
<< "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
|
2018-10-25 14:10:30 +02:00
|
|
|
<< "zone '" << zoneName << "' to endpoint '" << endpoint->GetName() << "'.";
|
|
|
|
|
|
|
|
ConfigDirInformation config = LoadConfigDir(zoneDir);
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
configUpdateV1->Set(zoneName, config.UpdateV1);
|
|
|
|
configUpdateV2->Set(zoneName, config.UpdateV2);
|
|
|
|
configUpdateChecksums->Set(zoneName, config.Checksums);
|
2014-05-13 15:57:02 +02:00
|
|
|
}
|
|
|
|
|
2018-01-11 11:17:38 +01:00
|
|
|
Dictionary::Ptr message = new Dictionary({
|
|
|
|
{ "jsonrpc", "2.0" },
|
|
|
|
{ "method", "config::Update" },
|
|
|
|
{ "params", new Dictionary({
|
|
|
|
{ "update", configUpdateV1 },
|
2018-10-25 14:10:30 +02:00
|
|
|
{ "update_v2", configUpdateV2 }, /* Since 2.4.2. */
|
|
|
|
{ "checksums", configUpdateChecksums } /* Since 2.11.0. */
|
2018-01-11 11:17:38 +01:00
|
|
|
}) }
|
|
|
|
});
|
2014-05-13 15:57:02 +02:00
|
|
|
|
|
|
|
aclient->SendMessage(message);
|
|
|
|
}
|
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2015-08-04 14:47:44 +02:00
|
|
|
Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
|
2014-05-13 15:57:02 +02:00
|
|
|
{
|
2018-10-23 18:30:19 +02:00
|
|
|
/* Verify permissions and trust relationship. */
|
2015-08-04 14:47:44 +02:00
|
|
|
if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
|
2014-05-13 15:57:02 +02:00
|
|
|
return Empty;
|
|
|
|
|
2014-05-15 10:13:32 +02:00
|
|
|
ApiListener::Ptr listener = ApiListener::GetInstance();
|
|
|
|
|
2014-06-12 14:31:07 +02:00
|
|
|
if (!listener) {
|
|
|
|
Log(LogCritical, "ApiListener", "No instance available.");
|
2014-05-15 10:13:32 +02:00
|
|
|
return Empty;
|
2014-06-12 14:31:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!listener->GetAcceptConfig()) {
|
2014-10-20 10:09:57 +02:00
|
|
|
Log(LogWarning, "ApiListener")
|
2017-12-19 15:50:05 +01:00
|
|
|
<< "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
|
2014-06-12 14:31:07 +02:00
|
|
|
return Empty;
|
|
|
|
}
|
2014-05-15 10:13:32 +02:00
|
|
|
|
2019-05-13 11:26:39 +02:00
|
|
|
/* 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);
|
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
String apiZonesStageDir = GetApiZonesStageDir();
|
2018-10-25 11:44:49 +02:00
|
|
|
String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
|
|
|
|
String fromZoneName = GetFromZoneName(origin->FromZone);
|
|
|
|
|
2017-04-28 16:51:23 +02:00
|
|
|
Log(LogInformation, "ApiListener")
|
2018-10-25 11:44:49 +02:00
|
|
|
<< "Applying config update from endpoint '" << fromEndpointName
|
|
|
|
<< "' of zone '" << fromZoneName << "'.";
|
2017-04-28 16:51:23 +02:00
|
|
|
|
2016-01-26 10:46:27 +01:00
|
|
|
Dictionary::Ptr updateV1 = params->Get("update");
|
|
|
|
Dictionary::Ptr updateV2 = params->Get("update_v2");
|
2014-05-13 13:18:27 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* New since 2.11.0. */
|
|
|
|
Dictionary::Ptr checksums;
|
|
|
|
|
|
|
|
if (params->Contains("checksums"))
|
|
|
|
checksums = params->Get("checksums");
|
|
|
|
|
2014-05-13 13:18:27 +02:00
|
|
|
bool configChange = false;
|
2018-09-27 17:12:02 +02:00
|
|
|
std::vector<String> relativePaths;
|
2014-05-13 13:18:27 +02:00
|
|
|
|
2018-09-28 13:54:54 +02:00
|
|
|
/*
|
|
|
|
* We can and must safely purge the staging directory, as the difference is taken between
|
|
|
|
* runtime production config and newly received configuration.
|
|
|
|
*/
|
2018-09-28 16:58:59 +02:00
|
|
|
if (Utility::PathExists(apiZonesStageDir))
|
|
|
|
Utility::RemoveDirRecursive(apiZonesStageDir);
|
|
|
|
|
2018-09-28 13:54:54 +02:00
|
|
|
Utility::MkDirP(apiZonesStageDir, 0700);
|
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/* Analyse and process the update. */
|
2016-01-26 10:46:27 +01:00
|
|
|
ObjectLock olock(updateV1);
|
2016-08-25 06:19:44 +02:00
|
|
|
for (const Dictionary::Pair& kv : updateV1) {
|
2018-09-27 17:12:02 +02:00
|
|
|
|
|
|
|
/* Check for the configured zones. */
|
2018-09-27 17:55:54 +02:00
|
|
|
String zoneName = kv.first;
|
|
|
|
Zone::Ptr zone = Zone::GetByName(zoneName);
|
2014-05-13 15:57:02 +02:00
|
|
|
|
|
|
|
if (!zone) {
|
2014-10-20 10:09:57 +02:00
|
|
|
Log(LogWarning, "ApiListener")
|
2018-10-25 11:44:49 +02:00
|
|
|
<< "Ignoring config update from endpoint '" << fromEndpointName
|
|
|
|
<< "' for unknown zone '" << zoneName << "'.";
|
2015-03-09 08:43:46 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-09-27 17:12:02 +02:00
|
|
|
/* Whether we already have configuration in zones.d. */
|
2018-09-27 17:55:54 +02:00
|
|
|
if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
|
2018-10-25 11:44:49 +02:00
|
|
|
Log(LogInformation, "ApiListener")
|
|
|
|
<< "Ignoring config update from endpoint '" << fromEndpointName
|
|
|
|
<< "' for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";
|
2014-05-13 15:57:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-09-27 17:12:02 +02:00
|
|
|
/* Put the received configuration into our stage directory. */
|
2018-10-25 17:47:38 +02:00
|
|
|
String productionConfigZoneDir = GetApiZonesDir() + zoneName;
|
|
|
|
String stageConfigZoneDir = GetApiZonesStageDir() + zoneName;
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
Utility::MkDirP(productionConfigZoneDir, 0700);
|
|
|
|
Utility::MkDirP(stageConfigZoneDir, 0700);
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/* Merge the config information. */
|
2016-01-26 10:46:27 +01:00
|
|
|
ConfigDirInformation newConfigInfo;
|
|
|
|
newConfigInfo.UpdateV1 = kv.second;
|
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* Load metadata. */
|
2016-01-26 10:46:27 +01:00
|
|
|
if (updateV2)
|
|
|
|
newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
|
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* Load checksums. */
|
|
|
|
if (checksums)
|
|
|
|
newConfigInfo.Checksums = checksums->Get(kv.first);
|
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/* Load the current production config details. */
|
2018-10-25 17:47:38 +02:00
|
|
|
ConfigDirInformation productionConfigInfo = LoadConfigDir(productionConfigZoneDir);
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
Dictionary::Ptr productionConfig = MergeConfigUpdate(productionConfigInfo);
|
|
|
|
Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
|
2014-05-13 13:18:27 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* If we have received 'checksums' via cluster message, go for it.
|
|
|
|
* Otherwise do the old timestamp dance.
|
2018-09-27 17:12:02 +02:00
|
|
|
*/
|
2018-10-25 17:47:38 +02:00
|
|
|
if (checksums) {
|
|
|
|
/* Calculate and compare the checksums. */
|
|
|
|
String productionConfigChecksum = GetGlobalChecksum(productionConfigInfo);
|
|
|
|
String newConfigChecksum = GetGlobalChecksum(newConfigInfo);
|
2014-05-13 15:57:02 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
Log(LogWarning, "ApiListener")
|
|
|
|
<< "Received configuration for zone '" << zoneName << "' from endpoint '"
|
|
|
|
<< fromEndpointName << "' with checksum '" << newConfigChecksum << "'."
|
|
|
|
<< "Our production configuration has checksum '" << productionConfigChecksum << "'.";
|
|
|
|
|
|
|
|
/* 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"))
|
|
|
|
productionTimestamp = 0;
|
|
|
|
else
|
|
|
|
productionTimestamp = productionConfig->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 (productionTimestamp >= newTimestamp) {
|
|
|
|
Log(LogInformation, "ApiListener")
|
|
|
|
<< "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;
|
|
|
|
}
|
2018-10-25 14:10:30 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* 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;
|
|
|
|
}
|
|
|
|
}
|
2018-10-25 14:10:30 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* Update the .timestamp file. */
|
|
|
|
String tsPath = stageConfigZoneDir + "/.timestamp";
|
|
|
|
if (!Utility::PathExists(tsPath)) {
|
|
|
|
std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
|
|
|
|
fp << std::fixed << newTimestamp;
|
|
|
|
fp.close();
|
|
|
|
}
|
|
|
|
}
|
2018-10-25 14:10:30 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* Dump the received configuration for this zone into the stage directory. */
|
|
|
|
size_t numBytes = 0;
|
2018-10-25 14:10:30 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
{
|
|
|
|
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;
|
2018-10-25 14:10:30 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* Store the relative config file path for later validation and activation. */
|
2018-10-25 14:10:30 +02:00
|
|
|
relativePaths.push_back(zoneName + "/" + kv.first);
|
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
String path = stageConfigZoneDir + "/" + kv.first;
|
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
Log(LogInformation, "ApiListener")
|
2018-10-25 17:47:38 +02:00
|
|
|
<< "Stage: Updating received configuration file '" << path << "' for zone '" << zoneName << "'.";
|
2018-10-25 14:10:30 +02:00
|
|
|
|
|
|
|
/* 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
Log(LogInformation, "ApiListener")
|
|
|
|
<< "Applying configuration file update for path '" << stageConfigZoneDir << "' ("
|
|
|
|
<< numBytes << " Bytes).";
|
2018-10-25 14:10:30 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
/* 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;
|
2018-10-25 14:10:30 +02:00
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
String path = stageConfigZoneDir + "/" + kv.first;
|
|
|
|
(void) unlink(path.CStr());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-10-25 14:10:30 +02:00
|
|
|
}
|
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
if (configChange) {
|
|
|
|
/* Spawn a validation process. On success, move the staged configuration
|
|
|
|
* into production and restart.
|
|
|
|
*/
|
|
|
|
AsyncTryActivateZonesStage(relativePaths);
|
2018-10-25 14:10:30 +02:00
|
|
|
}
|
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
return Empty;
|
2018-10-25 14:10:30 +02:00
|
|
|
}
|
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/**
|
|
|
|
* Callback for stage config validation.
|
|
|
|
* When validation was successful, the configuration is copied from
|
|
|
|
* stage to production and a restart is triggered.
|
|
|
|
* On failure, there's no restart and this is logged.
|
|
|
|
*
|
|
|
|
* @param pr Result of the validation process.
|
2018-10-24 17:06:17 +02:00
|
|
|
* @param relativePaths Collected paths including the zone name, which are copied from stage to current directories.
|
2018-10-23 18:30:19 +02:00
|
|
|
*/
|
2018-09-27 17:12:02 +02:00
|
|
|
void ApiListener::TryActivateZonesStageCallback(const ProcessResult& pr,
|
2018-09-28 11:26:47 +02:00
|
|
|
const std::vector<String>& relativePaths)
|
2018-09-27 17:12:02 +02:00
|
|
|
{
|
2018-10-24 17:06:17 +02:00
|
|
|
String apiZonesDir = GetApiZonesDir();
|
|
|
|
String apiZonesStageDir = GetApiZonesStageDir();
|
|
|
|
|
|
|
|
String logFile = apiZonesStageDir + "/startup.log";
|
2018-09-27 17:12:02 +02:00
|
|
|
std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
|
|
|
|
fpLog << pr.Output;
|
|
|
|
fpLog.close();
|
|
|
|
|
2018-10-24 17:06:17 +02:00
|
|
|
String statusFile = apiZonesStageDir + "/status";
|
2018-09-27 17:12:02 +02:00
|
|
|
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) {
|
2018-09-27 19:49:16 +02:00
|
|
|
Log(LogInformation, "ApiListener")
|
2018-10-24 17:06:17 +02:00
|
|
|
<< "Config validation for stage '" << apiZonesStageDir << "' was OK, replacing into '" << apiZonesDir << "' and triggering reload.";
|
2018-09-28 13:54:54 +02:00
|
|
|
|
|
|
|
/* Purge production before copying stage. */
|
2018-09-28 16:58:59 +02:00
|
|
|
if (Utility::PathExists(apiZonesDir))
|
|
|
|
Utility::RemoveDirRecursive(apiZonesDir);
|
|
|
|
|
2018-09-28 13:54:54 +02:00
|
|
|
Utility::MkDirP(apiZonesDir, 0700);
|
2018-09-27 19:49:16 +02:00
|
|
|
|
2018-09-28 13:54:54 +02:00
|
|
|
/* Copy all synced configuration files from stage to production. */
|
2018-09-27 17:12:02 +02:00
|
|
|
for (const String& path : relativePaths) {
|
2018-09-28 11:26:47 +02:00
|
|
|
Log(LogNotice, "ApiListener")
|
|
|
|
<< "Copying file '" << path << "' from config sync staging to production zones directory.";
|
2018-09-27 17:12:02 +02:00
|
|
|
|
2018-10-24 17:06:17 +02:00
|
|
|
String stagePath = apiZonesStageDir + path;
|
|
|
|
String currentPath = apiZonesDir + path;
|
2018-09-27 18:31:08 +02:00
|
|
|
|
2018-09-28 13:54:54 +02:00
|
|
|
Utility::MkDirP(Utility::DirName(currentPath), 0700);
|
2018-09-27 18:31:08 +02:00
|
|
|
|
|
|
|
Utility::CopyFile(stagePath, currentPath);
|
2018-09-27 17:12:02 +02:00
|
|
|
}
|
|
|
|
|
2018-09-28 11:08:58 +02:00
|
|
|
ApiListener::Ptr listener = ApiListener::GetInstance();
|
|
|
|
|
|
|
|
if (listener)
|
2018-10-23 17:23:34 +02:00
|
|
|
listener->ClearLastFailedZonesStageValidation();
|
|
|
|
|
|
|
|
Application::RequestRestart();
|
|
|
|
|
|
|
|
return;
|
2018-09-27 17:12:02 +02:00
|
|
|
}
|
2018-10-23 17:23:34 +02:00
|
|
|
|
|
|
|
/* Error case. */
|
|
|
|
Log(LogCritical, "ApiListener")
|
2018-10-24 17:06:17 +02:00
|
|
|
<< "Config validation failed for staged cluster config sync in '" << apiZonesStageDir
|
2018-10-23 17:23:34 +02:00
|
|
|
<< "'. Aborting. Logs: '" << logFile << "'";
|
|
|
|
|
|
|
|
ApiListener::Ptr listener = ApiListener::GetInstance();
|
|
|
|
|
|
|
|
if (listener)
|
|
|
|
listener->UpdateLastFailedZonesStageValidation(pr.Output);
|
2018-09-27 17:12:02 +02:00
|
|
|
}
|
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/**
|
|
|
|
* Spawns a new validation process and waits for its output.
|
|
|
|
* Sets 'System.ZonesStageVarDir' to override the config validation zone dirs with our current stage.
|
|
|
|
*
|
2018-10-24 17:06:17 +02:00
|
|
|
* @param relativePaths Required for later file operations in the callback. Provides the zone name plus path in a list.
|
2018-10-23 18:30:19 +02:00
|
|
|
*/
|
2018-10-24 17:06:17 +02:00
|
|
|
void ApiListener::AsyncTryActivateZonesStage(const std::vector<String>& relativePaths)
|
2018-09-27 17:12:02 +02:00
|
|
|
{
|
|
|
|
VERIFY(Application::GetArgC() >= 1);
|
|
|
|
|
2018-09-27 20:05:25 +02:00
|
|
|
/* Inherit parent process args. */
|
2018-09-27 17:12:02 +02:00
|
|
|
Array::Ptr args = new Array({
|
|
|
|
Application::GetExePath(Application::GetArgV()[0]),
|
|
|
|
});
|
|
|
|
|
2018-09-27 20:05:25 +02:00
|
|
|
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");
|
2018-10-23 18:30:19 +02:00
|
|
|
|
|
|
|
/* Set the ZonesStageDir. This creates our own local chroot without any additional automated zone includes. */
|
2018-09-27 20:05:25 +02:00
|
|
|
args->Add("--define");
|
2018-09-27 20:13:54 +02:00
|
|
|
args->Add("System.ZonesStageVarDir=" + GetApiZonesStageDir());
|
2018-09-27 20:05:25 +02:00
|
|
|
|
2018-09-27 17:12:02 +02:00
|
|
|
Process::Ptr process = new Process(Process::PrepareCommand(args));
|
|
|
|
process->SetTimeout(300);
|
2018-10-24 17:06:17 +02:00
|
|
|
process->Run(std::bind(&TryActivateZonesStageCallback, _1, relativePaths));
|
2018-09-27 17:12:02 +02:00
|
|
|
}
|
2018-09-28 11:08:58 +02:00
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/**
|
|
|
|
* Update the structure from the last failed validation output.
|
|
|
|
* Uses the current timestamp.
|
|
|
|
*
|
|
|
|
* @param log The process output from the config validation.
|
|
|
|
*/
|
2018-09-28 11:08:58 +02:00
|
|
|
void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
|
|
|
|
{
|
|
|
|
Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
|
|
|
|
{ "log", log },
|
|
|
|
{ "ts", Utility::GetTime() }
|
|
|
|
});
|
|
|
|
|
|
|
|
SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
|
|
|
|
}
|
2018-10-23 17:23:34 +02:00
|
|
|
|
2018-10-23 18:30:19 +02:00
|
|
|
/**
|
|
|
|
* Clear the structure for the last failed reload.
|
|
|
|
*
|
|
|
|
*/
|
2018-10-23 17:23:34 +02:00
|
|
|
void ApiListener::ClearLastFailedZonesStageValidation()
|
|
|
|
{
|
|
|
|
SetLastFailedZonesStageValidation(Dictionary::Ptr());
|
|
|
|
}
|
2018-10-25 14:10:30 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate a config checksum.
|
|
|
|
*
|
2018-10-25 14:14:19 +02:00
|
|
|
* @param content String content used for generating the checksum.
|
|
|
|
* @returns The checksum as string.
|
2018-10-25 14:10:30 +02:00
|
|
|
*/
|
|
|
|
String ApiListener::GetChecksum(const String& content)
|
|
|
|
{
|
|
|
|
return SHA256(content);
|
|
|
|
}
|
|
|
|
|
2018-10-25 17:47:38 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2018-10-25 14:10:30 +02:00
|
|
|
/**
|
|
|
|
* Load the given config dir and read their file content into the config structure.
|
|
|
|
*
|
|
|
|
* @param dir Path to the config directory.
|
2018-10-25 17:47:38 +02:00
|
|
|
* @returns ConfigDirInformation structure.
|
2018-10-25 14:10:30 +02:00
|
|
|
*/
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|