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;
20 void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
22 CONTEXT("Creating config update for file '" + file + "'");
24 Log(LogNotice, "ApiListener")
25 << "Creating config update for file '" << file << "'.";
27 std::ifstream fp(file.CStr(), std::ifstream::binary);
31 String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
33 Dictionary::Ptr update;
35 if (Utility::Match("*.conf", file))
36 update = config.UpdateV1;
38 update = config.UpdateV2;
40 update->Set(file.SubStr(path.GetLength()), content);
43 Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
45 Dictionary::Ptr result = new Dictionary();
48 config.UpdateV1->CopyTo(result);
51 config.UpdateV2->CopyTo(result);
56 ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
58 ConfigDirInformation config;
59 config.UpdateV1 = new Dictionary();
60 config.UpdateV2 = new Dictionary();
61 Utility::GlobRecursive(dir, "*", std::bind(&ApiListener::ConfigGlobHandler, std::ref(config), dir, _1), GlobFile);
65 bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo,
66 const String& configDir, const String& zoneName, std::vector<String>& relativePaths, bool authoritative)
68 bool configChange = false;
70 Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo);
71 Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
75 if (!oldConfig->Contains("/.timestamp"))
78 oldTimestamp = oldConfig->Get("/.timestamp");
82 if (!newConfig->Contains("/.timestamp"))
83 newTimestamp = Utility::GetTime();
85 newTimestamp = newConfig->Get("/.timestamp");
87 /* skip update if our configuration files are more recent */
88 if (oldTimestamp >= newTimestamp) {
89 Log(LogNotice, "ApiListener")
90 << "Our configuration is more recent than the received configuration update."
91 << " Ignoring configuration file update for path '" << configDir << "'. Current timestamp '"
92 << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' ("
93 << std::fixed << std::setprecision(6) << oldTimestamp
94 << ") >= received timestamp '"
95 << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
96 << newTimestamp << ").";
103 ObjectLock olock(newConfig);
104 for (const Dictionary::Pair& kv : newConfig) {
105 if (oldConfig->Get(kv.first) != kv.second) {
106 if (!Utility::Match("*/.timestamp", kv.first))
109 /* Store the relative config file path for later. */
110 relativePaths.push_back(zoneName + "/" + kv.first);
112 String path = configDir + "/" + kv.first;
113 Log(LogInformation, "ApiListener")
114 << "Updating configuration file: " << path;
116 /* Sync string content only. */
117 String content = kv.second;
119 /* Generate a directory tree (zones/1/2/3 might not exist yet). */
120 Utility::MkDirP(Utility::DirName(path), 0755);
121 std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
125 numBytes += content.GetLength();
130 /* Update with staging information TODO - use `authoritative` as flag. */
131 Log(LogInformation, "ApiListener")
132 << "Applying configuration file update for path '" << configDir << "' (" << numBytes << " Bytes). Received timestamp '"
133 << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
134 << std::fixed << std::setprecision(6) << newTimestamp
135 << "), Current timestamp '"
136 << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' ("
137 << oldTimestamp << ").";
139 ObjectLock xlock(oldConfig);
140 for (const Dictionary::Pair& kv : oldConfig) {
141 if (!newConfig->Contains(kv.first)) {
144 String path = configDir + "/" + kv.first;
145 (void) unlink(path.CStr());
149 String tsPath = configDir + "/.timestamp";
150 if (!Utility::PathExists(tsPath)) {
151 std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
152 fp << std::fixed << newTimestamp;
157 String authPath = configDir + "/.authoritative";
158 if (!Utility::PathExists(authPath)) {
159 std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
167 void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const
169 ConfigDirInformation newConfigInfo;
170 newConfigInfo.UpdateV1 = new Dictionary();
171 newConfigInfo.UpdateV2 = new Dictionary();
173 for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zone->GetName())) {
174 ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
177 ObjectLock olock(newConfigPart.UpdateV1);
178 for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
179 newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second);
184 ObjectLock olock(newConfigPart.UpdateV2);
185 for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
186 newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second);
191 int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
196 String oldDir = Configuration::DataDir + "/api/zones/" + zone->GetName();
198 Log(LogInformation, "ApiListener")
199 << "Copying " << sumUpdates << " zone configuration files for zone '" << zone->GetName() << "' to '" << oldDir << "'.";
201 Utility::MkDirP(oldDir, 0700);
203 ConfigDirInformation oldConfigInfo = LoadConfigDir(oldDir);
205 std::vector<String> relativePaths;
206 UpdateConfigDir(oldConfigInfo, newConfigInfo, oldDir, zone->GetName(), relativePaths, true);
209 void ApiListener::SyncZoneDirs() const
211 for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
214 } catch (const std::exception&) {
220 void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
222 Endpoint::Ptr endpoint = aclient->GetEndpoint();
225 Zone::Ptr azone = endpoint->GetZone();
226 Zone::Ptr lzone = Zone::GetLocalZone();
228 /* don't try to send config updates to our master */
229 if (!azone->IsChildOf(lzone))
232 Dictionary::Ptr configUpdateV1 = new Dictionary();
233 Dictionary::Ptr configUpdateV2 = new Dictionary();
235 String zonesDir = Configuration::DataDir + "/api/zones";
237 for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
238 String zoneDir = zonesDir + "/" + zone->GetName();
240 if (!zone->IsChildOf(azone) && !zone->IsGlobal())
243 if (!Utility::PathExists(zoneDir))
246 Log(LogInformation, "ApiListener")
247 << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
248 << "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'.";
250 ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName());
251 configUpdateV1->Set(zone->GetName(), config.UpdateV1);
252 configUpdateV2->Set(zone->GetName(), config.UpdateV2);
255 Dictionary::Ptr message = new Dictionary({
256 { "jsonrpc", "2.0" },
257 { "method", "config::Update" },
258 { "params", new Dictionary({
259 { "update", configUpdateV1 },
260 { "update_v2", configUpdateV2 }
264 aclient->SendMessage(message);
267 Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
269 if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
272 ApiListener::Ptr listener = ApiListener::GetInstance();
275 Log(LogCritical, "ApiListener", "No instance available.");
279 if (!listener->GetAcceptConfig()) {
280 Log(LogWarning, "ApiListener")
281 << "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
285 /* Only one transaction is allowed, concurrent message handlers need to wait.
286 * This affects two parent endpoints sending the config in the same moment.
288 boost::mutex::scoped_lock lock(m_ConfigSyncStageLock);
290 Log(LogInformation, "ApiListener")
291 << "Applying config update from endpoint '" << origin->FromClient->GetEndpoint()->GetName()
292 << "' of zone '" << GetFromZoneName(origin->FromZone) << "'.";
294 Dictionary::Ptr updateV1 = params->Get("update");
295 Dictionary::Ptr updateV2 = params->Get("update_v2");
297 bool configChange = false;
298 std::vector<String> relativePaths;
300 ObjectLock olock(updateV1);
301 for (const Dictionary::Pair& kv : updateV1) {
303 /* Check for the configured zones. */
304 String zoneName = kv.first;
305 Zone::Ptr zone = Zone::GetByName(zoneName);
308 Log(LogWarning, "ApiListener")
309 << "Ignoring config update for unknown zone '" << zoneName << "'.";
313 /* Whether we already have configuration in zones.d. */
314 if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
315 Log(LogWarning, "ApiListener")
316 << "Ignoring config update for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";
320 /* Put the received configuration into our stage directory. */
321 String currentConfigDir = GetApiZonesDir() + zoneName;
322 String stageConfigDir = GetApiZonesStageDir() + zoneName;
324 Utility::MkDirP(currentConfigDir, 0700);
325 Utility::MkDirP(stageConfigDir, 0700);
327 ConfigDirInformation newConfigInfo;
328 newConfigInfo.UpdateV1 = kv.second;
331 newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
333 Dictionary::Ptr newConfig = kv.second;
334 ConfigDirInformation currentConfigInfo = LoadConfigDir(currentConfigDir);
336 /* Move the received configuration into our stage directory first. */
337 if (UpdateConfigDir(currentConfigInfo, newConfigInfo, stageConfigDir, zoneName, relativePaths, false))
342 /* Spawn a validation process. On success, move the staged configuration
343 * into production and restart.
345 AsyncTryActivateZonesStage(GetApiZonesStageDir(), GetApiZonesDir(), relativePaths);
351 void ApiListener::TryActivateZonesStageCallback(const ProcessResult& pr,
352 const String& stageConfigDir, const String& currentConfigDir,
353 const std::vector<String>& relativePaths)
355 String logFile = GetApiZonesStageDir() + "/startup.log";
356 std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
360 String statusFile = GetApiZonesStageDir() + "/status";
361 std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
362 fpStatus << pr.ExitStatus;
365 /* validation went fine, copy stage and reload */
366 if (pr.ExitStatus == 0) {
367 Log(LogInformation, "ApiListener")
368 << "Config validation for stage '" << GetApiZonesStageDir() << "' was OK, copying into '" << GetApiZonesDir() << "' and triggering reload.";
370 for (const String& path : relativePaths) {
371 Log(LogNotice, "ApiListener")
372 << "Copying file '" << path << "' from config sync staging to production zones directory.";
374 String stagePath = GetApiZonesStageDir() + path;
375 String currentPath = GetApiZonesDir() + path;
377 Utility::MkDirP(Utility::DirName(currentPath), 0755);
379 Utility::CopyFile(stagePath, currentPath);
382 Application::RequestRestart();
384 Log(LogCritical, "ApiListener")
385 << "Config validation failed for staged cluster config sync in '" << GetApiZonesStageDir()
386 << "'. Aborting. Logs: '" << logFile << "'";
388 ApiListener::Ptr listener = ApiListener::GetInstance();
391 listener->UpdateLastFailedZonesStageValidation(pr.Output);
395 void ApiListener::AsyncTryActivateZonesStage(const String& stageConfigDir, const String& currentConfigDir,
396 const std::vector<String>& relativePaths)
398 VERIFY(Application::GetArgC() >= 1);
400 /* Inherit parent process args. */
401 Array::Ptr args = new Array({
402 Application::GetExePath(Application::GetArgV()[0]),
405 for (int i = 1; i < Application::GetArgC(); i++) {
406 String argV = Application::GetArgV()[i];
408 if (argV == "-d" || argV == "--daemonize")
414 args->Add("--validate");
415 args->Add("--define");
416 args->Add("System.ZonesStageVarDir=" + GetApiZonesStageDir());
418 Process::Ptr process = new Process(Process::PrepareCommand(args));
419 process->SetTimeout(300);
420 process->Run(std::bind(&TryActivateZonesStageCallback, _1, stageConfigDir, currentConfigDir, relativePaths));
423 void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
425 Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
427 { "ts", Utility::GetTime() }
430 SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);