1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2
3 #include "remote/apilistener.hpp"
4 #include "remote/apifunction.hpp"
5 #include "remote/configobjectutility.hpp"
6 #include "remote/jsonrpc.hpp"
7 #include "base/configtype.hpp"
8 #include "base/json.hpp"
9 #include "base/convert.hpp"
10 #include "config/vmops.hpp"
11 #include <fstream>
12
13 using namespace icinga;
14
15 REGISTER_APIFUNCTION(UpdateObject, config, &ApiListener::ConfigUpdateObjectAPIHandler);
16 REGISTER_APIFUNCTION(DeleteObject, config, &ApiListener::ConfigDeleteObjectAPIHandler);
17
__anon501a055d0102() 18 INITIALIZE_ONCE([]() {
19 ConfigObject::OnActiveChanged.connect(&ApiListener::ConfigUpdateObjectHandler);
20 ConfigObject::OnVersionChanged.connect(&ApiListener::ConfigUpdateObjectHandler);
21 });
22
ConfigUpdateObjectHandler(const ConfigObject::Ptr & object,const Value & cookie)23 void ApiListener::ConfigUpdateObjectHandler(const ConfigObject::Ptr& object, const Value& cookie)
24 {
25 ApiListener::Ptr listener = ApiListener::GetInstance();
26
27 if (!listener)
28 return;
29
30 if (object->IsActive()) {
31 /* Sync object config */
32 listener->UpdateConfigObject(object, cookie);
33 } else if (!object->IsActive() && object->GetExtension("ConfigObjectDeleted")) {
34 /* Delete object */
35 listener->DeleteConfigObject(object, cookie);
36 }
37 }
38
ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr & origin,const Dictionary::Ptr & params)39 Value ApiListener::ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
40 {
41 Log(LogNotice, "ApiListener")
42 << "Received config update for object: " << JsonEncode(params);
43
44 /* check permissions */
45 ApiListener::Ptr listener = ApiListener::GetInstance();
46
47 if (!listener)
48 return Empty;
49
50 String objType = params->Get("type");
51 String objName = params->Get("name");
52
53 Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
54
55 String identity = origin->FromClient->GetIdentity();
56
57 /* discard messages if the client is not configured on this node */
58 if (!endpoint) {
59 Log(LogNotice, "ApiListener")
60 << "Discarding 'config update object' message from '" << identity << "': Invalid endpoint origin (client not allowed).";
61 return Empty;
62 }
63
64 Zone::Ptr endpointZone = endpoint->GetZone();
65
66 /* discard messages if the sender is in a child zone */
67 if (!Zone::GetLocalZone()->IsChildOf(endpointZone)) {
68 Log(LogNotice, "ApiListener")
69 << "Discarding 'config update object' message"
70 << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
71 << " for object '" << objName << "' of type '" << objType << "'. Sender is in a child zone.";
72 return Empty;
73 }
74
75 String objZone = params->Get("zone");
76
77 if (!objZone.IsEmpty() && !Zone::GetByName(objZone)) {
78 Log(LogNotice, "ApiListener")
79 << "Discarding 'config update object' message"
80 << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
81 << " for object '" << objName << "' of type '" << objType << "'. Objects zone '" << objZone << "' isn't known locally.";
82 return Empty;
83 }
84
85 /* ignore messages if the endpoint does not accept config */
86 if (!listener->GetAcceptConfig()) {
87 Log(LogWarning, "ApiListener")
88 << "Ignoring config update"
89 << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
90 << " for object '" << objName << "' of type '" << objType << "'. '" << listener->GetName() << "' does not accept config.";
91 return Empty;
92 }
93
94 /* update the object */
95 double objVersion = params->Get("version");
96
97 Type::Ptr ptype = Type::GetByName(objType);
98 auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
99
100 if (!ctype) {
101 // This never happens with icinga cluster endpoints, only with development errors.
102 Log(LogCritical, "ApiListener")
103 << "Config type '" << objType << "' does not exist.";
104 return Empty;
105 }
106
107 ConfigObject::Ptr object = ctype->GetObject(objName);
108
109 String config = params->Get("config");
110
111 bool newObject = false;
112
113 if (!object && !config.IsEmpty()) {
114 newObject = true;
115
116 /* object does not exist, create it through the API */
117 Array::Ptr errors = new Array();
118
119 /*
120 * Create the config object through our internal API.
121 * IMPORTANT: Pass the origin to prevent cluster sync loops.
122 */
123 if (!ConfigObjectUtility::CreateObject(ptype, objName, config, errors, nullptr, origin)) {
124 Log(LogCritical, "ApiListener")
125 << "Could not create object '" << objName << "':";
126
127 ObjectLock olock(errors);
128 for (const String& error : errors) {
129 Log(LogCritical, "ApiListener", error);
130 }
131
132 return Empty;
133 }
134
135 object = ctype->GetObject(objName);
136
137 if (!object)
138 return Empty;
139
140 /* object was created, update its version */
141 object->SetVersion(objVersion, false, origin);
142 }
143
144 if (!object)
145 return Empty;
146
147 /* update object attributes if version was changed or if this is a new object */
148 if (newObject || objVersion <= object->GetVersion()) {
149 Log(LogNotice, "ApiListener")
150 << "Discarding config update"
151 << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
152 << " for object '" << object->GetName()
153 << "': Object version " << std::fixed << object->GetVersion()
154 << " is more recent than the received version " << std::fixed << objVersion << ".";
155
156 return Empty;
157 }
158
159 Log(LogNotice, "ApiListener")
160 << "Processing config update"
161 << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
162 << " for object '" << object->GetName()
163 << "': Object version " << object->GetVersion()
164 << " is older than the received version " << objVersion << ".";
165
166 Dictionary::Ptr modified_attributes = params->Get("modified_attributes");
167
168 if (modified_attributes) {
169 ObjectLock olock(modified_attributes);
170 for (const Dictionary::Pair& kv : modified_attributes) {
171 /* update all modified attributes
172 * but do not update the object version yet.
173 * This triggers cluster events otherwise.
174 */
175 object->ModifyAttribute(kv.first, kv.second, false);
176 }
177 }
178
179 /* check whether original attributes changed and restore them locally */
180 Array::Ptr newOriginalAttributes = params->Get("original_attributes");
181 Dictionary::Ptr objOriginalAttributes = object->GetOriginalAttributes();
182
183 if (newOriginalAttributes && objOriginalAttributes) {
184 std::vector<String> restoreAttrs;
185
186 {
187 ObjectLock xlock(objOriginalAttributes);
188 for (const Dictionary::Pair& kv : objOriginalAttributes) {
189 /* original attribute was removed, restore it */
190 if (!newOriginalAttributes->Contains(kv.first))
191 restoreAttrs.push_back(kv.first);
192 }
193 }
194
195 for (const String& key : restoreAttrs) {
196 /* do not update the object version yet. */
197 object->RestoreAttribute(key, false);
198 }
199 }
200
201 /* keep the object version in sync with the sender */
202 object->SetVersion(objVersion, false, origin);
203
204 return Empty;
205 }
206
ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr & origin,const Dictionary::Ptr & params)207 Value ApiListener::ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
208 {
209 Log(LogNotice, "ApiListener")
210 << "Received config delete for object: " << JsonEncode(params);
211
212 /* check permissions */
213 ApiListener::Ptr listener = ApiListener::GetInstance();
214
215 if (!listener)
216 return Empty;
217
218 String objType = params->Get("type");
219 String objName = params->Get("name");
220
221 Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
222
223 String identity = origin->FromClient->GetIdentity();
224
225 if (!endpoint) {
226 Log(LogNotice, "ApiListener")
227 << "Discarding 'config delete object' message from '" << identity << "': Invalid endpoint origin (client not allowed).";
228 return Empty;
229 }
230
231 Zone::Ptr endpointZone = endpoint->GetZone();
232
233 /* discard messages if the sender is in a child zone */
234 if (!Zone::GetLocalZone()->IsChildOf(endpointZone)) {
235 Log(LogNotice, "ApiListener")
236 << "Discarding 'config delete object' message"
237 << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
238 << " for object '" << objName << "' of type '" << objType << "'. Sender is in a child zone.";
239 return Empty;
240 }
241
242 if (!listener->GetAcceptConfig()) {
243 Log(LogWarning, "ApiListener")
244 << "Ignoring config delete"
245 << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
246 << " for object '" << objName << "' of type '" << objType << "'. '" << listener->GetName() << "' does not accept config.";
247 return Empty;
248 }
249
250 /* delete the object */
251 Type::Ptr ptype = Type::GetByName(objType);
252 auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
253
254 if (!ctype) {
255 // This never happens with icinga cluster endpoints, only with development errors.
256 Log(LogCritical, "ApiListener")
257 << "Config type '" << objType << "' does not exist.";
258 return Empty;
259 }
260
261 ConfigObject::Ptr object = ctype->GetObject(objName);
262
263 if (!object) {
264 Log(LogNotice, "ApiListener")
265 << "Could not delete non-existent object '" << objName << "' with type '" << params->Get("type") << "'.";
266 return Empty;
267 }
268
269 if (object->GetPackage() != "_api") {
270 Log(LogCritical, "ApiListener")
271 << "Could not delete object '" << objName << "': Not created by the API.";
272 return Empty;
273 }
274
275 Log(LogNotice, "ApiListener")
276 << "Processing config delete"
277 << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
278 << " for object '" << object->GetName() << "'.";
279
280 Array::Ptr errors = new Array();
281
282 /*
283 * Delete the config object through our internal API.
284 * IMPORTANT: Pass the origin to prevent cluster sync loops.
285 */
286 if (!ConfigObjectUtility::DeleteObject(object, true, errors, nullptr, origin)) {
287 Log(LogCritical, "ApiListener", "Could not delete object:");
288
289 ObjectLock olock(errors);
290 for (const String& error : errors) {
291 Log(LogCritical, "ApiListener", error);
292 }
293 }
294
295 return Empty;
296 }
297
UpdateConfigObject(const ConfigObject::Ptr & object,const MessageOrigin::Ptr & origin,const JsonRpcConnection::Ptr & client)298 void ApiListener::UpdateConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
299 const JsonRpcConnection::Ptr& client)
300 {
301 /* only send objects to zones which have access to the object */
302 if (client) {
303 Zone::Ptr target_zone = client->GetEndpoint()->GetZone();
304
305 if (target_zone && !target_zone->CanAccessObject(object)) {
306 Log(LogDebug, "ApiListener")
307 << "Not sending 'update config' message to unauthorized zone '" << target_zone->GetName() << "'"
308 << " for object: '" << object->GetName() << "'.";
309
310 return;
311 }
312 }
313
314 if (object->GetPackage() != "_api" && object->GetVersion() == 0)
315 return;
316
317 Dictionary::Ptr params = new Dictionary();
318
319 Dictionary::Ptr message = new Dictionary({
320 { "jsonrpc", "2.0" },
321 { "method", "config::UpdateObject" },
322 { "params", params }
323 });
324
325 params->Set("name", object->GetName());
326 params->Set("type", object->GetReflectionType()->GetName());
327 params->Set("version", object->GetVersion());
328
329 String zoneName = object->GetZoneName();
330
331 if (!zoneName.IsEmpty())
332 params->Set("zone", zoneName);
333
334 if (object->GetPackage() == "_api") {
335 String file;
336
337 try {
338 file = ConfigObjectUtility::GetObjectConfigPath(object->GetReflectionType(), object->GetName());
339 } catch (const std::exception& ex) {
340 Log(LogNotice, "ApiListener")
341 << "Cannot sync object '" << object->GetName() << "': " << ex.what();
342 return;
343 }
344
345 std::ifstream fp(file.CStr(), std::ifstream::binary);
346 if (!fp)
347 return;
348
349 String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
350 params->Set("config", content);
351 }
352
353 Dictionary::Ptr original_attributes = object->GetOriginalAttributes();
354 Dictionary::Ptr modified_attributes = new Dictionary();
355 ArrayData newOriginalAttributes;
356
357 if (original_attributes) {
358 ObjectLock olock(original_attributes);
359 for (const Dictionary::Pair& kv : original_attributes) {
360 std::vector<String> tokens = kv.first.Split(".");
361
362 Value value = object;
363 for (const String& token : tokens) {
364 value = VMOps::GetField(value, token);
365 }
366
367 modified_attributes->Set(kv.first, value);
368
369 newOriginalAttributes.push_back(kv.first);
370 }
371 }
372
373 params->Set("modified_attributes", modified_attributes);
374
375 /* only send the original attribute keys */
376 params->Set("original_attributes", new Array(std::move(newOriginalAttributes)));
377
378 #ifdef I2_DEBUG
379 Log(LogDebug, "ApiListener")
380 << "Sent update for object '" << object->GetName() << "': " << JsonEncode(params);
381 #endif /* I2_DEBUG */
382
383 if (client)
384 client->SendMessage(message);
385 else {
386 Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone());
387
388 if (!target)
389 target = Zone::GetLocalZone();
390
391 RelayMessage(origin, target, message, false);
392 }
393 }
394
395
DeleteConfigObject(const ConfigObject::Ptr & object,const MessageOrigin::Ptr & origin,const JsonRpcConnection::Ptr & client)396 void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
397 const JsonRpcConnection::Ptr& client)
398 {
399 if (object->GetPackage() != "_api")
400 return;
401
402 /* only send objects to zones which have access to the object */
403 if (client) {
404 Zone::Ptr target_zone = client->GetEndpoint()->GetZone();
405
406 if (target_zone && !target_zone->CanAccessObject(object)) {
407 Log(LogDebug, "ApiListener")
408 << "Not sending 'delete config' message to unauthorized zone '" << target_zone->GetName() << "'"
409 << " for object: '" << object->GetName() << "'.";
410
411 return;
412 }
413 }
414
415 Dictionary::Ptr params = new Dictionary();
416
417 Dictionary::Ptr message = new Dictionary({
418 { "jsonrpc", "2.0" },
419 { "method", "config::DeleteObject" },
420 { "params", params }
421 });
422
423 params->Set("name", object->GetName());
424 params->Set("type", object->GetReflectionType()->GetName());
425 params->Set("version", object->GetVersion());
426
427
428 #ifdef I2_DEBUG
429 Log(LogDebug, "ApiListener")
430 << "Sent delete for object '" << object->GetName() << "': " << JsonEncode(params);
431 #endif /* I2_DEBUG */
432
433 if (client)
434 client->SendMessage(message);
435 else {
436 Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone());
437
438 if (!target)
439 target = Zone::GetLocalZone();
440
441 RelayMessage(origin, target, message, true);
442 }
443 }
444
445 /* Initial sync on connect for new endpoints */
SendRuntimeConfigObjects(const JsonRpcConnection::Ptr & aclient)446 void ApiListener::SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient)
447 {
448 Endpoint::Ptr endpoint = aclient->GetEndpoint();
449 ASSERT(endpoint);
450
451 Zone::Ptr azone = endpoint->GetZone();
452
453 Log(LogInformation, "ApiListener")
454 << "Syncing runtime objects to endpoint '" << endpoint->GetName() << "'.";
455
456 for (const Type::Ptr& type : Type::GetAllTypes()) {
457 auto *dtype = dynamic_cast<ConfigType *>(type.get());
458
459 if (!dtype)
460 continue;
461
462 for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
463 /* don't sync objects for non-matching parent-child zones */
464 if (!azone->CanAccessObject(object))
465 continue;
466
467 /* send the config object to the connected client */
468 UpdateConfigObject(object, nullptr, aclient);
469 }
470 }
471
472 Log(LogInformation, "ApiListener")
473 << "Finished syncing runtime objects to endpoint '" << endpoint->GetName() << "'.";
474 }
475