mirror of
				https://github.com/Icinga/icinga2.git
				synced 2025-10-31 11:14:10 +01:00 
			
		
		
		
	This was accidentally broken by #9627 because during config sync, a config validation happens that uses `--define System.ZonesStageVarDir=...` which fails on the now frozen namespace. This commit changes this to use `Internal.ZonesStageVarDir` instead. After all, this is used for internal functionality, users should not directly interact with this flag. Additionally, it no longer freezes the `Internal` namespace which actually allows using `Internal.ZonesStageVarDir` in the first place. This also fixes `--define Internal.Debug*` which was also broken by said PR. Freezing of the `Internal` namespace is not necessary for performance reasons as it's not searched implicitly (for example when accessing `globals.x`) and should users actually interact with it, they should know by that name that they are on their own.
		
			
				
	
	
		
			888 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			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;
 | |
| }
 |