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