]> granicus.if.org Git - icinga2/blob - lib/remote/apilistener-filesync.cpp
Improve logging and code quality
[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 void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
21 {
22         CONTEXT("Creating config update for file '" + file + "'");
23
24         Log(LogNotice, "ApiListener")
25                 << "Creating config update for file '" << file << "'.";
26
27         std::ifstream fp(file.CStr(), std::ifstream::binary);
28         if (!fp)
29                 return;
30
31         String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
32
33         Dictionary::Ptr update;
34
35         if (Utility::Match("*.conf", file))
36                 update = config.UpdateV1;
37         else
38                 update = config.UpdateV2;
39
40         update->Set(file.SubStr(path.GetLength()), content);
41 }
42
43 Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
44 {
45         Dictionary::Ptr result = new Dictionary();
46
47         if (config.UpdateV1)
48                 config.UpdateV1->CopyTo(result);
49
50         if (config.UpdateV2)
51                 config.UpdateV2->CopyTo(result);
52
53         return result;
54 }
55
56 ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
57 {
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);
62         return config;
63 }
64
65 bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo,
66         const String& configDir, const String& zoneName, std::vector<String>& relativePaths, bool authoritative)
67 {
68         bool configChange = false;
69
70         Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo);
71         Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
72
73         double oldTimestamp;
74
75         if (!oldConfig->Contains("/.timestamp"))
76                 oldTimestamp = 0;
77         else
78                 oldTimestamp = oldConfig->Get("/.timestamp");
79
80         double newTimestamp;
81
82         if (!newConfig->Contains("/.timestamp"))
83                 newTimestamp = Utility::GetTime();
84         else
85                 newTimestamp = newConfig->Get("/.timestamp");
86
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 << ").";
97                 return false;
98         }
99
100         size_t numBytes = 0;
101
102         {
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))
107                                         configChange = true;
108
109                                 /* Store the relative config file path for later. */
110                                 relativePaths.push_back(zoneName + "/" + kv.first);
111
112                                 String path = configDir + "/" + kv.first;
113                                 Log(LogInformation, "ApiListener")
114                                         << "Updating configuration file: " << path;
115
116                                 /* Sync string content only. */
117                                 String content = kv.second;
118
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);
122                                 fp << content;
123                                 fp.close();
124
125                                 numBytes += content.GetLength();
126                         }
127                 }
128         }
129
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 << ").";
138
139         ObjectLock xlock(oldConfig);
140         for (const Dictionary::Pair& kv : oldConfig) {
141                 if (!newConfig->Contains(kv.first)) {
142                         configChange = true;
143
144                         String path = configDir + "/" + kv.first;
145                         (void) unlink(path.CStr());
146                 }
147         }
148
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;
153                 fp.close();
154         }
155
156         if (authoritative) {
157                 String authPath = configDir + "/.authoritative";
158                 if (!Utility::PathExists(authPath)) {
159                         std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
160                         fp.close();
161                 }
162         }
163
164         return configChange;
165 }
166
167 void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const
168 {
169         ConfigDirInformation newConfigInfo;
170         newConfigInfo.UpdateV1 = new Dictionary();
171         newConfigInfo.UpdateV2 = new Dictionary();
172
173         for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zone->GetName())) {
174                 ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
175
176                 {
177                         ObjectLock olock(newConfigPart.UpdateV1);
178                         for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
179                                 newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second);
180                         }
181                 }
182
183                 {
184                         ObjectLock olock(newConfigPart.UpdateV2);
185                         for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
186                                 newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second);
187                         }
188                 }
189         }
190
191         int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
192
193         if (sumUpdates == 0)
194                 return;
195
196         String oldDir = Configuration::DataDir + "/api/zones/" + zone->GetName();
197
198         Log(LogInformation, "ApiListener")
199                 << "Copying " << sumUpdates << " zone configuration files for zone '" << zone->GetName() << "' to '" << oldDir << "'.";
200
201         Utility::MkDirP(oldDir, 0700);
202
203         ConfigDirInformation oldConfigInfo = LoadConfigDir(oldDir);
204
205         std::vector<String> relativePaths;
206         UpdateConfigDir(oldConfigInfo, newConfigInfo, oldDir, zone->GetName(), relativePaths, true);
207 }
208
209 void ApiListener::SyncZoneDirs() const
210 {
211         for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
212                 try {
213                         SyncZoneDir(zone);
214                 } catch (const std::exception&) {
215                         continue;
216                 }
217         }
218 }
219
220 void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
221 {
222         Endpoint::Ptr endpoint = aclient->GetEndpoint();
223         ASSERT(endpoint);
224
225         Zone::Ptr azone = endpoint->GetZone();
226         Zone::Ptr lzone = Zone::GetLocalZone();
227
228         /* don't try to send config updates to our master */
229         if (!azone->IsChildOf(lzone))
230                 return;
231
232         Dictionary::Ptr configUpdateV1 = new Dictionary();
233         Dictionary::Ptr configUpdateV2 = new Dictionary();
234
235         String zonesDir = Configuration::DataDir + "/api/zones";
236
237         for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
238                 String zoneDir = zonesDir + "/" + zone->GetName();
239
240                 if (!zone->IsChildOf(azone) && !zone->IsGlobal())
241                         continue;
242
243                 if (!Utility::PathExists(zoneDir))
244                         continue;
245
246                 Log(LogInformation, "ApiListener")
247                         << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
248                         << "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'.";
249
250                 ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName());
251                 configUpdateV1->Set(zone->GetName(), config.UpdateV1);
252                 configUpdateV2->Set(zone->GetName(), config.UpdateV2);
253         }
254
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 }
261                 }) }
262         });
263
264         aclient->SendMessage(message);
265 }
266
267 Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
268 {
269         if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
270                 return Empty;
271
272         ApiListener::Ptr listener = ApiListener::GetInstance();
273
274         if (!listener) {
275                 Log(LogCritical, "ApiListener", "No instance available.");
276                 return Empty;
277         }
278
279         if (!listener->GetAcceptConfig()) {
280                 Log(LogWarning, "ApiListener")
281                         << "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
282                 return Empty;
283         }
284
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.
287          */
288         boost::mutex::scoped_lock lock(m_ConfigSyncStageLock);
289
290         Log(LogInformation, "ApiListener")
291                 << "Applying config update from endpoint '" << origin->FromClient->GetEndpoint()->GetName()
292                 << "' of zone '" << GetFromZoneName(origin->FromZone) << "'.";
293
294         Dictionary::Ptr updateV1 = params->Get("update");
295         Dictionary::Ptr updateV2 = params->Get("update_v2");
296
297         bool configChange = false;
298         std::vector<String> relativePaths;
299
300         ObjectLock olock(updateV1);
301         for (const Dictionary::Pair& kv : updateV1) {
302
303                 /* Check for the configured zones. */
304                 String zoneName = kv.first;
305                 Zone::Ptr zone = Zone::GetByName(zoneName);
306
307                 if (!zone) {
308                         Log(LogWarning, "ApiListener")
309                                 << "Ignoring config update for unknown zone '" << zoneName << "'.";
310                         continue;
311                 }
312
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.";
317                         continue;
318                 }
319
320                 /* Put the received configuration into our stage directory. */
321                 String currentConfigDir = GetApiZonesDir() + zoneName;
322                 String stageConfigDir = GetApiZonesStageDir() + zoneName;
323
324                 Utility::MkDirP(currentConfigDir, 0700);
325                 Utility::MkDirP(stageConfigDir, 0700);
326
327                 ConfigDirInformation newConfigInfo;
328                 newConfigInfo.UpdateV1 = kv.second;
329
330                 if (updateV2)
331                         newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
332
333                 Dictionary::Ptr newConfig = kv.second;
334                 ConfigDirInformation currentConfigInfo = LoadConfigDir(currentConfigDir);
335
336                 /* Move the received configuration into our stage directory first. */
337                 if (UpdateConfigDir(currentConfigInfo, newConfigInfo, stageConfigDir, zoneName, relativePaths, false))
338                         configChange = true;
339         }
340
341         if (configChange) {
342                 /* Spawn a validation process. On success, move the staged configuration
343                  * into production and restart.
344                  */
345                 AsyncTryActivateZonesStage(GetApiZonesStageDir(), GetApiZonesDir(), relativePaths);
346         }
347
348         return Empty;
349 }
350
351 void ApiListener::TryActivateZonesStageCallback(const ProcessResult& pr,
352         const String& stageConfigDir, const String& currentConfigDir,
353         const std::vector<String>& relativePaths)
354 {
355         String logFile = GetApiZonesStageDir() + "/startup.log";
356         std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
357         fpLog << pr.Output;
358         fpLog.close();
359
360         String statusFile = GetApiZonesStageDir() + "/status";
361         std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
362         fpStatus << pr.ExitStatus;
363         fpStatus.close();
364
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.";
369
370                 for (const String& path : relativePaths) {
371                         Log(LogNotice, "ApiListener")
372                                 << "Copying file '" << path << "' from config sync staging to production zones directory.";
373
374                         String stagePath = GetApiZonesStageDir() + path;
375                         String currentPath = GetApiZonesDir() + path;
376
377                         Utility::MkDirP(Utility::DirName(currentPath), 0755);
378
379                         Utility::CopyFile(stagePath, currentPath);
380                 }
381
382                 Application::RequestRestart();
383         } else {
384                 Log(LogCritical, "ApiListener")
385                         << "Config validation failed for staged cluster config sync in '" << GetApiZonesStageDir()
386                         << "'. Aborting. Logs: '" << logFile << "'";
387
388                 ApiListener::Ptr listener = ApiListener::GetInstance();
389
390                 if (listener)
391                         listener->UpdateLastFailedZonesStageValidation(pr.Output);
392         }
393 }
394
395 void ApiListener::AsyncTryActivateZonesStage(const String& stageConfigDir, const String& currentConfigDir,
396         const std::vector<String>& relativePaths)
397 {
398         VERIFY(Application::GetArgC() >= 1);
399
400         /* Inherit parent process args. */
401         Array::Ptr args = new Array({
402                 Application::GetExePath(Application::GetArgV()[0]),
403         });
404
405         for (int i = 1; i < Application::GetArgC(); i++) {
406                 String argV = Application::GetArgV()[i];
407
408                 if (argV == "-d" || argV == "--daemonize")
409                         continue;
410
411                 args->Add(argV);
412         }
413
414         args->Add("--validate");
415         args->Add("--define");
416         args->Add("System.ZonesStageVarDir=" + GetApiZonesStageDir());
417
418         Process::Ptr process = new Process(Process::PrepareCommand(args));
419         process->SetTimeout(300);
420         process->Run(std::bind(&TryActivateZonesStageCallback, _1, stageConfigDir, currentConfigDir, relativePaths));
421 }
422
423 void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
424 {
425         Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
426                 { "log", log },
427                 { "ts", Utility::GetTime() }
428         });
429
430         SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
431 }