1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2 
3 #include "remote/apilistener.hpp"
4 #include "remote/apifunction.hpp"
5 #include "config/configcompiler.hpp"
6 #include "base/tlsutility.hpp"
7 #include "base/json.hpp"
8 #include "base/configtype.hpp"
9 #include "base/logger.hpp"
10 #include "base/convert.hpp"
11 #include "base/application.hpp"
12 #include "base/exception.hpp"
13 #include "base/shared.hpp"
14 #include "base/utility.hpp"
15 #include <fstream>
16 #include <iomanip>
17 #include <thread>
18 
19 using namespace icinga;
20 
21 REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
22 
23 std::mutex ApiListener::m_ConfigSyncStageLock;
24 
25 /**
26  * Entrypoint for updating all authoritative configs from /etc/zones.d, packages, etc.
27  * into var/lib/icinga2/api/zones
28  */
SyncLocalZoneDirs() const29 void ApiListener::SyncLocalZoneDirs() const
30 {
31 	for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
32 		try {
33 			SyncLocalZoneDir(zone);
34 		} catch (const std::exception&) {
35 			continue;
36 		}
37 	}
38 }
39 
40 /**
41  * Sync a zone directory where we have an authoritative copy (zones.d, packages, etc.)
42  *
43  * This function collects the registered zone config dirs from
44  * the config compiler and reads the file content into the config
45  * information structure.
46  *
47  * Returns early when there are no updates.
48  *
49  * @param zone Pointer to the zone object being synced.
50  */
SyncLocalZoneDir(const Zone::Ptr & zone) const51 void ApiListener::SyncLocalZoneDir(const Zone::Ptr& zone) const
52 {
53 	if (!zone)
54 		return;
55 
56 	ConfigDirInformation newConfigInfo;
57 	newConfigInfo.UpdateV1 = new Dictionary();
58 	newConfigInfo.UpdateV2 = new Dictionary();
59 	newConfigInfo.Checksums = new Dictionary();
60 
61 	String zoneName = zone->GetName();
62 
63 	// Load registered zone paths, e.g. '_etc', '_api' and user packages.
64 	for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
65 		ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
66 
67 		// Config files '*.conf'.
68 		{
69 			ObjectLock olock(newConfigPart.UpdateV1);
70 			for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
71 				String path = "/" + zf.Tag + kv.first;
72 
73 				newConfigInfo.UpdateV1->Set(path, kv.second);
74 				newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
75 			}
76 		}
77 
78 		// Meta files.
79 		{
80 			ObjectLock olock(newConfigPart.UpdateV2);
81 			for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
82 				String path = "/" + zf.Tag + kv.first;
83 
84 				newConfigInfo.UpdateV2->Set(path, kv.second);
85 				newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
86 			}
87 		}
88 	}
89 
90 	size_t sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
91 
92 	// Return early if there are no updates.
93 	if (sumUpdates == 0)
94 		return;
95 
96 	String productionZonesDir = GetApiZonesDir() + zoneName;
97 
98 	Log(LogInformation, "ApiListener")
99 		<< "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << productionZonesDir << "'.";
100 
101 	// Purge files to allow deletion via zones.d.
102 	if (Utility::PathExists(productionZonesDir))
103 		Utility::RemoveDirRecursive(productionZonesDir);
104 
105 	Utility::MkDirP(productionZonesDir, 0700);
106 
107 	// Copy content and add additional meta data.
108 	size_t numBytes = 0;
109 
110 	/* Note: We cannot simply copy directories here.
111 	 *
112 	 * Zone directories are registered from everywhere and we already
113 	 * have read their content into memory with LoadConfigDir().
114 	 */
115 	Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
116 
117 	{
118 		ObjectLock olock(newConfig);
119 
120 		for (const Dictionary::Pair& kv : newConfig) {
121 			String dst = productionZonesDir + "/" + kv.first;
122 
123 			Utility::MkDirP(Utility::DirName(dst), 0755);
124 
125 			Log(LogInformation, "ApiListener")
126 				<< "Updating configuration file: " << dst;
127 
128 			String content = kv.second;
129 
130 			std::ofstream fp(dst.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
131 
132 			fp << content;
133 			fp.close();
134 
135 			numBytes += content.GetLength();
136 		}
137 	}
138 
139 	// Additional metadata.
140 	String tsPath = productionZonesDir + "/.timestamp";
141 
142 	if (!Utility::PathExists(tsPath)) {
143 		std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
144 
145 		fp << std::fixed << Utility::GetTime();
146 		fp.close();
147 	}
148 
149 	String authPath = productionZonesDir + "/.authoritative";
150 
151 	if (!Utility::PathExists(authPath)) {
152 		std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
153 		fp.close();
154 	}
155 
156 	// Checksums.
157 	String checksumsPath = productionZonesDir + "/.checksums";
158 
159 	if (Utility::PathExists(checksumsPath))
160 		Utility::Remove(checksumsPath);
161 
162 	std::ofstream fp(checksumsPath.CStr(), std::ofstream::out | std::ostream::trunc);
163 
164 	fp << std::fixed << JsonEncode(newConfigInfo.Checksums);
165 	fp.close();
166 
167 	Log(LogNotice, "ApiListener")
168 		<< "Updated meta data for cluster config sync. Checksum: '" << checksumsPath
169 		<< "', timestamp: '" << tsPath << "', auth: '" << authPath << "'.";
170 }
171 
172 /**
173  * Entrypoint for sending a file based config update to a cluster client.
174  * This includes security checks for zone relations.
175  * Loads the zone config files where this client belongs to
176  * and sends the 'config::Update' JSON-RPC message.
177  *
178  * @param aclient Connected JSON-RPC client.
179  */
SendConfigUpdate(const JsonRpcConnection::Ptr & aclient)180 void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
181 {
182 	Endpoint::Ptr endpoint = aclient->GetEndpoint();
183 	ASSERT(endpoint);
184 
185 	Zone::Ptr clientZone = endpoint->GetZone();
186 	Zone::Ptr localZone = Zone::GetLocalZone();
187 
188 	// Don't send config updates to parent zones
189 	if (!clientZone->IsChildOf(localZone))
190 		return;
191 
192 	Dictionary::Ptr configUpdateV1 = new Dictionary();
193 	Dictionary::Ptr configUpdateV2 = new Dictionary();
194 	Dictionary::Ptr configUpdateChecksums = new Dictionary(); // new since 2.11
195 
196 	String zonesDir = GetApiZonesDir();
197 
198 	for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
199 		String zoneName = zone->GetName();
200 		String zoneDir = zonesDir + zoneName;
201 
202 		// Only sync child and global zones.
203 		if (!zone->IsChildOf(clientZone) && !zone->IsGlobal())
204 			continue;
205 
206 		// Zone was configured, but there's no configuration directory.
207 		if (!Utility::PathExists(zoneDir))
208 			continue;
209 
210 		Log(LogInformation, "ApiListener")
211 			<< "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
212 			<< "zone '" << zoneName << "' to endpoint '" << endpoint->GetName() << "'.";
213 
214 		ConfigDirInformation config = LoadConfigDir(zoneDir);
215 
216 		configUpdateV1->Set(zoneName, config.UpdateV1);
217 		configUpdateV2->Set(zoneName, config.UpdateV2);
218 		configUpdateChecksums->Set(zoneName, config.Checksums); // new since 2.11
219 	}
220 
221 	Dictionary::Ptr message = new Dictionary({
222 		{ "jsonrpc", "2.0" },
223 		{ "method", "config::Update" },
224 		{ "params", new Dictionary({
225 			{ "update", configUpdateV1 },
226 			{ "update_v2", configUpdateV2 },	// Since 2.4.2.
227 			{ "checksums", configUpdateChecksums } 	// Since 2.11.0.
228 		}) }
229 	});
230 
231 	aclient->SendMessage(message);
232 }
233 
CompareTimestampsConfigChange(const Dictionary::Ptr & productionConfig,const Dictionary::Ptr & receivedConfig,const String & stageConfigZoneDir)234 static bool CompareTimestampsConfigChange(const Dictionary::Ptr& productionConfig, const Dictionary::Ptr& receivedConfig,
235 	const String& stageConfigZoneDir)
236 {
237 	double productionTimestamp;
238 	double receivedTimestamp;
239 
240 	// Missing production timestamp means that something really broke. Always trigger a config change then.
241 	if (!productionConfig->Contains("/.timestamp"))
242 		productionTimestamp = 0;
243 	else
244 		productionTimestamp = productionConfig->Get("/.timestamp");
245 
246 	// Missing received config timestamp means that something really broke. Always trigger a config change then.
247 	if (!receivedConfig->Contains("/.timestamp"))
248 		receivedTimestamp = Utility::GetTime() + 10;
249 	else
250 		receivedTimestamp = receivedConfig->Get("/.timestamp");
251 
252 	bool configChange;
253 
254 	// Skip update if our configuration files are more recent.
255 	if (productionTimestamp >= receivedTimestamp) {
256 
257 		Log(LogInformation, "ApiListener")
258 			<< "Our production configuration is more recent than the received configuration update."
259 			<< " Ignoring configuration file update for path '" << stageConfigZoneDir << "'. Current timestamp '"
260 			<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", productionTimestamp) << "' ("
261 			<< std::fixed << std::setprecision(6) << productionTimestamp
262 			<< ") >= received timestamp '"
263 			<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", receivedTimestamp) << "' ("
264 			<< receivedTimestamp << ").";
265 
266 		configChange = false;
267 
268 	} else {
269 		configChange = true;
270 	}
271 
272 	// Update the .timestamp file inside the staging directory.
273 	String tsPath = stageConfigZoneDir + "/.timestamp";
274 
275 	if (!Utility::PathExists(tsPath)) {
276 		std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
277 		fp << std::fixed << receivedTimestamp;
278 		fp.close();
279 	}
280 
281 	return configChange;
282 }
283 
284 /**
285  * Registered handler when a new config::Update message is received.
286  *
287  * Checks destination and permissions first, locks the transaction and analyses the update.
288  * The newly received configuration is not copied to production immediately,
289  * but into the staging directory first.
290  * Last, the async validation and restart is triggered.
291  *
292  * @param origin Where this message came from.
293  * @param params Message parameters including the config updates.
294  * @returns Empty, required by the interface.
295  */
ConfigUpdateHandler(const MessageOrigin::Ptr & origin,const Dictionary::Ptr & params)296 Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
297 {
298 	// Verify permissions and trust relationship.
299 	if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
300 		return Empty;
301 
302 	ApiListener::Ptr listener = ApiListener::GetInstance();
303 
304 	if (!listener) {
305 		Log(LogCritical, "ApiListener", "No instance available.");
306 		return Empty;
307 	}
308 
309 	if (!listener->GetAcceptConfig()) {
310 		Log(LogWarning, "ApiListener")
311 			<< "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
312 		return Empty;
313 	}
314 
315 	std::thread([origin, params, listener]() {
316 		try {
317 			listener->HandleConfigUpdate(origin, params);
318 		} catch (const std::exception& ex) {
319 			auto msg ("Exception during config sync: " + DiagnosticInformation(ex));
320 
321 			Log(LogCritical, "ApiListener") << msg;
322 			listener->UpdateLastFailedZonesStageValidation(msg);
323 		}
324 	}).detach();
325 	return Empty;
326 }
327 
HandleConfigUpdate(const MessageOrigin::Ptr & origin,const Dictionary::Ptr & params)328 void ApiListener::HandleConfigUpdate(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
329 {
330 	/* Only one transaction is allowed, concurrent message handlers need to wait.
331 	 * This affects two parent endpoints sending the config in the same moment.
332 	 */
333 	std::lock_guard<std::mutex> lock(m_ConfigSyncStageLock);
334 
335 	String apiZonesStageDir = GetApiZonesStageDir();
336 	String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
337 	String fromZoneName = GetFromZoneName(origin->FromZone);
338 
339 	Log(LogInformation, "ApiListener")
340 		<< "Applying config update from endpoint '" << fromEndpointName
341 		<< "' of zone '" << fromZoneName << "'.";
342 
343 	// Config files.
344 	Dictionary::Ptr updateV1 = params->Get("update");
345 	// Meta data files: .timestamp, etc.
346 	Dictionary::Ptr updateV2 = params->Get("update_v2");
347 
348 	// New since 2.11.0.
349 	Dictionary::Ptr checksums;
350 
351 	if (params->Contains("checksums"))
352 		checksums = params->Get("checksums");
353 
354 	bool configChange = false;
355 
356 	// Keep track of the relative config paths for later validation and copying. TODO: Find a better algorithm.
357 	std::vector<String> relativePaths;
358 
359 	/*
360 	 * We can and must safely purge the staging directory, as the difference is taken between
361 	 * runtime production config and newly received configuration.
362 	 * This is needed to not mix deleted/changed content between received and stage
363 	 * config.
364 	 */
365 	if (Utility::PathExists(apiZonesStageDir))
366 		Utility::RemoveDirRecursive(apiZonesStageDir);
367 
368 	Utility::MkDirP(apiZonesStageDir, 0700);
369 
370 	// Analyse and process the update.
371 	size_t count = 0;
372 
373 	ObjectLock olock(updateV1);
374 
375 	for (const Dictionary::Pair& kv : updateV1) {
376 
377 		// Check for the configured zones.
378 		String zoneName = kv.first;
379 		Zone::Ptr zone = Zone::GetByName(zoneName);
380 
381 		if (!zone) {
382 			Log(LogWarning, "ApiListener")
383 				<< "Ignoring config update from endpoint '" << fromEndpointName
384 				<< "' for unknown zone '" << zoneName << "'.";
385 
386 			continue;
387 		}
388 
389 		// Ignore updates where we have an authoritive copy in etc/zones.d, packages, etc.
390 		if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
391 			Log(LogInformation, "ApiListener")
392 				<< "Ignoring config update from endpoint '" << fromEndpointName
393 				<< "' for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";
394 
395 			continue;
396 		}
397 
398 		// Put the received configuration into our stage directory.
399 		String productionConfigZoneDir = GetApiZonesDir() + zoneName;
400 		String stageConfigZoneDir = GetApiZonesStageDir() + zoneName;
401 
402 		Utility::MkDirP(productionConfigZoneDir, 0700);
403 		Utility::MkDirP(stageConfigZoneDir, 0700);
404 
405 		// Merge the config information.
406 		ConfigDirInformation newConfigInfo;
407 		newConfigInfo.UpdateV1 = kv.second;
408 
409 		// Load metadata.
410 		if (updateV2)
411 			newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
412 
413 		// Load checksums. New since 2.11.
414 		if (checksums)
415 			newConfigInfo.Checksums = checksums->Get(kv.first);
416 
417 		// Load the current production config details.
418 		ConfigDirInformation productionConfigInfo = LoadConfigDir(productionConfigZoneDir);
419 
420 		// Merge updateV1 and updateV2
421 		Dictionary::Ptr productionConfig = MergeConfigUpdate(productionConfigInfo);
422 		Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
423 
424 		bool timestampChanged = false;
425 
426 		if (CompareTimestampsConfigChange(productionConfig, newConfig, stageConfigZoneDir)) {
427 			timestampChanged = true;
428 		}
429 
430 		/* If we have received 'checksums' via cluster message, go for it.
431 		 * Otherwise do the old timestamp dance for versions < 2.11.
432 		 */
433 		if (checksums) {
434 			Log(LogInformation, "ApiListener")
435 				<< "Received configuration for zone '" << zoneName << "' from endpoint '"
436 				<< fromEndpointName << "'. Comparing the timestamp and checksums.";
437 
438 			if (timestampChanged) {
439 
440 				if (CheckConfigChange(productionConfigInfo, newConfigInfo))
441 					configChange = true;
442 			}
443 
444 		} else {
445 			/* Fallback to timestamp handling when the parent endpoint didn't send checks.
446 			 * This can happen when the satellite is 2.11 and the master is 2.10.
447 			 *
448 			 * TODO: Deprecate and remove this behaviour in 2.13+.
449 			 */
450 
451 			Log(LogWarning, "ApiListener")
452 				<< "Received configuration update without checksums from parent endpoint "
453 				<< fromEndpointName << ". This behaviour is deprecated. Please upgrade the parent endpoint to 2.11+";
454 
455 			if (timestampChanged) {
456 				configChange = true;
457 			}
458 
459 			// Keep another hack when there's a timestamp file missing.
460 			{
461 				ObjectLock olock(newConfig);
462 
463 				for (const Dictionary::Pair &kv : newConfig) {
464 
465 					// This is super expensive with a string content comparison.
466 					if (productionConfig->Get(kv.first) != kv.second) {
467 						if (!Utility::Match("*/.timestamp", kv.first))
468 							configChange = true;
469 					}
470 				}
471 			}
472 		}
473 
474 		// Dump the received configuration for this zone into the stage directory.
475 		size_t numBytes = 0;
476 
477 		{
478 			ObjectLock olock(newConfig);
479 
480 			for (const Dictionary::Pair& kv : newConfig) {
481 
482 				/* Store the relative config file path for later validation and activation.
483 				 * IMPORTANT: Store this prior to any filters.
484 				 * */
485 				relativePaths.push_back(zoneName + "/" + kv.first);
486 
487 				String path = stageConfigZoneDir + "/" + kv.first;
488 
489 				if (Utility::Match("*.conf", path)) {
490 					Log(LogInformation, "ApiListener")
491 						<< "Stage: Updating received configuration file '" << path << "' for zone '" << zoneName << "'.";
492 				}
493 
494 				// Parent nodes < 2.11 always send this, avoid this bug and deny its receival prior to writing it on disk.
495 				if (Utility::BaseName(path) == ".authoritative")
496 					continue;
497 
498 				// Sync string content only.
499 				String content = kv.second;
500 
501 				// Generate a directory tree (zones/1/2/3 might not exist yet).
502 				Utility::MkDirP(Utility::DirName(path), 0755);
503 
504 				// Write the content to file.
505 				std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
506 				fp << content;
507 				fp.close();
508 
509 				numBytes += content.GetLength();
510 			}
511 		}
512 
513 		Log(LogInformation, "ApiListener")
514 			<< "Applying configuration file update for path '" << stageConfigZoneDir << "' ("
515 			<< numBytes << " Bytes).";
516 
517 		if (timestampChanged) {
518 			// If the update removes a path, delete it on disk and signal a config change.
519 			ObjectLock xlock(productionConfig);
520 
521 			for (const Dictionary::Pair& kv : productionConfig) {
522 				if (!newConfig->Contains(kv.first)) {
523 					configChange = true;
524 
525 					String path = stageConfigZoneDir + "/" + kv.first;
526 					Utility::Remove(path);
527 				}
528 			}
529 		}
530 
531 		count++;
532 	}
533 
534 	/*
535 	 * We have processed all configuration files and stored them in the staging directory.
536 	 *
537 	 * We need to store them locally for later analysis. A config change means
538 	 * that we will validate the configuration in a separate process sandbox,
539 	 * and only copy the configuration to production when everything is ok.
540 	 *
541 	 * A successful validation also triggers the final restart.
542 	 */
543 	if (configChange) {
544 		Log(LogInformation, "ApiListener")
545 			<< "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
546 			<< "' are different to production, triggering validation and reload.";
547 		TryActivateZonesStage(relativePaths);
548 	} else {
549 		Log(LogInformation, "ApiListener")
550 			<< "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
551 			<< "' are equal to production, skipping validation and reload.";
552 		ClearLastFailedZonesStageValidation();
553 	}
554 }
555 
556 /**
557  * Spawns a new validation process with 'System.ZonesStageVarDir' set to override the config validation zone dirs with
558  * our current stage. Then waits for the validation result and if it was successful, the configuration is copied from
559  * stage to production and a restart is triggered. On validation failure, there is no restart and this is logged.
560  *
561  * The caller of this function must hold m_ConfigSyncStageLock.
562  *
563  * @param relativePaths Collected paths including the zone name, which are copied from stage to current directories.
564  */
TryActivateZonesStage(const std::vector<String> & relativePaths)565 void ApiListener::TryActivateZonesStage(const std::vector<String>& relativePaths)
566 {
567 	VERIFY(Application::GetArgC() >= 1);
568 
569 	/* Inherit parent process args. */
570 	Array::Ptr args = new Array({
571 		Application::GetExePath(Application::GetArgV()[0]),
572 	});
573 
574 	for (int i = 1; i < Application::GetArgC(); i++) {
575 		String argV = Application::GetArgV()[i];
576 
577 		if (argV == "-d" || argV == "--daemonize")
578 			continue;
579 
580 		args->Add(argV);
581 	}
582 
583 	args->Add("--validate");
584 
585 	// Set the ZonesStageDir. This creates our own local chroot without any additional automated zone includes.
586 	args->Add("--define");
587 	args->Add("System.ZonesStageVarDir=" + GetApiZonesStageDir());
588 
589 	Process::Ptr process = new Process(Process::PrepareCommand(args));
590 	process->SetTimeout(Application::GetReloadTimeout());
591 
592 	process->Run();
593 	const ProcessResult& pr = process->WaitForResult();
594 
595 	String apiZonesDir = GetApiZonesDir();
596 	String apiZonesStageDir = GetApiZonesStageDir();
597 
598 	String logFile = apiZonesStageDir + "/startup.log";
599 	std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
600 	fpLog << pr.Output;
601 	fpLog.close();
602 
603 	String statusFile = apiZonesStageDir + "/status";
604 	std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
605 	fpStatus << pr.ExitStatus;
606 	fpStatus.close();
607 
608 	// Validation went fine, copy stage and reload.
609 	if (pr.ExitStatus == 0) {
610 		Log(LogInformation, "ApiListener")
611 			<< "Config validation for stage '" << apiZonesStageDir << "' was OK, replacing into '" << apiZonesDir << "' and triggering reload.";
612 
613 		// Purge production before copying stage.
614 		if (Utility::PathExists(apiZonesDir))
615 			Utility::RemoveDirRecursive(apiZonesDir);
616 
617 		Utility::MkDirP(apiZonesDir, 0700);
618 
619 		// Copy all synced configuration files from stage to production.
620 		for (const String& path : relativePaths) {
621 			if (!Utility::PathExists(apiZonesStageDir + path))
622 				continue;
623 
624 			Log(LogInformation, "ApiListener")
625 				<< "Copying file '" << path << "' from config sync staging to production zones directory.";
626 
627 			String stagePath = apiZonesStageDir + path;
628 			String currentPath = apiZonesDir + path;
629 
630 			Utility::MkDirP(Utility::DirName(currentPath), 0700);
631 
632 			Utility::CopyFile(stagePath, currentPath);
633 		}
634 
635 		// Clear any failed deployment before
636 		ApiListener::Ptr listener = ApiListener::GetInstance();
637 
638 		if (listener)
639 			listener->ClearLastFailedZonesStageValidation();
640 
641 		Application::RequestRestart();
642 
643 		// All good, return early.
644 		return;
645 	}
646 
647 	// Error case.
648 	Log(LogCritical, "ApiListener")
649 		<< "Config validation failed for staged cluster config sync in '" << apiZonesStageDir
650 		<< "'. Aborting. Logs: '" << logFile << "'";
651 
652 	ApiListener::Ptr listener = ApiListener::GetInstance();
653 
654 	if (listener)
655 		listener->UpdateLastFailedZonesStageValidation(pr.Output);
656 }
657 
658 /**
659  * Update the structure from the last failed validation output.
660  * Uses the current timestamp.
661  *
662  * @param log The process output from the config validation.
663  */
UpdateLastFailedZonesStageValidation(const String & log)664 void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
665 {
666 	Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
667 		{ "log", log },
668 		{ "ts", Utility::GetTime() }
669 	});
670 
671 	SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
672 }
673 
674 /**
675  * Clear the structure for the last failed reload.
676  *
677  */
ClearLastFailedZonesStageValidation()678 void ApiListener::ClearLastFailedZonesStageValidation()
679 {
680 	SetLastFailedZonesStageValidation(Dictionary::Ptr());
681 }
682 
683 /**
684  * Generate a config checksum.
685  *
686  * @param content String content used for generating the checksum.
687  * @returns The checksum as string.
688  */
GetChecksum(const String & content)689 String ApiListener::GetChecksum(const String& content)
690 {
691 	return SHA256(content);
692 }
693 
CheckConfigChange(const ConfigDirInformation & oldConfig,const ConfigDirInformation & newConfig)694 bool ApiListener::CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig)
695 {
696 	Dictionary::Ptr oldChecksums = oldConfig.Checksums;
697 	Dictionary::Ptr newChecksums = newConfig.Checksums;
698 
699 	// TODO: Figure out whether normal users need this for debugging.
700 	Log(LogDebug, "ApiListener")
701 		<< "Checking for config change between stage and production. Old (" << oldChecksums->GetLength() << "): '"
702 		<< JsonEncode(oldChecksums)
703 		<< "' vs. new (" << newChecksums->GetLength() << "): '"
704 		<< JsonEncode(newChecksums) << "'.";
705 
706 	/* Since internal files are synced here too, we can not depend on length.
707 	 * So we need to go through both checksum sets to cover the cases"everything is new" and "everything was deleted".
708 	 */
709 	{
710 		ObjectLock olock(oldChecksums);
711 		for (const Dictionary::Pair& kv : oldChecksums) {
712 			String path = kv.first;
713 			String oldChecksum = kv.second;
714 
715 			/* Ignore internal files, especially .timestamp and .checksums.
716 			 *
717 			 * If we don't, this results in "always change" restart loops.
718 			 */
719 			if (Utility::Match("/.*", path)) {
720 				Log(LogDebug, "ApiListener")
721 					<< "Ignoring old internal file '" << path << "'.";
722 
723 				continue;
724 			}
725 
726 			Log(LogDebug, "ApiListener")
727 				<< "Checking " << path << " for old checksum: " << oldChecksum << ".";
728 
729 			// Check if key exists first for more verbose logging.
730 			// Note: Don't do this later on.
731 			if (!newChecksums->Contains(path)) {
732 				Log(LogDebug, "ApiListener")
733 					<< "File '" << path << "' was deleted by remote.";
734 
735 				return true;
736 			}
737 
738 			String newChecksum = newChecksums->Get(path);
739 
740 			if (newChecksum != kv.second) {
741 				Log(LogDebug, "ApiListener")
742 					<< "Path '" << path << "' doesn't match old checksum '"
743 					<< oldChecksum << "' with new checksum '" << newChecksum << "'.";
744 
745 				return true;
746 			}
747 		}
748 	}
749 
750 	{
751 		ObjectLock olock(newChecksums);
752 		for (const Dictionary::Pair& kv : newChecksums) {
753 			String path = kv.first;
754 			String newChecksum = kv.second;
755 
756 			/* Ignore internal files, especially .timestamp and .checksums.
757 			 *
758 			 * If we don't, this results in "always change" restart loops.
759 			 */
760 			if (Utility::Match("/.*", path)) {
761 				Log(LogDebug, "ApiListener")
762 					<< "Ignoring new internal file '" << path << "'.";
763 
764 				continue;
765 			}
766 
767 			Log(LogDebug, "ApiListener")
768 				<< "Checking " << path << " for new checksum: " << newChecksum << ".";
769 
770 			// Check if the checksum exists, checksums in both sets have already been compared
771 			if (!oldChecksums->Contains(path)) {
772 				Log(LogDebug, "ApiListener")
773 					<< "File '" << path << "' was added by remote.";
774 
775 				return true;
776 			}
777 		}
778 	}
779 
780 	return false;
781 }
782 
783 /**
784  * Load the given config dir and read their file content into the config structure.
785  *
786  * @param dir Path to the config directory.
787  * @returns ConfigDirInformation structure.
788  */
LoadConfigDir(const String & dir)789 ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
790 {
791 	ConfigDirInformation config;
792 	config.UpdateV1 = new Dictionary();
793 	config.UpdateV2 = new Dictionary();
794 	config.Checksums = new Dictionary();
795 
796 	Utility::GlobRecursive(dir, "*", [&config, dir](const String& file) { ConfigGlobHandler(config, dir, file); }, GlobFile);
797 	return config;
798 }
799 
800 /**
801  * Read the given file and store it in the config information structure.
802  * Callback function for Glob().
803  *
804  * @param config Reference to the config information object.
805  * @param path File path.
806  * @param file Full file name.
807  */
ConfigGlobHandler(ConfigDirInformation & config,const String & path,const String & file)808 void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
809 {
810 	// Avoid loading the authoritative marker for syncs at all cost.
811 	if (Utility::BaseName(file) == ".authoritative")
812 		return;
813 
814 	CONTEXT("Creating config update for file '" + file + "'");
815 
816 	Log(LogNotice, "ApiListener")
817 		<< "Creating config update for file '" << file << "'.";
818 
819 	std::ifstream fp(file.CStr(), std::ifstream::binary);
820 	if (!fp)
821 		return;
822 
823 	String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
824 
825 	Dictionary::Ptr update;
826 	String relativePath = file.SubStr(path.GetLength());
827 
828 	/*
829 	 * 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp).
830 	 *
831 	 * **Keep this intact to stay compatible with older clients.**
832 	 */
833 	String sanitizedContent = Utility::ValidateUTF8(content);
834 
835 	if (Utility::Match("*.conf", file)) {
836 		update = config.UpdateV1;
837 
838 		// Configuration files should be automatically sanitized with UTF8.
839 		update->Set(relativePath, sanitizedContent);
840 	} else {
841 		update = config.UpdateV2;
842 
843 		/*
844 		 * Ensure that only valid UTF8 content is being read for the cluster config sync.
845 		 * Binary files are not supported when wrapped into JSON encoded messages.
846 		 * Rationale: https://github.com/Icinga/icinga2/issues/7382
847 		 */
848 		if (content != sanitizedContent) {
849 			Log(LogCritical, "ApiListener")
850 				<< "Ignoring file '" << file << "' for cluster config sync: Does not contain valid UTF8. Binary files are not supported.";
851 			return;
852 		}
853 
854 		update->Set(relativePath, content);
855 	}
856 
857 	/* Calculate a checksum for each file (and a global one later).
858 	 *
859 	 * IMPORTANT: Ignore the .authoritative file above, this must not be synced.
860 	 * */
861 	config.Checksums->Set(relativePath, GetChecksum(content));
862 }
863 
864 /**
865  * Compatibility helper for merging config update v1 and v2 into a global result.
866  *
867  * @param config Config information structure.
868  * @returns Dictionary which holds the merged information.
869  */
MergeConfigUpdate(const ConfigDirInformation & config)870 Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
871 {
872 	Dictionary::Ptr result = new Dictionary();
873 
874 	if (config.UpdateV1)
875 		config.UpdateV1->CopyTo(result);
876 
877 	if (config.UpdateV2)
878 		config.UpdateV2->CopyTo(result);
879 
880 	return result;
881 }
882