]> granicus.if.org Git - icinga2/blob - lib/remote/apilistener-filesync.cpp
Merge pull request #6662 from Icinga/bugfix/keep-http-connection-open-until-stream-eof
[icinga2] / lib / remote / apilistener-filesync.cpp
1 /******************************************************************************
2  * Icinga 2                                                                   *
3  * Copyright (C) 2012-2018 Icinga Development Team (https://icinga.com/)      *
4  *                                                                            *
5  * This program is free software; you can redistribute it and/or              *
6  * modify it under the terms of the GNU General Public License                *
7  * as published by the Free Software Foundation; either version 2             *
8  * of the License, or (at your option) any later version.                     *
9  *                                                                            *
10  * This program is distributed in the hope that it will be useful,            *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
13  * GNU General Public License for more details.                               *
14  *                                                                            *
15  * You should have received a copy of the GNU General Public License          *
16  * along with this program; if not, write to the Free Software Foundation     *
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.             *
18  ******************************************************************************/
19
20 #include "remote/apilistener.hpp"
21 #include "remote/apifunction.hpp"
22 #include "config/configcompiler.hpp"
23 #include "base/configtype.hpp"
24 #include "base/logger.hpp"
25 #include "base/convert.hpp"
26 #include "base/exception.hpp"
27 #include <fstream>
28 #include <iomanip>
29
30 using namespace icinga;
31
32 REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
33
34 void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
35 {
36         CONTEXT("Creating config update for file '" + file + "'");
37
38         Log(LogNotice, "ApiListener")
39                 << "Creating config update for file '" << file << "'.";
40
41         std::ifstream fp(file.CStr(), std::ifstream::binary);
42         if (!fp)
43                 return;
44
45         String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
46
47         Dictionary::Ptr update;
48
49         if (Utility::Match("*.conf", file))
50                 update = config.UpdateV1;
51         else
52                 update = config.UpdateV2;
53
54         update->Set(file.SubStr(path.GetLength()), content);
55 }
56
57 Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
58 {
59         Dictionary::Ptr result = new Dictionary();
60
61         if (config.UpdateV1)
62                 config.UpdateV1->CopyTo(result);
63
64         if (config.UpdateV2)
65                 config.UpdateV2->CopyTo(result);
66
67         return result;
68 }
69
70 ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
71 {
72         ConfigDirInformation config;
73         config.UpdateV1 = new Dictionary();
74         config.UpdateV2 = new Dictionary();
75         Utility::GlobRecursive(dir, "*", std::bind(&ApiListener::ConfigGlobHandler, std::ref(config), dir, _1), GlobFile);
76         return config;
77 }
78
79 bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo, const String& configDir, bool authoritative)
80 {
81         bool configChange = false;
82
83         Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo);
84         Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
85
86         double oldTimestamp;
87
88         if (!oldConfig->Contains("/.timestamp"))
89                 oldTimestamp = 0;
90         else
91                 oldTimestamp = oldConfig->Get("/.timestamp");
92
93         double newTimestamp;
94
95         if (!newConfig->Contains("/.timestamp"))
96                 newTimestamp = Utility::GetTime();
97         else
98                 newTimestamp = newConfig->Get("/.timestamp");
99
100         /* skip update if our configuration files are more recent */
101         if (oldTimestamp >= newTimestamp) {
102                 Log(LogNotice, "ApiListener")
103                         << "Our configuration is more recent than the received configuration update."
104                         << " Ignoring configuration file update for path '" << configDir << "'. Current timestamp '"
105                         << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' ("
106                         << std::fixed << std::setprecision(6) << oldTimestamp
107                         << ") >= received timestamp '"
108                         << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
109                         << newTimestamp << ").";
110                 return false;
111         }
112
113         size_t numBytes = 0;
114
115         {
116                 ObjectLock olock(newConfig);
117                 for (const Dictionary::Pair& kv : newConfig) {
118                         if (oldConfig->Get(kv.first) != kv.second) {
119                                 if (!Utility::Match("*/.timestamp", kv.first))
120                                         configChange = true;
121
122                                 String path = configDir + "/" + kv.first;
123                                 Log(LogInformation, "ApiListener")
124                                         << "Updating configuration file: " << path;
125
126                                 /* Sync string content only. */
127                                 String content = kv.second;
128
129                                 /* Generate a directory tree (zones/1/2/3 might not exist yet). */
130                                 Utility::MkDirP(Utility::DirName(path), 0755);
131                                 std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
132                                 fp << content;
133                                 fp.close();
134
135                                 numBytes += content.GetLength();
136                         }
137                 }
138         }
139
140         Log(LogInformation, "ApiListener")
141                 << "Applying configuration file update for path '" << configDir << "' (" << numBytes << " Bytes). Received timestamp '"
142                 << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
143                 << std::fixed << std::setprecision(6) << newTimestamp
144                 << "), Current timestamp '"
145                 << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' ("
146                 << oldTimestamp << ").";
147
148         ObjectLock xlock(oldConfig);
149         for (const Dictionary::Pair& kv : oldConfig) {
150                 if (!newConfig->Contains(kv.first)) {
151                         configChange = true;
152
153                         String path = configDir + "/" + kv.first;
154                         (void) unlink(path.CStr());
155                 }
156         }
157
158         String tsPath = configDir + "/.timestamp";
159         if (!Utility::PathExists(tsPath)) {
160                 std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
161                 fp << std::fixed << newTimestamp;
162                 fp.close();
163         }
164
165         if (authoritative) {
166                 String authPath = configDir + "/.authoritative";
167                 if (!Utility::PathExists(authPath)) {
168                         std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
169                         fp.close();
170                 }
171         }
172
173         return configChange;
174 }
175
176 void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const
177 {
178         ConfigDirInformation newConfigInfo;
179         newConfigInfo.UpdateV1 = new Dictionary();
180         newConfigInfo.UpdateV2 = new Dictionary();
181
182         for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zone->GetName())) {
183                 ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
184
185                 {
186                         ObjectLock olock(newConfigPart.UpdateV1);
187                         for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
188                                 newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second);
189                         }
190                 }
191
192                 {
193                         ObjectLock olock(newConfigPart.UpdateV2);
194                         for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
195                                 newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second);
196                         }
197                 }
198         }
199
200         int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
201
202         if (sumUpdates == 0)
203                 return;
204
205         String oldDir = Configuration::DataDir + "/api/zones/" + zone->GetName();
206
207         Log(LogInformation, "ApiListener")
208                 << "Copying " << sumUpdates << " zone configuration files for zone '" << zone->GetName() << "' to '" << oldDir << "'.";
209
210         Utility::MkDirP(oldDir, 0700);
211
212         ConfigDirInformation oldConfigInfo = LoadConfigDir(oldDir);
213
214         UpdateConfigDir(oldConfigInfo, newConfigInfo, oldDir, true);
215 }
216
217 void ApiListener::SyncZoneDirs() const
218 {
219         for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
220                 try {
221                         SyncZoneDir(zone);
222                 } catch (const std::exception&) {
223                         continue;
224                 }
225         }
226 }
227
228 void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
229 {
230         Endpoint::Ptr endpoint = aclient->GetEndpoint();
231         ASSERT(endpoint);
232
233         Zone::Ptr azone = endpoint->GetZone();
234         Zone::Ptr lzone = Zone::GetLocalZone();
235
236         /* don't try to send config updates to our master */
237         if (!azone->IsChildOf(lzone))
238                 return;
239
240         Dictionary::Ptr configUpdateV1 = new Dictionary();
241         Dictionary::Ptr configUpdateV2 = new Dictionary();
242
243         String zonesDir = Configuration::DataDir + "/api/zones";
244
245         for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
246                 String zoneDir = zonesDir + "/" + zone->GetName();
247
248                 if (!zone->IsChildOf(azone) && !zone->IsGlobal())
249                         continue;
250
251                 if (!Utility::PathExists(zoneDir))
252                         continue;
253
254                 Log(LogInformation, "ApiListener")
255                         << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
256                         << "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'.";
257
258                 ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName());
259                 configUpdateV1->Set(zone->GetName(), config.UpdateV1);
260                 configUpdateV2->Set(zone->GetName(), config.UpdateV2);
261         }
262
263         Dictionary::Ptr message = new Dictionary({
264                 { "jsonrpc", "2.0" },
265                 { "method", "config::Update" },
266                 { "params", new Dictionary({
267                         { "update", configUpdateV1 },
268                         { "update_v2", configUpdateV2 }
269                 }) }
270         });
271
272         aclient->SendMessage(message);
273 }
274
275 Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
276 {
277         if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
278                 return Empty;
279
280         ApiListener::Ptr listener = ApiListener::GetInstance();
281
282         if (!listener) {
283                 Log(LogCritical, "ApiListener", "No instance available.");
284                 return Empty;
285         }
286
287         if (!listener->GetAcceptConfig()) {
288                 Log(LogWarning, "ApiListener")
289                         << "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
290                 return Empty;
291         }
292
293         Log(LogInformation, "ApiListener")
294                 << "Applying config update from endpoint '" << origin->FromClient->GetEndpoint()->GetName()
295                 << "' of zone '" << GetFromZoneName(origin->FromZone) << "'.";
296
297         Dictionary::Ptr updateV1 = params->Get("update");
298         Dictionary::Ptr updateV2 = params->Get("update_v2");
299
300         bool configChange = false;
301
302         ObjectLock olock(updateV1);
303         for (const Dictionary::Pair& kv : updateV1) {
304                 Zone::Ptr zone = Zone::GetByName(kv.first);
305
306                 if (!zone) {
307                         Log(LogWarning, "ApiListener")
308                                 << "Ignoring config update for unknown zone '" << kv.first << "'.";
309                         continue;
310                 }
311
312                 if (ConfigCompiler::HasZoneConfigAuthority(kv.first)) {
313                         Log(LogWarning, "ApiListener")
314                                 << "Ignoring config update for zone '" << kv.first << "' because we have an authoritative version of the zone's config.";
315                         continue;
316                 }
317
318                 String oldDir = Configuration::DataDir + "/api/zones/" + zone->GetName();
319
320                 Utility::MkDirP(oldDir, 0700);
321
322                 ConfigDirInformation newConfigInfo;
323                 newConfigInfo.UpdateV1 = kv.second;
324
325                 if (updateV2)
326                         newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
327
328                 Dictionary::Ptr newConfig = kv.second;
329                 ConfigDirInformation oldConfigInfo = LoadConfigDir(oldDir);
330
331                 if (UpdateConfigDir(oldConfigInfo, newConfigInfo, oldDir, false))
332                         configChange = true;
333         }
334
335         if (configChange) {
336                 Log(LogInformation, "ApiListener", "Restarting after configuration change.");
337                 Application::RequestRestart();
338         }
339
340         return Empty;
341 }