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