1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
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"
14 using namespace icinga;
16 REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
18 boost::mutex ApiListener::m_ConfigSyncStageLock;
21 * Read the given file and store it in the config information structure.
22 * Callback function for Glob().
24 * @param config Reference to the config information object.
25 * @param path File path.
26 * @param file Full file name.
28 void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
30 CONTEXT("Creating config update for file '" + file + "'");
32 Log(LogNotice, "ApiListener")
33 << "Creating config update for file '" << file << "'.";
35 std::ifstream fp(file.CStr(), std::ifstream::binary);
39 String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
41 Dictionary::Ptr update;
44 * 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp).
46 * **Keep this intact to stay compatible with older clients.**
48 if (Utility::Match("*.conf", file))
49 update = config.UpdateV1;
51 update = config.UpdateV2;
53 update->Set(file.SubStr(path.GetLength()), content);
57 * Compatibility helper for merging config update v1 and v2 into a global result.
59 * @param config Config information structure.
60 * @returns Dictionary which holds the merged information.
62 Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
64 Dictionary::Ptr result = new Dictionary();
67 config.UpdateV1->CopyTo(result);
70 config.UpdateV2->CopyTo(result);
76 * Load the given config dir and read their file content into the config structure.
78 * @param dir Path to the config directory.
79 * @returns ConfigInformation structure.
81 ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
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);
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.
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)
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.
107 bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo,
108 const String& configDir, const String& zoneName, std::vector<String>& relativePaths, bool authoritative)
110 bool configChange = false;
112 Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo);
113 Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
117 if (!oldConfig->Contains("/.timestamp"))
120 oldTimestamp = oldConfig->Get("/.timestamp");
124 if (!newConfig->Contains("/.timestamp"))
125 newTimestamp = Utility::GetTime();
127 newTimestamp = newConfig->Get("/.timestamp");
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 << ").";
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))
151 /* Store the relative config file path for later. */
152 relativePaths.push_back(zoneName + "/" + kv.first);
154 String path = configDir + "/" + kv.first;
155 Log(LogInformation, "ApiListener")
156 << "Updating configuration file: " << path;
158 /* Sync string content only. */
159 String content = kv.second;
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);
167 numBytes += content.GetLength();
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 << ").";
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)) {
188 String path = configDir + "/" + kv.first;
189 (void) unlink(path.CStr());
193 /* Consider that one of the paths leaves an empty directory here. Such is not copied from stage to prod and purged then automtically. */
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;
203 String authPath = configDir + "/.authoritative";
204 if (!Utility::PathExists(authPath)) {
205 std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
214 * Sync a zone directory where we have an authoritative copy (zones.d, etc.)
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.
220 * Returns early when there are no updates.
222 * @param zone Pointer to the zone object being synced.
224 void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const
229 ConfigDirInformation newConfigInfo;
230 newConfigInfo.UpdateV1 = new Dictionary();
231 newConfigInfo.UpdateV2 = new Dictionary();
233 String zoneName = zone->GetName();
235 for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
236 ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
239 ObjectLock olock(newConfigPart.UpdateV1);
240 for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
241 newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second);
246 ObjectLock olock(newConfigPart.UpdateV2);
247 for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
248 newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second);
253 int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
258 String currentDir = Configuration::DataDir + "/api/zones/" + zoneName;
260 Log(LogInformation, "ApiListener")
261 << "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << currentDir << "'.";
263 ConfigDirInformation oldConfigInfo = LoadConfigDir(currentDir);
265 /* Purge files to allow deletion via zones.d. */
266 Utility::RemoveDirRecursive(currentDir);
267 Utility::MkDirP(currentDir, 0700);
269 std::vector<String> relativePaths;
270 UpdateConfigDir(oldConfigInfo, newConfigInfo, currentDir, zoneName, relativePaths, true);
274 * Entrypoint for updating all authoritative configs into var/lib/icinga2/api/zones
277 void ApiListener::SyncZoneDirs() const
279 for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
282 } catch (const std::exception&) {
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.
294 * @param aclient Connected JSON-RPC client.
296 void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
298 Endpoint::Ptr endpoint = aclient->GetEndpoint();
301 Zone::Ptr azone = endpoint->GetZone();
302 Zone::Ptr lzone = Zone::GetLocalZone();
304 /* don't try to send config updates to our master */
305 if (!azone->IsChildOf(lzone))
308 Dictionary::Ptr configUpdateV1 = new Dictionary();
309 Dictionary::Ptr configUpdateV2 = new Dictionary();
311 String zonesDir = Configuration::DataDir + "/api/zones";
313 for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
314 String zoneDir = zonesDir + "/" + zone->GetName();
316 if (!zone->IsChildOf(azone) && !zone->IsGlobal())
319 if (!Utility::PathExists(zoneDir))
322 Log(LogInformation, "ApiListener")
323 << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
324 << "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'.";
326 ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName());
327 configUpdateV1->Set(zone->GetName(), config.UpdateV1);
328 configUpdateV2->Set(zone->GetName(), config.UpdateV2);
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 }
340 aclient->SendMessage(message);
344 * Registered handler when a new config::Update message is received.
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.
351 * @param origin Where this message came from.
352 * @param params Message parameters including the config updates.
353 * @returns Empty, required by the interface.
355 Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
357 /* Verify permissions and trust relationship. */
358 if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
361 ApiListener::Ptr listener = ApiListener::GetInstance();
364 Log(LogCritical, "ApiListener", "No instance available.");
368 if (!listener->GetAcceptConfig()) {
369 Log(LogWarning, "ApiListener")
370 << "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
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.
377 boost::mutex::scoped_lock lock(m_ConfigSyncStageLock);
379 Log(LogInformation, "ApiListener")
380 << "Applying config update from endpoint '" << origin->FromClient->GetEndpoint()->GetName()
381 << "' of zone '" << GetFromZoneName(origin->FromZone) << "'.";
383 Dictionary::Ptr updateV1 = params->Get("update");
384 Dictionary::Ptr updateV2 = params->Get("update_v2");
386 bool configChange = false;
387 std::vector<String> relativePaths;
390 * We can and must safely purge the staging directory, as the difference is taken between
391 * runtime production config and newly received configuration.
393 String apiZonesStageDir = GetApiZonesStageDir();
395 if (Utility::PathExists(apiZonesStageDir))
396 Utility::RemoveDirRecursive(apiZonesStageDir);
398 Utility::MkDirP(apiZonesStageDir, 0700);
400 /* Analyse and process the update. */
401 ObjectLock olock(updateV1);
402 for (const Dictionary::Pair& kv : updateV1) {
404 /* Check for the configured zones. */
405 String zoneName = kv.first;
406 Zone::Ptr zone = Zone::GetByName(zoneName);
409 Log(LogWarning, "ApiListener")
410 << "Ignoring config update for unknown zone '" << zoneName << "'.";
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.";
421 /* Put the received configuration into our stage directory. */
422 String currentConfigDir = GetApiZonesDir() + zoneName;
423 String stageConfigDir = GetApiZonesStageDir() + zoneName;
425 Utility::MkDirP(currentConfigDir, 0700);
426 Utility::MkDirP(stageConfigDir, 0700);
428 /* Merge the config information. */
429 ConfigDirInformation newConfigInfo;
430 newConfigInfo.UpdateV1 = kv.second;
433 newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
435 /* Load the current production config details. */
436 ConfigDirInformation currentConfigInfo = LoadConfigDir(currentConfigDir);
438 /* Diff the current production configuration with the received configuration.
439 * If there was a change, collect a signal for later stage validation.
441 if (UpdateConfigDir(currentConfigInfo, newConfigInfo, stageConfigDir, zoneName, relativePaths, false))
446 /* Spawn a validation process. On success, move the staged configuration
447 * into production and restart.
449 AsyncTryActivateZonesStage(GetApiZonesStageDir(), GetApiZonesDir(), relativePaths);
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.
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.
466 void ApiListener::TryActivateZonesStageCallback(const ProcessResult& pr,
467 const String& stageConfigDir, const String& currentConfigDir,
468 const std::vector<String>& relativePaths)
470 String logFile = GetApiZonesStageDir() + "/startup.log";
471 std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
475 String statusFile = GetApiZonesStageDir() + "/status";
476 std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
477 fpStatus << pr.ExitStatus;
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.";
485 String apiZonesDir = GetApiZonesDir();
487 /* Purge production before copying stage. */
488 if (Utility::PathExists(apiZonesDir))
489 Utility::RemoveDirRecursive(apiZonesDir);
491 Utility::MkDirP(apiZonesDir, 0700);
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.";
498 String stagePath = GetApiZonesStageDir() + path;
499 String currentPath = GetApiZonesDir() + path;
501 Utility::MkDirP(Utility::DirName(currentPath), 0700);
503 Utility::CopyFile(stagePath, currentPath);
506 ApiListener::Ptr listener = ApiListener::GetInstance();
509 listener->ClearLastFailedZonesStageValidation();
511 Application::RequestRestart();
517 Log(LogCritical, "ApiListener")
518 << "Config validation failed for staged cluster config sync in '" << GetApiZonesStageDir()
519 << "'. Aborting. Logs: '" << logFile << "'";
521 ApiListener::Ptr listener = ApiListener::GetInstance();
524 listener->UpdateLastFailedZonesStageValidation(pr.Output);
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.
531 * @param stageConfigDir TODO
532 * @param currentConfigDir TODO
533 * @param relativePaths Required for later file operations in the callback.
535 void ApiListener::AsyncTryActivateZonesStage(const String& stageConfigDir, const String& currentConfigDir,
536 const std::vector<String>& relativePaths)
538 VERIFY(Application::GetArgC() >= 1);
540 /* Inherit parent process args. */
541 Array::Ptr args = new Array({
542 Application::GetExePath(Application::GetArgV()[0]),
545 for (int i = 1; i < Application::GetArgC(); i++) {
546 String argV = Application::GetArgV()[i];
548 if (argV == "-d" || argV == "--daemonize")
554 args->Add("--validate");
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());
560 Process::Ptr process = new Process(Process::PrepareCommand(args));
561 process->SetTimeout(300);
562 process->Run(std::bind(&TryActivateZonesStageCallback, _1, stageConfigDir, currentConfigDir, relativePaths));
566 * Update the structure from the last failed validation output.
567 * Uses the current timestamp.
569 * @param log The process output from the config validation.
571 void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
573 Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
575 { "ts", Utility::GetTime() }
578 SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
582 * Clear the structure for the last failed reload.
585 void ApiListener::ClearLastFailedZonesStageValidation()
587 SetLastFailedZonesStageValidation(Dictionary::Ptr());