]> granicus.if.org Git - icinga2/blob - lib/remote/apilistener-filesync.cpp
4b31673f699d44527014fa292d12efda50dfd98e
[icinga2] / lib / remote / apilistener-filesync.cpp
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/configtype.hpp"
7 #include "base/logger.hpp"
8 #include "base/convert.hpp"
9 #include "base/exception.hpp"
10 #include "base/utility.hpp"
11 #include <fstream>
12 #include <iomanip>
13
14 using namespace icinga;
15
16 REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
17
18 boost::mutex ApiListener::m_ConfigSyncStageLock;
19
20 /**
21  * Read the given file and store it in the config information structure.
22  * Callback function for Glob().
23  *
24  * @param config Reference to the config information object.
25  * @param path File path.
26  * @param file Full file name.
27  */
28 void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
29 {
30         CONTEXT("Creating config update for file '" + file + "'");
31
32         Log(LogNotice, "ApiListener")
33                 << "Creating config update for file '" << file << "'.";
34
35         std::ifstream fp(file.CStr(), std::ifstream::binary);
36         if (!fp)
37                 return;
38
39         String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
40
41         Dictionary::Ptr update;
42
43         /*
44          * 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp).
45          *
46          * **Keep this intact to stay compatible with older clients.**
47          */
48         if (Utility::Match("*.conf", file))
49                 update = config.UpdateV1;
50         else
51                 update = config.UpdateV2;
52
53         update->Set(file.SubStr(path.GetLength()), content);
54 }
55
56 /**
57  * Compatibility helper for merging config update v1 and v2 into a global result.
58  *
59  * @param config Config information structure.
60  * @returns Dictionary which holds the merged information.
61  */
62 Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
63 {
64         Dictionary::Ptr result = new Dictionary();
65
66         if (config.UpdateV1)
67                 config.UpdateV1->CopyTo(result);
68
69         if (config.UpdateV2)
70                 config.UpdateV2->CopyTo(result);
71
72         return result;
73 }
74
75 /**
76  * Load the given config dir and read their file content into the config structure.
77  *
78  * @param dir Path to the config directory.
79  * @returns ConfigInformation structure.
80  */
81 ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
82 {
83         ConfigDirInformation config;
84         config.UpdateV1 = new Dictionary();
85         config.UpdateV2 = new Dictionary();
86         Utility::GlobRecursive(dir, "*", std::bind(&ApiListener::ConfigGlobHandler, std::ref(config), dir, _1), GlobFile);
87         return config;
88 }
89
90 /**
91  * Diffs the old current configuration with the new configuration
92  * and copies the collected content. Detects whether a change
93  * happened, this is used for later restarts.
94  *
95  * This generic function is called in two situations:
96  * - Local zones.d to var/lib/api/zones copy on the master (authoritative: true)
97  * - Received config update on a cluster node (authoritative: false)
98  *
99  * @param oldConfigInfo Config information struct for the current old deployed config.
100  * @param newConfigInfo Config information struct for the received synced config.
101  * @param configDir Destination for copying new files (production, or stage dir).
102  * @param zoneName Currently processed zone, for storing the relative paths for later.
103  * @param relativePaths Reference which stores all updated config path destinations.
104  * @param Whether we're authoritative for this config.
105  * @returns Whether a config change happened.
106  */
107 bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo,
108         const String& configDir, const String& zoneName, std::vector<String>& relativePaths, bool authoritative)
109 {
110         bool configChange = false;
111
112         Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo);
113         Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
114
115         double oldTimestamp;
116
117         if (!oldConfig->Contains("/.timestamp"))
118                 oldTimestamp = 0;
119         else
120                 oldTimestamp = oldConfig->Get("/.timestamp");
121
122         double newTimestamp;
123
124         if (!newConfig->Contains("/.timestamp"))
125                 newTimestamp = Utility::GetTime();
126         else
127                 newTimestamp = newConfig->Get("/.timestamp");
128
129         /* skip update if our configuration files are more recent */
130         if (oldTimestamp >= newTimestamp) {
131                 Log(LogNotice, "ApiListener")
132                         << "Our configuration is more recent than the received configuration update."
133                         << " Ignoring configuration file update for path '" << configDir << "'. Current timestamp '"
134                         << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' ("
135                         << std::fixed << std::setprecision(6) << oldTimestamp
136                         << ") >= received timestamp '"
137                         << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
138                         << newTimestamp << ").";
139                 return false;
140         }
141
142         size_t numBytes = 0;
143
144         {
145                 ObjectLock olock(newConfig);
146                 for (const Dictionary::Pair& kv : newConfig) {
147                         if (oldConfig->Get(kv.first) != kv.second) {
148                                 if (!Utility::Match("*/.timestamp", kv.first))
149                                         configChange = true;
150
151                                 /* Store the relative config file path for later. */
152                                 relativePaths.push_back(zoneName + "/" + kv.first);
153
154                                 String path = configDir + "/" + kv.first;
155                                 Log(LogInformation, "ApiListener")
156                                         << "Updating configuration file: " << path;
157
158                                 /* Sync string content only. */
159                                 String content = kv.second;
160
161                                 /* Generate a directory tree (zones/1/2/3 might not exist yet). */
162                                 Utility::MkDirP(Utility::DirName(path), 0755);
163                                 std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
164                                 fp << content;
165                                 fp.close();
166
167                                 numBytes += content.GetLength();
168                         }
169                 }
170         }
171
172         /* Log something whether we're authoritative or receing a staged config. */
173         Log(LogInformation, "ApiListener")
174                 << "Applying configuration file update for " << (authoritative ? "" : "stage ")
175                 << "path '" << configDir << "' (" << numBytes << " Bytes). Received timestamp '"
176                 << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
177                 << std::fixed << std::setprecision(6) << newTimestamp
178                 << "), Current timestamp '"
179                 << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' ("
180                 << oldTimestamp << ").";
181
182         /* If the update removes a path, delete it on disk. */
183         ObjectLock xlock(oldConfig);
184         for (const Dictionary::Pair& kv : oldConfig) {
185                 if (!newConfig->Contains(kv.first)) {
186                         configChange = true;
187
188                         String path = configDir + "/" + kv.first;
189                         (void) unlink(path.CStr());
190                 }
191         }
192
193         /* Consider that one of the paths leaves an empty directory here. Such is not copied from stage to prod and purged then automtically. */
194
195         String tsPath = configDir + "/.timestamp";
196         if (!Utility::PathExists(tsPath)) {
197                 std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
198                 fp << std::fixed << newTimestamp;
199                 fp.close();
200         }
201
202         if (authoritative) {
203                 String authPath = configDir + "/.authoritative";
204                 if (!Utility::PathExists(authPath)) {
205                         std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
206                         fp.close();
207                 }
208         }
209
210         return configChange;
211 }
212
213 /**
214  * Sync a zone directory where we have an authoritative copy (zones.d, etc.)
215  *
216  * This function collects the registered zone config dirs from
217  * the config compiler and reads the file content into the config
218  * information structure.
219  *
220  * Returns early when there are no updates.
221  *
222  * @param zone Pointer to the zone object being synced.
223  */
224 void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const
225 {
226         if (!zone)
227                 return;
228
229         ConfigDirInformation newConfigInfo;
230         newConfigInfo.UpdateV1 = new Dictionary();
231         newConfigInfo.UpdateV2 = new Dictionary();
232
233         String zoneName = zone->GetName();
234
235         for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
236                 ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
237
238                 {
239                         ObjectLock olock(newConfigPart.UpdateV1);
240                         for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
241                                 newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second);
242                         }
243                 }
244
245                 {
246                         ObjectLock olock(newConfigPart.UpdateV2);
247                         for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
248                                 newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second);
249                         }
250                 }
251         }
252
253         int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
254
255         if (sumUpdates == 0)
256                 return;
257
258         String currentDir = Configuration::DataDir + "/api/zones/" + zoneName;
259
260         Log(LogInformation, "ApiListener")
261                 << "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << currentDir << "'.";
262
263         ConfigDirInformation oldConfigInfo = LoadConfigDir(currentDir);
264
265         /* Purge files to allow deletion via zones.d. */
266         Utility::RemoveDirRecursive(currentDir);
267         Utility::MkDirP(currentDir, 0700);
268
269         std::vector<String> relativePaths;
270         UpdateConfigDir(oldConfigInfo, newConfigInfo, currentDir, zoneName, relativePaths, true);
271 }
272
273 /**
274  * Entrypoint for updating all authoritative configs into var/lib/icinga2/api/zones
275  *
276  */
277 void ApiListener::SyncZoneDirs() const
278 {
279         for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
280                 try {
281                         SyncZoneDir(zone);
282                 } catch (const std::exception&) {
283                         continue;
284                 }
285         }
286 }
287
288 /**
289  * Entrypoint for sending a file based config update to a cluster client.
290  * This includes security checks for zone relations.
291  * Loads the zone config files where this client belongs to
292  * and sends the 'config::Update' JSON-RPC message.
293  *
294  * @param aclient Connected JSON-RPC client.
295  */
296 void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
297 {
298         Endpoint::Ptr endpoint = aclient->GetEndpoint();
299         ASSERT(endpoint);
300
301         Zone::Ptr azone = endpoint->GetZone();
302         Zone::Ptr lzone = Zone::GetLocalZone();
303
304         /* don't try to send config updates to our master */
305         if (!azone->IsChildOf(lzone))
306                 return;
307
308         Dictionary::Ptr configUpdateV1 = new Dictionary();
309         Dictionary::Ptr configUpdateV2 = new Dictionary();
310
311         String zonesDir = Configuration::DataDir + "/api/zones";
312
313         for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
314                 String zoneDir = zonesDir + "/" + zone->GetName();
315
316                 if (!zone->IsChildOf(azone) && !zone->IsGlobal())
317                         continue;
318
319                 if (!Utility::PathExists(zoneDir))
320                         continue;
321
322                 Log(LogInformation, "ApiListener")
323                         << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
324                         << "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'.";
325
326                 ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName());
327                 configUpdateV1->Set(zone->GetName(), config.UpdateV1);
328                 configUpdateV2->Set(zone->GetName(), config.UpdateV2);
329         }
330
331         Dictionary::Ptr message = new Dictionary({
332                 { "jsonrpc", "2.0" },
333                 { "method", "config::Update" },
334                 { "params", new Dictionary({
335                         { "update", configUpdateV1 },
336                         { "update_v2", configUpdateV2 }
337                 }) }
338         });
339
340         aclient->SendMessage(message);
341 }
342
343 /**
344  * Registered handler when a new config::Update message is received.
345  *
346  * Checks destination and permissions first, then analyses the update.
347  * The newly received configuration is not copied to production immediately,
348  * but into the staging directory first.
349  * Last, the async validation and restart is triggered.
350  *
351  * @param origin Where this message came from.
352  * @param params Message parameters including the config updates.
353  * @returns Empty, required by the interface.
354  */
355 Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
356 {
357         /* Verify permissions and trust relationship. */
358         if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
359                 return Empty;
360
361         ApiListener::Ptr listener = ApiListener::GetInstance();
362
363         if (!listener) {
364                 Log(LogCritical, "ApiListener", "No instance available.");
365                 return Empty;
366         }
367
368         if (!listener->GetAcceptConfig()) {
369                 Log(LogWarning, "ApiListener")
370                         << "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
371                 return Empty;
372         }
373
374         /* Only one transaction is allowed, concurrent message handlers need to wait.
375          * This affects two parent endpoints sending the config in the same moment.
376          */
377         boost::mutex::scoped_lock lock(m_ConfigSyncStageLock);
378
379         Log(LogInformation, "ApiListener")
380                 << "Applying config update from endpoint '" << origin->FromClient->GetEndpoint()->GetName()
381                 << "' of zone '" << GetFromZoneName(origin->FromZone) << "'.";
382
383         Dictionary::Ptr updateV1 = params->Get("update");
384         Dictionary::Ptr updateV2 = params->Get("update_v2");
385
386         bool configChange = false;
387         std::vector<String> relativePaths;
388
389         /*
390          * We can and must safely purge the staging directory, as the difference is taken between
391          * runtime production config and newly received configuration.
392          */
393         String apiZonesStageDir = GetApiZonesStageDir();
394
395         if (Utility::PathExists(apiZonesStageDir))
396                 Utility::RemoveDirRecursive(apiZonesStageDir);
397
398         Utility::MkDirP(apiZonesStageDir, 0700);
399
400         /* Analyse and process the update. */
401         ObjectLock olock(updateV1);
402         for (const Dictionary::Pair& kv : updateV1) {
403
404                 /* Check for the configured zones. */
405                 String zoneName = kv.first;
406                 Zone::Ptr zone = Zone::GetByName(zoneName);
407
408                 if (!zone) {
409                         Log(LogWarning, "ApiListener")
410                                 << "Ignoring config update for unknown zone '" << zoneName << "'.";
411                         continue;
412                 }
413
414                 /* Whether we already have configuration in zones.d. */
415                 if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
416                         Log(LogWarning, "ApiListener")
417                                 << "Ignoring config update for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";
418                         continue;
419                 }
420
421                 /* Put the received configuration into our stage directory. */
422                 String currentConfigDir = GetApiZonesDir() + zoneName;
423                 String stageConfigDir = GetApiZonesStageDir() + zoneName;
424
425                 Utility::MkDirP(currentConfigDir, 0700);
426                 Utility::MkDirP(stageConfigDir, 0700);
427
428                 /* Merge the config information. */
429                 ConfigDirInformation newConfigInfo;
430                 newConfigInfo.UpdateV1 = kv.second;
431
432                 if (updateV2)
433                         newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
434
435                 /* Load the current production config details. */
436                 ConfigDirInformation currentConfigInfo = LoadConfigDir(currentConfigDir);
437
438                 /* Diff the current production configuration with the received configuration.
439                  * If there was a change, collect a signal for later stage validation.
440                  */
441                 if (UpdateConfigDir(currentConfigInfo, newConfigInfo, stageConfigDir, zoneName, relativePaths, false))
442                         configChange = true;
443         }
444
445         if (configChange) {
446                 /* Spawn a validation process. On success, move the staged configuration
447                  * into production and restart.
448                  */
449                 AsyncTryActivateZonesStage(GetApiZonesStageDir(), GetApiZonesDir(), relativePaths);
450         }
451
452         return Empty;
453 }
454
455 /**
456  * Callback for stage config validation.
457  * When validation was successful, the configuration is copied from
458  * stage to production and a restart is triggered.
459  * On failure, there's no restart and this is logged.
460  *
461  * @param pr Result of the validation process.
462  * @param stageConfigDir TODO
463  * @param currentConfigDir TODO
464  * @param relativePaths Collected paths which are copied from stage to current.
465  */
466 void ApiListener::TryActivateZonesStageCallback(const ProcessResult& pr,
467         const String& stageConfigDir, const String& currentConfigDir,
468         const std::vector<String>& relativePaths)
469 {
470         String logFile = GetApiZonesStageDir() + "/startup.log";
471         std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
472         fpLog << pr.Output;
473         fpLog.close();
474
475         String statusFile = GetApiZonesStageDir() + "/status";
476         std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
477         fpStatus << pr.ExitStatus;
478         fpStatus.close();
479
480         /* validation went fine, copy stage and reload */
481         if (pr.ExitStatus == 0) {
482                 Log(LogInformation, "ApiListener")
483                         << "Config validation for stage '" << GetApiZonesStageDir() << "' was OK, replacing into '" << GetApiZonesDir() << "' and triggering reload.";
484
485                 String apiZonesDir = GetApiZonesDir();
486
487                 /* Purge production before copying stage. */
488                 if (Utility::PathExists(apiZonesDir))
489                         Utility::RemoveDirRecursive(apiZonesDir);
490
491                 Utility::MkDirP(apiZonesDir, 0700);
492
493                 /* Copy all synced configuration files from stage to production. */
494                 for (const String& path : relativePaths) {
495                         Log(LogNotice, "ApiListener")
496                                 << "Copying file '" << path << "' from config sync staging to production zones directory.";
497
498                         String stagePath = GetApiZonesStageDir() + path;
499                         String currentPath = GetApiZonesDir() + path;
500
501                         Utility::MkDirP(Utility::DirName(currentPath), 0700);
502
503                         Utility::CopyFile(stagePath, currentPath);
504                 }
505
506                 ApiListener::Ptr listener = ApiListener::GetInstance();
507
508                 if (listener)
509                         listener->ClearLastFailedZonesStageValidation();
510
511                 Application::RequestRestart();
512
513                 return;
514         }
515
516         /* Error case. */
517         Log(LogCritical, "ApiListener")
518                 << "Config validation failed for staged cluster config sync in '" << GetApiZonesStageDir()
519                 << "'. Aborting. Logs: '" << logFile << "'";
520
521         ApiListener::Ptr listener = ApiListener::GetInstance();
522
523         if (listener)
524                 listener->UpdateLastFailedZonesStageValidation(pr.Output);
525 }
526
527 /**
528  * Spawns a new validation process and waits for its output.
529  * Sets 'System.ZonesStageVarDir' to override the config validation zone dirs with our current stage.
530  *
531  * @param stageConfigDir TODO
532  * @param currentConfigDir TODO
533  * @param relativePaths Required for later file operations in the callback.
534  */
535 void ApiListener::AsyncTryActivateZonesStage(const String& stageConfigDir, const String& currentConfigDir,
536         const std::vector<String>& relativePaths)
537 {
538         VERIFY(Application::GetArgC() >= 1);
539
540         /* Inherit parent process args. */
541         Array::Ptr args = new Array({
542                 Application::GetExePath(Application::GetArgV()[0]),
543         });
544
545         for (int i = 1; i < Application::GetArgC(); i++) {
546                 String argV = Application::GetArgV()[i];
547
548                 if (argV == "-d" || argV == "--daemonize")
549                         continue;
550
551                 args->Add(argV);
552         }
553
554         args->Add("--validate");
555
556         /* Set the ZonesStageDir. This creates our own local chroot without any additional automated zone includes. */
557         args->Add("--define");
558         args->Add("System.ZonesStageVarDir=" + GetApiZonesStageDir());
559
560         Process::Ptr process = new Process(Process::PrepareCommand(args));
561         process->SetTimeout(300);
562         process->Run(std::bind(&TryActivateZonesStageCallback, _1, stageConfigDir, currentConfigDir, relativePaths));
563 }
564
565 /**
566  * Update the structure from the last failed validation output.
567  * Uses the current timestamp.
568  *
569  * @param log The process output from the config validation.
570  */
571 void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
572 {
573         Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
574                 { "log", log },
575                 { "ts", Utility::GetTime() }
576         });
577
578         SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
579 }
580
581 /**
582  * Clear the structure for the last failed reload.
583  *
584  */
585 void ApiListener::ClearLastFailedZonesStageValidation()
586 {
587         SetLastFailedZonesStageValidation(Dictionary::Ptr());
588 }