1 /******************************************************************************
3 * Copyright (C) 2012-2018 Icinga Development Team (https://icinga.com/) *
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. *
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. *
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 ******************************************************************************/
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"
30 using namespace icinga;
32 REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
34 void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
36 CONTEXT("Creating config update for file '" + file + "'");
38 Log(LogNotice, "ApiListener")
39 << "Creating config update for file '" << file << "'.";
41 std::ifstream fp(file.CStr(), std::ifstream::binary);
45 String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
47 Dictionary::Ptr update;
49 if (Utility::Match("*.conf", file))
50 update = config.UpdateV1;
52 update = config.UpdateV2;
54 update->Set(file.SubStr(path.GetLength()), content);
57 Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
59 Dictionary::Ptr result = new Dictionary();
62 config.UpdateV1->CopyTo(result);
65 config.UpdateV2->CopyTo(result);
70 ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
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);
79 bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo, const String& configDir, bool authoritative)
81 bool configChange = false;
83 Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo);
84 Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
88 if (!oldConfig->Contains("/.timestamp"))
91 oldTimestamp = oldConfig->Get("/.timestamp");
95 if (!newConfig->Contains("/.timestamp"))
96 newTimestamp = Utility::GetTime();
98 newTimestamp = newConfig->Get("/.timestamp");
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 << ").";
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))
122 String path = configDir + "/" + kv.first;
123 Log(LogInformation, "ApiListener")
124 << "Updating configuration file: " << path;
126 /* Sync string content only. */
127 String content = kv.second;
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);
135 numBytes += content.GetLength();
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 << ").";
148 ObjectLock xlock(oldConfig);
149 for (const Dictionary::Pair& kv : oldConfig) {
150 if (!newConfig->Contains(kv.first)) {
153 String path = configDir + "/" + kv.first;
154 (void) unlink(path.CStr());
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;
166 String authPath = configDir + "/.authoritative";
167 if (!Utility::PathExists(authPath)) {
168 std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
176 void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const
178 ConfigDirInformation newConfigInfo;
179 newConfigInfo.UpdateV1 = new Dictionary();
180 newConfigInfo.UpdateV2 = new Dictionary();
182 for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zone->GetName())) {
183 ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
186 ObjectLock olock(newConfigPart.UpdateV1);
187 for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
188 newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second);
193 ObjectLock olock(newConfigPart.UpdateV2);
194 for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
195 newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second);
200 int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
205 String oldDir = Configuration::DataDir + "/api/zones/" + zone->GetName();
207 Log(LogInformation, "ApiListener")
208 << "Copying " << sumUpdates << " zone configuration files for zone '" << zone->GetName() << "' to '" << oldDir << "'.";
210 Utility::MkDirP(oldDir, 0700);
212 ConfigDirInformation oldConfigInfo = LoadConfigDir(oldDir);
214 UpdateConfigDir(oldConfigInfo, newConfigInfo, oldDir, true);
217 void ApiListener::SyncZoneDirs() const
219 for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
222 } catch (const std::exception&) {
228 void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
230 Endpoint::Ptr endpoint = aclient->GetEndpoint();
233 Zone::Ptr azone = endpoint->GetZone();
234 Zone::Ptr lzone = Zone::GetLocalZone();
236 /* don't try to send config updates to our master */
237 if (!azone->IsChildOf(lzone))
240 Dictionary::Ptr configUpdateV1 = new Dictionary();
241 Dictionary::Ptr configUpdateV2 = new Dictionary();
243 String zonesDir = Configuration::DataDir + "/api/zones";
245 for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
246 String zoneDir = zonesDir + "/" + zone->GetName();
248 if (!zone->IsChildOf(azone) && !zone->IsGlobal())
251 if (!Utility::PathExists(zoneDir))
254 Log(LogInformation, "ApiListener")
255 << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
256 << "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'.";
258 ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName());
259 configUpdateV1->Set(zone->GetName(), config.UpdateV1);
260 configUpdateV2->Set(zone->GetName(), config.UpdateV2);
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 }
272 aclient->SendMessage(message);
275 Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
277 if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
280 ApiListener::Ptr listener = ApiListener::GetInstance();
283 Log(LogCritical, "ApiListener", "No instance available.");
287 if (!listener->GetAcceptConfig()) {
288 Log(LogWarning, "ApiListener")
289 << "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
293 Log(LogInformation, "ApiListener")
294 << "Applying config update from endpoint '" << origin->FromClient->GetEndpoint()->GetName()
295 << "' of zone '" << GetFromZoneName(origin->FromZone) << "'.";
297 Dictionary::Ptr updateV1 = params->Get("update");
298 Dictionary::Ptr updateV2 = params->Get("update_v2");
300 bool configChange = false;
302 ObjectLock olock(updateV1);
303 for (const Dictionary::Pair& kv : updateV1) {
304 Zone::Ptr zone = Zone::GetByName(kv.first);
307 Log(LogWarning, "ApiListener")
308 << "Ignoring config update for unknown zone '" << kv.first << "'.";
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.";
318 String oldDir = Configuration::DataDir + "/api/zones/" + zone->GetName();
320 Utility::MkDirP(oldDir, 0700);
322 ConfigDirInformation newConfigInfo;
323 newConfigInfo.UpdateV1 = kv.second;
326 newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
328 Dictionary::Ptr newConfig = kv.second;
329 ConfigDirInformation oldConfigInfo = LoadConfigDir(oldDir);
331 if (UpdateConfigDir(oldConfigInfo, newConfigInfo, oldDir, false))
336 Log(LogInformation, "ApiListener", "Restarting after configuration change.");
337 Application::RequestRestart();