2015-08-20 16:43:03 +02:00
/******************************************************************************
* Icinga 2 *
2018-01-02 12:06:00 +01:00
* Copyright ( C ) 2012 - 2018 Icinga Development Team ( https : //www.icinga.com/) *
2015-08-20 16:43:03 +02:00
* *
* This program is free software ; you can redistribute it and / or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation ; either version 2 *
* of the License , or ( at your option ) any later version . *
* *
* This program is distributed in the hope that it will be useful , *
* but WITHOUT ANY WARRANTY ; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the *
* GNU General Public License for more details . *
* *
* You should have received a copy of the GNU General Public License *
* along with this program ; if not , write to the Free Software Foundation *
* Inc . , 51 Franklin St , Fifth Floor , Boston , MA 02110 - 1301 , USA . *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
# include "remote/apilistener.hpp"
# include "remote/apifunction.hpp"
# include "remote/configobjectutility.hpp"
# include "remote/jsonrpc.hpp"
# include "base/configtype.hpp"
# include "base/json.hpp"
2015-09-11 14:09:46 +02:00
# include "base/convert.hpp"
2015-09-29 14:21:57 +02:00
# include "config/vmops.hpp"
# include <boost/algorithm/string/split.hpp>
# include <boost/algorithm/string/classification.hpp>
2015-08-20 16:43:03 +02:00
# include <fstream>
using namespace icinga ;
REGISTER_APIFUNCTION ( UpdateObject , config , & ApiListener : : ConfigUpdateObjectAPIHandler ) ;
REGISTER_APIFUNCTION ( DeleteObject , config , & ApiListener : : ConfigDeleteObjectAPIHandler ) ;
2016-08-27 09:35:08 +02:00
INITIALIZE_ONCE ( [ ] ( ) {
2015-08-20 16:43:03 +02:00
ConfigObject : : OnActiveChanged . connect ( & ApiListener : : ConfigUpdateObjectHandler ) ;
ConfigObject : : OnVersionChanged . connect ( & ApiListener : : ConfigUpdateObjectHandler ) ;
2016-08-27 09:35:08 +02:00
} ) ;
2015-08-20 16:43:03 +02:00
void ApiListener : : ConfigUpdateObjectHandler ( const ConfigObject : : Ptr & object , const Value & cookie )
{
ApiListener : : Ptr listener = ApiListener : : GetInstance ( ) ;
2015-09-28 16:08:14 +02:00
if ( ! listener )
2015-08-20 16:43:03 +02:00
return ;
if ( object - > IsActive ( ) ) {
/* Sync object config */
listener - > UpdateConfigObject ( object , cookie ) ;
2015-09-15 17:56:34 +02:00
} else if ( ! object - > IsActive ( ) & & object - > GetExtension ( " ConfigObjectDeleted " ) ) {
2015-08-20 16:43:03 +02:00
/* Delete object */
2015-09-10 17:48:06 +02:00
listener - > DeleteConfigObject ( object , cookie ) ;
2015-08-20 16:43:03 +02:00
}
}
Value ApiListener : : ConfigUpdateObjectAPIHandler ( const MessageOrigin : : Ptr & origin , const Dictionary : : Ptr & params )
{
2015-09-17 13:54:09 +02:00
Log ( LogNotice , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Received update for object: " < < JsonEncode ( params ) ;
2015-08-20 16:43:03 +02:00
2015-09-10 16:54:05 +02:00
/* check permissions */
ApiListener : : Ptr listener = ApiListener : : GetInstance ( ) ;
2015-09-28 16:08:14 +02:00
if ( ! listener )
2015-09-10 16:54:05 +02:00
return Empty ;
2015-09-30 08:41:09 +02:00
String objType = params - > Get ( " type " ) ;
String objName = params - > Get ( " name " ) ;
2015-09-10 16:54:05 +02:00
Endpoint : : Ptr endpoint = origin - > FromClient - > GetEndpoint ( ) ;
2017-02-02 13:47:18 +01:00
/* discard messages if the client is not configured on this node */
2015-09-10 16:54:05 +02:00
if ( ! endpoint ) {
Log ( LogNotice , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Discarding 'config update object' message from ' " < < origin - > FromClient - > GetIdentity ( ) < < " ': Invalid endpoint origin (client not allowed). " ;
2015-09-10 16:54:05 +02:00
return Empty ;
}
2015-10-15 14:05:39 +02:00
/* discard messages if the sender is in a child zone */
if ( ! Zone : : GetLocalZone ( ) - > IsChildOf ( endpoint - > GetZone ( ) ) ) {
2017-02-02 13:47:18 +01:00
Log ( LogNotice , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Discarding 'config update object' message from ' "
< < origin - > FromClient - > GetIdentity ( ) < < " ' for object ' "
< < objName < < " ' of type ' " < < objType < < " '. Sender is in a child zone. " ;
2017-02-02 13:47:18 +01:00
return Empty ;
}
/* ignore messages if the endpoint does not accept config */
if ( ! listener - > GetAcceptConfig ( ) ) {
Log ( LogWarning , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Ignoring config update from ' " < < origin - > FromClient - > GetIdentity ( ) < < " ' for object ' " < < objName < < " ' of type ' " < < objType < < " '. ' " < < listener - > GetName ( ) < < " ' does not accept config. " ;
2015-09-17 13:54:09 +02:00
return Empty ;
}
2015-09-10 16:54:05 +02:00
/* update the object */
2015-10-15 11:10:46 +02:00
double objVersion = params - > Get ( " version " ) ;
2015-09-11 14:09:46 +02:00
2016-08-16 11:02:10 +02:00
Type : : Ptr ptype = Type : : GetByName ( objType ) ;
2018-01-04 09:07:03 +01:00
auto * ctype = dynamic_cast < ConfigType * > ( ptype . get ( ) ) ;
2015-08-20 16:43:03 +02:00
2016-08-16 11:02:10 +02:00
if ( ! ctype ) {
2015-08-20 16:43:03 +02:00
Log ( LogCritical , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Config type ' " < < objType < < " ' does not exist. " ;
2015-08-20 16:43:03 +02:00
return Empty ;
}
2016-08-16 11:02:10 +02:00
ConfigObject : : Ptr object = ctype - > GetObject ( objName ) ;
2015-08-20 16:43:03 +02:00
2015-09-29 14:21:57 +02:00
String config = params - > Get ( " config " ) ;
2016-10-11 10:53:51 +02:00
bool newObject = false ;
2015-09-29 14:21:57 +02:00
if ( ! object & & ! config . IsEmpty ( ) ) {
2016-10-11 10:53:51 +02:00
newObject = true ;
2015-09-11 14:09:46 +02:00
/* object does not exist, create it through the API */
2015-08-20 16:43:03 +02:00
Array : : Ptr errors = new Array ( ) ;
2015-09-29 14:21:57 +02:00
2016-08-16 11:02:10 +02:00
if ( ! ConfigObjectUtility : : CreateObject ( ptype ,
2017-12-19 15:50:05 +01:00
objName , config , errors ) ) {
2015-09-11 14:09:46 +02:00
Log ( LogCritical , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Could not create object ' " < < objName < < " ': " ;
2015-08-20 16:43:03 +02:00
2017-12-19 15:50:05 +01:00
ObjectLock olock ( errors ) ;
2016-08-25 06:19:44 +02:00
for ( const String & error : errors ) {
2017-12-19 15:50:05 +01:00
Log ( LogCritical , " ApiListener " , error ) ;
}
2015-09-11 14:09:46 +02:00
2015-09-10 16:54:05 +02:00
return Empty ;
}
2015-09-29 14:21:57 +02:00
2016-08-16 11:02:10 +02:00
object = ctype - > GetObject ( objName ) ;
2016-02-09 09:13:43 +01:00
2016-02-12 14:11:50 +01:00
if ( ! object )
return Empty ;
/* object was created, update its version */
object - > SetVersion ( objVersion , false , origin ) ;
2015-09-29 14:21:57 +02:00
}
2015-09-30 10:04:37 +02:00
if ( ! object )
return Empty ;
2015-09-29 14:21:57 +02:00
2016-10-11 10:53:51 +02:00
/* update object attributes if version was changed or if this is a new object */
if ( newObject | | objVersion < = object - > GetVersion ( ) ) {
2015-09-29 14:21:57 +02:00
Log ( LogNotice , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Discarding config update for object ' " < < object - > GetName ( )
< < " ': Object version " < < std : : fixed < < object - > GetVersion ( )
< < " is more recent than the received version " < < std : : fixed < < objVersion < < " . " ;
2015-09-30 10:04:37 +02:00
2015-09-29 14:21:57 +02:00
return Empty ;
2015-08-20 16:43:03 +02:00
}
2015-09-30 10:04:37 +02:00
Log ( LogNotice , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Processing config update for object ' " < < object - > GetName ( )
< < " ': Object version " < < object - > GetVersion ( )
< < " is older than the received version " < < objVersion < < " . " ;
2015-09-30 10:04:37 +02:00
Dictionary : : Ptr modified_attributes = params - > Get ( " modified_attributes " ) ;
if ( modified_attributes ) {
ObjectLock olock ( modified_attributes ) ;
2016-08-25 06:19:44 +02:00
for ( const Dictionary : : Pair & kv : modified_attributes ) {
2015-09-30 10:04:37 +02:00
/* update all modified attributes
* but do not update the object version yet .
* This triggers cluster events otherwise .
*/
object - > ModifyAttribute ( kv . first , kv . second , false ) ;
}
}
2015-10-22 14:32:14 +02:00
/* check whether original attributes changed and restore them locally */
Array : : Ptr newOriginalAttributes = params - > Get ( " original_attributes " ) ;
Dictionary : : Ptr objOriginalAttributes = object - > GetOriginalAttributes ( ) ;
if ( newOriginalAttributes & & objOriginalAttributes ) {
std : : vector < String > restoreAttrs ;
{
ObjectLock xlock ( objOriginalAttributes ) ;
2016-08-25 06:19:44 +02:00
for ( const Dictionary : : Pair & kv : objOriginalAttributes ) {
2015-10-22 14:32:14 +02:00
/* original attribute was removed, restore it */
if ( ! newOriginalAttributes - > Contains ( kv . first ) )
restoreAttrs . push_back ( kv . first ) ;
}
}
2016-08-25 06:19:44 +02:00
for ( const String & key : restoreAttrs ) {
2015-10-22 14:32:14 +02:00
/* do not update the object version yet. */
object - > RestoreAttribute ( key , false ) ;
}
}
2015-09-30 10:04:37 +02:00
/* keep the object version in sync with the sender */
object - > SetVersion ( objVersion , false , origin ) ;
2015-08-20 16:43:03 +02:00
return Empty ;
}
Value ApiListener : : ConfigDeleteObjectAPIHandler ( const MessageOrigin : : Ptr & origin , const Dictionary : : Ptr & params )
{
2015-09-17 13:54:09 +02:00
Log ( LogNotice , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Received update for object: " < < JsonEncode ( params ) ;
2015-09-10 17:48:06 +02:00
/* check permissions */
ApiListener : : Ptr listener = ApiListener : : GetInstance ( ) ;
2015-09-28 16:08:14 +02:00
if ( ! listener )
2015-09-10 17:48:06 +02:00
return Empty ;
if ( ! listener - > GetAcceptConfig ( ) ) {
Log ( LogWarning , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Ignoring config update. ' " < < listener - > GetName ( ) < < " ' does not accept config. " ;
2015-09-10 17:48:06 +02:00
return Empty ;
}
Endpoint : : Ptr endpoint = origin - > FromClient - > GetEndpoint ( ) ;
if ( ! endpoint ) {
Log ( LogNotice , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Discarding 'config update object' message from ' " < < origin - > FromClient - > GetIdentity ( ) < < " ': Invalid endpoint origin (client not allowed). " ;
2015-09-10 17:48:06 +02:00
return Empty ;
}
2015-10-15 14:05:39 +02:00
/* discard messages if the sender is in a child zone */
if ( ! Zone : : GetLocalZone ( ) - > IsChildOf ( endpoint - > GetZone ( ) ) ) {
Log ( LogWarning , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Discarding 'config update object' message from ' "
< < origin - > FromClient - > GetIdentity ( ) < < " '. " ;
2015-09-17 13:54:09 +02:00
return Empty ;
}
2015-09-10 17:48:06 +02:00
/* delete the object */
2016-08-16 11:02:10 +02:00
Type : : Ptr ptype = Type : : GetByName ( params - > Get ( " type " ) ) ;
2018-01-04 09:07:03 +01:00
auto * ctype = dynamic_cast < ConfigType * > ( ptype . get ( ) ) ;
2015-09-10 17:48:06 +02:00
2016-08-16 11:02:10 +02:00
if ( ! ctype ) {
2015-09-10 17:48:06 +02:00
Log ( LogCritical , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Config type ' " < < params - > Get ( " type " ) < < " ' does not exist. " ;
2015-09-10 17:48:06 +02:00
return Empty ;
}
2016-08-16 11:02:10 +02:00
ConfigObject : : Ptr object = ctype - > GetObject ( params - > Get ( " name " ) ) ;
2015-09-10 17:48:06 +02:00
2015-09-29 14:21:57 +02:00
if ( ! object ) {
Log ( LogNotice , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Could not delete non-existent object ' " < < params - > Get ( " name " ) < < " ' with type ' " < < params - > Get ( " type " ) < < " '. " ;
2015-09-29 14:21:57 +02:00
return Empty ;
}
2015-09-10 17:48:06 +02:00
2015-09-29 14:21:57 +02:00
if ( object - > GetPackage ( ) ! = " _api " ) {
Log ( LogCritical , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Could not delete object ' " < < object - > GetName ( ) < < " ': Not created by the API. " ;
2015-09-29 14:21:57 +02:00
return Empty ;
}
2015-09-10 17:48:06 +02:00
2015-09-29 14:21:57 +02:00
Array : : Ptr errors = new Array ( ) ;
if ( ! ConfigObjectUtility : : DeleteObject ( object , true , errors ) ) {
Log ( LogCritical , " ApiListener " , " Could not delete object: " ) ;
ObjectLock olock ( errors ) ;
2016-08-25 06:19:44 +02:00
for ( const String & error : errors ) {
2015-09-29 14:21:57 +02:00
Log ( LogCritical , " ApiListener " , error ) ;
2015-09-10 17:48:06 +02:00
}
}
2015-08-20 16:43:03 +02:00
return Empty ;
}
void ApiListener : : UpdateConfigObject ( const ConfigObject : : Ptr & object , const MessageOrigin : : Ptr & origin ,
2017-12-19 15:50:05 +01:00
const JsonRpcConnection : : Ptr & client )
2015-08-20 16:43:03 +02:00
{
2015-09-30 15:07:20 +02:00
/* only send objects to zones which have access to the object */
if ( client ) {
Zone : : Ptr target_zone = client - > GetEndpoint ( ) - > GetZone ( ) ;
if ( target_zone & & ! target_zone - > CanAccessObject ( object ) ) {
Log ( LogDebug , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Not sending 'update config' message to unauthorized zone ' " < < target_zone - > GetName ( ) < < " ' "
< < " for object: ' " < < object - > GetName ( ) < < " '. " ;
2015-09-30 15:07:20 +02:00
return ;
}
}
2015-10-15 14:05:39 +02:00
2015-11-08 21:22:06 +01:00
if ( object - > GetPackage ( ) ! = " _api " & & object - > GetVersion ( ) = = 0 )
return ;
2015-08-20 16:43:03 +02:00
Dictionary : : Ptr params = new Dictionary ( ) ;
2018-01-11 11:17:38 +01:00
Dictionary : : Ptr message = new Dictionary ( {
{ " jsonrpc " , " 2.0 " } ,
{ " method " , " config::UpdateObject " } ,
{ " params " , params }
} ) ;
2015-08-20 16:43:03 +02:00
params - > Set ( " name " , object - > GetName ( ) ) ;
2016-08-15 14:39:33 +02:00
params - > Set ( " type " , object - > GetReflectionType ( ) - > GetName ( ) ) ;
2015-08-20 16:43:03 +02:00
params - > Set ( " version " , object - > GetVersion ( ) ) ;
2015-09-29 14:21:57 +02:00
if ( object - > GetPackage ( ) = = " _api " ) {
String file = ConfigObjectUtility : : GetObjectConfigPath ( object - > GetReflectionType ( ) , object - > GetName ( ) ) ;
2015-08-20 16:43:03 +02:00
2015-09-29 14:21:57 +02:00
std : : ifstream fp ( file . CStr ( ) , std : : ifstream : : binary ) ;
if ( ! fp )
return ;
2015-08-20 16:43:03 +02:00
2015-09-29 14:21:57 +02:00
String content ( ( std : : istreambuf_iterator < char > ( fp ) ) , std : : istreambuf_iterator < char > ( ) ) ;
params - > Set ( " config " , content ) ;
}
2015-08-20 16:43:03 +02:00
Dictionary : : Ptr original_attributes = object - > GetOriginalAttributes ( ) ;
Dictionary : : Ptr modified_attributes = new Dictionary ( ) ;
2018-01-11 11:17:38 +01:00
ArrayData newOriginalAttributes ;
2015-08-20 16:43:03 +02:00
if ( original_attributes ) {
ObjectLock olock ( original_attributes ) ;
2016-08-25 06:19:44 +02:00
for ( const Dictionary : : Pair & kv : original_attributes ) {
2015-09-29 14:21:57 +02:00
std : : vector < String > tokens ;
boost : : algorithm : : split ( tokens , kv . first , boost : : is_any_of ( " . " ) ) ;
Value value = object ;
2016-08-25 06:19:44 +02:00
for ( const String & token : tokens ) {
2015-09-29 14:21:57 +02:00
value = VMOps : : GetField ( value , token ) ;
}
2015-08-20 16:43:03 +02:00
modified_attributes - > Set ( kv . first , value ) ;
2015-10-22 14:32:14 +02:00
2018-01-11 11:17:38 +01:00
newOriginalAttributes . push_back ( kv . first ) ;
2015-08-20 16:43:03 +02:00
}
}
params - > Set ( " modified_attributes " , modified_attributes ) ;
2015-10-22 14:32:14 +02:00
/* only send the original attribute keys */
2018-01-11 11:17:38 +01:00
params - > Set ( " original_attributes " , new Array ( std : : move ( newOriginalAttributes ) ) ) ;
2015-08-20 16:43:03 +02:00
2015-09-17 13:54:09 +02:00
# ifdef I2_DEBUG
Log ( LogDebug , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Sent update for object ' " < < object - > GetName ( ) < < " ': " < < JsonEncode ( params ) ;
2015-09-17 13:54:09 +02:00
# endif /* I2_DEBUG */
2015-08-20 16:43:03 +02:00
if ( client )
JsonRpc : : SendMessage ( client - > GetStream ( ) , message ) ;
2016-10-11 10:53:51 +02:00
else {
Zone : : Ptr target = static_pointer_cast < Zone > ( object - > GetZone ( ) ) ;
if ( ! target )
target = Zone : : GetLocalZone ( ) ;
2016-11-10 17:15:06 +01:00
RelayMessage ( origin , target , message , false ) ;
2016-10-11 10:53:51 +02:00
}
2015-08-20 16:43:03 +02:00
}
2015-09-10 17:48:06 +02:00
void ApiListener : : DeleteConfigObject ( const ConfigObject : : Ptr & object , const MessageOrigin : : Ptr & origin ,
2017-12-19 15:50:05 +01:00
const JsonRpcConnection : : Ptr & client )
2015-09-10 17:48:06 +02:00
{
if ( object - > GetPackage ( ) ! = " _api " )
return ;
2015-09-30 15:07:20 +02:00
/* only send objects to zones which have access to the object */
if ( client ) {
Zone : : Ptr target_zone = client - > GetEndpoint ( ) - > GetZone ( ) ;
if ( target_zone & & ! target_zone - > CanAccessObject ( object ) ) {
Log ( LogDebug , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Not sending 'delete config' message to unauthorized zone ' " < < target_zone - > GetName ( ) < < " ' "
< < " for object: ' " < < object - > GetName ( ) < < " '. " ;
2015-09-30 15:07:20 +02:00
return ;
}
}
2015-09-10 17:48:06 +02:00
Dictionary : : Ptr params = new Dictionary ( ) ;
2018-01-11 11:17:38 +01:00
Dictionary : : Ptr message = new Dictionary ( {
{ " jsonrpc " , " 2.0 " } ,
{ " method " , " config::DeleteObject " } ,
{ " params " , params }
} ) ;
2015-09-10 17:48:06 +02:00
params - > Set ( " name " , object - > GetName ( ) ) ;
2016-08-15 14:39:33 +02:00
params - > Set ( " type " , object - > GetReflectionType ( ) - > GetName ( ) ) ;
2015-09-10 17:48:06 +02:00
params - > Set ( " version " , object - > GetVersion ( ) ) ;
2015-09-17 13:54:09 +02:00
# ifdef I2_DEBUG
Log ( LogDebug , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Sent delete for object ' " < < object - > GetName ( ) < < " ': " < < JsonEncode ( params ) ;
2015-09-17 13:54:09 +02:00
# endif /* I2_DEBUG */
2015-09-10 17:48:06 +02:00
if ( client )
JsonRpc : : SendMessage ( client - > GetStream ( ) , message ) ;
2016-10-11 10:53:51 +02:00
else {
Zone : : Ptr target = static_pointer_cast < Zone > ( object - > GetZone ( ) ) ;
if ( ! target )
target = Zone : : GetLocalZone ( ) ;
RelayMessage ( origin , target , message , false ) ;
}
2015-09-10 17:48:06 +02:00
}
2015-09-15 16:09:56 +02:00
2015-09-17 13:54:09 +02:00
/* Initial sync on connect for new endpoints */
2015-09-15 16:09:56 +02:00
void ApiListener : : SendRuntimeConfigObjects ( const JsonRpcConnection : : Ptr & aclient )
{
Endpoint : : Ptr endpoint = aclient - > GetEndpoint ( ) ;
ASSERT ( endpoint ) ;
Zone : : Ptr azone = endpoint - > GetZone ( ) ;
Log ( LogInformation , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Syncing runtime objects to endpoint ' " < < endpoint - > GetName ( ) < < " '. " ;
2015-09-15 16:09:56 +02:00
2016-08-25 06:19:44 +02:00
for ( const Type : : Ptr & type : Type : : GetAllTypes ( ) ) {
2018-01-04 09:07:03 +01:00
auto * dtype = dynamic_cast < ConfigType * > ( type . get ( ) ) ;
2016-08-16 11:02:10 +02:00
if ( ! dtype )
continue ;
2016-08-25 06:19:44 +02:00
for ( const ConfigObject : : Ptr & object : dtype - > GetObjects ( ) ) {
2015-09-17 13:54:09 +02:00
/* don't sync objects for non-matching parent-child zones */
2015-10-15 14:05:39 +02:00
if ( ! azone - > CanAccessObject ( object ) )
2015-09-17 13:54:09 +02:00
continue ;
2015-09-15 16:09:56 +02:00
/* send the config object to the connected client */
2017-11-30 08:36:35 +01:00
UpdateConfigObject ( object , nullptr , aclient ) ;
2015-09-15 16:09:56 +02:00
}
}
2016-11-10 17:44:05 +01:00
Log ( LogInformation , " ApiListener " )
2017-12-19 15:50:05 +01:00
< < " Finished syncing runtime objects to endpoint ' " < < endpoint - > GetName ( ) < < " '. " ;
2015-09-15 16:09:56 +02:00
}