summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:32:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:32:39 +0000
commit56ae875861ab260b80a030f50c4aff9f9dc8fff0 (patch)
tree531412110fc901a5918c7f7442202804a83cada9 /lib
parentInitial commit. (diff)
downloadicinga2-56ae875861ab260b80a030f50c4aff9f9dc8fff0.tar.xz
icinga2-56ae875861ab260b80a030f50c4aff9f9dc8fff0.zip
Adding upstream version 2.14.2.upstream/2.14.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib')
-rw-r--r--lib/CMakeLists.txt60
-rw-r--r--lib/base/CMakeLists.txt160
-rw-r--r--lib/base/application-environment.cpp17
-rw-r--r--lib/base/application-version.cpp17
-rw-r--r--lib/base/application.cpp1238
-rw-r--r--lib/base/application.hpp170
-rw-r--r--lib/base/application.ti14
-rw-r--r--lib/base/array-script.cpp260
-rw-r--r--lib/base/array.cpp380
-rw-r--r--lib/base/array.hpp117
-rw-r--r--lib/base/atomic-file.cpp123
-rw-r--r--lib/base/atomic-file.hpp41
-rw-r--r--lib/base/atomic.hpp91
-rw-r--r--lib/base/base64.cpp53
-rw-r--r--lib/base/base64.hpp25
-rw-r--r--lib/base/boolean-script.cpp26
-rw-r--r--lib/base/boolean.cpp9
-rw-r--r--lib/base/boolean.hpp27
-rw-r--r--lib/base/bulker.hpp119
-rw-r--r--lib/base/configobject-script.cpp36
-rw-r--r--lib/base/configobject.cpp701
-rw-r--r--lib/base/configobject.hpp101
-rw-r--r--lib/base/configobject.ti94
-rw-r--r--lib/base/configtype.cpp76
-rw-r--r--lib/base/configtype.hpp64
-rw-r--r--lib/base/configuration.cpp379
-rw-r--r--lib/base/configuration.hpp157
-rw-r--r--lib/base/configuration.ti164
-rw-r--r--lib/base/configwriter.cpp260
-rw-r--r--lib/base/configwriter.hpp67
-rw-r--r--lib/base/console.cpp203
-rw-r--r--lib/base/console.hpp91
-rw-r--r--lib/base/context.cpp64
-rw-r--r--lib/base/context.hpp54
-rw-r--r--lib/base/convert.cpp46
-rw-r--r--lib/base/convert.hpp84
-rw-r--r--lib/base/datetime-script.cpp28
-rw-r--r--lib/base/datetime.cpp58
-rw-r--r--lib/base/datetime.hpp40
-rw-r--r--lib/base/datetime.ti15
-rw-r--r--lib/base/debug.hpp49
-rw-r--r--lib/base/debuginfo.cpp98
-rw-r--r--lib/base/debuginfo.hpp36
-rw-r--r--lib/base/defer.hpp54
-rw-r--r--lib/base/dependencygraph.cpp50
-rw-r--r--lib/base/dependencygraph.hpp34
-rw-r--r--lib/base/dictionary-script.cpp119
-rw-r--r--lib/base/dictionary.cpp317
-rw-r--r--lib/base/dictionary.hpp91
-rw-r--r--lib/base/exception.cpp507
-rw-r--r--lib/base/exception.hpp166
-rw-r--r--lib/base/fifo.cpp124
-rw-r--r--lib/base/fifo.hpp48
-rw-r--r--lib/base/filelogger.cpp59
-rw-r--r--lib/base/filelogger.hpp33
-rw-r--r--lib/base/filelogger.ti17
-rw-r--r--lib/base/function-script.cpp50
-rw-r--r--lib/base/function.cpp37
-rw-r--r--lib/base/function.hpp89
-rw-r--r--lib/base/function.ti18
-rw-r--r--lib/base/functionwrapper.hpp149
-rw-r--r--lib/base/i2-base.hpp79
-rw-r--r--lib/base/initialize.cpp13
-rw-r--r--lib/base/initialize.hpp49
-rw-r--r--lib/base/io-engine.cpp155
-rw-r--r--lib/base/io-engine.hpp216
-rw-r--r--lib/base/journaldlogger.cpp87
-rw-r--r--lib/base/journaldlogger.hpp44
-rw-r--r--lib/base/journaldlogger.ti21
-rw-r--r--lib/base/json-script.cpp28
-rw-r--r--lib/base/json.cpp525
-rw-r--r--lib/base/json.hpp19
-rw-r--r--lib/base/lazy-init.hpp72
-rw-r--r--lib/base/library.cpp68
-rw-r--r--lib/base/library.hpp41
-rw-r--r--lib/base/loader.cpp38
-rw-r--r--lib/base/loader.hpp61
-rw-r--r--lib/base/logger.cpp326
-rw-r--r--lib/base/logger.hpp149
-rw-r--r--lib/base/logger.ti17
-rw-r--r--lib/base/math-script.cpp184
-rw-r--r--lib/base/namespace-script.cpp84
-rw-r--r--lib/base/namespace.cpp189
-rw-r--r--lib/base/namespace.hpp105
-rw-r--r--lib/base/netstring.cpp334
-rw-r--r--lib/base/netstring.hpp43
-rw-r--r--lib/base/networkstream.cpp81
-rw-r--r--lib/base/networkstream.hpp39
-rw-r--r--lib/base/number-script.cpp25
-rw-r--r--lib/base/number.cpp9
-rw-r--r--lib/base/number.hpp27
-rw-r--r--lib/base/object-packer.cpp246
-rw-r--r--lib/base/object-packer.hpp18
-rw-r--r--lib/base/object-script.cpp45
-rw-r--r--lib/base/object.cpp275
-rw-r--r--lib/base/object.hpp225
-rw-r--r--lib/base/objectlock.cpp55
-rw-r--r--lib/base/objectlock.hpp35
-rw-r--r--lib/base/objecttype.cpp57
-rw-r--r--lib/base/objecttype.hpp29
-rw-r--r--lib/base/perfdatavalue.cpp395
-rw-r--r--lib/base/perfdatavalue.hpp38
-rw-r--r--lib/base/perfdatavalue.ti20
-rw-r--r--lib/base/primitivetype.cpp64
-rw-r--r--lib/base/primitivetype.hpp62
-rw-r--r--lib/base/process.cpp1207
-rw-r--r--lib/base/process.hpp117
-rw-r--r--lib/base/reference-script.cpp35
-rw-r--r--lib/base/reference.cpp38
-rw-r--r--lib/base/reference.hpp40
-rw-r--r--lib/base/registry.hpp121
-rw-r--r--lib/base/ringbuffer.cpp91
-rw-r--r--lib/base/ringbuffer.hpp45
-rw-r--r--lib/base/scriptframe.cpp130
-rw-r--r--lib/base/scriptframe.hpp42
-rw-r--r--lib/base/scriptglobal.cpp110
-rw-r--r--lib/base/scriptglobal.hpp35
-rw-r--r--lib/base/scriptutils.cpp570
-rw-r--r--lib/base/scriptutils.hpp54
-rw-r--r--lib/base/serializer.cpp331
-rw-r--r--lib/base/serializer.hpp34
-rw-r--r--lib/base/shared-memory.hpp45
-rw-r--r--lib/base/shared-object.hpp73
-rw-r--r--lib/base/shared.hpp101
-rw-r--r--lib/base/singleton.hpp29
-rw-r--r--lib/base/socket.cpp430
-rw-r--r--lib/base/socket.hpp66
-rw-r--r--lib/base/stacktrace.cpp43
-rw-r--r--lib/base/stacktrace.hpp31
-rw-r--r--lib/base/statsfunction.hpp17
-rw-r--r--lib/base/stdiostream.cpp57
-rw-r--r--lib/base/stdiostream.hpp36
-rw-r--r--lib/base/stream.cpp149
-rw-r--r--lib/base/stream.hpp133
-rw-r--r--lib/base/streamlogger.cpp119
-rw-r--r--lib/base/streamlogger.hpp47
-rw-r--r--lib/base/streamlogger.ti14
-rw-r--r--lib/base/string-script.cpp138
-rw-r--r--lib/base/string.cpp468
-rw-r--r--lib/base/string.hpp208
-rw-r--r--lib/base/sysloglogger.cpp144
-rw-r--r--lib/base/sysloglogger.hpp56
-rw-r--r--lib/base/sysloglogger.ti19
-rw-r--r--lib/base/tcpsocket.cpp211
-rw-r--r--lib/base/tcpsocket.hpp102
-rw-r--r--lib/base/threadpool.cpp51
-rw-r--r--lib/base/threadpool.hpp101
-rw-r--r--lib/base/timer.cpp354
-rw-r--r--lib/base/timer.hpp65
-rw-r--r--lib/base/tlsstream.cpp71
-rw-r--r--lib/base/tlsstream.hpp129
-rw-r--r--lib/base/tlsutility.cpp1086
-rw-r--r--lib/base/tlsutility.hpp94
-rw-r--r--lib/base/type.cpp217
-rw-r--r--lib/base/type.hpp148
-rw-r--r--lib/base/typetype-script.cpp31
-rw-r--r--lib/base/unix.hpp49
-rw-r--r--lib/base/unixsocket.cpp53
-rw-r--r--lib/base/unixsocket.hpp32
-rw-r--r--lib/base/utility.cpp1975
-rw-r--r--lib/base/utility.hpp200
-rw-r--r--lib/base/value-operators.cpp719
-rw-r--r--lib/base/value.cpp264
-rw-r--r--lib/base/value.hpp251
-rw-r--r--lib/base/win32.hpp35
-rw-r--r--lib/base/windowseventloglogger-provider.mc5
-rw-r--r--lib/base/windowseventloglogger.cpp83
-rw-r--r--lib/base/windowseventloglogger.hpp37
-rw-r--r--lib/base/windowseventloglogger.ti15
-rw-r--r--lib/base/workqueue.cpp318
-rw-r--r--lib/base/workqueue.hpp154
-rw-r--r--lib/checker/CMakeLists.txt34
-rw-r--r--lib/checker/checkercomponent.cpp358
-rw-r--r--lib/checker/checkercomponent.hpp99
-rw-r--r--lib/checker/checkercomponent.ti18
-rw-r--r--lib/cli/CMakeLists.txt49
-rw-r--r--lib/cli/apisetupcommand.cpp59
-rw-r--r--lib/cli/apisetupcommand.hpp31
-rw-r--r--lib/cli/apisetuputility.cpp205
-rw-r--r--lib/cli/apisetuputility.hpp39
-rw-r--r--lib/cli/calistcommand.cpp89
-rw-r--r--lib/cli/calistcommand.hpp33
-rw-r--r--lib/cli/caremovecommand.cpp93
-rw-r--r--lib/cli/caremovecommand.hpp30
-rw-r--r--lib/cli/carestorecommand.cpp88
-rw-r--r--lib/cli/carestorecommand.hpp30
-rw-r--r--lib/cli/casigncommand.cpp108
-rw-r--r--lib/cli/casigncommand.hpp30
-rw-r--r--lib/cli/clicommand.cpp373
-rw-r--r--lib/cli/clicommand.hpp79
-rw-r--r--lib/cli/consolecommand.cpp723
-rw-r--r--lib/cli/consolecommand.hpp61
-rw-r--r--lib/cli/daemoncommand.cpp882
-rw-r--r--lib/cli/daemoncommand.hpp31
-rw-r--r--lib/cli/daemonutility.cpp285
-rw-r--r--lib/cli/daemonutility.hpp27
-rw-r--r--lib/cli/editline.hpp19
-rw-r--r--lib/cli/featuredisablecommand.cpp55
-rw-r--r--lib/cli/featuredisablecommand.hpp33
-rw-r--r--lib/cli/featureenablecommand.cpp50
-rw-r--r--lib/cli/featureenablecommand.hpp32
-rw-r--r--lib/cli/featurelistcommand.cpp34
-rw-r--r--lib/cli/featurelistcommand.hpp28
-rw-r--r--lib/cli/featureutility.cpp243
-rw-r--r--lib/cli/featureutility.hpp42
-rw-r--r--lib/cli/i2-cli.hpp14
-rw-r--r--lib/cli/internalsignalcommand.cpp67
-rw-r--r--lib/cli/internalsignalcommand.hpp33
-rw-r--r--lib/cli/nodesetupcommand.cpp559
-rw-r--r--lib/cli/nodesetupcommand.hpp36
-rw-r--r--lib/cli/nodeutility.cpp378
-rw-r--r--lib/cli/nodeutility.hpp49
-rw-r--r--lib/cli/nodewizardcommand.cpp815
-rw-r--r--lib/cli/nodewizardcommand.hpp36
-rw-r--r--lib/cli/objectlistcommand.cpp145
-rw-r--r--lib/cli/objectlistcommand.hpp36
-rw-r--r--lib/cli/objectlistutility.cpp155
-rw-r--r--lib/cli/objectlistutility.hpp34
-rw-r--r--lib/cli/pkinewcacommand.cpp29
-rw-r--r--lib/cli/pkinewcacommand.hpp29
-rw-r--r--lib/cli/pkinewcertcommand.cpp66
-rw-r--r--lib/cli/pkinewcertcommand.hpp32
-rw-r--r--lib/cli/pkirequestcommand.cpp93
-rw-r--r--lib/cli/pkirequestcommand.hpp32
-rw-r--r--lib/cli/pkisavecertcommand.cpp89
-rw-r--r--lib/cli/pkisavecertcommand.hpp32
-rw-r--r--lib/cli/pkisigncsrcommand.cpp56
-rw-r--r--lib/cli/pkisigncsrcommand.hpp32
-rw-r--r--lib/cli/pkiticketcommand.cpp55
-rw-r--r--lib/cli/pkiticketcommand.hpp31
-rw-r--r--lib/cli/pkiverifycommand.cpp226
-rw-r--r--lib/cli/pkiverifycommand.hpp32
-rw-r--r--lib/cli/variablegetcommand.cpp75
-rw-r--r--lib/cli/variablegetcommand.hpp34
-rw-r--r--lib/cli/variablelistcommand.cpp52
-rw-r--r--lib/cli/variablelistcommand.hpp34
-rw-r--r--lib/cli/variableutility.cpp76
-rw-r--r--lib/cli/variableutility.hpp31
-rw-r--r--lib/compat/CMakeLists.txt38
-rw-r--r--lib/compat/compatlogger.cpp614
-rw-r--r--lib/compat/compatlogger.hpp60
-rw-r--r--lib/compat/compatlogger.ti23
-rw-r--r--lib/compat/externalcommandlistener.cpp150
-rw-r--r--lib/compat/externalcommandlistener.hpp41
-rw-r--r--lib/compat/externalcommandlistener.ti20
-rw-r--r--lib/config/CMakeLists.txt47
-rw-r--r--lib/config/activationcontext.cpp61
-rw-r--r--lib/config/activationcontext.hpp46
-rw-r--r--lib/config/applyrule-targeted.cpp266
-rw-r--r--lib/config/applyrule.cpp189
-rw-r--r--lib/config/applyrule.hpp126
-rw-r--r--lib/config/config_lexer.ll253
-rw-r--r--lib/config/config_parser.yy1243
-rw-r--r--lib/config/configcompiler.cpp364
-rw-r--r--lib/config/configcompiler.hpp161
-rw-r--r--lib/config/configcompilercontext.cpp57
-rw-r--r--lib/config/configcompilercontext.hpp42
-rw-r--r--lib/config/configfragment.hpp26
-rw-r--r--lib/config/configitem.cpp849
-rw-r--r--lib/config/configitem.hpp106
-rw-r--r--lib/config/configitembuilder.cpp120
-rw-r--r--lib/config/configitembuilder.hpp58
-rw-r--r--lib/config/expression.cpp1068
-rw-r--r--lib/config/expression.hpp986
-rw-r--r--lib/config/i2-config.hpp16
-rw-r--r--lib/config/objectrule.cpp18
-rw-r--r--lib/config/objectrule.hpp33
-rw-r--r--lib/config/vmops.hpp274
-rw-r--r--lib/db_ido/CMakeLists.txt40
-rw-r--r--lib/db_ido/commanddbobject.cpp31
-rw-r--r--lib/db_ido/commanddbobject.hpp30
-rw-r--r--lib/db_ido/db_ido-itl.conf19
-rw-r--r--lib/db_ido/dbconnection.cpp583
-rw-r--r--lib/db_ido/dbconnection.hpp138
-rw-r--r--lib/db_ido/dbconnection.ti82
-rw-r--r--lib/db_ido/dbevents.cpp1884
-rw-r--r--lib/db_ido/dbevents.hpp128
-rw-r--r--lib/db_ido/dbobject.cpp430
-rw-r--r--lib/db_ido/dbobject.hpp112
-rw-r--r--lib/db_ido/dbquery.cpp52
-rw-r--r--lib/db_ido/dbquery.hpp72
-rw-r--r--lib/db_ido/dbreference.cpp19
-rw-r--r--lib/db_ido/dbreference.hpp30
-rw-r--r--lib/db_ido/dbtype.cpp141
-rw-r--r--lib/db_ido/dbtype.hpp90
-rw-r--r--lib/db_ido/dbvalue.cpp69
-rw-r--r--lib/db_ido/dbvalue.hpp52
-rw-r--r--lib/db_ido/endpointdbobject.cpp91
-rw-r--r--lib/db_ido/endpointdbobject.hpp37
-rw-r--r--lib/db_ido/hostdbobject.cpp423
-rw-r--r--lib/db_ido/hostdbobject.hpp38
-rw-r--r--lib/db_ido/hostgroupdbobject.cpp33
-rw-r--r--lib/db_ido/hostgroupdbobject.hpp34
-rw-r--r--lib/db_ido/i2-db_ido.hpp14
-rw-r--r--lib/db_ido/idochecktask.cpp197
-rw-r--r--lib/db_ido/idochecktask.hpp29
-rw-r--r--lib/db_ido/servicedbobject.cpp359
-rw-r--r--lib/db_ido/servicedbobject.hpp41
-rw-r--r--lib/db_ido/servicegroupdbobject.cpp32
-rw-r--r--lib/db_ido/servicegroupdbobject.hpp31
-rw-r--r--lib/db_ido/timeperioddbobject.cpp85
-rw-r--r--lib/db_ido/timeperioddbobject.hpp33
-rw-r--r--lib/db_ido/userdbobject.cpp161
-rw-r--r--lib/db_ido/userdbobject.hpp35
-rw-r--r--lib/db_ido/usergroupdbobject.cpp30
-rw-r--r--lib/db_ido/usergroupdbobject.hpp31
-rw-r--r--lib/db_ido/zonedbobject.cpp38
-rw-r--r--lib/db_ido/zonedbobject.hpp31
-rw-r--r--lib/db_ido_mysql/CMakeLists.txt41
-rw-r--r--lib/db_ido_mysql/idomysqlconnection.cpp1269
-rw-r--r--lib/db_ido_mysql/idomysqlconnection.hpp114
-rw-r--r--lib/db_ido_mysql/idomysqlconnection.ti42
-rw-r--r--lib/db_ido_mysql/schema/mysql.sql1666
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.0.2.sql20
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.1.0.sql17
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.11.0.sql89
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.12.7.sql15
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.13.0.sql23
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.13.3.sql15
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.2.0.sql23
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.3.0.sql26
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.4.0.sql75
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.5.0.sql103
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.6.0.sql151
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.8.0.sql81
-rw-r--r--lib/db_ido_mysql/schema/upgrade/2.8.1.sql67
-rw-r--r--lib/db_ido_pgsql/CMakeLists.txt41
-rw-r--r--lib/db_ido_pgsql/idopgsqlconnection.cpp1029
-rw-r--r--lib/db_ido_pgsql/idopgsqlconnection.hpp99
-rw-r--r--lib/db_ido_pgsql/idopgsqlconnection.ti39
-rw-r--r--lib/db_ido_pgsql/schema/pgsql.sql1733
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.0.2.sql17
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.1.0.sql17
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.2.0.sql21
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.3.0.sql26
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.4.0.sql185
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.5.0.sql85
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.6.0.sql161
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.8.0.sql32
-rw-r--r--lib/db_ido_pgsql/schema/upgrade/2.8.1.sql19
-rw-r--r--lib/icinga/CMakeLists.txt76
-rw-r--r--lib/icinga/apiactions.cpp962
-rw-r--r--lib/icinga/apiactions.hpp42
-rw-r--r--lib/icinga/apievents.cpp438
-rw-r--r--lib/icinga/apievents.hpp51
-rw-r--r--lib/icinga/checkable-check.cpp709
-rw-r--r--lib/icinga/checkable-comment.cpp75
-rw-r--r--lib/icinga/checkable-dependency.cpp176
-rw-r--r--lib/icinga/checkable-downtime.cpp64
-rw-r--r--lib/icinga/checkable-event.cpp81
-rw-r--r--lib/icinga/checkable-flapping.cpp114
-rw-r--r--lib/icinga/checkable-notification.cpp334
-rw-r--r--lib/icinga/checkable-script.cpp28
-rw-r--r--lib/icinga/checkable.cpp322
-rw-r--r--lib/icinga/checkable.hpp264
-rw-r--r--lib/icinga/checkable.ti192
-rw-r--r--lib/icinga/checkcommand.cpp22
-rw-r--r--lib/icinga/checkcommand.hpp32
-rw-r--r--lib/icinga/checkcommand.ti14
-rw-r--r--lib/icinga/checkresult.cpp34
-rw-r--r--lib/icinga/checkresult.hpp28
-rw-r--r--lib/icinga/checkresult.ti72
-rw-r--r--lib/icinga/cib.cpp346
-rw-r--r--lib/icinga/cib.hpp91
-rw-r--r--lib/icinga/clusterevents-check.cpp379
-rw-r--r--lib/icinga/clusterevents.cpp1623
-rw-r--r--lib/icinga/clusterevents.hpp102
-rw-r--r--lib/icinga/command.cpp68
-rw-r--r--lib/icinga/command.hpp30
-rw-r--r--lib/icinga/command.ti54
-rw-r--r--lib/icinga/comment.cpp258
-rw-r--r--lib/icinga/comment.hpp59
-rw-r--r--lib/icinga/comment.ti80
-rw-r--r--lib/icinga/compatutility.cpp302
-rw-r--r--lib/icinga/compatutility.hpp56
-rw-r--r--lib/icinga/customvarobject.cpp49
-rw-r--r--lib/icinga/customvarobject.hpp31
-rw-r--r--lib/icinga/customvarobject.ti15
-rw-r--r--lib/icinga/dependency-apply.cpp161
-rw-r--r--lib/icinga/dependency.cpp325
-rw-r--r--lib/icinga/dependency.hpp62
-rw-r--r--lib/icinga/dependency.ti101
-rw-r--r--lib/icinga/downtime.cpp584
-rw-r--r--lib/icinga/downtime.hpp99
-rw-r--r--lib/icinga/downtime.ti82
-rw-r--r--lib/icinga/envresolver.cpp20
-rw-r--r--lib/icinga/envresolver.hpp30
-rw-r--r--lib/icinga/eventcommand.cpp20
-rw-r--r--lib/icinga/eventcommand.hpp32
-rw-r--r--lib/icinga/eventcommand.ti15
-rw-r--r--lib/icinga/externalcommandprocessor.cpp2281
-rw-r--r--lib/icinga/externalcommandprocessor.hpp169
-rw-r--r--lib/icinga/host.cpp330
-rw-r--r--lib/icinga/host.hpp71
-rw-r--r--lib/icinga/host.ti48
-rw-r--r--lib/icinga/hostgroup.cpp108
-rw-r--r--lib/icinga/hostgroup.hpp43
-rw-r--r--lib/icinga/hostgroup.ti28
-rw-r--r--lib/icinga/i2-icinga.hpp15
-rw-r--r--lib/icinga/icinga-itl.conf15
-rw-r--r--lib/icinga/icingaapplication.cpp321
-rw-r--r--lib/icinga/icingaapplication.hpp52
-rw-r--r--lib/icinga/icingaapplication.ti41
-rw-r--r--lib/icinga/legacytimeperiod.cpp644
-rw-r--r--lib/icinga/legacytimeperiod.hpp45
-rw-r--r--lib/icinga/macroprocessor.cpp585
-rw-r--r--lib/icinga/macroprocessor.hpp75
-rw-r--r--lib/icinga/macroresolver.hpp31
-rw-r--r--lib/icinga/notification-apply.cpp161
-rw-r--r--lib/icinga/notification.cpp812
-rw-r--r--lib/icinga/notification.hpp135
-rw-r--r--lib/icinga/notification.ti111
-rw-r--r--lib/icinga/notificationcommand.cpp27
-rw-r--r--lib/icinga/notificationcommand.hpp36
-rw-r--r--lib/icinga/notificationcommand.ti14
-rw-r--r--lib/icinga/objectutils.cpp55
-rw-r--r--lib/icinga/objectutils.hpp29
-rw-r--r--lib/icinga/pluginutility.cpp218
-rw-r--r--lib/icinga/pluginutility.hpp42
-rw-r--r--lib/icinga/scheduleddowntime-apply.cpp159
-rw-r--r--lib/icinga/scheduleddowntime.cpp393
-rw-r--r--lib/icinga/scheduleddowntime.hpp60
-rw-r--r--lib/icinga/scheduleddowntime.ti76
-rw-r--r--lib/icinga/service-apply.cpp133
-rw-r--r--lib/icinga/service.cpp287
-rw-r--r--lib/icinga/service.hpp65
-rw-r--r--lib/icinga/service.ti71
-rw-r--r--lib/icinga/servicegroup.cpp111
-rw-r--r--lib/icinga/servicegroup.hpp43
-rw-r--r--lib/icinga/servicegroup.ti28
-rw-r--r--lib/icinga/timeperiod.cpp399
-rw-r--r--lib/icinga/timeperiod.hpp50
-rw-r--r--lib/icinga/timeperiod.ti47
-rw-r--r--lib/icinga/user.cpp103
-rw-r--r--lib/icinga/user.hpp44
-rw-r--r--lib/icinga/user.ti47
-rw-r--r--lib/icinga/usergroup.cpp128
-rw-r--r--lib/icinga/usergroup.hpp49
-rw-r--r--lib/icinga/usergroup.ti25
-rw-r--r--lib/icingadb/CMakeLists.txt32
-rw-r--r--lib/icingadb/icingadb-itl.conf24
-rw-r--r--lib/icingadb/icingadb-objects.cpp2966
-rw-r--r--lib/icingadb/icingadb-stats.cpp54
-rw-r--r--lib/icingadb/icingadb-utility.cpp319
-rw-r--r--lib/icingadb/icingadb.cpp311
-rw-r--r--lib/icingadb/icingadb.hpp241
-rw-r--r--lib/icingadb/icingadb.ti63
-rw-r--r--lib/icingadb/icingadbchecktask.cpp513
-rw-r--r--lib/icingadb/icingadbchecktask.hpp29
-rw-r--r--lib/icingadb/redisconnection.cpp773
-rw-r--r--lib/icingadb/redisconnection.hpp678
-rw-r--r--lib/livestatus/CMakeLists.txt65
-rw-r--r--lib/livestatus/aggregator.cpp18
-rw-r--r--lib/livestatus/aggregator.hpp44
-rw-r--r--lib/livestatus/andfilter.cpp15
-rw-r--r--lib/livestatus/andfilter.hpp26
-rw-r--r--lib/livestatus/attributefilter.cpp121
-rw-r--r--lib/livestatus/attributefilter.hpp33
-rw-r--r--lib/livestatus/avgaggregator.cpp38
-rw-r--r--lib/livestatus/avgaggregator.hpp42
-rw-r--r--lib/livestatus/column.cpp21
-rw-r--r--lib/livestatus/column.hpp37
-rw-r--r--lib/livestatus/combinerfilter.cpp10
-rw-r--r--lib/livestatus/combinerfilter.hpp31
-rw-r--r--lib/livestatus/commandstable.cpp142
-rw-r--r--lib/livestatus/commandstable.hpp41
-rw-r--r--lib/livestatus/commentstable.cpp178
-rw-r--r--lib/livestatus/commentstable.hpp49
-rw-r--r--lib/livestatus/contactgroupstable.cpp74
-rw-r--r--lib/livestatus/contactgroupstable.hpp39
-rw-r--r--lib/livestatus/contactstable.cpp278
-rw-r--r--lib/livestatus/contactstable.hpp50
-rw-r--r--lib/livestatus/countaggregator.cpp30
-rw-r--r--lib/livestatus/countaggregator.hpp37
-rw-r--r--lib/livestatus/downtimestable.cpp168
-rw-r--r--lib/livestatus/downtimestable.hpp51
-rw-r--r--lib/livestatus/endpointstable.cpp109
-rw-r--r--lib/livestatus/endpointstable.hpp41
-rw-r--r--lib/livestatus/filter.hpp28
-rw-r--r--lib/livestatus/historytable.hpp24
-rw-r--r--lib/livestatus/hostgroupstable.cpp473
-rw-r--r--lib/livestatus/hostgroupstable.hpp61
-rw-r--r--lib/livestatus/hoststable.cpp1517
-rw-r--r--lib/livestatus/hoststable.hpp133
-rw-r--r--lib/livestatus/i2-livestatus.hpp14
-rw-r--r--lib/livestatus/invavgaggregator.cpp38
-rw-r--r--lib/livestatus/invavgaggregator.hpp42
-rw-r--r--lib/livestatus/invsumaggregator.cpp37
-rw-r--r--lib/livestatus/invsumaggregator.hpp41
-rw-r--r--lib/livestatus/livestatuslistener.cpp211
-rw-r--r--lib/livestatus/livestatuslistener.hpp47
-rw-r--r--lib/livestatus/livestatuslistener.ti31
-rw-r--r--lib/livestatus/livestatuslogutility.cpp321
-rw-r--r--lib/livestatus/livestatuslogutility.hpp60
-rw-r--r--lib/livestatus/livestatusquery.cpp648
-rw-r--r--lib/livestatus/livestatusquery.hpp90
-rw-r--r--lib/livestatus/logtable.cpp229
-rw-r--r--lib/livestatus/logtable.hpp65
-rw-r--r--lib/livestatus/maxaggregator.cpp38
-rw-r--r--lib/livestatus/maxaggregator.hpp41
-rw-r--r--lib/livestatus/minaggregator.cpp45
-rw-r--r--lib/livestatus/minaggregator.hpp42
-rw-r--r--lib/livestatus/negatefilter.cpp14
-rw-r--r--lib/livestatus/negatefilter.hpp31
-rw-r--r--lib/livestatus/orfilter.cpp18
-rw-r--r--lib/livestatus/orfilter.hpp26
-rw-r--r--lib/livestatus/servicegroupstable.cpp323
-rw-r--r--lib/livestatus/servicegroupstable.hpp54
-rw-r--r--lib/livestatus/servicestable.cpp1200
-rw-r--r--lib/livestatus/servicestable.hpp115
-rw-r--r--lib/livestatus/statehisttable.cpp466
-rw-r--r--lib/livestatus/statehisttable.hpp75
-rw-r--r--lib/livestatus/statustable.cpp269
-rw-r--r--lib/livestatus/statustable.hpp61
-rw-r--r--lib/livestatus/stdaggregator.cpp40
-rw-r--r--lib/livestatus/stdaggregator.hpp43
-rw-r--r--lib/livestatus/sumaggregator.cpp37
-rw-r--r--lib/livestatus/sumaggregator.hpp41
-rw-r--r--lib/livestatus/table.cpp165
-rw-r--r--lib/livestatus/table.hpp73
-rw-r--r--lib/livestatus/timeperiodstable.cpp58
-rw-r--r--lib/livestatus/timeperiodstable.hpp39
-rw-r--r--lib/livestatus/zonestable.cpp92
-rw-r--r--lib/livestatus/zonestable.hpp40
-rw-r--r--lib/methods/CMakeLists.txt34
-rw-r--r--lib/methods/clusterchecktask.cpp117
-rw-r--r--lib/methods/clusterchecktask.hpp29
-rw-r--r--lib/methods/clusterzonechecktask.cpp218
-rw-r--r--lib/methods/clusterzonechecktask.hpp28
-rw-r--r--lib/methods/dummychecktask.cpp75
-rw-r--r--lib/methods/dummychecktask.hpp30
-rw-r--r--lib/methods/exceptionchecktask.cpp41
-rw-r--r--lib/methods/exceptionchecktask.hpp29
-rw-r--r--lib/methods/i2-methods.hpp15
-rw-r--r--lib/methods/icingachecktask.cpp209
-rw-r--r--lib/methods/icingachecktask.hpp29
-rw-r--r--lib/methods/ifwapichecktask.cpp531
-rw-r--r--lib/methods/ifwapichecktask.hpp27
-rw-r--r--lib/methods/methods-itl.conf90
-rw-r--r--lib/methods/nullchecktask.cpp50
-rw-r--r--lib/methods/nullchecktask.hpp30
-rw-r--r--lib/methods/nulleventtask.cpp26
-rw-r--r--lib/methods/nulleventtask.hpp30
-rw-r--r--lib/methods/pluginchecktask.cpp89
-rw-r--r--lib/methods/pluginchecktask.hpp33
-rw-r--r--lib/methods/plugineventtask.cpp61
-rw-r--r--lib/methods/plugineventtask.hpp33
-rw-r--r--lib/methods/pluginnotificationtask.cpp123
-rw-r--r--lib/methods/pluginnotificationtask.hpp36
-rw-r--r--lib/methods/randomchecktask.cpp65
-rw-r--r--lib/methods/randomchecktask.hpp29
-rw-r--r--lib/methods/sleepchecktask.cpp67
-rw-r--r--lib/methods/sleepchecktask.hpp30
-rw-r--r--lib/methods/timeperiodtask.cpp35
-rw-r--r--lib/methods/timeperiodtask.hpp28
-rw-r--r--lib/mysql_shim/CMakeLists.txt31
-rw-r--r--lib/mysql_shim/mysql_shim.def3
-rw-r--r--lib/mysql_shim/mysqlinterface.cpp119
-rw-r--r--lib/mysql_shim/mysqlinterface.hpp65
-rw-r--r--lib/notification/CMakeLists.txt34
-rw-r--r--lib/notification/notificationcomponent.cpp271
-rw-r--r--lib/notification/notificationcomponent.hpp38
-rw-r--r--lib/notification/notificationcomponent.ti19
-rw-r--r--lib/perfdata/CMakeLists.txt74
-rw-r--r--lib/perfdata/elasticsearchwriter.cpp685
-rw-r--r--lib/perfdata/elasticsearchwriter.hpp65
-rw-r--r--lib/perfdata/elasticsearchwriter.ti50
-rw-r--r--lib/perfdata/gelfwriter.cpp535
-rw-r--r--lib/perfdata/gelfwriter.hpp70
-rw-r--r--lib/perfdata/gelfwriter.ti45
-rw-r--r--lib/perfdata/graphitewriter.cpp514
-rw-r--r--lib/perfdata/graphitewriter.hpp69
-rw-r--r--lib/perfdata/graphitewriter.ti38
-rw-r--r--lib/perfdata/influxdb2writer.cpp44
-rw-r--r--lib/perfdata/influxdb2writer.hpp33
-rw-r--r--lib/perfdata/influxdb2writer.ti19
-rw-r--r--lib/perfdata/influxdbcommonwriter.cpp596
-rw-r--r--lib/perfdata/influxdbcommonwriter.hpp101
-rw-r--r--lib/perfdata/influxdbcommonwriter.ti88
-rw-r--r--lib/perfdata/influxdbwriter.cpp56
-rw-r--r--lib/perfdata/influxdbwriter.hpp31
-rw-r--r--lib/perfdata/influxdbwriter.ti35
-rw-r--r--lib/perfdata/opentsdbwriter.cpp525
-rw-r--r--lib/perfdata/opentsdbwriter.hpp62
-rw-r--r--lib/perfdata/opentsdbwriter.ti55
-rw-r--r--lib/perfdata/perfdatawriter.cpp201
-rw-r--r--lib/perfdata/perfdatawriter.hpp53
-rw-r--r--lib/perfdata/perfdatawriter.ti61
-rw-r--r--lib/pgsql_shim/CMakeLists.txt32
-rw-r--r--lib/pgsql_shim/pgsql_shim.def3
-rw-r--r--lib/pgsql_shim/pgsqlinterface.cpp108
-rw-r--r--lib/pgsql_shim/pgsqlinterface.hpp61
-rw-r--r--lib/remote/CMakeLists.txt67
-rw-r--r--lib/remote/actionshandler.cpp145
-rw-r--r--lib/remote/actionshandler.hpp32
-rw-r--r--lib/remote/apiaction.cpp40
-rw-r--r--lib/remote/apiaction.hpp69
-rw-r--r--lib/remote/apifunction.cpp35
-rw-r--r--lib/remote/apifunction.hpp59
-rw-r--r--lib/remote/apilistener-authority.cpp84
-rw-r--r--lib/remote/apilistener-configsync.cpp464
-rw-r--r--lib/remote/apilistener-filesync.cpp887
-rw-r--r--lib/remote/apilistener.cpp1970
-rw-r--r--lib/remote/apilistener.hpp265
-rw-r--r--lib/remote/apilistener.ti66
-rw-r--r--lib/remote/apiuser.cpp55
-rw-r--r--lib/remote/apiuser.hpp27
-rw-r--r--lib/remote/apiuser.ti31
-rw-r--r--lib/remote/configfileshandler.cpp94
-rw-r--r--lib/remote/configfileshandler.hpp30
-rw-r--r--lib/remote/configobjectslock.cpp24
-rw-r--r--lib/remote/configobjectslock.hpp72
-rw-r--r--lib/remote/configobjectutility.cpp377
-rw-r--r--lib/remote/configobjectutility.hpp47
-rw-r--r--lib/remote/configpackageshandler.cpp179
-rw-r--r--lib/remote/configpackageshandler.hpp54
-rw-r--r--lib/remote/configpackageutility.cpp413
-rw-r--r--lib/remote/configpackageutility.hpp73
-rw-r--r--lib/remote/configstageshandler.cpp225
-rw-r--r--lib/remote/configstageshandler.hpp56
-rw-r--r--lib/remote/consolehandler.cpp327
-rw-r--r--lib/remote/consolehandler.hpp50
-rw-r--r--lib/remote/createobjecthandler.cpp155
-rw-r--r--lib/remote/createobjecthandler.hpp30
-rw-r--r--lib/remote/deleteobjecthandler.cpp123
-rw-r--r--lib/remote/deleteobjecthandler.hpp30
-rw-r--r--lib/remote/endpoint.cpp138
-rw-r--r--lib/remote/endpoint.hpp68
-rw-r--r--lib/remote/endpoint.ti59
-rw-r--r--lib/remote/eventqueue.cpp351
-rw-r--r--lib/remote/eventqueue.hpp177
-rw-r--r--lib/remote/eventshandler.cpp137
-rw-r--r--lib/remote/eventshandler.hpp31
-rw-r--r--lib/remote/filterutility.cpp354
-rw-r--r--lib/remote/filterutility.hpp64
-rw-r--r--lib/remote/httphandler.cpp129
-rw-r--r--lib/remote/httphandler.hpp74
-rw-r--r--lib/remote/httpserverconnection.cpp613
-rw-r--r--lib/remote/httpserverconnection.hpp54
-rw-r--r--lib/remote/httputility.cpp80
-rw-r--r--lib/remote/httputility.hpp33
-rw-r--r--lib/remote/i2-remote.hpp14
-rw-r--r--lib/remote/infohandler.cpp100
-rw-r--r--lib/remote/infohandler.hpp30
-rw-r--r--lib/remote/jsonrpc.cpp157
-rw-r--r--lib/remote/jsonrpc.hpp39
-rw-r--r--lib/remote/jsonrpcconnection-heartbeat.cpp48
-rw-r--r--lib/remote/jsonrpcconnection-pki.cpp439
-rw-r--r--lib/remote/jsonrpcconnection.cpp388
-rw-r--r--lib/remote/jsonrpcconnection.hpp100
-rw-r--r--lib/remote/messageorigin.cpp10
-rw-r--r--lib/remote/messageorigin.hpp28
-rw-r--r--lib/remote/modifyobjecthandler.cpp168
-rw-r--r--lib/remote/modifyobjecthandler.hpp30
-rw-r--r--lib/remote/objectqueryhandler.cpp330
-rw-r--r--lib/remote/objectqueryhandler.hpp34
-rw-r--r--lib/remote/pkiutility.cpp452
-rw-r--r--lib/remote/pkiutility.hpp41
-rw-r--r--lib/remote/statushandler.cpp120
-rw-r--r--lib/remote/statushandler.hpp30
-rw-r--r--lib/remote/templatequeryhandler.cpp136
-rw-r--r--lib/remote/templatequeryhandler.hpp30
-rw-r--r--lib/remote/typequeryhandler.cpp156
-rw-r--r--lib/remote/typequeryhandler.hpp30
-rw-r--r--lib/remote/url-characters.hpp29
-rw-r--r--lib/remote/url.cpp363
-rw-r--r--lib/remote/url.hpp78
-rw-r--r--lib/remote/variablequeryhandler.cpp121
-rw-r--r--lib/remote/variablequeryhandler.hpp30
-rw-r--r--lib/remote/zone.cpp154
-rw-r--r--lib/remote/zone.hpp46
-rw-r--r--lib/remote/zone.ti25
672 files changed, 106563 insertions, 0 deletions
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
new file mode 100644
index 0000000..aadbb39
--- /dev/null
+++ b/lib/CMakeLists.txt
@@ -0,0 +1,60 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+add_subdirectory(base)
+add_subdirectory(cli)
+add_subdirectory(config)
+add_subdirectory(remote)
+add_subdirectory(icinga)
+add_subdirectory(methods)
+
+if(ICINGA2_WITH_CHECKER)
+ add_subdirectory(checker)
+endif()
+
+if(ICINGA2_WITH_COMPAT)
+ add_subdirectory(compat)
+endif()
+
+if(ICINGA2_WITH_MYSQL OR ICINGA2_WITH_PGSQL)
+ add_subdirectory(db_ido)
+endif()
+
+if(ICINGA2_WITH_MYSQL)
+ find_package(MySQL)
+
+ if(MYSQL_FOUND)
+ add_subdirectory(db_ido_mysql)
+ add_subdirectory(mysql_shim)
+ else()
+ message(FATAL_ERROR "You have selected MySQL support, but MySQL could not be found. You can disable the MySQL IDO module using -DICINGA2_WITH_MYSQL=OFF.")
+ endif()
+endif()
+
+if(ICINGA2_WITH_PGSQL)
+ find_package(PostgreSQL)
+
+ if(PostgreSQL_FOUND)
+ add_subdirectory(db_ido_pgsql)
+ add_subdirectory(pgsql_shim)
+ else()
+ message(FATAL_ERROR "You have selected PostgreSQL support, but PostgreSQL could not be found. You can disable the PostgreSQL IDO module using -DICINGA2_WITH_PGSQL=OFF.")
+ endif()
+endif()
+
+if(ICINGA2_WITH_LIVESTATUS)
+ add_subdirectory(livestatus)
+endif()
+
+if(ICINGA2_WITH_NOTIFICATION)
+ add_subdirectory(notification)
+endif()
+
+if(ICINGA2_WITH_PERFDATA)
+ add_subdirectory(perfdata)
+endif()
+
+if(ICINGA2_WITH_ICINGADB)
+ add_subdirectory(icingadb)
+endif()
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt
new file mode 100644
index 0000000..e50e330
--- /dev/null
+++ b/lib/base/CMakeLists.txt
@@ -0,0 +1,160 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(application.ti application-ti.cpp application-ti.hpp)
+mkclass_target(configobject.ti configobject-ti.cpp configobject-ti.hpp)
+mkclass_target(configuration.ti configuration-ti.cpp configuration-ti.hpp)
+mkclass_target(datetime.ti datetime-ti.cpp datetime-ti.hpp)
+mkclass_target(filelogger.ti filelogger-ti.cpp filelogger-ti.hpp)
+mkclass_target(function.ti function-ti.cpp function-ti.hpp)
+mkclass_target(journaldlogger.ti journaldlogger-ti.cpp journaldlogger-ti.hpp)
+mkclass_target(logger.ti logger-ti.cpp logger-ti.hpp)
+mkclass_target(perfdatavalue.ti perfdatavalue-ti.cpp perfdatavalue-ti.hpp)
+mkclass_target(streamlogger.ti streamlogger-ti.cpp streamlogger-ti.hpp)
+mkclass_target(sysloglogger.ti sysloglogger-ti.cpp sysloglogger-ti.hpp)
+
+set(base_SOURCES
+ i2-base.hpp
+ application.cpp application.hpp application-ti.hpp application-version.cpp application-environment.cpp
+ array.cpp array.hpp array-script.cpp
+ atomic.hpp
+ atomic-file.cpp atomic-file.hpp
+ base64.cpp base64.hpp
+ boolean.cpp boolean.hpp boolean-script.cpp
+ bulker.hpp
+ configobject.cpp configobject.hpp configobject-ti.hpp configobject-script.cpp
+ configtype.cpp configtype.hpp
+ configuration.cpp configuration.hpp configuration-ti.hpp
+ configwriter.cpp configwriter.hpp
+ console.cpp console.hpp
+ context.cpp context.hpp
+ convert.cpp convert.hpp
+ datetime.cpp datetime.hpp datetime-ti.hpp datetime-script.cpp
+ debug.hpp
+ debuginfo.cpp debuginfo.hpp
+ dependencygraph.cpp dependencygraph.hpp
+ dictionary.cpp dictionary.hpp dictionary-script.cpp
+ exception.cpp exception.hpp
+ fifo.cpp fifo.hpp
+ filelogger.cpp filelogger.hpp filelogger-ti.hpp
+ function.cpp function.hpp function-ti.hpp function-script.cpp functionwrapper.hpp
+ initialize.cpp initialize.hpp
+ io-engine.cpp io-engine.hpp
+ journaldlogger.cpp journaldlogger.hpp journaldlogger-ti.hpp
+ json.cpp json.hpp json-script.cpp
+ lazy-init.hpp
+ library.cpp library.hpp
+ loader.cpp loader.hpp
+ logger.cpp logger.hpp logger-ti.hpp
+ math-script.cpp
+ netstring.cpp netstring.hpp
+ networkstream.cpp networkstream.hpp
+ namespace.cpp namespace.hpp namespace-script.cpp
+ number.cpp number.hpp number-script.cpp
+ object.cpp object.hpp object-script.cpp
+ objectlock.cpp objectlock.hpp
+ object-packer.cpp object-packer.hpp
+ objecttype.cpp objecttype.hpp
+ perfdatavalue.cpp perfdatavalue.hpp perfdatavalue-ti.hpp
+ primitivetype.cpp primitivetype.hpp
+ process.cpp process.hpp
+ reference.cpp reference.hpp reference-script.cpp
+ registry.hpp
+ ringbuffer.cpp ringbuffer.hpp
+ scriptframe.cpp scriptframe.hpp
+ scriptglobal.cpp scriptglobal.hpp
+ scriptutils.cpp scriptutils.hpp
+ serializer.cpp serializer.hpp
+ shared.hpp
+ shared-memory.hpp
+ shared-object.hpp
+ singleton.hpp
+ socket.cpp socket.hpp
+ stacktrace.cpp stacktrace.hpp
+ statsfunction.hpp
+ stdiostream.cpp stdiostream.hpp
+ stream.cpp stream.hpp
+ streamlogger.cpp streamlogger.hpp streamlogger-ti.hpp
+ string.cpp string.hpp string-script.cpp
+ sysloglogger.cpp sysloglogger.hpp sysloglogger-ti.hpp
+ tcpsocket.cpp tcpsocket.hpp
+ threadpool.cpp threadpool.hpp
+ timer.cpp timer.hpp
+ tlsstream.cpp tlsstream.hpp
+ tlsutility.cpp tlsutility.hpp
+ type.cpp type.hpp typetype-script.cpp
+ unix.hpp
+ unixsocket.cpp unixsocket.hpp
+ utility.cpp utility.hpp
+ value.cpp value.hpp value-operators.cpp
+ win32.hpp
+ workqueue.cpp workqueue.hpp
+)
+
+if(WIN32)
+ mkclass_target(windowseventloglogger.ti windowseventloglogger-ti.cpp windowseventloglogger-ti.hpp)
+ list(APPEND base_SOURCES windowseventloglogger.cpp windowseventloglogger.hpp windowseventloglogger-ti.hpp)
+
+ # Generate a DLL containing message definitions for the Windows Event Viewer.
+ # See also: https://docs.microsoft.com/en-us/windows/win32/eventlog/reporting-an-event
+ add_custom_command(
+ OUTPUT windowseventloglogger-provider.rc windowseventloglogger-provider.h
+ COMMAND mc ARGS -U ${CMAKE_CURRENT_SOURCE_DIR}/windowseventloglogger-provider.mc
+ DEPENDS windowseventloglogger-provider.mc
+ )
+
+ list(APPEND base_SOURCES windowseventloglogger-provider.h)
+
+ add_custom_command(
+ OUTPUT windowseventloglogger-provider.res
+ COMMAND rc ARGS windowseventloglogger-provider.rc
+ DEPENDS windowseventloglogger-provider.rc
+ )
+
+ add_library(eventprovider MODULE windowseventloglogger-provider.res windowseventloglogger-provider.rc)
+ set_target_properties(eventprovider PROPERTIES LINKER_LANGUAGE CXX)
+ target_link_libraries(eventprovider PRIVATE -noentry)
+
+ install(TARGETS eventprovider LIBRARY DESTINATION ${CMAKE_INSTALL_SBINDIR})
+endif()
+
+set_property(
+ SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/application-version.cpp ${CMAKE_CURRENT_SOURCE_DIR}/journaldlogger.cpp
+ PROPERTY EXCLUDE_UNITY_BUILD TRUE
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(base base base_SOURCES)
+endif()
+
+if(HAVE_SYSTEMD)
+ find_path(SYSTEMD_INCLUDE_DIR
+ NAMES systemd/sd-daemon.h
+ HINTS ${SYSTEMD_ROOT_DIR})
+ include_directories(${SYSTEMD_INCLUDE_DIR})
+ set_property(
+ SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/journaldlogger.cpp
+ APPEND PROPERTY COMPILE_DEFINITIONS
+ SD_JOURNAL_SUPPRESS_LOCATION
+ )
+endif()
+
+add_library(base OBJECT ${base_SOURCES})
+
+include_directories(${icinga2_SOURCE_DIR}/third-party/execvpe)
+link_directories(${icinga2_BINARY_DIR}/third-party/execvpe)
+
+include_directories(${icinga2_SOURCE_DIR}/third-party/mmatch)
+link_directories(${icinga2_BINARY_DIR}/third-party/mmatch)
+
+include_directories(${icinga2_SOURCE_DIR}/third-party/socketpair)
+link_directories(${icinga2_BINARY_DIR}/third-party/socketpair)
+
+set_target_properties (
+ base PROPERTIES
+ FOLDER Lib
+)
+
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_CACHEDIR}\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_LOGDIR}/crash\")")
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/base/application-environment.cpp b/lib/base/application-environment.cpp
new file mode 100644
index 0000000..819783f
--- /dev/null
+++ b/lib/base/application-environment.cpp
@@ -0,0 +1,17 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/application.hpp"
+#include "base/scriptglobal.hpp"
+
+using namespace icinga;
+
+String Application::GetAppEnvironment()
+{
+ Value defaultValue = Empty;
+ return ScriptGlobal::Get("Environment", &defaultValue);
+}
+
+void Application::SetAppEnvironment(const String& name)
+{
+ ScriptGlobal::Set("Environment", name);
+}
diff --git a/lib/base/application-version.cpp b/lib/base/application-version.cpp
new file mode 100644
index 0000000..d17775b
--- /dev/null
+++ b/lib/base/application-version.cpp
@@ -0,0 +1,17 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/application.hpp"
+#include "icinga-version.h"
+#include "icinga-spec-version.h"
+
+using namespace icinga;
+
+String Application::GetAppVersion()
+{
+ return VERSION;
+}
+
+String Application::GetAppSpecVersion()
+{
+ return SPEC_VERSION;
+}
diff --git a/lib/base/application.cpp b/lib/base/application.cpp
new file mode 100644
index 0000000..89a0f55
--- /dev/null
+++ b/lib/base/application.cpp
@@ -0,0 +1,1238 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/application.hpp"
+#include "base/application-ti.cpp"
+#include "base/stacktrace.hpp"
+#include "base/timer.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/loader.hpp"
+#include "base/debug.hpp"
+#include "base/type.hpp"
+#include "base/convert.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/process.hpp"
+#include "base/tlsutility.hpp"
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/exception/errinfo_api_function.hpp>
+#include <boost/exception/errinfo_errno.hpp>
+#include <boost/exception/errinfo_file_name.hpp>
+#include <boost/stacktrace.hpp>
+#include <sstream>
+#include <iostream>
+#include <fstream>
+#include <thread>
+#ifdef __linux__
+#include <sys/prctl.h>
+#endif /* __linux__ */
+#ifdef _WIN32
+#include <windows.h>
+#else /* _WIN32 */
+#include <signal.h>
+#endif /* _WIN32 */
+
+using namespace icinga;
+
+#ifdef _WIN32
+/* MSVC throws unhandled C++ exceptions as SEH exceptions with this specific error code.
+ * There seems to be no system header that actually defines this constant.
+ * See also https://devblogs.microsoft.com/oldnewthing/20160915-00/?p=94316
+ */
+#define EXCEPTION_CODE_CXX_EXCEPTION 0xe06d7363
+#endif /* _WIN32 */
+
+REGISTER_TYPE(Application);
+
+boost::signals2::signal<void ()> Application::OnReopenLogs;
+Application::Ptr Application::m_Instance = nullptr;
+bool Application::m_ShuttingDown = false;
+bool Application::m_RequestRestart = false;
+bool Application::m_RequestReopenLogs = false;
+pid_t Application::m_ReloadProcess = 0;
+
+#ifndef _WIN32
+pid_t Application::m_UmbrellaProcess = 0;
+#endif /* _WIN32 */
+
+static bool l_Restarting = false;
+static bool l_InExceptionHandler = false;
+int Application::m_ArgC;
+char **Application::m_ArgV;
+double Application::m_StartTime;
+bool Application::m_ScriptDebuggerEnabled = false;
+
+#ifdef _WIN32
+double Application::m_LastReloadFailed = 0;
+#else /* _WIN32 */
+SharedMemory<Application::AtomicTs> Application::m_LastReloadFailed (0);
+#endif /* _WIN32 */
+
+#ifdef _WIN32
+static LPTOP_LEVEL_EXCEPTION_FILTER l_DefaultUnhandledExceptionFilter = nullptr;
+#endif /* _WIN32 */
+
+/**
+ * Constructor for the Application class.
+ */
+void Application::OnConfigLoaded()
+{
+ m_PidFile = nullptr;
+
+ ASSERT(m_Instance == nullptr);
+ m_Instance = this;
+}
+
+/**
+ * Destructor for the application class.
+ */
+void Application::Stop(bool runtimeRemoved)
+{
+ m_ShuttingDown = true;
+
+#ifdef _WIN32
+ WSACleanup();
+#endif /* _WIN32 */
+
+#ifdef _WIN32
+ ClosePidFile(true);
+#endif /* _WIN32 */
+
+ ObjectImpl<Application>::Stop(runtimeRemoved);
+}
+
+Application::~Application()
+{
+ m_Instance = nullptr;
+}
+
+void Application::Exit(int rc)
+{
+ std::cout.flush();
+ std::cerr.flush();
+
+ for (const Logger::Ptr& logger : Logger::GetLoggers()) {
+ logger->Flush();
+ }
+
+ UninitializeBase();
+ _exit(rc); // Yay, our static destructors are pretty much beyond repair at this point.
+}
+
+void Application::InitializeBase()
+{
+#ifdef _WIN32
+ /* disable GUI-based error messages for LoadLibrary() */
+ SetErrorMode(SEM_FAILCRITICALERRORS);
+
+ WSADATA wsaData;
+ if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("WSAStartup")
+ << errinfo_win32_error(WSAGetLastError()));
+ }
+#else /* _WIN32 */
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = SIG_IGN;
+ sigaction(SIGPIPE, &sa, nullptr);
+#endif /* _WIN32 */
+
+ Loader::ExecuteDeferredInitializers();
+
+ /* Make sure the thread pool gets initialized. */
+ GetTP().Start();
+
+ /* Make sure the timer thread gets initialized. */
+ Timer::Initialize();
+}
+
+void Application::UninitializeBase()
+{
+ Timer::Uninitialize();
+
+ GetTP().Stop();
+}
+
+/**
+ * Retrieves a pointer to the application singleton object.
+ *
+ * @returns The application object.
+ */
+Application::Ptr Application::GetInstance()
+{
+ return m_Instance;
+}
+
+void Application::SetResourceLimits()
+{
+#ifdef __linux__
+ rlimit rl;
+
+# ifdef RLIMIT_NOFILE
+ rlim_t fileLimit = Configuration::RLimitFiles;
+
+ if (fileLimit != 0) {
+ if (fileLimit < (rlim_t)GetDefaultRLimitFiles()) {
+ Log(LogWarning, "Application")
+ << "The user-specified value for RLimitFiles cannot be smaller than the default value (" << GetDefaultRLimitFiles() << "). Using the default value instead.";
+ fileLimit = GetDefaultRLimitFiles();
+ }
+
+ rl.rlim_cur = fileLimit;
+ rl.rlim_max = rl.rlim_cur;
+
+ if (setrlimit(RLIMIT_NOFILE, &rl) < 0)
+ Log(LogWarning, "Application")
+ << "Failed to adjust resource limit for open file handles (RLIMIT_NOFILE) with error \"" << strerror(errno) << "\"";
+# else /* RLIMIT_NOFILE */
+ Log(LogNotice, "Application", "System does not support adjusting the resource limit for open file handles (RLIMIT_NOFILE)");
+# endif /* RLIMIT_NOFILE */
+ }
+
+# ifdef RLIMIT_NPROC
+ rlim_t processLimit = Configuration::RLimitProcesses;
+
+ if (processLimit != 0) {
+ if (processLimit < (rlim_t)GetDefaultRLimitProcesses()) {
+ Log(LogWarning, "Application")
+ << "The user-specified value for RLimitProcesses cannot be smaller than the default value (" << GetDefaultRLimitProcesses() << "). Using the default value instead.";
+ processLimit = GetDefaultRLimitProcesses();
+ }
+
+ rl.rlim_cur = processLimit;
+ rl.rlim_max = rl.rlim_cur;
+
+ if (setrlimit(RLIMIT_NPROC, &rl) < 0)
+ Log(LogWarning, "Application")
+ << "Failed adjust resource limit for number of processes (RLIMIT_NPROC) with error \"" << strerror(errno) << "\"";
+# else /* RLIMIT_NPROC */
+ Log(LogNotice, "Application", "System does not support adjusting the resource limit for number of processes (RLIMIT_NPROC)");
+# endif /* RLIMIT_NPROC */
+ }
+
+# ifdef RLIMIT_STACK
+ int argc = Application::GetArgC();
+ char **argv = Application::GetArgV();
+ bool set_stack_rlimit = true;
+
+ for (int i = 0; i < argc; i++) {
+ if (strcmp(argv[i], "--no-stack-rlimit") == 0) {
+ set_stack_rlimit = false;
+ break;
+ }
+ }
+
+ if (getrlimit(RLIMIT_STACK, &rl) < 0) {
+ Log(LogWarning, "Application", "Could not determine resource limit for stack size (RLIMIT_STACK)");
+ rl.rlim_max = RLIM_INFINITY;
+ }
+
+ rlim_t stackLimit;
+
+ stackLimit = Configuration::RLimitStack;
+
+ if (stackLimit != 0) {
+ if (stackLimit < (rlim_t)GetDefaultRLimitStack()) {
+ Log(LogWarning, "Application")
+ << "The user-specified value for RLimitStack cannot be smaller than the default value (" << GetDefaultRLimitStack() << "). Using the default value instead.";
+ stackLimit = GetDefaultRLimitStack();
+ }
+
+ if (set_stack_rlimit)
+ rl.rlim_cur = stackLimit;
+ else
+ rl.rlim_cur = rl.rlim_max;
+
+ if (setrlimit(RLIMIT_STACK, &rl) < 0)
+ Log(LogWarning, "Application")
+ << "Failed adjust resource limit for stack size (RLIMIT_STACK) with error \"" << strerror(errno) << "\"";
+ else if (set_stack_rlimit) {
+ char **new_argv = static_cast<char **>(malloc(sizeof(char *) * (argc + 2)));
+
+ if (!new_argv) {
+ perror("malloc");
+ Exit(EXIT_FAILURE);
+ }
+
+ new_argv[0] = argv[0];
+ new_argv[1] = strdup("--no-stack-rlimit");
+
+ if (!new_argv[1]) {
+ perror("strdup");
+ exit(1);
+ }
+
+ for (int i = 1; i < argc; i++)
+ new_argv[i + 1] = argv[i];
+
+ new_argv[argc + 1] = nullptr;
+
+ (void) execvp(new_argv[0], new_argv);
+ perror("execvp");
+ _exit(EXIT_FAILURE);
+ }
+# else /* RLIMIT_STACK */
+ Log(LogNotice, "Application", "System does not support adjusting the resource limit for stack size (RLIMIT_STACK)");
+# endif /* RLIMIT_STACK */
+ }
+#endif /* __linux__ */
+}
+
+int Application::GetArgC()
+{
+ return m_ArgC;
+}
+
+void Application::SetArgC(int argc)
+{
+ m_ArgC = argc;
+}
+
+char **Application::GetArgV()
+{
+ return m_ArgV;
+}
+
+void Application::SetArgV(char **argv)
+{
+ m_ArgV = argv;
+}
+
+/**
+ * Processes events for registered sockets and timers and calls whatever
+ * handlers have been set up for these events.
+ */
+void Application::RunEventLoop()
+{
+ double lastLoop = Utility::GetTime();
+
+ while (!m_ShuttingDown) {
+ if (m_RequestRestart) {
+ m_RequestRestart = false; // we are now handling the request, once is enough
+
+#ifdef _WIN32
+ // are we already restarting? ignore request if we already are
+ if (!l_Restarting) {
+ l_Restarting = true;
+ m_ReloadProcess = StartReloadProcess();
+ }
+#else /* _WIN32 */
+ Log(LogNotice, "Application")
+ << "Got reload command, forwarding to umbrella process (PID " << m_UmbrellaProcess << ")";
+
+ (void)kill(m_UmbrellaProcess, SIGHUP);
+#endif /* _WIN32 */
+ } else {
+ /* Watches for changes to the system time. Adjusts timers if necessary. */
+ Utility::Sleep(2.5);
+
+ if (m_RequestReopenLogs) {
+ Log(LogNotice, "Application", "Reopening log files");
+ m_RequestReopenLogs = false;
+ OnReopenLogs();
+ }
+
+ double now = Utility::GetTime();
+ double timeDiff = lastLoop - now;
+
+ if (std::fabs(timeDiff) > 15) {
+ /* We made a significant jump in time. */
+ Log(LogInformation, "Application")
+ << "We jumped "
+ << (timeDiff < 0 ? "forward" : "backward")
+ << " in time: " << std::fabs(timeDiff) << " seconds";
+
+ Timer::AdjustTimers(-timeDiff);
+ }
+
+ lastLoop = now;
+ }
+ }
+
+ Log(LogInformation, "Application", "Shutting down...");
+
+ ConfigObject::StopObjects();
+ Application::GetInstance()->OnShutdown();
+
+#ifdef I2_DEBUG
+ UninitializeBase(); // Inspired from Exit()
+#endif /* I2_DEBUG */
+}
+
+bool Application::IsShuttingDown()
+{
+ return m_ShuttingDown;
+}
+
+bool Application::IsRestarting()
+{
+ return l_Restarting;
+}
+
+void Application::OnShutdown()
+{
+ /* Nothing to do here. */
+}
+
+static void ReloadProcessCallbackInternal(const ProcessResult& pr)
+{
+ if (pr.ExitStatus != 0) {
+ Application::SetLastReloadFailed(Utility::GetTime());
+ Log(LogCritical, "Application", "Found error in config: reloading aborted");
+ }
+#ifdef _WIN32
+ else
+ Application::Exit(7); /* keep this exit code in sync with icinga-app */
+#endif /* _WIN32 */
+}
+
+static void ReloadProcessCallback(const ProcessResult& pr)
+{
+ l_Restarting = false;
+
+ std::thread t([pr]() { ReloadProcessCallbackInternal(pr); });
+ t.detach();
+}
+
+pid_t Application::StartReloadProcess()
+{
+ // prepare arguments
+ ArrayData args;
+ args.push_back(GetExePath(m_ArgV[0]));
+
+ for (int i=1; i < Application::GetArgC(); i++) {
+ if (std::string(Application::GetArgV()[i]) != "--reload-internal")
+ args.push_back(Application::GetArgV()[i]);
+ else
+ i++; // the next parameter after --reload-internal is the pid, remove that too
+ }
+
+#ifndef _WIN32
+ args.push_back("--reload-internal");
+ args.push_back(Convert::ToString(Utility::GetPid()));
+#else /* _WIN32 */
+ args.push_back("--validate");
+#endif /* _WIN32 */
+
+ double reloadTimeout = Application::GetReloadTimeout();
+
+ Process::Ptr process = new Process(Process::PrepareCommand(new Array(std::move(args))));
+ process->SetTimeout(reloadTimeout);
+ process->Run(&ReloadProcessCallback);
+
+ Log(LogInformation, "Application")
+ << "Got reload command: Started new instance with PID '"
+ << (unsigned long)(process->GetPID()) << "' (timeout is "
+ << reloadTimeout << "s).";
+
+ return process->GetPID();
+}
+
+/**
+ * Signals the application to shut down during the next
+ * execution of the event loop.
+ */
+void Application::RequestShutdown()
+{
+ Log(LogInformation, "Application", "Received request to shut down.");
+
+ m_ShuttingDown = true;
+}
+
+/**
+ * Signals the application to restart during the next
+ * execution of the event loop.
+ */
+void Application::RequestRestart()
+{
+ m_RequestRestart = true;
+}
+
+/**
+ * Signals the application to reopen log files during the
+ * next execution of the event loop.
+ */
+void Application::RequestReopenLogs()
+{
+ m_RequestReopenLogs = true;
+}
+
+#ifndef _WIN32
+/**
+ * Sets the PID of the Icinga umbrella process.
+ *
+ * @param pid The PID of the Icinga umbrella process.
+ */
+void Application::SetUmbrellaProcess(pid_t pid)
+{
+ m_UmbrellaProcess = pid;
+}
+#endif /* _WIN32 */
+
+/**
+ * Retrieves the full path of the executable.
+ *
+ * @param argv0 The first command-line argument.
+ * @returns The path.
+ */
+String Application::GetExePath(const String& argv0)
+{
+ String executablePath;
+
+#ifndef _WIN32
+ char buffer[MAXPATHLEN];
+ if (!getcwd(buffer, sizeof(buffer))) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("getcwd")
+ << boost::errinfo_errno(errno));
+ }
+
+ String workingDirectory = buffer;
+
+ if (argv0[0] != '/')
+ executablePath = workingDirectory + "/" + argv0;
+ else
+ executablePath = argv0;
+
+ bool foundSlash = false;
+ for (size_t i = 0; i < argv0.GetLength(); i++) {
+ if (argv0[i] == '/') {
+ foundSlash = true;
+ break;
+ }
+ }
+
+ if (!foundSlash) {
+ String pathEnv = Utility::GetFromEnvironment("PATH");
+ if (!pathEnv.IsEmpty()) {
+ std::vector<String> paths = String(pathEnv).Split(":");
+
+ bool foundPath = false;
+ for (const String& path : paths) {
+ String pathTest = path + "/" + argv0;
+
+ if (access(pathTest.CStr(), X_OK) == 0) {
+ executablePath = pathTest;
+ foundPath = true;
+ break;
+ }
+ }
+
+ if (!foundPath) {
+ executablePath.Clear();
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not determine executable path."));
+ }
+ }
+ }
+
+ if (!realpath(executablePath.CStr(), buffer)) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("realpath")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(executablePath));
+ }
+
+ return buffer;
+#else /* _WIN32 */
+ char FullExePath[MAXPATHLEN];
+
+ if (!GetModuleFileName(nullptr, FullExePath, sizeof(FullExePath)))
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("GetModuleFileName")
+ << errinfo_win32_error(GetLastError()));
+
+ return FullExePath;
+#endif /* _WIN32 */
+}
+
+/**
+ * Display version and path information.
+ */
+void Application::DisplayInfoMessage(std::ostream& os, bool skipVersion)
+{
+ /* icinga-app prints its own version information, stack traces need it here. */
+ if (!skipVersion)
+ os << " Application version: " << GetAppVersion() << "\n\n";
+
+ os << "System information:\n"
+ << " Platform: " << Utility::GetPlatformName() << "\n"
+ << " Platform version: " << Utility::GetPlatformVersion() << "\n"
+ << " Kernel: " << Utility::GetPlatformKernel() << "\n"
+ << " Kernel version: " << Utility::GetPlatformKernelVersion() << "\n"
+ << " Architecture: " << Utility::GetPlatformArchitecture() << "\n";
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+
+ os << "\nBuild information:\n"
+ << " Compiler: " << systemNS->Get("BuildCompilerName") << " " << systemNS->Get("BuildCompilerVersion") << "\n"
+ << " Build host: " << systemNS->Get("BuildHostName") << "\n"
+ << " OpenSSL version: " << GetOpenSSLVersion() << "\n";
+
+ os << "\nApplication information:\n"
+ << "\nGeneral paths:\n"
+ << " Config directory: " << Configuration::ConfigDir << "\n"
+ << " Data directory: " << Configuration::DataDir << "\n"
+ << " Log directory: " << Configuration::LogDir << "\n"
+ << " Cache directory: " << Configuration::CacheDir << "\n"
+ << " Spool directory: " << Configuration::SpoolDir << "\n"
+ << " Run directory: " << Configuration::InitRunDir << "\n"
+ << "\nOld paths (deprecated):\n"
+ << " Installation root: " << Configuration::PrefixDir << "\n"
+ << " Sysconf directory: " << Configuration::SysconfDir << "\n"
+ << " Run directory (base): " << Configuration::RunDir << "\n"
+ << " Local state directory: " << Configuration::LocalStateDir << "\n"
+ << "\nInternal paths:\n"
+ << " Package data directory: " << Configuration::PkgDataDir << "\n"
+ << " State path: " << Configuration::StatePath << "\n"
+ << " Modified attributes path: " << Configuration::ModAttrPath << "\n"
+ << " Objects path: " << Configuration::ObjectsPath << "\n"
+ << " Vars path: " << Configuration::VarsPath << "\n"
+ << " PID path: " << Configuration::PidPath << "\n";
+
+}
+
+/**
+ * Displays a message that tells users what to do when they encounter a bug.
+ */
+void Application::DisplayBugMessage(std::ostream& os)
+{
+ os << "***" << "\n"
+ << "* This would indicate a runtime problem or configuration error. If you believe this is a bug in Icinga 2" << "\n"
+ << "* please submit a bug report at https://github.com/Icinga/icinga2 and include this stack trace as well as any other" << "\n"
+ << "* information that might be useful in order to reproduce this problem." << "\n"
+ << "***" << "\n";
+}
+
+String Application::GetCrashReportFilename()
+{
+ return Configuration::LogDir + "/crash/report." + Convert::ToString(Utility::GetTime());
+}
+
+
+void Application::AttachDebugger(const String& filename, bool interactive)
+{
+#ifndef _WIN32
+#ifdef __linux__
+ prctl(PR_SET_DUMPABLE, 1);
+#endif /* __linux __ */
+
+ String my_pid = Convert::ToString(Utility::GetPid());
+
+ pid_t pid = fork();
+
+ if (pid < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("fork")
+ << boost::errinfo_errno(errno));
+ }
+
+ if (pid == 0) {
+ if (!interactive) {
+ int fd = open(filename.CStr(), O_CREAT | O_RDWR | O_APPEND, 0600);
+
+ if (fd < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("open")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(filename));
+ }
+
+ if (fd != 1) {
+ /* redirect stdout to the file */
+ dup2(fd, 1);
+ close(fd);
+ }
+
+ /* redirect stderr to stdout */
+ if (fd != 2)
+ close(2);
+
+ dup2(1, 2);
+ }
+
+ char **argv;
+ char *my_pid_str = strdup(my_pid.CStr());
+
+ if (interactive) {
+ const char *uargv[] = {
+ "gdb",
+ "-p",
+ my_pid_str,
+ nullptr
+ };
+
+ argv = const_cast<char **>(uargv);
+
+ (void) execvp(argv[0], argv);
+ } else {
+ const char *uargv[] = {
+ "gdb",
+ "--batch",
+ "-p",
+ my_pid_str,
+ "-ex",
+ "thread apply all bt full",
+ "-ex",
+ "detach",
+ "-ex",
+ "quit",
+ nullptr
+ };
+
+ argv = const_cast<char **>(uargv);
+
+ (void) execvp(argv[0], argv);
+ }
+
+ perror("Failed to launch GDB");
+ free(my_pid_str);
+ _exit(0);
+ }
+
+ int status;
+ if (waitpid(pid, &status, 0) < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("waitpid")
+ << boost::errinfo_errno(errno));
+ }
+
+#ifdef __linux__
+ prctl(PR_SET_DUMPABLE, 0);
+#endif /* __linux __ */
+#else /* _WIN32 */
+ DebugBreak();
+#endif /* _WIN32 */
+}
+
+/**
+ * Signal handler for SIGUSR1. This signal causes Icinga to re-open
+ * its log files and is mainly for use by logrotate.
+ *
+ * @param - The signal number.
+ */
+void Application::SigUsr1Handler(int)
+{
+ Log(LogInformation, "Application")
+ << "Received USR1 signal, reopening application logs.";
+
+ RequestReopenLogs();
+}
+
+/**
+ * Signal handler for SIGABRT. Helps with debugging ASSERT()s.
+ *
+ * @param - The signal number.
+ */
+void Application::SigAbrtHandler(int)
+{
+#ifndef _WIN32
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGABRT, &sa, nullptr);
+#endif /* _WIN32 */
+
+ std::cerr << "Caught SIGABRT." << std::endl
+ << "Current time: " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime()) << std::endl
+ << std::endl;
+
+ String fname = GetCrashReportFilename();
+ String dirName = Utility::DirName(fname);
+
+ if (!Utility::PathExists(dirName)) {
+#ifndef _WIN32
+ if (mkdir(dirName.CStr(), 0700) < 0 && errno != EEXIST) {
+#else /*_ WIN32 */
+ if (mkdir(dirName.CStr()) < 0 && errno != EEXIST) {
+#endif /* _WIN32 */
+ std::cerr << "Could not create directory '" << dirName << "': Error " << errno << ", " << strerror(errno) << "\n";
+ }
+ }
+
+ bool interactive_debugger = Configuration::AttachDebugger;
+
+ if (!interactive_debugger) {
+ std::ofstream ofs;
+ ofs.open(fname.CStr());
+
+ Log(LogCritical, "Application")
+ << "Icinga 2 has terminated unexpectedly. Additional information can be found in '" << fname << "'" << "\n";
+
+ ofs << "Caught SIGABRT.\n"
+ << "Current time: " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime()) << "\n\n";
+
+ DisplayInfoMessage(ofs);
+
+ ofs << "\nStacktrace:\n" << StackTraceFormatter(boost::stacktrace::stacktrace()) << "\n";
+
+ DisplayBugMessage(ofs);
+
+ ofs << "\n";
+ ofs.close();
+ } else {
+ Log(LogCritical, "Application", "Icinga 2 has terminated unexpectedly. Attaching debugger...");
+ }
+
+ AttachDebugger(fname, interactive_debugger);
+}
+
+#ifdef _WIN32
+/**
+ * Console control handler. Prepares the application for cleanly
+ * shutting down during the next execution of the event loop.
+ */
+BOOL WINAPI Application::CtrlHandler(DWORD type)
+{
+ Application::Ptr instance = Application::GetInstance();
+
+ if (!instance)
+ return TRUE;
+
+ instance->RequestShutdown();
+
+ SetConsoleCtrlHandler(nullptr, FALSE);
+ return TRUE;
+}
+
+bool Application::IsProcessElevated() {
+ BOOL fIsElevated = FALSE;
+ DWORD dwError = ERROR_SUCCESS;
+ HANDLE hToken = nullptr;
+
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
+ dwError = GetLastError();
+ else {
+ TOKEN_ELEVATION elevation;
+ DWORD dwSize;
+
+ if (!GetTokenInformation(hToken, TokenElevation, &elevation, sizeof(elevation), &dwSize))
+ dwError = GetLastError();
+ else
+ fIsElevated = elevation.TokenIsElevated;
+ }
+
+ if (hToken) {
+ CloseHandle(hToken);
+ hToken = nullptr;
+ }
+
+ if (ERROR_SUCCESS != dwError) {
+ LPSTR mBuf = nullptr;
+ if (!FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ nullptr, dwError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), mBuf, 0, nullptr))
+ BOOST_THROW_EXCEPTION(std::runtime_error("Failed to format error message, last error was: " + dwError));
+ else
+ BOOST_THROW_EXCEPTION(std::runtime_error(mBuf));
+ }
+
+ return fIsElevated;
+}
+#endif /* _WIN32 */
+
+/**
+ * Handler for unhandled exceptions.
+ */
+void Application::ExceptionHandler()
+{
+ if (l_InExceptionHandler)
+ for (;;)
+ Utility::Sleep(5);
+
+ l_InExceptionHandler = true;
+
+#ifndef _WIN32
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGABRT, &sa, nullptr);
+#endif /* _WIN32 */
+
+ String fname = GetCrashReportFilename();
+ String dirName = Utility::DirName(fname);
+
+ if (!Utility::PathExists(dirName)) {
+#ifndef _WIN32
+ if (mkdir(dirName.CStr(), 0700) < 0 && errno != EEXIST) {
+#else /*_ WIN32 */
+ if (mkdir(dirName.CStr()) < 0 && errno != EEXIST) {
+#endif /* _WIN32 */
+ std::cerr << "Could not create directory '" << dirName << "': Error " << errno << ", " << strerror(errno) << "\n";
+ }
+ }
+
+ bool interactive_debugger = Configuration::AttachDebugger;
+
+ if (!interactive_debugger) {
+ std::ofstream ofs;
+ ofs.open(fname.CStr());
+
+ ofs << "Caught unhandled exception.\n"
+ << "Current time: " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime()) << "\n\n";
+
+ DisplayInfoMessage(ofs);
+
+ try {
+ RethrowUncaughtException();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "Application")
+ << DiagnosticInformation(ex, false) << "\n"
+ << "\n"
+ << "Additional information is available in '" << fname << "'" << "\n";
+
+ /* On platforms where HAVE_CXXABI_H is defined, we prefer to print the stack trace that was saved
+ * when the last exception was thrown. Everywhere else, we do not have this information so we
+ * collect a stack trace here, which might lack some information, for example when an exception
+ * is rethrown, but this is still better than nothing.
+ */
+ boost::stacktrace::stacktrace *stack = nullptr;
+#ifndef HAVE_CXXABI_H
+ boost::stacktrace::stacktrace local_stack;
+ stack = &local_stack;
+#endif /* HAVE_CXXABI_H */
+
+ ofs << "\n"
+ << DiagnosticInformation(ex, true, stack)
+ << "\n";
+ }
+
+ DisplayBugMessage(ofs);
+
+ ofs.close();
+ }
+
+ AttachDebugger(fname, interactive_debugger);
+
+ abort();
+}
+
+#ifdef _WIN32
+LONG CALLBACK Application::SEHUnhandledExceptionFilter(PEXCEPTION_POINTERS exi)
+{
+ /* If an unhandled C++ exception occurs with both a termination handler (std::set_terminate()) and an unhandled
+ * SEH filter (SetUnhandledExceptionFilter()) set, the latter one is called. However, our termination handler is
+ * better suited for dealing with C++ exceptions. In this case, the SEH exception will have a specific code and
+ * we can just call the default filter function which will take care of calling the termination handler.
+ */
+ if (exi->ExceptionRecord->ExceptionCode == EXCEPTION_CODE_CXX_EXCEPTION) {
+ return l_DefaultUnhandledExceptionFilter(exi);
+ }
+
+ if (l_InExceptionHandler)
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ l_InExceptionHandler = true;
+
+ String fname = GetCrashReportFilename();
+ String dirName = Utility::DirName(fname);
+
+ if (!Utility::PathExists(dirName)) {
+#ifndef _WIN32
+ if (mkdir(dirName.CStr(), 0700) < 0 && errno != EEXIST) {
+#else /*_ WIN32 */
+ if (mkdir(dirName.CStr()) < 0 && errno != EEXIST) {
+#endif /* _WIN32 */
+ std::cerr << "Could not create directory '" << dirName << "': Error " << errno << ", " << strerror(errno) << "\n";
+ }
+ }
+
+ std::ofstream ofs;
+ ofs.open(fname.CStr());
+
+ Log(LogCritical, "Application")
+ << "Icinga 2 has terminated unexpectedly. Additional information can be found in '" << fname << "'";
+
+ ofs << "Caught unhandled SEH exception.\n"
+ << "Current time: " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime()) << "\n\n";
+
+ DisplayInfoMessage(ofs);
+
+ std::ios::fmtflags savedflags(ofs.flags());
+ ofs << std::showbase << std::hex
+ << "\nSEH exception:\n"
+ << " Code: " << exi->ExceptionRecord->ExceptionCode << "\n"
+ << " Address: " << exi->ExceptionRecord->ExceptionAddress << "\n"
+ << " Flags: " << exi->ExceptionRecord->ExceptionFlags << "\n";
+ ofs.flags(savedflags);
+
+ ofs << "\nStacktrace:\n" << StackTraceFormatter(boost::stacktrace::stacktrace()) << "\n";
+
+ DisplayBugMessage(ofs);
+
+ return EXCEPTION_CONTINUE_SEARCH;
+}
+#endif /* _WIN32 */
+
+/**
+ * Installs the exception handlers.
+ */
+void Application::InstallExceptionHandlers()
+{
+ std::set_terminate(&Application::ExceptionHandler);
+
+#ifndef _WIN32
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = &Application::SigAbrtHandler;
+ sa.sa_flags = SA_RESTART;
+ sigaction(SIGABRT, &sa, nullptr);
+#else /* _WIN32 */
+ l_DefaultUnhandledExceptionFilter = SetUnhandledExceptionFilter(&Application::SEHUnhandledExceptionFilter);
+#endif /* _WIN32 */
+}
+
+/**
+ * Runs the application.
+ *
+ * @returns The application's exit code.
+ */
+int Application::Run()
+{
+#ifndef _WIN32
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = &Application::SigUsr1Handler;
+ sa.sa_flags = SA_RESTART;
+ sigaction(SIGUSR1, &sa, nullptr);
+#else /* _WIN32 */
+ SetConsoleCtrlHandler(&Application::CtrlHandler, TRUE);
+#endif /* _WIN32 */
+
+#ifdef _WIN32
+ try {
+ UpdatePidFile(Configuration::PidPath);
+ } catch (const std::exception&) {
+ Log(LogCritical, "Application")
+ << "Cannot update PID file '" << Configuration::PidPath << "'. Aborting.";
+ return EXIT_FAILURE;
+ }
+#endif /* _WIN32 */
+
+ SetStartTime(Utility::GetTime());
+
+ return Main();
+}
+
+void Application::UpdatePidFile(const String& filename)
+{
+ UpdatePidFile(filename, Utility::GetPid());
+}
+
+/**
+ * Grabs the PID file lock and updates the PID. Terminates the application
+ * if the PID file is already locked by another instance of the application.
+ *
+ * @param filename The name of the PID file.
+ * @param pid The PID to write; default is the current PID
+ */
+void Application::UpdatePidFile(const String& filename, pid_t pid)
+{
+ ObjectLock olock(this);
+
+ if (m_PidFile)
+ fclose(m_PidFile);
+
+ /* There's just no sane way of getting a file descriptor for a
+ * C++ ofstream which is why we're using FILEs here. */
+ m_PidFile = fopen(filename.CStr(), "r+");
+
+ if (!m_PidFile)
+ m_PidFile = fopen(filename.CStr(), "w");
+
+ if (!m_PidFile) {
+ Log(LogCritical, "Application")
+ << "Could not open PID file '" << filename << "'.";
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not open PID file '" + filename + "'"));
+ }
+
+#ifndef _WIN32
+ int fd = fileno(m_PidFile);
+
+ Utility::SetCloExec(fd);
+
+ struct flock lock;
+
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+
+ if (fcntl(fd, F_SETLK, &lock) < 0) {
+ Log(LogCritical, "Application", "Could not lock PID file. Make sure that only one instance of the application is running.");
+
+ Application::Exit(EXIT_FAILURE);
+ }
+
+ if (ftruncate(fd, 0) < 0) {
+ Log(LogCritical, "Application")
+ << "ftruncate() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("ftruncate")
+ << boost::errinfo_errno(errno));
+ }
+#endif /* _WIN32 */
+
+ fprintf(m_PidFile, "%lu\n", (unsigned long)pid);
+ fflush(m_PidFile);
+}
+
+/**
+ * Closes the PID file. Does nothing if the PID file is not currently open.
+ */
+void Application::ClosePidFile(bool unlink)
+{
+ ObjectLock olock(this);
+
+ if (m_PidFile) {
+ if (unlink) {
+ String pidpath = Configuration::PidPath;
+ ::unlink(pidpath.CStr());
+ }
+
+ fclose(m_PidFile);
+ }
+
+ m_PidFile = nullptr;
+}
+
+/**
+ * Checks if another process currently owns the pidfile and read it
+ *
+ * @param filename The name of the PID file.
+ * @returns 0: no process owning the pidfile, pid of the process otherwise
+ */
+pid_t Application::ReadPidFile(const String& filename)
+{
+ FILE *pidfile = fopen(filename.CStr(), "r");
+
+ if (!pidfile)
+ return 0;
+
+#ifndef _WIN32
+ int fd = fileno(pidfile);
+
+ struct flock lock;
+
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+
+ if (fcntl(fd, F_GETLK, &lock) < 0) {
+ int error = errno;
+ fclose(pidfile);
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("fcntl")
+ << boost::errinfo_errno(error));
+ }
+
+ if (lock.l_type == F_UNLCK) {
+ // nobody has locked the file: no icinga running
+ fclose(pidfile);
+ return -1;
+ }
+#endif /* _WIN32 */
+
+ pid_t runningpid;
+ int res = fscanf(pidfile, "%d", &runningpid);
+ fclose(pidfile);
+
+ // bogus result?
+ if (res != 1)
+ return 0;
+
+#ifdef _WIN32
+ HANDLE hProcess = OpenProcess(0, FALSE, runningpid);
+
+ if (!hProcess)
+ return 0;
+
+ CloseHandle(hProcess);
+#endif /* _WIN32 */
+
+ return runningpid;
+}
+
+int Application::GetDefaultRLimitFiles()
+{
+ return 16 * 1024;
+}
+
+int Application::GetDefaultRLimitProcesses()
+{
+ return 16 * 1024;
+}
+
+int Application::GetDefaultRLimitStack()
+{
+ return 256 * 1024;
+}
+
+double Application::GetReloadTimeout()
+{
+ return ScriptGlobal::Get("ReloadTimeout");
+}
+
+/**
+ * Returns the global thread pool.
+ *
+ * @returns The global thread pool.
+ */
+ThreadPool& Application::GetTP()
+{
+ static ThreadPool tp;
+ return tp;
+}
+
+double Application::GetStartTime()
+{
+ return m_StartTime;
+}
+
+void Application::SetStartTime(double ts)
+{
+ m_StartTime = ts;
+}
+
+double Application::GetUptime()
+{
+ return Utility::GetTime() - m_StartTime;
+}
+
+bool Application::GetScriptDebuggerEnabled()
+{
+ return m_ScriptDebuggerEnabled;
+}
+
+void Application::SetScriptDebuggerEnabled(bool enabled)
+{
+ m_ScriptDebuggerEnabled = enabled;
+}
+
+double Application::GetLastReloadFailed()
+{
+#ifdef _WIN32
+ return m_LastReloadFailed;
+#else /* _WIN32 */
+ return m_LastReloadFailed.Get().load();
+#endif /* _WIN32 */
+}
+
+void Application::SetLastReloadFailed(double ts)
+{
+#ifdef _WIN32
+ m_LastReloadFailed = ts;
+#else /* _WIN32 */
+ m_LastReloadFailed.Get().store(ts);
+#endif /* _WIN32 */
+}
+
+void Application::ValidateName(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Application>::ValidateName(lvalue, utils);
+
+ if (lvalue() != "app")
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "name" }, "Application object must be named 'app'."));
+}
diff --git a/lib/base/application.hpp b/lib/base/application.hpp
new file mode 100644
index 0000000..f45c8bd
--- /dev/null
+++ b/lib/base/application.hpp
@@ -0,0 +1,170 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APPLICATION_H
+#define APPLICATION_H
+
+#include "base/i2-base.hpp"
+#include "base/atomic.hpp"
+#include "base/application-ti.hpp"
+#include "base/logger.hpp"
+#include "base/configuration.hpp"
+#include "base/shared-memory.hpp"
+#include <cstdint>
+#include <iosfwd>
+#include <type_traits>
+
+namespace icinga
+{
+
+class ThreadPool;
+
+/**
+ * Abstract base class for applications.
+ *
+ * @ingroup base
+ */
+class Application : public ObjectImpl<Application> {
+public:
+ DECLARE_OBJECT(Application);
+
+ static boost::signals2::signal<void ()> OnReopenLogs;
+
+ ~Application() override;
+
+ static void InitializeBase();
+ static void UninitializeBase();
+
+ static Application::Ptr GetInstance();
+
+ static void Exit(int rc);
+
+ int Run();
+
+ /**
+ * Starts the application.
+ *
+ * @returns The exit code of the application.
+ */
+ virtual int Main() = 0;
+
+ static void SetResourceLimits();
+
+ static int GetArgC();
+ static void SetArgC(int argc);
+
+ static char **GetArgV();
+ static void SetArgV(char **argv);
+
+ static void InstallExceptionHandlers();
+
+ static void RequestShutdown();
+ static void RequestRestart();
+ static void RequestReopenLogs();
+
+#ifndef _WIN32
+ static void SetUmbrellaProcess(pid_t pid);
+#endif /* _WIN32 */
+
+ static bool IsShuttingDown();
+ static bool IsRestarting();
+
+ static void SetDebuggingSeverity(LogSeverity severity);
+ static LogSeverity GetDebuggingSeverity();
+
+ void UpdatePidFile(const String& filename);
+ void UpdatePidFile(const String& filename, pid_t pid);
+ void ClosePidFile(bool unlink);
+ static pid_t ReadPidFile(const String& filename);
+
+ static String GetExePath(const String& argv0);
+
+#ifdef _WIN32
+ static bool IsProcessElevated();
+#endif /* _WIN32 */
+
+ static int GetDefaultRLimitFiles();
+ static int GetDefaultRLimitProcesses();
+ static int GetDefaultRLimitStack();
+
+ static double GetReloadTimeout();
+
+ static ThreadPool& GetTP();
+
+ static String GetAppVersion();
+ static String GetAppSpecVersion();
+
+ static String GetAppEnvironment();
+ static void SetAppEnvironment(const String& name);
+
+ static double GetStartTime();
+ static void SetStartTime(double ts);
+
+ static double GetUptime();
+
+ static bool GetScriptDebuggerEnabled();
+ static void SetScriptDebuggerEnabled(bool enabled);
+
+ static double GetLastReloadFailed();
+ static void SetLastReloadFailed(double ts);
+
+ static void DisplayInfoMessage(std::ostream& os, bool skipVersion = false);
+
+protected:
+ void OnConfigLoaded() override;
+ void Stop(bool runtimeRemoved) override;
+
+ void RunEventLoop();
+
+ pid_t StartReloadProcess();
+
+ virtual void OnShutdown();
+
+ void ValidateName(const Lazy<String>& lvalue, const ValidationUtils& utils) final;
+
+private:
+ static Application::Ptr m_Instance; /**< The application instance. */
+
+ static bool m_ShuttingDown; /**< Whether the application is in the process of shutting down. */
+ static bool m_RequestRestart; /**< A restart was requested through SIGHUP */
+ static pid_t m_ReloadProcess; /**< The PID of a subprocess doing a reload, only valid when l_Restarting==true */
+ static bool m_RequestReopenLogs; /**< Whether we should re-open log files. */
+
+#ifndef _WIN32
+ static pid_t m_UmbrellaProcess; /**< The PID of the Icinga umbrella process */
+#endif /* _WIN32 */
+
+ static int m_ArgC; /**< The number of command-line arguments. */
+ static char **m_ArgV; /**< Command-line arguments. */
+ FILE *m_PidFile = nullptr; /**< The PID file */
+ static bool m_Debugging; /**< Whether debugging is enabled. */
+ static LogSeverity m_DebuggingSeverity; /**< Whether debugging severity is set. */
+ static double m_StartTime;
+ static double m_MainTime;
+ static bool m_ScriptDebuggerEnabled;
+#ifdef _WIN32
+ static double m_LastReloadFailed;
+#else /* _WIN32 */
+ typedef Atomic<std::conditional_t<Atomic<double>::is_always_lock_free, double, uint32_t>> AtomicTs;
+ static_assert(AtomicTs::is_always_lock_free);
+ static SharedMemory<AtomicTs> m_LastReloadFailed;
+#endif /* _WIN32 */
+
+#ifdef _WIN32
+ static BOOL WINAPI CtrlHandler(DWORD type);
+ static LONG WINAPI SEHUnhandledExceptionFilter(PEXCEPTION_POINTERS exi);
+#endif /* _WIN32 */
+
+ static void DisplayBugMessage(std::ostream& os);
+
+ static void SigAbrtHandler(int signum);
+ static void SigUsr1Handler(int signum);
+ static void ExceptionHandler();
+
+ static String GetCrashReportFilename();
+
+ static void AttachDebugger(const String& filename, bool interactive);
+};
+
+}
+
+#endif /* APPLICATION_H */
diff --git a/lib/base/application.ti b/lib/base/application.ti
new file mode 100644
index 0000000..3d5908a
--- /dev/null
+++ b/lib/base/application.ti
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library base;
+
+namespace icinga
+{
+
+abstract class Application : ConfigObject
+{
+};
+
+}
diff --git a/lib/base/array-script.cpp b/lib/base/array-script.cpp
new file mode 100644
index 0000000..a976683
--- /dev/null
+++ b/lib/base/array-script.cpp
@@ -0,0 +1,260 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/array.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+static double ArrayLen()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->GetLength();
+}
+
+static void ArraySet(int index, const Value& value)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Set(index, value);
+}
+
+static Value ArrayGet(int index)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Get(index);
+}
+
+static void ArrayAdd(const Value& value)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Add(value);
+}
+
+static void ArrayRemove(int index)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Remove(index);
+}
+
+static bool ArrayContains(const Value& value)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Contains(value);
+}
+
+static void ArrayClear()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Clear();
+}
+
+static Array::Ptr ArraySort(const std::vector<Value>& args)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ Array::Ptr arr = self->ShallowClone();
+
+ if (args.empty()) {
+ ObjectLock olock(arr);
+ std::sort(arr->Begin(), arr->End());
+ } else {
+ Function::Ptr function = args[0];
+ REQUIRE_NOT_NULL(function);
+
+ if (vframe->Sandboxed && !function->IsSideEffectFree())
+ BOOST_THROW_EXCEPTION(ScriptError("Sort function must be side-effect free."));
+
+ ObjectLock olock(arr);
+ std::sort(arr->Begin(), arr->End(), [&args](const Value& a, const Value& b) -> bool {
+ Function::Ptr cmp = args[0];
+ return cmp->Invoke({ a, b });
+ });
+ }
+
+ return arr;
+}
+
+static Array::Ptr ArrayShallowClone()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->ShallowClone();
+}
+
+static Value ArrayJoin(const Value& separator)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Join(separator);
+}
+
+static Array::Ptr ArrayReverse()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Reverse();
+}
+
+static Array::Ptr ArrayMap(const Function::Ptr& function)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ REQUIRE_NOT_NULL(function);
+
+ if (vframe->Sandboxed && !function->IsSideEffectFree())
+ BOOST_THROW_EXCEPTION(ScriptError("Map function must be side-effect free."));
+
+ ArrayData result;
+
+ ObjectLock olock(self);
+ for (const Value& item : self) {
+ result.push_back(function->Invoke({ item }));
+ }
+
+ return new Array(std::move(result));
+}
+
+static Value ArrayReduce(const Function::Ptr& function)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ REQUIRE_NOT_NULL(function);
+
+ if (vframe->Sandboxed && !function->IsSideEffectFree())
+ BOOST_THROW_EXCEPTION(ScriptError("Reduce function must be side-effect free."));
+
+ if (self->GetLength() == 0)
+ return Empty;
+
+ Value result = self->Get(0);
+
+ ObjectLock olock(self);
+ for (size_t i = 1; i < self->GetLength(); i++) {
+ result = function->Invoke({ result, self->Get(i) });
+ }
+
+ return result;
+}
+
+static Array::Ptr ArrayFilter(const Function::Ptr& function)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ REQUIRE_NOT_NULL(function);
+
+ if (vframe->Sandboxed && !function->IsSideEffectFree())
+ BOOST_THROW_EXCEPTION(ScriptError("Filter function must be side-effect free."));
+
+ ArrayData result;
+
+ ObjectLock olock(self);
+ for (const Value& item : self) {
+ if (function->Invoke({ item }))
+ result.push_back(item);
+ }
+
+ return new Array(std::move(result));
+}
+
+static bool ArrayAny(const Function::Ptr& function)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ REQUIRE_NOT_NULL(function);
+
+ if (vframe->Sandboxed && !function->IsSideEffectFree())
+ BOOST_THROW_EXCEPTION(ScriptError("Filter function must be side-effect free."));
+
+ ObjectLock olock(self);
+ for (const Value& item : self) {
+ if (function->Invoke({ item }))
+ return true;
+ }
+
+ return false;
+}
+
+static bool ArrayAll(const Function::Ptr& function)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ REQUIRE_NOT_NULL(function);
+
+ if (vframe->Sandboxed && !function->IsSideEffectFree())
+ BOOST_THROW_EXCEPTION(ScriptError("Filter function must be side-effect free."));
+
+ ObjectLock olock(self);
+ for (const Value& item : self) {
+ if (!function->Invoke({ item }))
+ return false;
+ }
+
+ return true;
+}
+static Array::Ptr ArrayUnique()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Unique();
+}
+
+static void ArrayFreeze()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Array::Ptr self = static_cast<Array::Ptr>(vframe->Self);
+ self->Freeze();
+}
+
+Object::Ptr Array::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "len", new Function("Array#len", ArrayLen, {}, true) },
+ { "set", new Function("Array#set", ArraySet, { "index", "value" }) },
+ { "get", new Function("Array#get", ArrayGet, { "index" }) },
+ { "add", new Function("Array#add", ArrayAdd, { "value" }) },
+ { "remove", new Function("Array#remove", ArrayRemove, { "index" }) },
+ { "contains", new Function("Array#contains", ArrayContains, { "value" }, true) },
+ { "clear", new Function("Array#clear", ArrayClear) },
+ { "sort", new Function("Array#sort", ArraySort, { "less_cmp" }, true) },
+ { "shallow_clone", new Function("Array#shallow_clone", ArrayShallowClone, {}, true) },
+ { "join", new Function("Array#join", ArrayJoin, { "separator" }, true) },
+ { "reverse", new Function("Array#reverse", ArrayReverse, {}, true) },
+ { "map", new Function("Array#map", ArrayMap, { "func" }, true) },
+ { "reduce", new Function("Array#reduce", ArrayReduce, { "reduce" }, true) },
+ { "filter", new Function("Array#filter", ArrayFilter, { "func" }, true) },
+ { "any", new Function("Array#any", ArrayAny, { "func" }, true) },
+ { "all", new Function("Array#all", ArrayAll, { "func" }, true) },
+ { "unique", new Function("Array#unique", ArrayUnique, {}, true) },
+ { "freeze", new Function("Array#freeze", ArrayFreeze, {}) }
+ });
+
+ return prototype;
+}
diff --git a/lib/base/array.cpp b/lib/base/array.cpp
new file mode 100644
index 0000000..08e06fa
--- /dev/null
+++ b/lib/base/array.cpp
@@ -0,0 +1,380 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include "base/debug.hpp"
+#include "base/primitivetype.hpp"
+#include "base/dictionary.hpp"
+#include "base/configwriter.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+template class std::vector<Value>;
+
+REGISTER_PRIMITIVE_TYPE(Array, Object, Array::GetPrototype());
+
+Array::Array(const ArrayData& other)
+ : m_Data(other)
+{ }
+
+Array::Array(ArrayData&& other)
+ : m_Data(std::move(other))
+{ }
+
+Array::Array(std::initializer_list<Value> init)
+ : m_Data(init)
+{ }
+
+/**
+ * Restrieves a value from an array.
+ *
+ * @param index The index.
+ * @returns The value.
+ */
+Value Array::Get(SizeType index) const
+{
+ ObjectLock olock(this);
+
+ return m_Data.at(index);
+}
+
+/**
+ * Sets a value in the array.
+ *
+ * @param index The index.
+ * @param value The value.
+ * @param overrideFrozen Whether to allow modifying frozen arrays.
+ */
+void Array::Set(SizeType index, const Value& value, bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Value in array must not be modified."));
+
+ m_Data.at(index) = value;
+}
+
+/**
+ * Sets a value in the array.
+ *
+ * @param index The index.
+ * @param value The value.
+ * @param overrideFrozen Whether to allow modifying frozen arrays.
+ */
+void Array::Set(SizeType index, Value&& value, bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ m_Data.at(index).Swap(value);
+}
+
+/**
+ * Adds a value to the array.
+ *
+ * @param value The value.
+ * @param overrideFrozen Whether to allow modifying frozen arrays.
+ */
+void Array::Add(Value value, bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ m_Data.push_back(std::move(value));
+}
+
+/**
+ * Returns an iterator to the beginning of the array.
+ *
+ * Note: Caller must hold the object lock while using the iterator.
+ *
+ * @returns An iterator.
+ */
+Array::Iterator Array::Begin()
+{
+ ASSERT(OwnsLock());
+
+ return m_Data.begin();
+}
+
+/**
+ * Returns an iterator to the end of the array.
+ *
+ * Note: Caller must hold the object lock while using the iterator.
+ *
+ * @returns An iterator.
+ */
+Array::Iterator Array::End()
+{
+ ASSERT(OwnsLock());
+
+ return m_Data.end();
+}
+
+/**
+ * Returns the number of elements in the array.
+ *
+ * @returns Number of elements.
+ */
+size_t Array::GetLength() const
+{
+ ObjectLock olock(this);
+
+ return m_Data.size();
+}
+
+/**
+ * Checks whether the array contains the specified value.
+ *
+ * @param value The value.
+ * @returns true if the array contains the value, false otherwise.
+ */
+bool Array::Contains(const Value& value) const
+{
+ ObjectLock olock(this);
+
+ return (std::find(m_Data.begin(), m_Data.end(), value) != m_Data.end());
+}
+
+/**
+ * Insert the given value at the specified index
+ *
+ * @param index The index
+ * @param value The value to add
+ * @param overrideFrozen Whether to allow modifying frozen arrays.
+ */
+void Array::Insert(SizeType index, Value value, bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ ASSERT(index <= m_Data.size());
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ m_Data.insert(m_Data.begin() + index, std::move(value));
+}
+
+/**
+ * Removes the specified index from the array.
+ *
+ * @param index The index.
+ * @param overrideFrozen Whether to allow modifying frozen arrays.
+ */
+void Array::Remove(SizeType index, bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ if (index >= m_Data.size())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Index to remove must be within bounds."));
+
+ m_Data.erase(m_Data.begin() + index);
+}
+
+/**
+ * Removes the item specified by the iterator from the array.
+ *
+ * @param it The iterator.
+ * @param overrideFrozen Whether to allow modifying frozen arrays.
+ */
+void Array::Remove(Array::Iterator it, bool overrideFrozen)
+{
+ ASSERT(OwnsLock());
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ m_Data.erase(it);
+}
+
+void Array::Resize(SizeType newSize, bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ m_Data.resize(newSize);
+}
+
+void Array::Clear(bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ m_Data.clear();
+}
+
+void Array::Reserve(SizeType newSize, bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ m_Data.reserve(newSize);
+}
+
+void Array::CopyTo(const Array::Ptr& dest) const
+{
+ ObjectLock olock(this);
+ ObjectLock xlock(dest);
+
+ if (dest->m_Frozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ std::copy(m_Data.begin(), m_Data.end(), std::back_inserter(dest->m_Data));
+}
+
+/**
+ * Makes a shallow copy of an array.
+ *
+ * @returns a copy of the array.
+ */
+Array::Ptr Array::ShallowClone() const
+{
+ Array::Ptr clone = new Array();
+ CopyTo(clone);
+ return clone;
+}
+
+/**
+ * Makes a deep clone of an array
+ * and its elements.
+ *
+ * @returns a copy of the array.
+ */
+Object::Ptr Array::Clone() const
+{
+ ArrayData arr;
+
+ ObjectLock olock(this);
+ for (const Value& val : m_Data) {
+ arr.push_back(val.Clone());
+ }
+
+ return new Array(std::move(arr));
+}
+
+Array::Ptr Array::Reverse() const
+{
+ Array::Ptr result = new Array();
+
+ ObjectLock olock(this);
+ ObjectLock xlock(result);
+
+ std::copy(m_Data.rbegin(), m_Data.rend(), std::back_inserter(result->m_Data));
+
+ return result;
+}
+
+void Array::Sort(bool overrideFrozen)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Array must not be modified."));
+
+ std::sort(m_Data.begin(), m_Data.end());
+}
+
+String Array::ToString() const
+{
+ std::ostringstream msgbuf;
+ ConfigWriter::EmitArray(msgbuf, 1, const_cast<Array *>(this));
+ return msgbuf.str();
+}
+
+Value Array::Join(const Value& separator) const
+{
+ Value result;
+ bool first = true;
+
+ ObjectLock olock(this);
+
+ for (const Value& item : m_Data) {
+ if (first) {
+ first = false;
+ } else {
+ result = result + separator;
+ }
+
+ result = result + item;
+ }
+
+ return result;
+}
+
+Array::Ptr Array::Unique() const
+{
+ std::set<Value> result;
+
+ ObjectLock olock(this);
+
+ for (const Value& item : m_Data) {
+ result.insert(item);
+ }
+
+ return Array::FromSet(result);
+}
+
+void Array::Freeze()
+{
+ ObjectLock olock(this);
+ m_Frozen = true;
+}
+
+Value Array::GetFieldByName(const String& field, bool sandboxed, const DebugInfo& debugInfo) const
+{
+ int index;
+
+ try {
+ index = Convert::ToLong(field);
+ } catch (...) {
+ return Object::GetFieldByName(field, sandboxed, debugInfo);
+ }
+
+ ObjectLock olock(this);
+
+ if (index < 0 || static_cast<size_t>(index) >= GetLength())
+ BOOST_THROW_EXCEPTION(ScriptError("Array index '" + Convert::ToString(index) + "' is out of bounds.", debugInfo));
+
+ return Get(index);
+}
+
+void Array::SetFieldByName(const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo)
+{
+ ObjectLock olock(this);
+
+ int index = Convert::ToLong(field);
+
+ if (index < 0)
+ BOOST_THROW_EXCEPTION(ScriptError("Array index '" + Convert::ToString(index) + "' is out of bounds.", debugInfo));
+
+ if (static_cast<size_t>(index) >= GetLength())
+ Resize(index + 1, overrideFrozen);
+
+ Set(index, value, overrideFrozen);
+}
+
+Array::Iterator icinga::begin(const Array::Ptr& x)
+{
+ return x->Begin();
+}
+
+Array::Iterator icinga::end(const Array::Ptr& x)
+{
+ return x->End();
+}
diff --git a/lib/base/array.hpp b/lib/base/array.hpp
new file mode 100644
index 0000000..2c9a9dd
--- /dev/null
+++ b/lib/base/array.hpp
@@ -0,0 +1,117 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ARRAY_H
+#define ARRAY_H
+
+#include "base/i2-base.hpp"
+#include "base/objectlock.hpp"
+#include "base/value.hpp"
+#include <boost/range/iterator.hpp>
+#include <vector>
+#include <set>
+
+namespace icinga
+{
+
+typedef std::vector<Value> ArrayData;
+
+/**
+ * An array of Value items.
+ *
+ * @ingroup base
+ */
+class Array final : public Object
+{
+public:
+ DECLARE_OBJECT(Array);
+
+ /**
+ * An iterator that can be used to iterate over array elements.
+ */
+ typedef std::vector<Value>::iterator Iterator;
+
+ typedef std::vector<Value>::size_type SizeType;
+
+ Array() = default;
+ Array(const ArrayData& other);
+ Array(ArrayData&& other);
+ Array(std::initializer_list<Value> init);
+
+ Value Get(SizeType index) const;
+ void Set(SizeType index, const Value& value, bool overrideFrozen = false);
+ void Set(SizeType index, Value&& value, bool overrideFrozen = false);
+ void Add(Value value, bool overrideFrozen = false);
+
+ Iterator Begin();
+ Iterator End();
+
+ size_t GetLength() const;
+ bool Contains(const Value& value) const;
+
+ void Insert(SizeType index, Value value, bool overrideFrozen = false);
+ void Remove(SizeType index, bool overrideFrozen = false);
+ void Remove(Iterator it, bool overrideFrozen = false);
+
+ void Resize(SizeType newSize, bool overrideFrozen = false);
+ void Clear(bool overrideFrozen = false);
+
+ void Reserve(SizeType newSize, bool overrideFrozen = false);
+
+ void CopyTo(const Array::Ptr& dest) const;
+ Array::Ptr ShallowClone() const;
+
+ static Object::Ptr GetPrototype();
+
+ template<typename T>
+ static Array::Ptr FromVector(const std::vector<T>& v)
+ {
+ Array::Ptr result = new Array();
+ ObjectLock olock(result);
+ std::copy(v.begin(), v.end(), std::back_inserter(result->m_Data));
+ return result;
+ }
+
+ template<typename T>
+ std::set<T> ToSet()
+ {
+ ObjectLock olock(this);
+ return std::set<T>(Begin(), End());
+ }
+
+ template<typename T>
+ static Array::Ptr FromSet(const std::set<T>& v)
+ {
+ Array::Ptr result = new Array();
+ ObjectLock olock(result);
+ std::copy(v.begin(), v.end(), std::back_inserter(result->m_Data));
+ return result;
+ }
+
+ Object::Ptr Clone() const override;
+
+ Array::Ptr Reverse() const;
+
+ void Sort(bool overrideFrozen = false);
+
+ String ToString() const override;
+ Value Join(const Value& separator) const;
+
+ Array::Ptr Unique() const;
+ void Freeze();
+
+ Value GetFieldByName(const String& field, bool sandboxed, const DebugInfo& debugInfo) const override;
+ void SetFieldByName(const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) override;
+
+private:
+ std::vector<Value> m_Data; /**< The data for the array. */
+ bool m_Frozen{false};
+};
+
+Array::Iterator begin(const Array::Ptr& x);
+Array::Iterator end(const Array::Ptr& x);
+
+}
+
+extern template class std::vector<icinga::Value>;
+
+#endif /* ARRAY_H */
diff --git a/lib/base/atomic-file.cpp b/lib/base/atomic-file.cpp
new file mode 100644
index 0000000..762f384
--- /dev/null
+++ b/lib/base/atomic-file.cpp
@@ -0,0 +1,123 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#include "base/atomic-file.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include <utility>
+
+#ifdef _WIN32
+# include <io.h>
+# include <windows.h>
+#else /* _WIN32 */
+# include <errno.h>
+# include <unistd.h>
+#endif /* _WIN32 */
+
+using namespace icinga;
+
+void AtomicFile::Write(String path, int mode, const String& content)
+{
+ AtomicFile af (path, mode);
+ af << content;
+ af.Commit();
+}
+
+AtomicFile::AtomicFile(String path, int mode) : m_Path(std::move(path))
+{
+ m_TempFilename = m_Path + ".tmp.XXXXXX";
+
+#ifdef _WIN32
+ m_Fd = Utility::MksTemp(&m_TempFilename[0]);
+#else /* _WIN32 */
+ m_Fd = mkstemp(&m_TempFilename[0]);
+#endif /* _WIN32 */
+
+ if (m_Fd < 0) {
+ auto error (errno);
+
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("mkstemp")
+ << boost::errinfo_errno(error)
+ << boost::errinfo_file_name(m_TempFilename));
+ }
+
+ try {
+ exceptions(failbit | badbit);
+
+ open(boost::iostreams::file_descriptor(
+ m_Fd,
+ // Rationale: https://github.com/boostorg/iostreams/issues/152
+ boost::iostreams::never_close_handle
+ ));
+
+ if (chmod(m_TempFilename.CStr(), mode) < 0) {
+ auto error (errno);
+
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("chmod")
+ << boost::errinfo_errno(error)
+ << boost::errinfo_file_name(m_TempFilename));
+ }
+ } catch (...) {
+ if (is_open()) {
+ close();
+ }
+
+ (void)::close(m_Fd);
+ (void)unlink(m_TempFilename.CStr());
+ throw;
+ }
+}
+
+AtomicFile::~AtomicFile()
+{
+ if (is_open()) {
+ try {
+ close();
+ } catch (...) {
+ // Destructor must not throw
+ }
+ }
+
+ if (m_Fd >= 0) {
+ (void)::close(m_Fd);
+ }
+
+ if (!m_TempFilename.IsEmpty()) {
+ (void)unlink(m_TempFilename.CStr());
+ }
+}
+
+void AtomicFile::Commit()
+{
+ flush();
+
+ auto h ((*this)->handle());
+
+#ifdef _WIN32
+ if (!FlushFileBuffers(h)) {
+ auto err (GetLastError());
+
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("FlushFileBuffers")
+ << errinfo_win32_error(err)
+ << boost::errinfo_file_name(m_TempFilename));
+ }
+#else /* _WIN32 */
+ if (fsync(h)) {
+ auto err (errno);
+
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("fsync")
+ << boost::errinfo_errno(err)
+ << boost::errinfo_file_name(m_TempFilename));
+ }
+#endif /* _WIN32 */
+
+ close();
+ (void)::close(m_Fd);
+ m_Fd = -1;
+
+ Utility::RenameFile(m_TempFilename, m_Path);
+ m_TempFilename = "";
+}
diff --git a/lib/base/atomic-file.hpp b/lib/base/atomic-file.hpp
new file mode 100644
index 0000000..c5c7897
--- /dev/null
+++ b/lib/base/atomic-file.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#ifndef ATOMIC_FILE_H
+#define ATOMIC_FILE_H
+
+#include "base/string.hpp"
+#include <boost/iostreams/device/file_descriptor.hpp>
+#include <boost/iostreams/stream.hpp>
+
+namespace icinga
+{
+
+/**
+ * Atomically replaces a file's content.
+ *
+ * @ingroup base
+ */
+class AtomicFile : public boost::iostreams::stream<boost::iostreams::file_descriptor>
+{
+public:
+ static void Write(String path, int mode, const String& content);
+
+ AtomicFile(String path, int mode);
+ ~AtomicFile();
+
+ inline const String& GetTempFilename() const noexcept
+ {
+ return m_TempFilename;
+ }
+
+ void Commit();
+
+private:
+ String m_Path;
+ String m_TempFilename;
+ int m_Fd;
+};
+
+}
+
+#endif /* ATOMIC_FILE_H */
diff --git a/lib/base/atomic.hpp b/lib/base/atomic.hpp
new file mode 100644
index 0000000..c8f169c
--- /dev/null
+++ b/lib/base/atomic.hpp
@@ -0,0 +1,91 @@
+/* Icinga 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+#ifndef ATOMIC_H
+#define ATOMIC_H
+
+#include <atomic>
+#include <mutex>
+#include <type_traits>
+#include <utility>
+
+namespace icinga
+{
+
+/**
+ * Extends std::atomic with an atomic constructor.
+ *
+ * @ingroup base
+ */
+template<class T>
+class Atomic : public std::atomic<T> {
+public:
+ /**
+ * Like std::atomic#atomic, but operates atomically
+ *
+ * @param desired Initial value
+ */
+ inline Atomic(T desired)
+ {
+ this->store(desired);
+ }
+
+ /**
+ * Like std::atomic#atomic, but operates atomically
+ *
+ * @param desired Initial value
+ * @param order Initial store operation's memory order
+ */
+ inline Atomic(T desired, std::memory_order order)
+ {
+ this->store(desired, order);
+ }
+};
+
+/**
+ * Wraps any T into a std::atomic<T>-like interface that locks using a mutex.
+ *
+ * In contrast to std::atomic<T>, Locked<T> is also valid for types that are not trivially copyable.
+ * In case T is trivially copyable, std::atomic<T> is almost certainly the better choice.
+ *
+ * @ingroup base
+ */
+template<typename T>
+class Locked
+{
+public:
+ inline T load() const
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_Value;
+ }
+
+ inline void store(T desired)
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ m_Value = std::move(desired);
+ }
+
+private:
+ mutable std::mutex m_Mutex;
+ T m_Value;
+};
+
+/**
+ * Type alias for std::atomic<T> if possible, otherwise Locked<T> is used as a fallback.
+ *
+ * @ingroup base
+ */
+template <typename T>
+using AtomicOrLocked =
+#if defined(__GNUC__) && __GNUC__ < 5
+ // GCC does not implement std::is_trivially_copyable until version 5.
+ typename std::conditional<std::is_fundamental<T>::value || std::is_pointer<T>::value, std::atomic<T>, Locked<T>>::type;
+#else /* defined(__GNUC__) && __GNUC__ < 5 */
+ typename std::conditional<std::is_trivially_copyable<T>::value, std::atomic<T>, Locked<T>>::type;
+#endif /* defined(__GNUC__) && __GNUC__ < 5 */
+
+}
+
+#endif /* ATOMIC_H */
diff --git a/lib/base/base64.cpp b/lib/base/base64.cpp
new file mode 100644
index 0000000..42999c3
--- /dev/null
+++ b/lib/base/base64.cpp
@@ -0,0 +1,53 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/base64.hpp"
+#include <openssl/bio.h>
+#include <openssl/evp.h>
+#include <openssl/buffer.h>
+#include <sstream>
+
+using namespace icinga;
+
+String Base64::Encode(const String& input)
+{
+ BIO *biomem = BIO_new(BIO_s_mem());
+ BIO *bio64 = BIO_new(BIO_f_base64());
+ BIO_push(bio64, biomem);
+ BIO_set_flags(bio64, BIO_FLAGS_BASE64_NO_NL);
+ BIO_write(bio64, input.CStr(), input.GetLength());
+ (void) BIO_flush(bio64);
+
+ char *outbuf;
+ long len = BIO_get_mem_data(biomem, &outbuf);
+
+ String ret = String(outbuf, outbuf + len);
+ BIO_free_all(bio64);
+
+ return ret;
+}
+
+String Base64::Decode(const String& input)
+{
+ BIO *biomem = BIO_new_mem_buf(
+ const_cast<char*>(input.CStr()), input.GetLength());
+ BIO *bio64 = BIO_new(BIO_f_base64());
+ BIO_push(bio64, biomem);
+ BIO_set_flags(bio64, BIO_FLAGS_BASE64_NO_NL);
+
+ auto *outbuf = new char[input.GetLength()];
+
+ size_t len = 0;
+ int rc;
+
+ while ((rc = BIO_read(bio64, outbuf + len, input.GetLength() - len)) > 0)
+ len += rc;
+
+ String ret = String(outbuf, outbuf + len);
+ BIO_free_all(bio64);
+ delete [] outbuf;
+
+ if (ret.IsEmpty() && !input.IsEmpty())
+ throw std::invalid_argument("Not a valid base64 string");
+
+ return ret;
+}
diff --git a/lib/base/base64.hpp b/lib/base/base64.hpp
new file mode 100644
index 0000000..8abbdbf
--- /dev/null
+++ b/lib/base/base64.hpp
@@ -0,0 +1,25 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef BASE64_H
+#define BASE64_H
+
+#include "remote/i2-remote.hpp"
+#include "base/string.hpp"
+
+namespace icinga
+{
+
+/**
+ * Base64
+ *
+ * @ingroup remote
+ */
+struct Base64
+{
+ static String Decode(const String& data);
+ static String Encode(const String& data);
+};
+
+}
+
+#endif /* BASE64_H */
diff --git a/lib/base/boolean-script.cpp b/lib/base/boolean-script.cpp
new file mode 100644
index 0000000..a9167ca
--- /dev/null
+++ b/lib/base/boolean-script.cpp
@@ -0,0 +1,26 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/boolean.hpp"
+#include "base/convert.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+
+using namespace icinga;
+
+static String BooleanToString()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ bool self = vframe->Self;
+ return self ? "true" : "false";
+}
+
+Object::Ptr Boolean::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "to_string", new Function("Boolean#to_string", BooleanToString, {}, true) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/boolean.cpp b/lib/base/boolean.cpp
new file mode 100644
index 0000000..683a727
--- /dev/null
+++ b/lib/base/boolean.cpp
@@ -0,0 +1,9 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/boolean.hpp"
+#include "base/primitivetype.hpp"
+
+using namespace icinga;
+
+REGISTER_BUILTIN_TYPE(Boolean, Boolean::GetPrototype());
+
diff --git a/lib/base/boolean.hpp b/lib/base/boolean.hpp
new file mode 100644
index 0000000..6533cb4
--- /dev/null
+++ b/lib/base/boolean.hpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef BOOLEAN_H
+#define BOOLEAN_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+
+namespace icinga {
+
+class Value;
+
+/**
+ * Boolean class.
+ */
+class Boolean
+{
+public:
+ static Object::Ptr GetPrototype();
+
+private:
+ Boolean();
+};
+
+}
+
+#endif /* BOOLEAN_H */
diff --git a/lib/base/bulker.hpp b/lib/base/bulker.hpp
new file mode 100644
index 0000000..2c30dc3
--- /dev/null
+++ b/lib/base/bulker.hpp
@@ -0,0 +1,119 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#ifndef BULKER_H
+#define BULKER_H
+
+#include <boost/config.hpp>
+#include <chrono>
+#include <condition_variable>
+#include <mutex>
+#include <queue>
+#include <utility>
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * A queue which outputs the input as bulks of a defined size
+ * or after a defined time, whichever is reached first
+ *
+ * @ingroup base
+ */
+template<class T>
+class Bulker
+{
+private:
+ typedef std::chrono::steady_clock Clock;
+
+public:
+ typedef std::vector<T> Container;
+ typedef typename Container::size_type SizeType;
+ typedef typename Clock::duration Duration;
+
+ Bulker(SizeType bulkSize, Duration threshold)
+ : m_BulkSize(bulkSize), m_Threshold(threshold), m_NextConsumption(NullTimePoint()) { }
+
+ void ProduceOne(T needle);
+ Container ConsumeMany();
+ SizeType Size();
+
+ inline SizeType GetBulkSize() const noexcept
+ {
+ return m_BulkSize;
+ }
+
+private:
+ typedef std::chrono::time_point<Clock> TimePoint;
+
+ static inline
+ TimePoint NullTimePoint()
+ {
+ return TimePoint::min();
+ }
+
+ inline void UpdateNextConsumption()
+ {
+ m_NextConsumption = Clock::now() + m_Threshold;
+ }
+
+ const SizeType m_BulkSize;
+ const Duration m_Threshold;
+
+ std::mutex m_Mutex;
+ std::condition_variable m_CV;
+ std::queue<Container> m_Bulks;
+ TimePoint m_NextConsumption;
+};
+
+template<class T>
+void Bulker<T>::ProduceOne(T needle)
+{
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ if (m_Bulks.empty() || m_Bulks.back().size() == m_BulkSize) {
+ m_Bulks.emplace();
+ }
+
+ m_Bulks.back().emplace_back(std::move(needle));
+
+ if (m_Bulks.size() == 1u && m_Bulks.back().size() == m_BulkSize) {
+ m_CV.notify_one();
+ }
+}
+
+template<class T>
+typename Bulker<T>::Container Bulker<T>::ConsumeMany()
+{
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ if (BOOST_UNLIKELY(m_NextConsumption == NullTimePoint())) {
+ UpdateNextConsumption();
+ }
+
+ auto deadline (m_NextConsumption);
+
+ m_CV.wait_until(lock, deadline, [this]() { return !m_Bulks.empty() && m_Bulks.front().size() == m_BulkSize; });
+ UpdateNextConsumption();
+
+ if (m_Bulks.empty()) {
+ return Container();
+ }
+
+ auto haystack (std::move(m_Bulks.front()));
+
+ m_Bulks.pop();
+ return haystack;
+}
+
+template<class T>
+typename Bulker<T>::SizeType Bulker<T>::Size()
+{
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ return m_Bulks.empty() ? 0 : (m_Bulks.size() - 1u) * m_BulkSize + m_Bulks.back().size();
+}
+
+}
+
+#endif /* BULKER_H */
diff --git a/lib/base/configobject-script.cpp b/lib/base/configobject-script.cpp
new file mode 100644
index 0000000..46a9ca2
--- /dev/null
+++ b/lib/base/configobject-script.cpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+
+using namespace icinga;
+
+static void ConfigObjectModifyAttribute(const String& attr, const Value& value)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ ConfigObject::Ptr self = vframe->Self;
+ REQUIRE_NOT_NULL(self);
+ return self->ModifyAttribute(attr, value);
+}
+
+static void ConfigObjectRestoreAttribute(const String& attr)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ ConfigObject::Ptr self = vframe->Self;
+ REQUIRE_NOT_NULL(self);
+ return self->RestoreAttribute(attr);
+}
+
+Object::Ptr ConfigObject::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "modify_attribute", new Function("ConfigObject#modify_attribute", ConfigObjectModifyAttribute, { "attr", "value" }, false) },
+ { "restore_attribute", new Function("ConfigObject#restore_attribute", ConfigObjectRestoreAttribute, { "attr", "value" }, false) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/configobject.cpp b/lib/base/configobject.cpp
new file mode 100644
index 0000000..4317771
--- /dev/null
+++ b/lib/base/configobject.cpp
@@ -0,0 +1,701 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/atomic-file.hpp"
+#include "base/configobject.hpp"
+#include "base/configobject-ti.cpp"
+#include "base/configtype.hpp"
+#include "base/serializer.hpp"
+#include "base/netstring.hpp"
+#include "base/json.hpp"
+#include "base/stdiostream.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/function.hpp"
+#include "base/initialize.hpp"
+#include "base/workqueue.hpp"
+#include "base/context.hpp"
+#include "base/application.hpp"
+#include <fstream>
+#include <boost/exception/errinfo_api_function.hpp>
+#include <boost/exception/errinfo_errno.hpp>
+#include <boost/exception/errinfo_file_name.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE_WITH_PROTOTYPE(ConfigObject, ConfigObject::GetPrototype());
+
+boost::signals2::signal<void (const ConfigObject::Ptr&)> ConfigObject::OnStateChanged;
+
+bool ConfigObject::IsActive() const
+{
+ return GetActive();
+}
+
+bool ConfigObject::IsPaused() const
+{
+ return GetPaused();
+}
+
+void ConfigObject::SetExtension(const String& key, const Value& value)
+{
+ Dictionary::Ptr extensions = GetExtensions();
+
+ if (!extensions) {
+ extensions = new Dictionary();
+ SetExtensions(extensions);
+ }
+
+ extensions->Set(key, value);
+}
+
+Value ConfigObject::GetExtension(const String& key)
+{
+ Dictionary::Ptr extensions = GetExtensions();
+
+ if (!extensions)
+ return Empty;
+
+ return extensions->Get(key);
+}
+
+void ConfigObject::ClearExtension(const String& key)
+{
+ Dictionary::Ptr extensions = GetExtensions();
+
+ if (!extensions)
+ return;
+
+ extensions->Remove(key);
+}
+
+class ModAttrValidationUtils final : public ValidationUtils
+{
+public:
+ bool ValidateName(const String& type, const String& name) const override
+ {
+ Type::Ptr ptype = Type::GetByName(type);
+ auto *dtype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (!dtype)
+ return false;
+
+ if (!dtype->GetObject(name))
+ return false;
+
+ return true;
+ }
+};
+
+void ConfigObject::ModifyAttribute(const String& attr, const Value& value, bool updateVersion)
+{
+ Dictionary::Ptr original_attributes = GetOriginalAttributes();
+ bool updated_original_attributes = false;
+
+ Type::Ptr type = GetReflectionType();
+
+ std::vector<String> tokens = attr.Split(".");
+
+ String fieldName = tokens[0];
+
+ int fid = type->GetFieldId(fieldName);
+ Field field = type->GetFieldInfo(fid);
+
+ if (field.Attributes & FANoUserModify)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Attribute cannot be modified."));
+
+ if (field.Attributes & FAConfig) {
+ if (!original_attributes) {
+ original_attributes = new Dictionary();
+ SetOriginalAttributes(original_attributes, true);
+ }
+ }
+
+ Value oldValue = GetField(fid);
+ Value newValue;
+
+ if (tokens.size() > 1) {
+ newValue = oldValue.Clone();
+ Value current = newValue;
+
+ if (current.IsEmpty()) {
+ current = new Dictionary();
+ newValue = current;
+ }
+
+ String prefix = tokens[0];
+
+ for (std::vector<String>::size_type i = 1; i < tokens.size() - 1; i++) {
+ if (!current.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Value must be a dictionary."));
+
+ Dictionary::Ptr dict = current;
+
+ const String& key = tokens[i];
+ prefix += "." + key;
+
+ if (!dict->Get(key, &current)) {
+ current = new Dictionary();
+ dict->Set(key, current);
+ }
+ }
+
+ if (!current.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Value must be a dictionary."));
+
+ Dictionary::Ptr dict = current;
+
+ const String& key = tokens[tokens.size() - 1];
+ prefix += "." + key;
+
+ /* clone it for original attributes */
+ oldValue = dict->Get(key).Clone();
+
+ if (field.Attributes & FAConfig) {
+ updated_original_attributes = true;
+
+ if (oldValue.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr oldDict = oldValue;
+ ObjectLock olock(oldDict);
+ for (const auto& kv : oldDict) {
+ String key = prefix + "." + kv.first;
+ if (!original_attributes->Contains(key))
+ original_attributes->Set(key, kv.second);
+ }
+
+ /* store the new value as null */
+ if (value.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr valueDict = value;
+ ObjectLock olock(valueDict);
+ for (const auto& kv : valueDict) {
+ String key = attr + "." + kv.first;
+ if (!original_attributes->Contains(key))
+ original_attributes->Set(key, Empty);
+ }
+ }
+ } else if (!original_attributes->Contains(attr))
+ original_attributes->Set(attr, oldValue);
+ }
+
+ dict->Set(key, value);
+ } else {
+ newValue = value;
+
+ if (field.Attributes & FAConfig) {
+ if (!original_attributes->Contains(attr)) {
+ updated_original_attributes = true;
+ original_attributes->Set(attr, oldValue);
+ }
+ }
+ }
+
+ ModAttrValidationUtils utils;
+ ValidateField(fid, Lazy<Value>{newValue}, utils);
+
+ SetField(fid, newValue);
+
+ if (updateVersion && (field.Attributes & FAConfig))
+ SetVersion(Utility::GetTime());
+
+ if (updated_original_attributes)
+ NotifyOriginalAttributes();
+}
+
+void ConfigObject::RestoreAttribute(const String& attr, bool updateVersion)
+{
+ Type::Ptr type = GetReflectionType();
+
+ std::vector<String> tokens = attr.Split(".");
+
+ String fieldName = tokens[0];
+
+ int fid = type->GetFieldId(fieldName);
+
+ Value currentValue = GetField(fid);
+
+ Dictionary::Ptr original_attributes = GetOriginalAttributes();
+
+ if (!original_attributes)
+ return;
+
+ Value oldValue = original_attributes->Get(attr);
+ Value newValue;
+
+ if (tokens.size() > 1) {
+ newValue = currentValue.Clone();
+ Value current = newValue;
+
+ if (current.IsEmpty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot restore non-existent object attribute"));
+
+ String prefix = tokens[0];
+
+ for (std::vector<String>::size_type i = 1; i < tokens.size() - 1; i++) {
+ if (!current.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Value must be a dictionary."));
+
+ Dictionary::Ptr dict = current;
+
+ const String& key = tokens[i];
+ prefix += "." + key;
+
+ if (!dict->Contains(key))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot restore non-existent object attribute"));
+
+ current = dict->Get(key);
+ }
+
+ if (!current.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Value must be a dictionary."));
+
+ Dictionary::Ptr dict = current;
+
+ const String& key = tokens[tokens.size() - 1];
+ prefix += "." + key;
+
+ std::vector<String> restoredAttrs;
+
+ {
+ ObjectLock olock(original_attributes);
+ for (const auto& kv : original_attributes) {
+ std::vector<String> originalTokens = String(kv.first).Split(".");
+
+ if (tokens.size() > originalTokens.size())
+ continue;
+
+ bool match = true;
+ for (std::vector<String>::size_type i = 0; i < tokens.size(); i++) {
+ if (tokens[i] != originalTokens[i]) {
+ match = false;
+ break;
+ }
+ }
+
+ if (!match)
+ continue;
+
+ Dictionary::Ptr dict;
+
+ if (tokens.size() == originalTokens.size())
+ dict = current;
+ else {
+ Value currentSub = current;
+
+ for (std::vector<String>::size_type i = tokens.size() - 1; i < originalTokens.size() - 1; i++) {
+ dict = currentSub;
+ currentSub = dict->Get(originalTokens[i]);
+
+ if (!currentSub.IsObjectType<Dictionary>()) {
+ currentSub = new Dictionary();
+ dict->Set(originalTokens[i], currentSub);
+ }
+ }
+
+ dict = currentSub;
+ }
+
+ dict->Set(originalTokens[originalTokens.size() - 1], kv.second);
+ restoredAttrs.push_back(kv.first);
+ }
+ }
+
+ for (const String& attr : restoredAttrs)
+ original_attributes->Remove(attr);
+
+
+ } else {
+ newValue = oldValue;
+ }
+
+ original_attributes->Remove(attr);
+ SetField(fid, newValue);
+
+ if (updateVersion)
+ SetVersion(Utility::GetTime());
+}
+
+bool ConfigObject::IsAttributeModified(const String& attr) const
+{
+ Dictionary::Ptr original_attributes = GetOriginalAttributes();
+
+ if (!original_attributes)
+ return false;
+
+ return original_attributes->Contains(attr);
+}
+
+void ConfigObject::Register()
+{
+ ASSERT(!OwnsLock());
+
+ TypeImpl<ConfigObject>::Ptr type = static_pointer_cast<TypeImpl<ConfigObject> >(GetReflectionType());
+ type->RegisterObject(this);
+}
+
+void ConfigObject::Unregister()
+{
+ ASSERT(!OwnsLock());
+
+ TypeImpl<ConfigObject>::Ptr type = static_pointer_cast<TypeImpl<ConfigObject> >(GetReflectionType());
+ type->UnregisterObject(this);
+}
+
+void ConfigObject::Start(bool runtimeCreated)
+{
+ ObjectImpl<ConfigObject>::Start(runtimeCreated);
+
+ ObjectLock olock(this);
+
+ SetStartCalled(true);
+}
+
+void ConfigObject::PreActivate()
+{
+ CONTEXT("Setting 'active' to true for object '" << GetName() << "' of type '" << GetReflectionType()->GetName() << "'");
+
+ ASSERT(!IsActive());
+ SetActive(true, true);
+}
+
+void ConfigObject::Activate(bool runtimeCreated, const Value& cookie)
+{
+ CONTEXT("Activating object '" << GetName() << "' of type '" << GetReflectionType()->GetName() << "'");
+
+ {
+ ObjectLock olock(this);
+
+ Start(runtimeCreated);
+
+ ASSERT(GetStartCalled());
+
+ if (GetHAMode() == HARunEverywhere)
+ SetAuthority(true);
+ }
+
+ NotifyActive(cookie);
+}
+
+void ConfigObject::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<ConfigObject>::Stop(runtimeRemoved);
+
+ ObjectLock olock(this);
+
+ SetStopCalled(true);
+}
+
+void ConfigObject::Deactivate(bool runtimeRemoved, const Value& cookie)
+{
+ CONTEXT("Deactivating object '" << GetName() << "' of type '" << GetReflectionType()->GetName() << "'");
+
+ {
+ ObjectLock olock(this);
+
+ if (!IsActive())
+ return;
+
+ SetActive(false, true);
+
+ SetAuthority(false);
+
+ Stop(runtimeRemoved);
+ }
+
+ ASSERT(GetStopCalled());
+
+ NotifyActive(cookie);
+}
+
+void ConfigObject::OnConfigLoaded()
+{
+ /* Nothing to do here. */
+}
+
+void ConfigObject::OnAllConfigLoaded()
+{
+ static ConfigType *ctype = dynamic_cast<ConfigType *>(Type::GetByName("Zone").get());
+ String zoneName = GetZoneName();
+
+ if (!zoneName.IsEmpty())
+ m_Zone = ctype->GetObject(zoneName);
+}
+
+void ConfigObject::CreateChildObjects(const Type::Ptr& childType)
+{
+ /* Nothing to do here. */
+}
+
+void ConfigObject::OnStateLoaded()
+{
+ /* Nothing to do here. */
+}
+
+void ConfigObject::Pause()
+{
+ SetPauseCalled(true);
+}
+
+void ConfigObject::Resume()
+{
+ SetResumeCalled(true);
+}
+
+void ConfigObject::SetAuthority(bool authority)
+{
+ ObjectLock olock(this);
+
+ if (authority && GetPaused()) {
+ SetResumeCalled(false);
+ Resume();
+ ASSERT(GetResumeCalled());
+ SetPaused(false);
+ } else if (!authority && !GetPaused()) {
+ SetPaused(true);
+ SetPauseCalled(false);
+ Pause();
+ ASSERT(GetPauseCalled());
+ }
+}
+
+void ConfigObject::DumpObjects(const String& filename, int attributeTypes)
+{
+ Log(LogInformation, "ConfigObject")
+ << "Dumping program state to file '" << filename << "'";
+
+ try {
+ Utility::Glob(filename + ".tmp.*", &Utility::Remove, GlobFile);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ConfigObject") << DiagnosticInformation(ex);
+ }
+
+ AtomicFile fp (filename, 0600);
+ StdioStream::Ptr sfp = new StdioStream(&fp, false);
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+ Dictionary::Ptr update = Serialize(object, attributeTypes);
+
+ if (!update)
+ continue;
+
+ Dictionary::Ptr persistentObject = new Dictionary({
+ { "type", type->GetName() },
+ { "name", object->GetName() },
+ { "update", update }
+ });
+
+ String json = JsonEncode(persistentObject);
+
+ NetString::WriteStringToStream(sfp, json);
+ }
+ }
+
+ sfp->Close();
+ fp.Commit();
+}
+
+void ConfigObject::RestoreObject(const String& message, int attributeTypes)
+{
+ Dictionary::Ptr persistentObject = JsonDecode(message);
+
+ String type = persistentObject->Get("type");
+ String name = persistentObject->Get("name");
+
+ ConfigObject::Ptr object = GetObject(type, name);
+
+ if (!object)
+ return;
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "ConfigObject")
+ << "Restoring object '" << name << "' of type '" << type << "'.";
+#endif /* I2_DEBUG */
+ Dictionary::Ptr update = persistentObject->Get("update");
+ Deserialize(object, update, false, attributeTypes);
+ object->OnStateLoaded();
+ object->SetStateLoaded(true);
+}
+
+void ConfigObject::RestoreObjects(const String& filename, int attributeTypes)
+{
+ if (!Utility::PathExists(filename))
+ return;
+
+ Log(LogInformation, "ConfigObject")
+ << "Restoring program state from file '" << filename << "'";
+
+ std::fstream fp;
+ fp.open(filename.CStr(), std::ios_base::in);
+
+ StdioStream::Ptr sfp = new StdioStream (&fp, false);
+
+ unsigned long restored = 0;
+
+ WorkQueue upq(25000, Configuration::Concurrency);
+ upq.SetName("ConfigObject::RestoreObjects");
+
+ String message;
+ StreamReadContext src;
+ for (;;) {
+ StreamReadStatus srs = NetString::ReadStringFromStream(sfp, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ upq.Enqueue([message, attributeTypes]() { RestoreObject(message, attributeTypes); });
+ restored++;
+ }
+
+ sfp->Close();
+
+ upq.Join();
+
+ unsigned long no_state = 0;
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+ if (!object->GetStateLoaded()) {
+ object->OnStateLoaded();
+ object->SetStateLoaded(true);
+
+ no_state++;
+ }
+ }
+ }
+
+ Log(LogInformation, "ConfigObject")
+ << "Restored " << restored << " objects. Loaded " << no_state << " new objects without state.";
+}
+
+void ConfigObject::StopObjects()
+{
+ std::vector<Type::Ptr> types = Type::GetAllTypes();
+
+ std::sort(types.begin(), types.end(), [](const Type::Ptr& a, const Type::Ptr& b) {
+ if (a->GetActivationPriority() > b->GetActivationPriority())
+ return true;
+ return false;
+ });
+
+ for (const Type::Ptr& type : types) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+#ifdef I2_DEBUG
+ Log(LogDebug, "ConfigObject")
+ << "Deactivate() called for config object '" << object->GetName() << "' with type '" << type->GetName() << "'.";
+#endif /* I2_DEBUG */
+ object->Deactivate();
+ }
+ }
+}
+
+void ConfigObject::DumpModifiedAttributes(const std::function<void(const ConfigObject::Ptr&, const String&, const Value&)>& callback)
+{
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+ Dictionary::Ptr originalAttributes = object->GetOriginalAttributes();
+
+ if (!originalAttributes)
+ continue;
+
+ ObjectLock olock(originalAttributes);
+ for (const Dictionary::Pair& kv : originalAttributes) {
+ String key = kv.first;
+
+ Type::Ptr type = object->GetReflectionType();
+
+ std::vector<String> tokens = key.Split(".");
+
+ String fieldName = tokens[0];
+ int fid = type->GetFieldId(fieldName);
+
+ Value currentValue = object->GetField(fid);
+ Value modifiedValue;
+
+ if (tokens.size() > 1) {
+ Value current = currentValue;
+
+ for (std::vector<String>::size_type i = 1; i < tokens.size() - 1; i++) {
+ if (!current.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Value must be a dictionary."));
+
+ Dictionary::Ptr dict = current;
+ const String& key = tokens[i];
+
+ if (!dict->Contains(key))
+ break;
+
+ current = dict->Get(key);
+ }
+
+ if (!current.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Value must be a dictionary."));
+
+ Dictionary::Ptr dict = current;
+ const String& key = tokens[tokens.size() - 1];
+
+ modifiedValue = dict->Get(key);
+ } else
+ modifiedValue = currentValue;
+
+ callback(object, key, modifiedValue);
+ }
+ }
+ }
+
+}
+
+ConfigObject::Ptr ConfigObject::GetObject(const String& type, const String& name)
+{
+ Type::Ptr ptype = Type::GetByName(type);
+ auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (!ctype)
+ return nullptr;
+
+ return ctype->GetObject(name);
+}
+
+ConfigObject::Ptr ConfigObject::GetZone() const
+{
+ return m_Zone;
+}
+
+Dictionary::Ptr ConfigObject::GetSourceLocation() const
+{
+ DebugInfo di = GetDebugInfo();
+
+ return new Dictionary({
+ { "path", di.Path },
+ { "first_line", di.FirstLine },
+ { "first_column", di.FirstColumn },
+ { "last_line", di.LastLine },
+ { "last_column", di.LastColumn }
+ });
+}
+
+NameComposer::~NameComposer()
+{ }
diff --git a/lib/base/configobject.hpp b/lib/base/configobject.hpp
new file mode 100644
index 0000000..5596363
--- /dev/null
+++ b/lib/base/configobject.hpp
@@ -0,0 +1,101 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGOBJECT_H
+#define CONFIGOBJECT_H
+
+#include "base/i2-base.hpp"
+#include "base/configobject-ti.hpp"
+#include "base/object.hpp"
+#include "base/type.hpp"
+#include "base/dictionary.hpp"
+#include <boost/signals2.hpp>
+
+namespace icinga
+{
+
+class ConfigType;
+
+/**
+ * A dynamic object that can be instantiated from the configuration file.
+ *
+ * @ingroup base
+ */
+class ConfigObject : public ObjectImpl<ConfigObject>
+{
+public:
+ DECLARE_OBJECT(ConfigObject);
+
+ static boost::signals2::signal<void (const ConfigObject::Ptr&)> OnStateChanged;
+
+ bool IsActive() const;
+ bool IsPaused() const;
+
+ void SetExtension(const String& key, const Value& value);
+ Value GetExtension(const String& key);
+ void ClearExtension(const String& key);
+
+ ConfigObject::Ptr GetZone() const;
+
+ void ModifyAttribute(const String& attr, const Value& value, bool updateVersion = true);
+ void RestoreAttribute(const String& attr, bool updateVersion = true);
+ bool IsAttributeModified(const String& attr) const;
+
+ void Register();
+ void Unregister();
+
+ void PreActivate();
+ void Activate(bool runtimeCreated = false, const Value& cookie = Empty);
+ void Deactivate(bool runtimeRemoved = false, const Value& cookie = Empty);
+ void SetAuthority(bool authority);
+
+ void Start(bool runtimeCreated = false) override;
+ void Stop(bool runtimeRemoved = false) override;
+
+ virtual void Pause();
+ virtual void Resume();
+
+ virtual void OnConfigLoaded();
+ virtual void CreateChildObjects(const Type::Ptr& childType);
+ virtual void OnAllConfigLoaded();
+ virtual void OnStateLoaded();
+
+ Dictionary::Ptr GetSourceLocation() const override;
+
+ template<typename T>
+ static intrusive_ptr<T> GetObject(const String& name)
+ {
+ typedef TypeImpl<T> ObjType;
+ auto *ptype = static_cast<ObjType *>(T::TypeInstance.get());
+ return static_pointer_cast<T>(ptype->GetObject(name));
+ }
+
+ static ConfigObject::Ptr GetObject(const String& type, const String& name);
+
+ static void DumpObjects(const String& filename, int attributeTypes = FAState);
+ static void RestoreObjects(const String& filename, int attributeTypes = FAState);
+ static void StopObjects();
+
+ static void DumpModifiedAttributes(const std::function<void(const ConfigObject::Ptr&, const String&, const Value&)>& callback);
+
+ static Object::Ptr GetPrototype();
+
+private:
+ ConfigObject::Ptr m_Zone;
+
+ static void RestoreObject(const String& message, int attributeTypes);
+};
+
+#define DECLARE_OBJECTNAME(klass) \
+ inline static String GetTypeName() \
+ { \
+ return #klass; \
+ } \
+ \
+ inline static intrusive_ptr<klass> GetByName(const String& name) \
+ { \
+ return ConfigObject::GetObject<klass>(name); \
+ }
+
+}
+
+#endif /* CONFIGOBJECT_H */
diff --git a/lib/base/configobject.ti b/lib/base/configobject.ti
new file mode 100644
index 0000000..ea67dfa
--- /dev/null
+++ b/lib/base/configobject.ti
@@ -0,0 +1,94 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/debuginfo.hpp"
+#include "base/configtype.hpp"
+
+library base;
+
+namespace icinga
+{
+
+code {{{
+enum HAMode
+{
+ HARunOnce,
+ HARunEverywhere
+};
+
+class NameComposer
+{
+public:
+ virtual ~NameComposer();
+
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const = 0;
+ virtual Dictionary::Ptr ParseName(const String& name) const = 0;
+};
+}}}
+
+abstract class ConfigObjectBase
+{ };
+
+code {{{
+class ConfigObjectBase : public ObjectImpl<ConfigObjectBase>
+{
+public:
+ inline DebugInfo GetDebugInfo() const
+ {
+ return m_DebugInfo;
+ }
+
+ void SetDebugInfo(const DebugInfo& di)
+ {
+ m_DebugInfo = di;
+ }
+
+ inline virtual void Start(bool /* runtimeCreated */)
+ { }
+
+ inline virtual void Stop(bool /* runtimeRemoved */)
+ { }
+
+private:
+ DebugInfo m_DebugInfo;
+};
+
+}}}
+
+abstract class ConfigObject : ConfigObjectBase < ConfigType
+{
+ [config, no_user_modify] String __name (Name);
+ [config, no_user_modify, required] String "name" (ShortName) {
+ get {{{
+ String shortName = m_ShortName.load();
+ if (shortName.IsEmpty())
+ return GetName();
+ else
+ return shortName;
+ }}}
+ };
+ [config, no_user_modify] name(Zone) zone (ZoneName);
+ [config, no_user_modify] String package;
+ [config, get_protected, no_user_modify] Array::Ptr templates;
+ [config, no_storage, no_user_modify] Dictionary::Ptr source_location {
+ get;
+ };
+ [get_protected, no_user_modify] bool active;
+ [get_protected, no_user_modify] bool paused {
+ default {{{ return true; }}}
+ };
+ [get_protected, no_user_view, no_user_modify] bool start_called;
+ [get_protected, no_user_view, no_user_modify] bool stop_called;
+ [get_protected, no_user_view, no_user_modify] bool pause_called;
+ [get_protected, no_user_view, no_user_modify] bool resume_called;
+ [enum] HAMode ha_mode (HAMode);
+ [protected, no_user_view, no_user_modify] Dictionary::Ptr extensions;
+
+ [protected, no_user_view, no_user_modify] bool state_loaded;
+ [no_user_modify] Dictionary::Ptr original_attributes;
+ [state, no_user_modify] double version {
+ default {{{ return 0; }}}
+ };
+ [no_user_view, no_user_modify] String icingadb_identifier;
+};
+
+}
diff --git a/lib/base/configtype.cpp b/lib/base/configtype.cpp
new file mode 100644
index 0000000..b266cd2
--- /dev/null
+++ b/lib/base/configtype.cpp
@@ -0,0 +1,76 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+ConfigType::~ConfigType()
+{ }
+
+ConfigObject::Ptr ConfigType::GetObject(const String& name) const
+{
+ std::shared_lock<decltype(m_Mutex)> lock (m_Mutex);
+
+ auto nt = m_ObjectMap.find(name);
+
+ if (nt == m_ObjectMap.end())
+ return nullptr;
+
+ return nt->second;
+}
+
+void ConfigType::RegisterObject(const ConfigObject::Ptr& object)
+{
+ String name = object->GetName();
+
+ {
+ std::unique_lock<decltype(m_Mutex)> lock (m_Mutex);
+
+ auto it = m_ObjectMap.find(name);
+
+ if (it != m_ObjectMap.end()) {
+ if (it->second == object)
+ return;
+
+ auto *type = dynamic_cast<Type *>(this);
+
+ BOOST_THROW_EXCEPTION(ScriptError("An object with type '" + type->GetName() + "' and name '" + name + "' already exists (" +
+ Convert::ToString(it->second->GetDebugInfo()) + "), new declaration: " + Convert::ToString(object->GetDebugInfo()),
+ object->GetDebugInfo()));
+ }
+
+ m_ObjectMap[name] = object;
+ m_ObjectVector.push_back(object);
+ }
+}
+
+void ConfigType::UnregisterObject(const ConfigObject::Ptr& object)
+{
+ String name = object->GetName();
+
+ {
+ std::unique_lock<decltype(m_Mutex)> lock (m_Mutex);
+
+ m_ObjectMap.erase(name);
+ m_ObjectVector.erase(std::remove(m_ObjectVector.begin(), m_ObjectVector.end(), object), m_ObjectVector.end());
+ }
+}
+
+std::vector<ConfigObject::Ptr> ConfigType::GetObjects() const
+{
+ std::shared_lock<decltype(m_Mutex)> lock (m_Mutex);
+ return m_ObjectVector;
+}
+
+std::vector<ConfigObject::Ptr> ConfigType::GetObjectsHelper(Type *type)
+{
+ return static_cast<TypeImpl<ConfigObject> *>(type)->GetObjects();
+}
+
+int ConfigType::GetObjectCount() const
+{
+ std::shared_lock<decltype(m_Mutex)> lock (m_Mutex);
+ return m_ObjectVector.size();
+}
diff --git a/lib/base/configtype.hpp b/lib/base/configtype.hpp
new file mode 100644
index 0000000..c77fc5e
--- /dev/null
+++ b/lib/base/configtype.hpp
@@ -0,0 +1,64 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGTYPE_H
+#define CONFIGTYPE_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+#include "base/type.hpp"
+#include "base/dictionary.hpp"
+#include <shared_mutex>
+#include <unordered_map>
+
+namespace icinga
+{
+
+class ConfigObject;
+
+class ConfigType
+{
+public:
+ virtual ~ConfigType();
+
+ intrusive_ptr<ConfigObject> GetObject(const String& name) const;
+
+ void RegisterObject(const intrusive_ptr<ConfigObject>& object);
+ void UnregisterObject(const intrusive_ptr<ConfigObject>& object);
+
+ std::vector<intrusive_ptr<ConfigObject> > GetObjects() const;
+
+ template<typename T>
+ static TypeImpl<T> *Get()
+ {
+ typedef TypeImpl<T> ObjType;
+ return static_cast<ObjType *>(T::TypeInstance.get());
+ }
+
+ template<typename T>
+ static std::vector<intrusive_ptr<T> > GetObjectsByType()
+ {
+ std::vector<intrusive_ptr<ConfigObject> > objects = GetObjectsHelper(T::TypeInstance.get());
+ std::vector<intrusive_ptr<T> > result;
+ result.reserve(objects.size());
+for (const auto& object : objects) {
+ result.push_back(static_pointer_cast<T>(object));
+ }
+ return result;
+ }
+
+ int GetObjectCount() const;
+
+private:
+ typedef std::unordered_map<String, intrusive_ptr<ConfigObject> > ObjectMap;
+ typedef std::vector<intrusive_ptr<ConfigObject> > ObjectVector;
+
+ mutable std::shared_timed_mutex m_Mutex;
+ ObjectMap m_ObjectMap;
+ ObjectVector m_ObjectVector;
+
+ static std::vector<intrusive_ptr<ConfigObject> > GetObjectsHelper(Type *type);
+};
+
+}
+
+#endif /* CONFIGTYPE_H */
diff --git a/lib/base/configuration.cpp b/lib/base/configuration.cpp
new file mode 100644
index 0000000..908c161
--- /dev/null
+++ b/lib/base/configuration.cpp
@@ -0,0 +1,379 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configuration.hpp"
+#include "base/configuration-ti.cpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Configuration);
+
+String Configuration::ApiBindHost = []() {
+#ifndef _WIN32
+ // Automatically fall back to an IPv4 default if socket() tells us that IPv6 is not supported.
+ int fd = socket(AF_INET6, SOCK_STREAM, 0);
+ if (fd < 0 && errno == EAFNOSUPPORT) {
+ return "0.0.0.0";
+ } else if (fd >= 0) {
+ close(fd);
+ }
+#endif /* _WIN32 */
+
+ return "::";
+}();
+
+String Configuration::ApiBindPort{"5665"};
+bool Configuration::AttachDebugger{false};
+String Configuration::CacheDir;
+int Configuration::Concurrency{1};
+bool Configuration::ConcurrencyWasModified{false};
+String Configuration::ConfigDir;
+String Configuration::DataDir;
+String Configuration::EventEngine;
+String Configuration::IncludeConfDir;
+String Configuration::InitRunDir;
+String Configuration::LogDir;
+String Configuration::ModAttrPath;
+String Configuration::ObjectsPath;
+String Configuration::PidPath;
+String Configuration::PkgDataDir;
+String Configuration::PrefixDir;
+String Configuration::ProgramData;
+int Configuration::RLimitFiles;
+int Configuration::RLimitProcesses;
+int Configuration::RLimitStack;
+String Configuration::RunAsGroup;
+String Configuration::RunAsUser;
+String Configuration::SpoolDir;
+String Configuration::StatePath;
+double Configuration::TlsHandshakeTimeout{10};
+String Configuration::VarsPath;
+String Configuration::ZonesDir;
+
+/* deprecated */
+String Configuration::LocalStateDir;
+String Configuration::RunDir;
+String Configuration::SysconfDir;
+
+/* internal */
+bool Configuration::m_ReadOnly{false};
+
+template<typename T>
+void HandleUserWrite(const String& name, T *target, const T& value, bool readOnly)
+{
+ if (readOnly)
+ BOOST_THROW_EXCEPTION(ScriptError("Configuration attribute '" + name + "' is read-only."));
+
+ *target = value;
+}
+
+String Configuration::GetApiBindHost() const
+{
+ return Configuration::ApiBindHost;
+}
+
+void Configuration::SetApiBindHost(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("ApiBindHost", &Configuration::ApiBindHost, val, m_ReadOnly);
+}
+
+String Configuration::GetApiBindPort() const
+{
+ return Configuration::ApiBindPort;
+}
+
+void Configuration::SetApiBindPort(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("ApiBindPort", &Configuration::ApiBindPort, val, m_ReadOnly);
+}
+
+bool Configuration::GetAttachDebugger() const
+{
+ return Configuration::AttachDebugger;
+}
+
+void Configuration::SetAttachDebugger(bool val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("AttachDebugger", &Configuration::AttachDebugger, val, m_ReadOnly);
+}
+
+String Configuration::GetCacheDir() const
+{
+ return Configuration::CacheDir;
+}
+
+void Configuration::SetCacheDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("CacheDir", &Configuration::CacheDir, val, m_ReadOnly);
+}
+
+int Configuration::GetConcurrency() const
+{
+ return Configuration::Concurrency;
+}
+
+void Configuration::SetConcurrency(int val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("Concurrency", &Configuration::Concurrency, val, m_ReadOnly);
+ Configuration::ConcurrencyWasModified = true;
+}
+
+String Configuration::GetConfigDir() const
+{
+ return Configuration::ConfigDir;
+}
+
+void Configuration::SetConfigDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("ConfigDir", &Configuration::ConfigDir, val, m_ReadOnly);
+}
+
+String Configuration::GetDataDir() const
+{
+ return Configuration::DataDir;
+}
+
+void Configuration::SetDataDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("DataDir", &Configuration::DataDir, val, m_ReadOnly);
+}
+
+String Configuration::GetEventEngine() const
+{
+ return Configuration::EventEngine;
+}
+
+void Configuration::SetEventEngine(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("EventEngine", &Configuration::EventEngine, val, m_ReadOnly);
+}
+
+String Configuration::GetIncludeConfDir() const
+{
+ return Configuration::IncludeConfDir;
+}
+
+void Configuration::SetIncludeConfDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("IncludeConfDir", &Configuration::IncludeConfDir, val, m_ReadOnly);
+}
+
+String Configuration::GetInitRunDir() const
+{
+ return Configuration::InitRunDir;
+}
+
+void Configuration::SetInitRunDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("InitRunDir", &Configuration::InitRunDir, val, m_ReadOnly);
+}
+
+String Configuration::GetLogDir() const
+{
+ return Configuration::LogDir;
+}
+
+void Configuration::SetLogDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("LogDir", &Configuration::LogDir, val, m_ReadOnly);
+}
+
+String Configuration::GetModAttrPath() const
+{
+ return Configuration::ModAttrPath;
+}
+
+void Configuration::SetModAttrPath(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("ModAttrPath", &Configuration::ModAttrPath, val, m_ReadOnly);
+}
+
+String Configuration::GetObjectsPath() const
+{
+ return Configuration::ObjectsPath;
+}
+
+void Configuration::SetObjectsPath(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("ObjectsPath", &Configuration::ObjectsPath, val, m_ReadOnly);
+}
+
+String Configuration::GetPidPath() const
+{
+ return Configuration::PidPath;
+}
+
+void Configuration::SetPidPath(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("PidPath", &Configuration::PidPath, val, m_ReadOnly);
+}
+
+String Configuration::GetPkgDataDir() const
+{
+ return Configuration::PkgDataDir;
+}
+
+void Configuration::SetPkgDataDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("PkgDataDir", &Configuration::PkgDataDir, val, m_ReadOnly);
+}
+
+String Configuration::GetPrefixDir() const
+{
+ return Configuration::PrefixDir;
+}
+
+void Configuration::SetPrefixDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("PrefixDir", &Configuration::PrefixDir, val, m_ReadOnly);
+}
+
+String Configuration::GetProgramData() const
+{
+ return Configuration::ProgramData;
+}
+
+void Configuration::SetProgramData(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("ProgramData", &Configuration::ProgramData, val, m_ReadOnly);
+}
+
+int Configuration::GetRLimitFiles() const
+{
+ return Configuration::RLimitFiles;
+}
+
+void Configuration::SetRLimitFiles(int val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("RLimitFiles", &Configuration::RLimitFiles, val, m_ReadOnly);
+}
+
+int Configuration::GetRLimitProcesses() const
+{
+ return RLimitProcesses;
+}
+
+void Configuration::SetRLimitProcesses(int val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("RLimitProcesses", &Configuration::RLimitProcesses, val, m_ReadOnly);
+}
+
+int Configuration::GetRLimitStack() const
+{
+ return Configuration::RLimitStack;
+}
+
+void Configuration::SetRLimitStack(int val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("RLimitStack", &Configuration::RLimitStack, val, m_ReadOnly);
+}
+
+String Configuration::GetRunAsGroup() const
+{
+ return Configuration::RunAsGroup;
+}
+
+void Configuration::SetRunAsGroup(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("RunAsGroup", &Configuration::RunAsGroup, val, m_ReadOnly);
+}
+
+String Configuration::GetRunAsUser() const
+{
+ return Configuration::RunAsUser;
+}
+
+void Configuration::SetRunAsUser(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("RunAsUser", &Configuration::RunAsUser, val, m_ReadOnly);
+}
+
+String Configuration::GetSpoolDir() const
+{
+ return Configuration::SpoolDir;
+}
+
+void Configuration::SetSpoolDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("SpoolDir", &Configuration::SpoolDir, val, m_ReadOnly);
+}
+
+String Configuration::GetStatePath() const
+{
+ return Configuration::StatePath;
+}
+
+void Configuration::SetStatePath(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("StatePath", &Configuration::StatePath, val, m_ReadOnly);
+}
+
+double Configuration::GetTlsHandshakeTimeout() const
+{
+ return Configuration::TlsHandshakeTimeout;
+}
+
+void Configuration::SetTlsHandshakeTimeout(double val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("TlsHandshakeTimeout", &Configuration::TlsHandshakeTimeout, val, m_ReadOnly);
+}
+
+String Configuration::GetVarsPath() const
+{
+ return Configuration::VarsPath;
+}
+
+void Configuration::SetVarsPath(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("VarsPath", &Configuration::VarsPath, val, m_ReadOnly);
+}
+
+String Configuration::GetZonesDir() const
+{
+ return Configuration::ZonesDir;
+}
+
+void Configuration::SetZonesDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("ZonesDir", &Configuration::ZonesDir, val, m_ReadOnly);
+}
+
+String Configuration::GetLocalStateDir() const
+{
+ return Configuration::LocalStateDir;
+}
+
+void Configuration::SetLocalStateDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("LocalStateDir", &Configuration::LocalStateDir, val, m_ReadOnly);
+}
+
+String Configuration::GetSysconfDir() const
+{
+ return Configuration::SysconfDir;
+}
+
+void Configuration::SetSysconfDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("SysconfDir", &Configuration::SysconfDir, val, m_ReadOnly);
+}
+
+String Configuration::GetRunDir() const
+{
+ return Configuration::RunDir;
+}
+
+void Configuration::SetRunDir(const String& val, bool suppress_events, const Value& cookie)
+{
+ HandleUserWrite("RunDir", &Configuration::RunDir, val, m_ReadOnly);
+}
+
+bool Configuration::GetReadOnly()
+{
+ return m_ReadOnly;
+}
+
+void Configuration::SetReadOnly(bool readOnly)
+{
+ m_ReadOnly = readOnly;
+}
diff --git a/lib/base/configuration.hpp b/lib/base/configuration.hpp
new file mode 100644
index 0000000..a5aed01
--- /dev/null
+++ b/lib/base/configuration.hpp
@@ -0,0 +1,157 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGURATION_H
+#define CONFIGURATION_H
+
+#include "base/i2-base.hpp"
+#include "base/configuration-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * Global configuration.
+ *
+ * @ingroup base
+ */
+class Configuration : public ObjectImpl<Configuration>
+{
+public:
+ DECLARE_OBJECT(Configuration);
+
+ String GetApiBindHost() const override;
+ void SetApiBindHost(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetApiBindPort() const override;
+ void SetApiBindPort(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ bool GetAttachDebugger() const override;
+ void SetAttachDebugger(bool value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetCacheDir() const override;
+ void SetCacheDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ int GetConcurrency() const override;
+ void SetConcurrency(int value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetConfigDir() const override;
+ void SetConfigDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetDataDir() const override;
+ void SetDataDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetEventEngine() const override;
+ void SetEventEngine(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetIncludeConfDir() const override;
+ void SetIncludeConfDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetInitRunDir() const override;
+ void SetInitRunDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetLogDir() const override;
+ void SetLogDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetModAttrPath() const override;
+ void SetModAttrPath(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetObjectsPath() const override;
+ void SetObjectsPath(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetPidPath() const override;
+ void SetPidPath(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetPkgDataDir() const override;
+ void SetPkgDataDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetPrefixDir() const override;
+ void SetPrefixDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetProgramData() const override;
+ void SetProgramData(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ int GetRLimitFiles() const override;
+ void SetRLimitFiles(int value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ int GetRLimitProcesses() const override;
+ void SetRLimitProcesses(int value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ int GetRLimitStack() const override;
+ void SetRLimitStack(int value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetRunAsGroup() const override;
+ void SetRunAsGroup(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetRunAsUser() const override;
+ void SetRunAsUser(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetSpoolDir() const override;
+ void SetSpoolDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetStatePath() const override;
+ void SetStatePath(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ double GetTlsHandshakeTimeout() const override;
+ void SetTlsHandshakeTimeout(double value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetVarsPath() const override;
+ void SetVarsPath(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetZonesDir() const override;
+ void SetZonesDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ /* deprecated */
+ String GetLocalStateDir() const override;
+ void SetLocalStateDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetSysconfDir() const override;
+ void SetSysconfDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ String GetRunDir() const override;
+ void SetRunDir(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ static bool GetReadOnly();
+ static void SetReadOnly(bool readOnly);
+
+ static String ApiBindHost;
+ static String ApiBindPort;
+ static bool AttachDebugger;
+ static String CacheDir;
+ static int Concurrency;
+ static bool ConcurrencyWasModified;
+ static String ConfigDir;
+ static String DataDir;
+ static String EventEngine;
+ static String IncludeConfDir;
+ static String InitRunDir;
+ static String LogDir;
+ static String ModAttrPath;
+ static String ObjectsPath;
+ static String PidPath;
+ static String PkgDataDir;
+ static String PrefixDir;
+ static String ProgramData;
+ static int RLimitFiles;
+ static int RLimitProcesses;
+ static int RLimitStack;
+ static String RunAsGroup;
+ static String RunAsUser;
+ static String SpoolDir;
+ static String StatePath;
+ static double TlsHandshakeTimeout;
+ static String VarsPath;
+ static String ZonesDir;
+
+ /* deprecated */
+ static String LocalStateDir;
+ static String RunDir;
+ static String SysconfDir;
+
+private:
+ static bool m_ReadOnly;
+
+};
+
+}
+
+#endif /* CONFIGURATION_H */
diff --git a/lib/base/configuration.ti b/lib/base/configuration.ti
new file mode 100644
index 0000000..72fa92d
--- /dev/null
+++ b/lib/base/configuration.ti
@@ -0,0 +1,164 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library base;
+
+namespace icinga
+{
+
+abstract class Configuration
+{
+ [config, no_storage, virtual] String ApiBindHost {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String ApiBindPort {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] bool AttachDebugger {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String CacheDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] int Concurrency {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String ConfigDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String DataDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String EventEngine {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String IncludeConfDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String InitRunDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String LogDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String ModAttrPath {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String ObjectsPath {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String PidPath {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String PkgDataDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String PrefixDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String ProgramData {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] int RLimitFiles {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] int RLimitProcesses {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] int RLimitStack {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String RunAsGroup {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String RunAsUser {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String SpoolDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String StatePath {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] double TlsHandshakeTimeout {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String VarsPath {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String ZonesDir {
+ get;
+ set;
+ };
+
+ /* deprecated */
+ [config, no_storage, virtual] String LocalStateDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String RunDir {
+ get;
+ set;
+ };
+
+ [config, no_storage, virtual] String SysconfDir {
+ get;
+ set;
+ };
+};
+
+}
diff --git a/lib/base/configwriter.cpp b/lib/base/configwriter.cpp
new file mode 100644
index 0000000..c9dd582
--- /dev/null
+++ b/lib/base/configwriter.cpp
@@ -0,0 +1,260 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configwriter.hpp"
+#include "base/exception.hpp"
+#include <boost/regex.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <set>
+#include <iterator>
+
+using namespace icinga;
+
+void ConfigWriter::EmitBoolean(std::ostream& fp, bool val)
+{
+ fp << (val ? "true" : "false");
+}
+
+void ConfigWriter::EmitNumber(std::ostream& fp, double val)
+{
+ fp << std::fixed << val;
+}
+
+void ConfigWriter::EmitString(std::ostream& fp, const String& val)
+{
+ fp << "\"" << EscapeIcingaString(val) << "\"";
+}
+
+void ConfigWriter::EmitEmpty(std::ostream& fp)
+{
+ fp << "null";
+}
+
+void ConfigWriter::EmitArray(std::ostream& fp, int indentLevel, const Array::Ptr& val)
+{
+ fp << "[ ";
+ EmitArrayItems(fp, indentLevel, val);
+ if (val->GetLength() > 0)
+ fp << " ";
+ fp << "]";
+}
+
+void ConfigWriter::EmitArrayItems(std::ostream& fp, int indentLevel, const Array::Ptr& val)
+{
+ bool first = true;
+
+ ObjectLock olock(val);
+ for (const Value& item : val) {
+ if (first)
+ first = false;
+ else
+ fp << ", ";
+
+ EmitValue(fp, indentLevel, item);
+ }
+}
+
+void ConfigWriter::EmitScope(std::ostream& fp, int indentLevel, const Dictionary::Ptr& val,
+ const Array::Ptr& imports, bool splitDot)
+{
+ fp << "{";
+
+ if (imports && imports->GetLength() > 0) {
+ ObjectLock xlock(imports);
+ for (const Value& import : imports) {
+ fp << "\n";
+ EmitIndent(fp, indentLevel);
+ fp << "import \"" << import << "\"";
+ }
+
+ fp << "\n";
+ }
+
+ if (val) {
+ ObjectLock olock(val);
+ for (const Dictionary::Pair& kv : val) {
+ fp << "\n";
+ EmitIndent(fp, indentLevel);
+
+ if (splitDot) {
+ std::vector<String> tokens = kv.first.Split(".");
+
+ EmitIdentifier(fp, tokens[0], true);
+
+ for (std::vector<String>::size_type i = 1; i < tokens.size(); i++) {
+ fp << "[";
+ EmitString(fp, tokens[i]);
+ fp << "]";
+ }
+ } else
+ EmitIdentifier(fp, kv.first, true);
+
+ fp << " = ";
+ EmitValue(fp, indentLevel + 1, kv.second);
+ }
+ }
+
+ fp << "\n";
+ EmitIndent(fp, indentLevel - 1);
+ fp << "}";
+}
+
+void ConfigWriter::EmitValue(std::ostream& fp, int indentLevel, const Value& val)
+{
+ if (val.IsObjectType<Array>())
+ EmitArray(fp, indentLevel, val);
+ else if (val.IsObjectType<Dictionary>())
+ EmitScope(fp, indentLevel, val);
+ else if (val.IsObjectType<ConfigIdentifier>())
+ EmitIdentifier(fp, static_cast<ConfigIdentifier::Ptr>(val)->GetName(), false);
+ else if (val.IsString())
+ EmitString(fp, val);
+ else if (val.IsNumber())
+ EmitNumber(fp, val);
+ else if (val.IsBoolean())
+ EmitBoolean(fp, val);
+ else if (val.IsEmpty())
+ EmitEmpty(fp);
+}
+
+void ConfigWriter::EmitRaw(std::ostream& fp, const String& val)
+{
+ fp << val;
+}
+
+void ConfigWriter::EmitIndent(std::ostream& fp, int indentLevel)
+{
+ for (int i = 0; i < indentLevel; i++)
+ fp << "\t";
+}
+
+void ConfigWriter::EmitIdentifier(std::ostream& fp, const String& identifier, bool inAssignment)
+{
+ static std::set<String> keywords;
+ static std::mutex mutex;
+
+ {
+ std::unique_lock<std::mutex> lock(mutex);
+ if (keywords.empty()) {
+ const std::vector<String>& vkeywords = GetKeywords();
+ std::copy(vkeywords.begin(), vkeywords.end(), std::inserter(keywords, keywords.begin()));
+ }
+ }
+
+ if (keywords.find(identifier) != keywords.end()) {
+ fp << "@" << identifier;
+ return;
+ }
+
+ boost::regex expr("^[a-zA-Z_][a-zA-Z0-9\\_]*$");
+ boost::smatch what;
+ if (boost::regex_search(identifier.GetData(), what, expr))
+ fp << identifier;
+ else if (inAssignment)
+ EmitString(fp, identifier);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid identifier"));
+}
+
+void ConfigWriter::EmitConfigItem(std::ostream& fp, const String& type, const String& name, bool isTemplate,
+ bool ignoreOnError, const Array::Ptr& imports, const Dictionary::Ptr& attrs)
+{
+ if (isTemplate)
+ fp << "template ";
+ else
+ fp << "object ";
+
+ EmitIdentifier(fp, type, false);
+ fp << " ";
+ EmitString(fp, name);
+
+ if (ignoreOnError)
+ fp << " ignore_on_error";
+
+ fp << " ";
+ EmitScope(fp, 1, attrs, imports, true);
+}
+
+void ConfigWriter::EmitComment(std::ostream& fp, const String& text)
+{
+ fp << "/* " << text << " */\n";
+}
+
+void ConfigWriter::EmitFunctionCall(std::ostream& fp, const String& name, const Array::Ptr& arguments)
+{
+ EmitIdentifier(fp, name, false);
+ fp << "(";
+ EmitArrayItems(fp, 0, arguments);
+ fp << ")";
+}
+
+String ConfigWriter::EscapeIcingaString(const String& str)
+{
+ String result = str;
+ boost::algorithm::replace_all(result, "\\", "\\\\");
+ boost::algorithm::replace_all(result, "\n", "\\n");
+ boost::algorithm::replace_all(result, "\t", "\\t");
+ boost::algorithm::replace_all(result, "\r", "\\r");
+ boost::algorithm::replace_all(result, "\b", "\\b");
+ boost::algorithm::replace_all(result, "\f", "\\f");
+ boost::algorithm::replace_all(result, "\"", "\\\"");
+ return result;
+}
+
+const std::vector<String>& ConfigWriter::GetKeywords()
+{
+ static std::vector<String> keywords;
+ static std::mutex mutex;
+ std::unique_lock<std::mutex> lock(mutex);
+
+ if (keywords.empty()) {
+ keywords.emplace_back("object");
+ keywords.emplace_back("template");
+ keywords.emplace_back("include");
+ keywords.emplace_back("include_recursive");
+ keywords.emplace_back("include_zones");
+ keywords.emplace_back("library");
+ keywords.emplace_back("null");
+ keywords.emplace_back("true");
+ keywords.emplace_back("false");
+ keywords.emplace_back("const");
+ keywords.emplace_back("var");
+ keywords.emplace_back("this");
+ keywords.emplace_back("globals");
+ keywords.emplace_back("locals");
+ keywords.emplace_back("use");
+ keywords.emplace_back("using");
+ keywords.emplace_back("namespace");
+ keywords.emplace_back("default");
+ keywords.emplace_back("ignore_on_error");
+ keywords.emplace_back("current_filename");
+ keywords.emplace_back("current_line");
+ keywords.emplace_back("apply");
+ keywords.emplace_back("to");
+ keywords.emplace_back("where");
+ keywords.emplace_back("import");
+ keywords.emplace_back("assign");
+ keywords.emplace_back("ignore");
+ keywords.emplace_back("function");
+ keywords.emplace_back("return");
+ keywords.emplace_back("break");
+ keywords.emplace_back("continue");
+ keywords.emplace_back("for");
+ keywords.emplace_back("if");
+ keywords.emplace_back("else");
+ keywords.emplace_back("while");
+ keywords.emplace_back("throw");
+ keywords.emplace_back("try");
+ keywords.emplace_back("except");
+ }
+
+ return keywords;
+}
+
+ConfigIdentifier::ConfigIdentifier(String identifier)
+ : m_Name(std::move(identifier))
+{ }
+
+String ConfigIdentifier::GetName() const
+{
+ return m_Name;
+}
diff --git a/lib/base/configwriter.hpp b/lib/base/configwriter.hpp
new file mode 100644
index 0000000..a0c70f7
--- /dev/null
+++ b/lib/base/configwriter.hpp
@@ -0,0 +1,67 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGWRITER_H
+#define CONFIGWRITER_H
+
+#include "base/object.hpp"
+#include "base/array.hpp"
+#include "base/dictionary.hpp"
+#include <iosfwd>
+
+namespace icinga
+{
+
+/**
+ * A config identifier.
+ *
+ * @ingroup base
+ */
+class ConfigIdentifier final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigIdentifier);
+
+ ConfigIdentifier(String name);
+
+ String GetName() const;
+
+private:
+ String m_Name;
+};
+
+/**
+ * A configuration writer.
+ *
+ * @ingroup base
+ */
+class ConfigWriter
+{
+public:
+ static void EmitBoolean(std::ostream& fp, bool val);
+ static void EmitNumber(std::ostream& fp, double val);
+ static void EmitString(std::ostream& fp, const String& val);
+ static void EmitEmpty(std::ostream& fp);
+ static void EmitArray(std::ostream& fp, int indentLevel, const Array::Ptr& val);
+ static void EmitArrayItems(std::ostream& fp, int indentLevel, const Array::Ptr& val);
+ static void EmitScope(std::ostream& fp, int indentLevel, const Dictionary::Ptr& val,
+ const Array::Ptr& imports = nullptr, bool splitDot = false);
+ static void EmitValue(std::ostream& fp, int indentLevel, const Value& val);
+ static void EmitRaw(std::ostream& fp, const String& val);
+ static void EmitIndent(std::ostream& fp, int indentLevel);
+
+ static void EmitIdentifier(std::ostream& fp, const String& identifier, bool inAssignment);
+ static void EmitConfigItem(std::ostream& fp, const String& type, const String& name, bool isTemplate,
+ bool ignoreOnError, const Array::Ptr& imports, const Dictionary::Ptr& attrs);
+
+ static void EmitComment(std::ostream& fp, const String& text);
+ static void EmitFunctionCall(std::ostream& fp, const String& name, const Array::Ptr& arguments);
+
+ static const std::vector<String>& GetKeywords();
+private:
+ static String EscapeIcingaString(const String& str);
+ ConfigWriter();
+};
+
+}
+
+#endif /* CONFIGWRITER_H */
diff --git a/lib/base/console.cpp b/lib/base/console.cpp
new file mode 100644
index 0000000..99a5fad
--- /dev/null
+++ b/lib/base/console.cpp
@@ -0,0 +1,203 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/console.hpp"
+#include "base/initialize.hpp"
+#include <iostream>
+
+using namespace icinga;
+
+static ConsoleType l_ConsoleType = Console_Dumb;
+
+static void InitializeConsole()
+{
+ l_ConsoleType = Console_Dumb;
+
+#ifndef _WIN32
+ if (isatty(1))
+ l_ConsoleType = Console_VT100;
+#else /* _WIN32 */
+ l_ConsoleType = Console_Windows;
+#endif /* _WIN32 */
+}
+
+INITIALIZE_ONCE(InitializeConsole);
+
+ConsoleColorTag::ConsoleColorTag(int color, ConsoleType consoleType)
+ : m_Color(color), m_ConsoleType(consoleType)
+{ }
+
+std::ostream& icinga::operator<<(std::ostream& fp, const ConsoleColorTag& cct)
+{
+#ifndef _WIN32
+ if (cct.m_ConsoleType == Console_VT100 || Console::GetType(fp) == Console_VT100)
+ Console::PrintVT100ColorCode(fp, cct.m_Color);
+#else /* _WIN32 */
+ if (Console::GetType(fp) == Console_Windows) {
+ fp.flush();
+ Console::SetWindowsConsoleColor(fp, cct.m_Color);
+ }
+#endif /* _WIN32 */
+
+ return fp;
+}
+
+void Console::SetType(std::ostream& fp, ConsoleType type)
+{
+ if (&fp == &std::cout || &fp == &std::cerr)
+ l_ConsoleType = type;
+}
+
+ConsoleType Console::GetType(std::ostream& fp)
+{
+ if (&fp == &std::cout || &fp == &std::cerr)
+ return l_ConsoleType;
+ else
+ return Console_Dumb;
+}
+
+#ifndef _WIN32
+void Console::PrintVT100ColorCode(std::ostream& fp, int color)
+{
+ if (color == Console_Normal) {
+ fp << "\33[0m";
+ return;
+ }
+
+ switch (color & 0xff) {
+ case Console_ForegroundBlack:
+ fp << "\33[30m";
+ break;
+ case Console_ForegroundRed:
+ fp << "\33[31m";
+ break;
+ case Console_ForegroundGreen:
+ fp << "\33[32m";
+ break;
+ case Console_ForegroundYellow:
+ fp << "\33[33m";
+ break;
+ case Console_ForegroundBlue:
+ fp << "\33[34m";
+ break;
+ case Console_ForegroundMagenta:
+ fp << "\33[35m";
+ break;
+ case Console_ForegroundCyan:
+ fp << "\33[36m";
+ break;
+ case Console_ForegroundWhite:
+ fp << "\33[37m";
+ break;
+ }
+
+ switch (color & 0xff00) {
+ case Console_BackgroundBlack:
+ fp << "\33[40m";
+ break;
+ case Console_BackgroundRed:
+ fp << "\33[41m";
+ break;
+ case Console_BackgroundGreen:
+ fp << "\33[42m";
+ break;
+ case Console_BackgroundYellow:
+ fp << "\33[43m";
+ break;
+ case Console_BackgroundBlue:
+ fp << "\33[44m";
+ break;
+ case Console_BackgroundMagenta:
+ fp << "\33[45m";
+ break;
+ case Console_BackgroundCyan:
+ fp << "\33[46m";
+ break;
+ case Console_BackgroundWhite:
+ fp << "\33[47m";
+ break;
+ }
+
+ if (color & Console_Bold)
+ fp << "\33[1m";
+}
+#else /* _WIN32 */
+void Console::SetWindowsConsoleColor(std::ostream& fp, int color)
+{
+ CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
+ HANDLE hConsole;
+
+ if (&fp == &std::cout)
+ hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
+ else if (&fp == &std::cerr)
+ hConsole = GetStdHandle(STD_ERROR_HANDLE);
+ else
+ return;
+
+ if (!GetConsoleScreenBufferInfo(hConsole, &consoleInfo))
+ return;
+
+ WORD attrs = 0;
+
+ if (color == Console_Normal)
+ attrs = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
+
+ switch (color & 0xff) {
+ case Console_ForegroundBlack:
+ attrs |= 0;
+ break;
+ case Console_ForegroundRed:
+ attrs |= FOREGROUND_RED;
+ break;
+ case Console_ForegroundGreen:
+ attrs |= FOREGROUND_GREEN;
+ break;
+ case Console_ForegroundYellow:
+ attrs |= FOREGROUND_RED | FOREGROUND_GREEN;
+ break;
+ case Console_ForegroundBlue:
+ attrs |= FOREGROUND_BLUE;
+ break;
+ case Console_ForegroundMagenta:
+ attrs |= FOREGROUND_RED | FOREGROUND_BLUE;
+ break;
+ case Console_ForegroundCyan:
+ attrs |= FOREGROUND_GREEN | FOREGROUND_BLUE;
+ break;
+ case Console_ForegroundWhite:
+ attrs |= FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
+ break;
+ }
+
+ switch (color & 0xff00) {
+ case Console_BackgroundBlack:
+ attrs |= 0;
+ break;
+ case Console_BackgroundRed:
+ attrs |= BACKGROUND_RED;
+ break;
+ case Console_BackgroundGreen:
+ attrs |= BACKGROUND_GREEN;
+ break;
+ case Console_BackgroundYellow:
+ attrs |= BACKGROUND_RED | BACKGROUND_GREEN;
+ break;
+ case Console_BackgroundBlue:
+ attrs |= BACKGROUND_BLUE;
+ break;
+ case Console_BackgroundMagenta:
+ attrs |= BACKGROUND_RED | BACKGROUND_BLUE;
+ break;
+ case Console_BackgroundCyan:
+ attrs |= BACKGROUND_GREEN | BACKGROUND_BLUE;
+ break;
+ case Console_BackgroundWhite:
+ attrs |= BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE;
+ break;
+ }
+
+ if (color & Console_Bold)
+ attrs |= FOREGROUND_INTENSITY;
+
+ SetConsoleTextAttribute(hConsole, attrs);
+}
+#endif /* _WIN32 */
diff --git a/lib/base/console.hpp b/lib/base/console.hpp
new file mode 100644
index 0000000..f5b8c9a
--- /dev/null
+++ b/lib/base/console.hpp
@@ -0,0 +1,91 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONSOLE_H
+#define CONSOLE_H
+
+#include "base/i2-base.hpp"
+#include <iosfwd>
+
+namespace icinga
+{
+
+enum ConsoleColor
+{
+ Console_Normal,
+
+ // bit 0-7: foreground
+ Console_ForegroundBlack = 1,
+ Console_ForegroundRed = 2,
+ Console_ForegroundGreen = 3,
+ Console_ForegroundYellow = 4,
+ Console_ForegroundBlue = 5,
+ Console_ForegroundMagenta = 6,
+ Console_ForegroundCyan = 7,
+ Console_ForegroundWhite = 8,
+
+ // bit 8-15: background
+ Console_BackgroundBlack = 256,
+ Console_BackgroundRed = 266,
+ Console_BackgroundGreen = 267,
+ Console_BackgroundYellow = 268,
+ Console_BackgroundBlue = 269,
+ Console_BackgroundMagenta = 270,
+ Console_BackgroundCyan = 271,
+ Console_BackgroundWhite = 272,
+
+ // bit 16-23: flags
+ Console_Bold = 65536
+};
+
+enum ConsoleType
+{
+ Console_Autodetect = -1,
+
+ Console_Dumb,
+#ifndef _WIN32
+ Console_VT100,
+#else /* _WIN32 */
+ Console_Windows,
+#endif /* _WIN32 */
+};
+
+class ConsoleColorTag
+{
+public:
+ ConsoleColorTag(int color, ConsoleType consoleType = Console_Autodetect);
+
+ friend std::ostream& operator<<(std::ostream& fp, const ConsoleColorTag& cct);
+
+private:
+ int m_Color;
+ int m_ConsoleType;
+};
+
+std::ostream& operator<<(std::ostream& fp, const ConsoleColorTag& cct);
+
+/**
+ * Console utilities.
+ *
+ * @ingroup base
+ */
+class Console
+{
+public:
+ static void DetectType();
+
+ static void SetType(std::ostream& fp, ConsoleType type);
+ static ConsoleType GetType(std::ostream& fp);
+
+#ifndef _WIN32
+ static void PrintVT100ColorCode(std::ostream& fp, int color);
+#else /* _WIN32 */
+ static void SetWindowsConsoleColor(std::ostream& fp, int color);
+#endif /* _WIN32 */
+
+private:
+ Console();
+};
+
+}
+
+#endif /* CONSOLE_H */
diff --git a/lib/base/context.cpp b/lib/base/context.cpp
new file mode 100644
index 0000000..9c0a781
--- /dev/null
+++ b/lib/base/context.cpp
@@ -0,0 +1,64 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/context.hpp"
+#include <boost/thread/tss.hpp>
+#include <iostream>
+#include <sstream>
+#include <utility>
+
+using namespace icinga;
+
+static boost::thread_specific_ptr<std::vector<std::function<void(std::ostream&)>>> l_Frames;
+
+ContextFrame::ContextFrame(std::function<void(std::ostream&)> message)
+{
+ GetFrames().emplace_back(std::move(message));
+}
+
+ContextFrame::~ContextFrame()
+{
+ GetFrames().pop_back();
+}
+
+std::vector<std::function<void(std::ostream&)>>& ContextFrame::GetFrames()
+{
+ if (!l_Frames.get())
+ l_Frames.reset(new std::vector<std::function<void(std::ostream&)>>());
+
+ return *l_Frames;
+}
+
+ContextTrace::ContextTrace()
+{
+ for (auto frame (ContextFrame::GetFrames().rbegin()); frame != ContextFrame::GetFrames().rend(); ++frame) {
+ std::ostringstream oss;
+
+ (*frame)(oss);
+ m_Frames.emplace_back(oss.str());
+ }
+}
+
+void ContextTrace::Print(std::ostream& fp) const
+{
+ if (m_Frames.empty())
+ return;
+
+ fp << "\n";
+
+ int i = 0;
+ for (const String& frame : m_Frames) {
+ fp << "\t(" << i << ") " << frame << "\n";
+ i++;
+ }
+}
+
+size_t ContextTrace::GetLength() const
+{
+ return m_Frames.size();
+}
+
+std::ostream& icinga::operator<<(std::ostream& stream, const ContextTrace& trace)
+{
+ trace.Print(stream);
+ return stream;
+}
diff --git a/lib/base/context.hpp b/lib/base/context.hpp
new file mode 100644
index 0000000..d6fe733
--- /dev/null
+++ b/lib/base/context.hpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONTEXT_H
+#define CONTEXT_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include <functional>
+#include <vector>
+
+namespace icinga
+{
+
+class ContextTrace
+{
+public:
+ ContextTrace();
+
+ void Print(std::ostream& fp) const;
+
+ size_t GetLength() const;
+
+private:
+ std::vector<String> m_Frames;
+};
+
+std::ostream& operator<<(std::ostream& stream, const ContextTrace& trace);
+
+/**
+ * A context frame.
+ *
+ * @ingroup base
+ */
+class ContextFrame
+{
+public:
+ ContextFrame(std::function<void(std::ostream&)> message);
+ ~ContextFrame();
+
+private:
+ static std::vector<std::function<void(std::ostream&)>>& GetFrames();
+
+ friend class ContextTrace;
+};
+
+/* The currentContextFrame variable has to be volatile in order to prevent
+ * the compiler from optimizing it away. */
+#define CONTEXT(message) volatile icinga::ContextFrame currentContextFrame ([&](std::ostream& _CONTEXT_stream) { \
+_CONTEXT_stream << message; \
+})
+
+}
+
+#endif /* CONTEXT_H */
diff --git a/lib/base/convert.cpp b/lib/base/convert.cpp
new file mode 100644
index 0000000..19d3e44
--- /dev/null
+++ b/lib/base/convert.cpp
@@ -0,0 +1,46 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/convert.hpp"
+#include "base/datetime.hpp"
+#include <boost/lexical_cast.hpp>
+#include <iomanip>
+
+using namespace icinga;
+
+String Convert::ToString(const String& val)
+{
+ return val;
+}
+
+String Convert::ToString(const Value& val)
+{
+ return val;
+}
+
+String Convert::ToString(double val)
+{
+ double integral;
+ double fractional = std::modf(val, &integral);
+
+ std::ostringstream msgbuf;
+ if (fractional == 0) {
+ msgbuf << std::setprecision(0);
+ }
+ msgbuf << std::fixed << val;
+ return msgbuf.str();
+}
+
+double Convert::ToDateTimeValue(double val)
+{
+ return val;
+}
+
+double Convert::ToDateTimeValue(const Value& val)
+{
+ if (val.IsNumber())
+ return val;
+ else if (val.IsObjectType<DateTime>())
+ return static_cast<DateTime::Ptr>(val)->GetValue();
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Not a DateTime value."));
+}
diff --git a/lib/base/convert.hpp b/lib/base/convert.hpp
new file mode 100644
index 0000000..e0754b3
--- /dev/null
+++ b/lib/base/convert.hpp
@@ -0,0 +1,84 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONVERT_H
+#define CONVERT_H
+
+#include "base/i2-base.hpp"
+#include "base/value.hpp"
+#include <boost/lexical_cast.hpp>
+
+namespace icinga
+{
+
+/**
+ * Utility class for converting types.
+ *
+ * @ingroup base
+ */
+class Convert
+{
+public:
+ template<typename T>
+ static long ToLong(const T& val)
+ {
+ try {
+ return boost::lexical_cast<long>(val);
+ } catch (const std::exception&) {
+ std::ostringstream msgbuf;
+ msgbuf << "Can't convert '" << val << "' to an integer.";
+ BOOST_THROW_EXCEPTION(std::invalid_argument(msgbuf.str()));
+ }
+ }
+
+ template<typename T>
+ static double ToDouble(const T& val)
+ {
+ try {
+ return boost::lexical_cast<double>(val);
+ } catch (const std::exception&) {
+ std::ostringstream msgbuf;
+ msgbuf << "Can't convert '" << val << "' to a floating point number.";
+ BOOST_THROW_EXCEPTION(std::invalid_argument(msgbuf.str()));
+ }
+ }
+
+ static long ToLong(const Value& val)
+ {
+ return val;
+ }
+
+ static long ToLong(double val)
+ {
+ return static_cast<long>(val);
+ }
+
+ static double ToDouble(const Value& val)
+ {
+ return val;
+ }
+
+ static bool ToBool(const Value& val)
+ {
+ return val.ToBool();
+ }
+
+ template<typename T>
+ static String ToString(const T& val)
+ {
+ return boost::lexical_cast<std::string>(val);
+ }
+
+ static String ToString(const String& val);
+ static String ToString(const Value& val);
+ static String ToString(double val);
+
+ static double ToDateTimeValue(double val);
+ static double ToDateTimeValue(const Value& val);
+
+private:
+ Convert();
+};
+
+}
+
+#endif /* CONVERT_H */
diff --git a/lib/base/datetime-script.cpp b/lib/base/datetime-script.cpp
new file mode 100644
index 0000000..6c18381
--- /dev/null
+++ b/lib/base/datetime-script.cpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/datetime.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+static String DateTimeFormat(const String& format)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ DateTime::Ptr self = static_cast<DateTime::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ return self->Format(format);
+}
+
+Object::Ptr DateTime::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "format", new Function("DateTime#format", DateTimeFormat, { "format" }) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/datetime.cpp b/lib/base/datetime.cpp
new file mode 100644
index 0000000..aa7b5e5
--- /dev/null
+++ b/lib/base/datetime.cpp
@@ -0,0 +1,58 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/datetime.hpp"
+#include "base/datetime-ti.cpp"
+#include "base/utility.hpp"
+#include "base/primitivetype.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE_WITH_PROTOTYPE(DateTime, DateTime::GetPrototype());
+
+DateTime::DateTime(double value)
+ : m_Value(value)
+{ }
+
+DateTime::DateTime(const std::vector<Value>& args)
+{
+ if (args.empty())
+ m_Value = Utility::GetTime();
+ else if (args.size() == 3 || args.size() == 6) {
+ struct tm tms;
+ tms.tm_year = args[0] - 1900;
+ tms.tm_mon = args[1] - 1;
+ tms.tm_mday = args[2];
+
+ if (args.size() == 6) {
+ tms.tm_hour = args[3];
+ tms.tm_min = args[4];
+ tms.tm_sec = args[5];
+ } else {
+ tms.tm_hour = 0;
+ tms.tm_min = 0;
+ tms.tm_sec = 0;
+ }
+
+ tms.tm_isdst = -1;
+
+ m_Value = mktime(&tms);
+ } else if (args.size() == 1)
+ m_Value = args[0];
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid number of arguments for the DateTime constructor."));
+}
+
+double DateTime::GetValue() const
+{
+ return m_Value;
+}
+
+String DateTime::Format(const String& format) const
+{
+ return Utility::FormatDateTime(format.CStr(), m_Value);
+}
+
+String DateTime::ToString() const
+{
+ return Format("%Y-%m-%d %H:%M:%S %z");
+}
diff --git a/lib/base/datetime.hpp b/lib/base/datetime.hpp
new file mode 100644
index 0000000..e7b8a1f
--- /dev/null
+++ b/lib/base/datetime.hpp
@@ -0,0 +1,40 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DATETIME_H
+#define DATETIME_H
+
+#include "base/i2-base.hpp"
+#include "base/datetime-ti.hpp"
+#include "base/value.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * A date/time value.
+ *
+ * @ingroup base
+ */
+class DateTime final : public ObjectImpl<DateTime>
+{
+public:
+ DECLARE_OBJECT(DateTime);
+
+ DateTime(double value);
+ DateTime(const std::vector<Value>& args);
+
+ String Format(const String& format) const;
+
+ double GetValue() const override;
+ String ToString() const override;
+
+ static Object::Ptr GetPrototype();
+
+private:
+ double m_Value;
+};
+
+}
+
+#endif /* DATETIME_H */
diff --git a/lib/base/datetime.ti b/lib/base/datetime.ti
new file mode 100644
index 0000000..b9d7375
--- /dev/null
+++ b/lib/base/datetime.ti
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+library base;
+
+namespace icinga
+{
+
+vararg_constructor class DateTime
+{
+ [state, no_storage] Timestamp value {
+ get;
+ };
+};
+
+}
diff --git a/lib/base/debug.hpp b/lib/base/debug.hpp
new file mode 100644
index 0000000..54b424c
--- /dev/null
+++ b/lib/base/debug.hpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DEBUG_H
+#define DEBUG_H
+
+#include "i2-base.hpp"
+
+#ifndef I2_DEBUG
+# define ASSERT(expr) ((void)0)
+#else /* I2_DEBUG */
+# define ASSERT(expr) ((expr) ? 0 : icinga_assert_fail(#expr, __FILE__, __LINE__))
+#endif /* I2_DEBUG */
+
+#define VERIFY(expr) ((expr) ? 0 : icinga_assert_fail(#expr, __FILE__, __LINE__))
+
+#ifdef _MSC_VER
+# define NORETURNPRE __declspec(noreturn)
+#else /* _MSC_VER */
+# define NORETURNPRE
+#endif /* _MSC_VER */
+
+#ifdef __GNUC__
+# define NORETURNPOST __attribute__((noreturn))
+#else /* __GNUC__ */
+# define NORETURNPOST
+#endif /* __GNUC__ */
+
+NORETURNPRE int icinga_assert_fail(const char *expr, const char *file, int line) NORETURNPOST;
+
+#ifdef _MSC_VER
+# pragma warning( push )
+# pragma warning( disable : 4646 ) /* function declared with __declspec(noreturn) has non-void return type */
+#endif /* _MSC_VER */
+
+inline int icinga_assert_fail(const char *expr, const char *file, int line)
+{
+ fprintf(stderr, "%s:%d: assertion failed: %s\n", file, line, expr);
+ std::abort();
+
+#if !defined(__GNUC__) && !defined(_MSC_VER)
+ return 0;
+#endif /* !defined(__GNUC__) && !defined(_MSC_VER) */
+}
+
+#ifdef _MSC_VER
+# pragma warning( pop )
+#endif /* _MSC_VER */
+
+#endif /* DEBUG_H */
diff --git a/lib/base/debuginfo.cpp b/lib/base/debuginfo.cpp
new file mode 100644
index 0000000..99006ac
--- /dev/null
+++ b/lib/base/debuginfo.cpp
@@ -0,0 +1,98 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/debuginfo.hpp"
+#include "base/convert.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+/**
+ * Outputs a DebugInfo struct to a stream.
+ *
+ * @param out The output stream.
+ * @param val The DebugInfo struct.
+ * @returns The output stream.
+ */
+std::ostream& icinga::operator<<(std::ostream& out, const DebugInfo& val)
+{
+ out << "in " << val.Path << ": "
+ << val.FirstLine << ":" << val.FirstColumn
+ << "-"
+ << val.LastLine << ":" << val.LastColumn;
+
+ return out;
+}
+
+DebugInfo icinga::DebugInfoRange(const DebugInfo& start, const DebugInfo& end)
+{
+ DebugInfo result;
+ result.Path = start.Path;
+ result.FirstLine = start.FirstLine;
+ result.FirstColumn = start.FirstColumn;
+ result.LastLine = end.LastLine;
+ result.LastColumn = end.LastColumn;
+ return result;
+}
+
+#define EXTRA_LINES 2
+
+void icinga::ShowCodeLocation(std::ostream& out, const DebugInfo& di, bool verbose)
+{
+ if (di.Path.IsEmpty())
+ return;
+
+ out << "Location: " << di;
+
+ std::ifstream ifs;
+ ifs.open(di.Path.CStr(), std::ifstream::in);
+
+ int lineno = 0;
+ char line[1024];
+
+ while (ifs.good() && lineno <= di.LastLine + EXTRA_LINES) {
+ if (lineno == 0)
+ out << "\n";
+
+ lineno++;
+
+ ifs.getline(line, sizeof(line));
+
+ for (int i = 0; line[i]; i++)
+ if (line[i] == '\t')
+ line[i] = ' ';
+
+ int extra_lines = verbose ? EXTRA_LINES : 0;
+
+ if (lineno < di.FirstLine - extra_lines || lineno > di.LastLine + extra_lines)
+ continue;
+
+ String pathInfo = di.Path + "(" + Convert::ToString(lineno) + "): ";
+ out << pathInfo;
+ out << line << "\n";
+
+ if (lineno >= di.FirstLine && lineno <= di.LastLine) {
+ int start, end;
+
+ start = 0;
+ end = strlen(line);
+
+ if (lineno == di.FirstLine)
+ start = di.FirstColumn - 1;
+
+ if (lineno == di.LastLine)
+ end = di.LastColumn;
+
+ if (start < 0) {
+ end -= start;
+ start = 0;
+ }
+
+ out << String(pathInfo.GetLength(), ' ');
+ out << String(start, ' ');
+ out << String(end - start, '^');
+
+ out << "\n";
+ }
+ }
+}
+
diff --git a/lib/base/debuginfo.hpp b/lib/base/debuginfo.hpp
new file mode 100644
index 0000000..d47db91
--- /dev/null
+++ b/lib/base/debuginfo.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DEBUGINFO_H
+#define DEBUGINFO_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+
+namespace icinga
+{
+
+/**
+ * Debug information for a configuration element.
+ *
+ * @ingroup config
+ */
+struct DebugInfo
+{
+ String Path;
+
+ int FirstLine{0};
+ int FirstColumn{0};
+
+ int LastLine{0};
+ int LastColumn{0};
+};
+
+std::ostream& operator<<(std::ostream& out, const DebugInfo& val);
+
+DebugInfo DebugInfoRange(const DebugInfo& start, const DebugInfo& end);
+
+void ShowCodeLocation(std::ostream& out, const DebugInfo& di, bool verbose = true);
+
+}
+
+#endif /* DEBUGINFO_H */
diff --git a/lib/base/defer.hpp b/lib/base/defer.hpp
new file mode 100644
index 0000000..2a23261
--- /dev/null
+++ b/lib/base/defer.hpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DEFER
+#define DEFER
+
+#include <functional>
+#include <utility>
+
+namespace icinga
+{
+
+/**
+ * An action to be executed at end of scope.
+ *
+ * @ingroup base
+ */
+class Defer
+{
+public:
+ inline
+ Defer(std::function<void()> func) : m_Func(std::move(func))
+ {
+ }
+
+ Defer(const Defer&) = delete;
+ Defer(Defer&&) = delete;
+ Defer& operator=(const Defer&) = delete;
+ Defer& operator=(Defer&&) = delete;
+
+ inline
+ ~Defer()
+ {
+ if (m_Func) {
+ try {
+ m_Func();
+ } catch (...) {
+ // https://stackoverflow.com/questions/130117/throwing-exceptions-out-of-a-destructor
+ }
+ }
+ }
+
+ inline
+ void Cancel()
+ {
+ m_Func = nullptr;
+ }
+
+private:
+ std::function<void()> m_Func;
+};
+
+}
+
+#endif /* DEFER */
diff --git a/lib/base/dependencygraph.cpp b/lib/base/dependencygraph.cpp
new file mode 100644
index 0000000..025eb3e
--- /dev/null
+++ b/lib/base/dependencygraph.cpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/dependencygraph.hpp"
+
+using namespace icinga;
+
+std::mutex DependencyGraph::m_Mutex;
+std::map<Object *, std::map<Object *, int> > DependencyGraph::m_Dependencies;
+
+void DependencyGraph::AddDependency(Object *parent, Object *child)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Dependencies[child][parent]++;
+}
+
+void DependencyGraph::RemoveDependency(Object *parent, Object *child)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ auto& refs = m_Dependencies[child];
+ auto it = refs.find(parent);
+
+ if (it == refs.end())
+ return;
+
+ it->second--;
+
+ if (it->second == 0)
+ refs.erase(it);
+
+ if (refs.empty())
+ m_Dependencies.erase(child);
+}
+
+std::vector<Object::Ptr> DependencyGraph::GetParents(const Object::Ptr& child)
+{
+ std::vector<Object::Ptr> objects;
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ auto it = m_Dependencies.find(child.get());
+
+ if (it != m_Dependencies.end()) {
+ typedef std::pair<Object *, int> kv_pair;
+ for (const kv_pair& kv : it->second) {
+ objects.emplace_back(kv.first);
+ }
+ }
+
+ return objects;
+}
diff --git a/lib/base/dependencygraph.hpp b/lib/base/dependencygraph.hpp
new file mode 100644
index 0000000..51aa90e
--- /dev/null
+++ b/lib/base/dependencygraph.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DEPENDENCYGRAPH_H
+#define DEPENDENCYGRAPH_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+#include <map>
+#include <mutex>
+
+namespace icinga {
+
+/**
+ * A graph that tracks dependencies between objects.
+ *
+ * @ingroup base
+ */
+class DependencyGraph
+{
+public:
+ static void AddDependency(Object *parent, Object *child);
+ static void RemoveDependency(Object *parent, Object *child);
+ static std::vector<Object::Ptr> GetParents(const Object::Ptr& child);
+
+private:
+ DependencyGraph();
+
+ static std::mutex m_Mutex;
+ static std::map<Object *, std::map<Object *, int> > m_Dependencies;
+};
+
+}
+
+#endif /* DEPENDENCYGRAPH_H */
diff --git a/lib/base/dictionary-script.cpp b/lib/base/dictionary-script.cpp
new file mode 100644
index 0000000..ad19c5b
--- /dev/null
+++ b/lib/base/dictionary-script.cpp
@@ -0,0 +1,119 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/array.hpp"
+
+using namespace icinga;
+
+static double DictionaryLen()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->GetLength();
+}
+
+static void DictionarySet(const String& key, const Value& value)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Set(key, value);
+}
+
+static Value DictionaryGet(const String& key)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Get(key);
+}
+
+static void DictionaryRemove(const String& key)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Remove(key);
+}
+
+static void DictionaryClear()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Clear();
+}
+
+static bool DictionaryContains(const String& key)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Contains(key);
+}
+
+static Dictionary::Ptr DictionaryShallowClone()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->ShallowClone();
+}
+
+static Array::Ptr DictionaryKeys()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ ArrayData keys;
+ ObjectLock olock(self);
+ for (const Dictionary::Pair& kv : self) {
+ keys.push_back(kv.first);
+ }
+ return new Array(std::move(keys));
+}
+
+static Array::Ptr DictionaryValues()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ ArrayData values;
+ ObjectLock olock(self);
+ for (const Dictionary::Pair& kv : self) {
+ values.push_back(kv.second);
+ }
+ return new Array(std::move(values));
+}
+
+static void DictionaryFreeze()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Dictionary::Ptr self = static_cast<Dictionary::Ptr>(vframe->Self);
+ self->Freeze();
+}
+
+Object::Ptr Dictionary::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "len", new Function("Dictionary#len", DictionaryLen, {}, true) },
+ { "set", new Function("Dictionary#set", DictionarySet, { "key", "value" }) },
+ { "get", new Function("Dictionary#get", DictionaryGet, { "key" }) },
+ { "remove", new Function("Dictionary#remove", DictionaryRemove, { "key" }) },
+ { "clear", new Function("Dictionary#clear", DictionaryClear, {}) },
+ { "contains", new Function("Dictionary#contains", DictionaryContains, { "key" }, true) },
+ { "shallow_clone", new Function("Dictionary#shallow_clone", DictionaryShallowClone, {}, true) },
+ { "keys", new Function("Dictionary#keys", DictionaryKeys, {}, true) },
+ { "values", new Function("Dictionary#values", DictionaryValues, {}, true) },
+ { "freeze", new Function("Dictionary#freeze", DictionaryFreeze, {}) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/dictionary.cpp b/lib/base/dictionary.cpp
new file mode 100644
index 0000000..8d3f80d
--- /dev/null
+++ b/lib/base/dictionary.cpp
@@ -0,0 +1,317 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/dictionary.hpp"
+#include "base/objectlock.hpp"
+#include "base/debug.hpp"
+#include "base/primitivetype.hpp"
+#include "base/configwriter.hpp"
+#include <sstream>
+
+using namespace icinga;
+
+template class std::map<String, Value>;
+
+REGISTER_PRIMITIVE_TYPE(Dictionary, Object, Dictionary::GetPrototype());
+
+Dictionary::Dictionary(const DictionaryData& other)
+{
+ for (const auto& kv : other)
+ m_Data.insert(kv);
+}
+
+Dictionary::Dictionary(DictionaryData&& other)
+{
+ for (auto& kv : other)
+ m_Data.insert(std::move(kv));
+}
+
+Dictionary::Dictionary(std::initializer_list<Dictionary::Pair> init)
+ : m_Data(init)
+{ }
+
+/**
+ * Retrieves a value from a dictionary.
+ *
+ * @param key The key whose value should be retrieved.
+ * @returns The value of an empty value if the key was not found.
+ */
+Value Dictionary::Get(const String& key) const
+{
+ std::shared_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ auto it = m_Data.find(key);
+
+ if (it == m_Data.end())
+ return Empty;
+
+ return it->second;
+}
+
+/**
+ * Retrieves a value from a dictionary.
+ *
+ * @param key The key whose value should be retrieved.
+ * @param result The value of the dictionary item (only set when the key exists)
+ * @returns true if the key exists, false otherwise.
+ */
+bool Dictionary::Get(const String& key, Value *result) const
+{
+ std::shared_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ auto it = m_Data.find(key);
+
+ if (it == m_Data.end())
+ return false;
+
+ *result = it->second;
+ return true;
+}
+
+/**
+ * Retrieves a value's address from a dictionary.
+ *
+ * @param key The key whose value's address should be retrieved.
+ * @returns nullptr if the key was not found.
+ */
+const Value * Dictionary::GetRef(const String& key) const
+{
+ std::shared_lock<std::shared_timed_mutex> lock (m_DataMutex);
+ auto it (m_Data.find(key));
+
+ return it == m_Data.end() ? nullptr : &it->second;
+}
+
+/**
+ * Sets a value in the dictionary.
+ *
+ * @param key The key.
+ * @param value The value.
+ * @param overrideFrozen Whether to allow modifying frozen dictionaries.
+ */
+void Dictionary::Set(const String& key, Value value, bool overrideFrozen)
+{
+ ObjectLock olock(this);
+ std::unique_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ if (m_Frozen && !overrideFrozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Value in dictionary must not be modified."));
+
+ m_Data[key] = std::move(value);
+}
+
+/**
+ * Returns the number of elements in the dictionary.
+ *
+ * @returns Number of elements.
+ */
+size_t Dictionary::GetLength() const
+{
+ std::shared_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ return m_Data.size();
+}
+
+/**
+ * Checks whether the dictionary contains the specified key.
+ *
+ * @param key The key.
+ * @returns true if the dictionary contains the key, false otherwise.
+ */
+bool Dictionary::Contains(const String& key) const
+{
+ std::shared_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ return (m_Data.find(key) != m_Data.end());
+}
+
+/**
+ * Returns an iterator to the beginning of the dictionary.
+ *
+ * Note: Caller must hold the object lock while using the iterator.
+ *
+ * @returns An iterator.
+ */
+Dictionary::Iterator Dictionary::Begin()
+{
+ ASSERT(OwnsLock());
+
+ return m_Data.begin();
+}
+
+/**
+ * Returns an iterator to the end of the dictionary.
+ *
+ * Note: Caller must hold the object lock while using the iterator.
+ *
+ * @returns An iterator.
+ */
+Dictionary::Iterator Dictionary::End()
+{
+ ASSERT(OwnsLock());
+
+ return m_Data.end();
+}
+
+/**
+ * Removes the item specified by the iterator from the dictionary.
+ *
+ * @param it The iterator.
+ */
+void Dictionary::Remove(Dictionary::Iterator it)
+{
+ ASSERT(OwnsLock());
+ std::unique_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ if (m_Frozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionary must not be modified."));
+
+ m_Data.erase(it);
+}
+
+/**
+ * Removes the specified key from the dictionary.
+ *
+ * @param key The key.
+ */
+void Dictionary::Remove(const String& key)
+{
+ ObjectLock olock(this);
+ std::unique_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ if (m_Frozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionary must not be modified."));
+
+ Dictionary::Iterator it;
+ it = m_Data.find(key);
+
+ if (it == m_Data.end())
+ return;
+
+ m_Data.erase(it);
+}
+
+/**
+ * Removes all dictionary items.
+ */
+void Dictionary::Clear()
+{
+ ObjectLock olock(this);
+ std::unique_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ if (m_Frozen)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionary must not be modified."));
+
+ m_Data.clear();
+}
+
+void Dictionary::CopyTo(const Dictionary::Ptr& dest) const
+{
+ std::shared_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ for (const Dictionary::Pair& kv : m_Data) {
+ dest->Set(kv.first, kv.second);
+ }
+}
+
+/**
+ * Makes a shallow copy of a dictionary.
+ *
+ * @returns a copy of the dictionary.
+ */
+Dictionary::Ptr Dictionary::ShallowClone() const
+{
+ Dictionary::Ptr clone = new Dictionary();
+ CopyTo(clone);
+ return clone;
+}
+
+/**
+ * Makes a deep clone of a dictionary
+ * and its elements.
+ *
+ * @returns a copy of the dictionary.
+ */
+Object::Ptr Dictionary::Clone() const
+{
+ DictionaryData dict;
+
+ {
+ std::shared_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ dict.reserve(GetLength());
+
+ for (const Dictionary::Pair& kv : m_Data) {
+ dict.emplace_back(kv.first, kv.second.Clone());
+ }
+ }
+
+ return new Dictionary(std::move(dict));
+}
+
+/**
+ * Returns an ordered vector containing all keys
+ * which are currently set in this directory.
+ *
+ * @returns an ordered vector of key names
+ */
+std::vector<String> Dictionary::GetKeys() const
+{
+ std::shared_lock<std::shared_timed_mutex> lock (m_DataMutex);
+
+ std::vector<String> keys;
+
+ for (const Dictionary::Pair& kv : m_Data) {
+ keys.push_back(kv.first);
+ }
+
+ return keys;
+}
+
+String Dictionary::ToString() const
+{
+ std::ostringstream msgbuf;
+ ConfigWriter::EmitScope(msgbuf, 1, const_cast<Dictionary *>(this));
+ return msgbuf.str();
+}
+
+void Dictionary::Freeze()
+{
+ ObjectLock olock(this);
+ m_Frozen = true;
+}
+
+Value Dictionary::GetFieldByName(const String& field, bool, const DebugInfo& debugInfo) const
+{
+ Value value;
+
+ if (Get(field, &value))
+ return value;
+ else
+ return GetPrototypeField(const_cast<Dictionary *>(this), field, false, debugInfo);
+}
+
+void Dictionary::SetFieldByName(const String& field, const Value& value, bool overrideFrozen, const DebugInfo&)
+{
+ Set(field, value, overrideFrozen);
+}
+
+bool Dictionary::HasOwnField(const String& field) const
+{
+ return Contains(field);
+}
+
+bool Dictionary::GetOwnField(const String& field, Value *result) const
+{
+ return Get(field, result);
+}
+
+Dictionary::Iterator icinga::begin(const Dictionary::Ptr& x)
+{
+ return x->Begin();
+}
+
+Dictionary::Iterator icinga::end(const Dictionary::Ptr& x)
+{
+ return x->End();
+}
+
diff --git a/lib/base/dictionary.hpp b/lib/base/dictionary.hpp
new file mode 100644
index 0000000..ffccd63
--- /dev/null
+++ b/lib/base/dictionary.hpp
@@ -0,0 +1,91 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DICTIONARY_H
+#define DICTIONARY_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+#include "base/value.hpp"
+#include <boost/range/iterator.hpp>
+#include <map>
+#include <shared_mutex>
+#include <vector>
+
+namespace icinga
+{
+
+typedef std::vector<std::pair<String, Value> > DictionaryData;
+
+/**
+ * A container that holds key-value pairs.
+ *
+ * @ingroup base
+ */
+class Dictionary final : public Object
+{
+public:
+ DECLARE_OBJECT(Dictionary);
+
+ /**
+ * An iterator that can be used to iterate over dictionary elements.
+ */
+ typedef std::map<String, Value>::iterator Iterator;
+
+ typedef std::map<String, Value>::size_type SizeType;
+
+ typedef std::map<String, Value>::value_type Pair;
+
+ Dictionary() = default;
+ Dictionary(const DictionaryData& other);
+ Dictionary(DictionaryData&& other);
+ Dictionary(std::initializer_list<Pair> init);
+
+ Value Get(const String& key) const;
+ bool Get(const String& key, Value *result) const;
+ const Value * GetRef(const String& key) const;
+ void Set(const String& key, Value value, bool overrideFrozen = false);
+ bool Contains(const String& key) const;
+
+ Iterator Begin();
+ Iterator End();
+
+ size_t GetLength() const;
+
+ void Remove(const String& key);
+
+ void Remove(Iterator it);
+
+ void Clear();
+
+ void CopyTo(const Dictionary::Ptr& dest) const;
+ Dictionary::Ptr ShallowClone() const;
+
+ std::vector<String> GetKeys() const;
+
+ static Object::Ptr GetPrototype();
+
+ Object::Ptr Clone() const override;
+
+ String ToString() const override;
+
+ void Freeze();
+
+ Value GetFieldByName(const String& field, bool sandboxed, const DebugInfo& debugInfo) const override;
+ void SetFieldByName(const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) override;
+ bool HasOwnField(const String& field) const override;
+ bool GetOwnField(const String& field, Value *result) const override;
+
+private:
+ std::map<String, Value> m_Data; /**< The data for the dictionary. */
+ mutable std::shared_timed_mutex m_DataMutex;
+ bool m_Frozen{false};
+};
+
+Dictionary::Iterator begin(const Dictionary::Ptr& x);
+Dictionary::Iterator end(const Dictionary::Ptr& x);
+
+}
+
+extern template class std::map<icinga::String, icinga::Value>;
+
+#endif /* DICTIONARY_H */
diff --git a/lib/base/exception.cpp b/lib/base/exception.cpp
new file mode 100644
index 0000000..57b324b
--- /dev/null
+++ b/lib/base/exception.cpp
@@ -0,0 +1,507 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/exception.hpp"
+#include "base/stacktrace.hpp"
+#include <boost/thread/tss.hpp>
+#include <utility>
+
+#ifdef _WIN32
+# include "base/utility.hpp"
+#endif /* _WIN32 */
+
+#ifdef HAVE_CXXABI_H
+# include <cxxabi.h>
+#endif /* HAVE_CXXABI_H */
+
+using namespace icinga;
+
+static boost::thread_specific_ptr<boost::stacktrace::stacktrace> l_LastExceptionStack;
+static boost::thread_specific_ptr<ContextTrace> l_LastExceptionContext;
+
+#ifdef HAVE_CXXABI_H
+
+#ifdef _LIBCPPABI_VERSION
+class libcxx_type_info : public std::type_info
+{
+public:
+ ~libcxx_type_info() override;
+
+ virtual void noop1() const;
+ virtual void noop2() const;
+ virtual bool can_catch(const libcxx_type_info *thrown_type, void *&adjustedPtr) const = 0;
+};
+#endif /* _LIBCPPABI_VERSION */
+
+
+#if defined(__GLIBCXX__) || defined(_LIBCPPABI_VERSION)
+/**
+ * Attempts to cast an exception to a destination type
+ *
+ * @param obj Exception to be casted
+ * @param src Type information of obj
+ * @param dst Information of which type to cast to
+ * @return Pointer to the exception if the cast is possible, nullptr otherwise
+ */
+inline void *cast_exception(void *obj, const std::type_info *src, const std::type_info *dst)
+{
+#ifdef __GLIBCXX__
+ void *thrown_ptr = obj;
+
+ /* Check if the exception is a pointer type. */
+ if (src->__is_pointer_p())
+ thrown_ptr = *(void **)thrown_ptr;
+
+ if (dst->__do_catch(src, &thrown_ptr, 1))
+ return thrown_ptr;
+ else
+ return nullptr;
+#else /* __GLIBCXX__ */
+ const auto *srcInfo = static_cast<const libcxx_type_info *>(src);
+ const auto *dstInfo = static_cast<const libcxx_type_info *>(dst);
+
+ void *adj = obj;
+
+ if (dstInfo->can_catch(srcInfo, adj))
+ return adj;
+ else
+ return nullptr;
+#endif /* __GLIBCXX__ */
+
+}
+#else /* defined(__GLIBCXX__) || defined(_LIBCPPABI_VERSION) */
+#define NO_CAST_EXCEPTION
+#endif /* defined(__GLIBCXX__) || defined(_LIBCPPABI_VERSION) */
+
+# if __clang_major__ > 3 || (__clang_major__ == 3 && __clang_minor__ > 3)
+# define TYPEINFO_TYPE std::type_info
+# else
+# define TYPEINFO_TYPE void
+# endif
+
+# if !defined(__GLIBCXX__) && !defined(_WIN32)
+static boost::thread_specific_ptr<void *> l_LastExceptionObj;
+static boost::thread_specific_ptr<TYPEINFO_TYPE *> l_LastExceptionPvtInfo;
+
+typedef void (*DestCallback)(void *);
+static boost::thread_specific_ptr<DestCallback> l_LastExceptionDest;
+# endif /* !__GLIBCXX__ && !_WIN32 */
+
+extern "C" void __cxa_throw(void *obj, TYPEINFO_TYPE *pvtinfo, void (*dest)(void *));
+#endif /* HAVE_CXXABI_H */
+
+void icinga::RethrowUncaughtException()
+{
+#if defined(__GLIBCXX__) || !defined(HAVE_CXXABI_H)
+ throw;
+#else /* __GLIBCXX__ || !HAVE_CXXABI_H */
+ __cxa_throw(*l_LastExceptionObj.get(), *l_LastExceptionPvtInfo.get(), *l_LastExceptionDest.get());
+#endif /* __GLIBCXX__ || !HAVE_CXXABI_H */
+}
+
+#ifdef HAVE_CXXABI_H
+extern "C"
+void __cxa_throw(void *obj, TYPEINFO_TYPE *pvtinfo, void (*dest)(void *))
+{
+ /* This function overrides an internal function of libstdc++ that is called when a C++ exception is thrown in order
+ * to capture as much information as possible at that time and then call the original implementation. This
+ * information includes:
+ * - stack trace (for later use in DiagnosticInformation)
+ * - context trace (for later use in DiagnosticInformation)
+ */
+
+ auto *tinfo = static_cast<std::type_info *>(pvtinfo);
+
+ typedef void (*cxa_throw_fn)(void *, std::type_info *, void (*)(void *)) __attribute__((noreturn));
+ static cxa_throw_fn real_cxa_throw;
+
+#if !defined(__GLIBCXX__) && !defined(_WIN32)
+ l_LastExceptionObj.reset(new void *(obj));
+ l_LastExceptionPvtInfo.reset(new TYPEINFO_TYPE *(pvtinfo));
+ l_LastExceptionDest.reset(new DestCallback(dest));
+#endif /* !defined(__GLIBCXX__) && !defined(_WIN32) */
+
+ // resolve symbol to original implementation of __cxa_throw for the call at the end of this function
+ if (real_cxa_throw == nullptr)
+ real_cxa_throw = (cxa_throw_fn)dlsym(RTLD_NEXT, "__cxa_throw");
+
+#ifndef NO_CAST_EXCEPTION
+ void *uex = cast_exception(obj, tinfo, &typeid(user_error));
+ auto *ex = reinterpret_cast<boost::exception *>(cast_exception(obj, tinfo, &typeid(boost::exception)));
+
+ if (!uex) {
+#endif /* NO_CAST_EXCEPTION */
+ // save the current stack trace in a thread-local variable
+ boost::stacktrace::stacktrace stack;
+ SetLastExceptionStack(stack);
+
+#ifndef NO_CAST_EXCEPTION
+ // save the current stack trace in the boost exception error info if the exception is a boost::exception
+ if (ex && !boost::get_error_info<StackTraceErrorInfo>(*ex))
+ *ex << StackTraceErrorInfo(stack);
+ }
+#endif /* NO_CAST_EXCEPTION */
+
+ ContextTrace context;
+ SetLastExceptionContext(context);
+
+#ifndef NO_CAST_EXCEPTION
+ // save the current context trace in the boost exception error info if the exception is a boost::exception
+ if (ex && !boost::get_error_info<ContextTraceErrorInfo>(*ex))
+ *ex << ContextTraceErrorInfo(context);
+#endif /* NO_CAST_EXCEPTION */
+
+ real_cxa_throw(obj, tinfo, dest);
+}
+#endif /* HAVE_CXXABI_H */
+
+boost::stacktrace::stacktrace *icinga::GetLastExceptionStack()
+{
+ return l_LastExceptionStack.get();
+}
+
+void icinga::SetLastExceptionStack(const boost::stacktrace::stacktrace& trace)
+{
+ l_LastExceptionStack.reset(new boost::stacktrace::stacktrace(trace));
+}
+
+ContextTrace *icinga::GetLastExceptionContext()
+{
+ return l_LastExceptionContext.get();
+}
+
+void icinga::SetLastExceptionContext(const ContextTrace& context)
+{
+ l_LastExceptionContext.reset(new ContextTrace(context));
+}
+
+String icinga::DiagnosticInformation(const std::exception& ex, bool verbose, boost::stacktrace::stacktrace *stack, ContextTrace *context)
+{
+ std::ostringstream result;
+
+ String message = ex.what();
+
+#ifdef _WIN32
+ const auto *win32_err = dynamic_cast<const win32_error *>(&ex);
+ if (win32_err) {
+ message = to_string(*win32_err);
+ }
+#endif /* _WIN32 */
+
+ const auto *vex = dynamic_cast<const ValidationError *>(&ex);
+
+ if (message.IsEmpty())
+ result << boost::diagnostic_information(ex) << "\n";
+ else
+ result << "Error: " << message << "\n";
+
+ const auto *dex = dynamic_cast<const ScriptError *>(&ex);
+
+ if (dex && !dex->GetDebugInfo().Path.IsEmpty())
+ ShowCodeLocation(result, dex->GetDebugInfo());
+
+ if (vex) {
+ DebugInfo di;
+
+ ConfigObject::Ptr dobj = vex->GetObject();
+ if (dobj)
+ di = dobj->GetDebugInfo();
+
+ Dictionary::Ptr currentHint = vex->GetDebugHint();
+ Array::Ptr messages;
+
+ if (currentHint) {
+ for (const String& attr : vex->GetAttributePath()) {
+ Dictionary::Ptr props = currentHint->Get("properties");
+
+ if (!props)
+ break;
+
+ currentHint = props->Get(attr);
+
+ if (!currentHint)
+ break;
+
+ messages = currentHint->Get("messages");
+ }
+ }
+
+ if (messages && messages->GetLength() > 0) {
+ Array::Ptr message = messages->Get(messages->GetLength() - 1);
+
+ di.Path = message->Get(1);
+ di.FirstLine = message->Get(2);
+ di.FirstColumn = message->Get(3);
+ di.LastLine = message->Get(4);
+ di.LastColumn = message->Get(5);
+ }
+
+ if (!di.Path.IsEmpty())
+ ShowCodeLocation(result, di);
+ }
+
+ const auto *uex = dynamic_cast<const user_error *>(&ex);
+ const auto *pex = dynamic_cast<const posix_error *>(&ex);
+
+ if (!uex && !pex && verbose) {
+ // Print the first of the following stack traces (if any exists)
+ // 1. stack trace from boost exception error information
+ const boost::stacktrace::stacktrace *st = boost::get_error_info<StackTraceErrorInfo>(ex);
+ // 2. stack trace explicitly passed as a parameter
+ if (!st) {
+ st = stack;
+ }
+ // 3. stack trace saved when the last exception was thrown
+ if (!st) {
+ st = GetLastExceptionStack();
+ }
+
+ if (st && !st->empty()) {
+ result << "\nStacktrace:\n" << StackTraceFormatter(*st);
+ }
+ }
+
+ // Print the first of the following context traces (if any exists)
+ // 1. context trace from boost exception error information
+ const ContextTrace *ct = boost::get_error_info<ContextTraceErrorInfo>(ex);
+ // 2. context trace explicitly passed as a parameter
+ if (!ct) {
+ ct = context;
+ }
+ // 3. context trace saved when the last exception was thrown
+ if (!ct) {
+ ct = GetLastExceptionContext();
+ }
+
+ if (ct && ct->GetLength() > 0) {
+ result << "\nContext:\n" << *ct;
+ }
+
+ return result.str();
+}
+
+String icinga::DiagnosticInformation(const boost::exception_ptr& eptr, bool verbose)
+{
+ boost::stacktrace::stacktrace *pt = GetLastExceptionStack();
+ boost::stacktrace::stacktrace stack;
+
+ ContextTrace *pc = GetLastExceptionContext();
+ ContextTrace context;
+
+ if (pt)
+ stack = *pt;
+
+ if (pc)
+ context = *pc;
+
+ try {
+ boost::rethrow_exception(eptr);
+ } catch (const std::exception& ex) {
+ return DiagnosticInformation(ex, verbose, pt ? &stack : nullptr, pc ? &context : nullptr);
+ }
+
+ return boost::diagnostic_information(eptr);
+}
+
+ScriptError::ScriptError(String message)
+ : m_Message(std::move(message)), m_IncompleteExpr(false)
+{ }
+
+ScriptError::ScriptError(String message, DebugInfo di, bool incompleteExpr)
+ : m_Message(std::move(message)), m_DebugInfo(std::move(di)), m_IncompleteExpr(incompleteExpr), m_HandledByDebugger(false)
+{ }
+
+const char *ScriptError::what() const throw()
+{
+ return m_Message.CStr();
+}
+
+DebugInfo ScriptError::GetDebugInfo() const
+{
+ return m_DebugInfo;
+}
+
+bool ScriptError::IsIncompleteExpression() const
+{
+ return m_IncompleteExpr;
+}
+
+bool ScriptError::IsHandledByDebugger() const
+{
+ return m_HandledByDebugger;
+}
+
+void ScriptError::SetHandledByDebugger(bool handled)
+{
+ m_HandledByDebugger = handled;
+}
+
+posix_error::~posix_error() throw()
+{
+ free(m_Message);
+}
+
+const char *posix_error::what() const throw()
+{
+ if (!m_Message) {
+ std::ostringstream msgbuf;
+
+ const char * const *func = boost::get_error_info<boost::errinfo_api_function>(*this);
+
+ if (func)
+ msgbuf << "Function call '" << *func << "'";
+ else
+ msgbuf << "Function call";
+
+ const std::string *fname = boost::get_error_info<boost::errinfo_file_name>(*this);
+
+ if (fname)
+ msgbuf << " for file '" << *fname << "'";
+
+ msgbuf << " failed";
+
+ const int *errnum = boost::get_error_info<boost::errinfo_errno>(*this);
+
+ if (errnum)
+ msgbuf << " with error code " << *errnum << ", '" << strerror(*errnum) << "'";
+
+ String str = msgbuf.str();
+ m_Message = strdup(str.CStr());
+ }
+
+ return m_Message;
+}
+
+ValidationError::ValidationError(const ConfigObject::Ptr& object, const std::vector<String>& attributePath, const String& message)
+ : m_Object(object), m_AttributePath(attributePath), m_Message(message)
+{
+ String path;
+
+ for (const String& attribute : attributePath) {
+ if (!path.IsEmpty())
+ path += " -> ";
+
+ path += "'" + attribute + "'";
+ }
+
+ Type::Ptr type = object->GetReflectionType();
+ m_What = "Validation failed for object '" + object->GetName() + "' of type '" + type->GetName() + "'";
+
+ if (!path.IsEmpty())
+ m_What += "; Attribute " + path;
+
+ m_What += ": " + message;
+}
+
+ValidationError::~ValidationError() throw()
+{ }
+
+const char *ValidationError::what() const throw()
+{
+ return m_What.CStr();
+}
+
+ConfigObject::Ptr ValidationError::GetObject() const
+{
+ return m_Object;
+}
+
+std::vector<String> ValidationError::GetAttributePath() const
+{
+ return m_AttributePath;
+}
+
+String ValidationError::GetMessage() const
+{
+ return m_Message;
+}
+
+void ValidationError::SetDebugHint(const Dictionary::Ptr& dhint)
+{
+ m_DebugHint = dhint;
+}
+
+Dictionary::Ptr ValidationError::GetDebugHint() const
+{
+ return m_DebugHint;
+}
+
+std::string icinga::to_string(const StackTraceErrorInfo&)
+{
+ return "";
+}
+
+#ifdef _WIN32
+const char *win32_error::what() const noexcept
+{
+ return "win32_error";
+}
+
+std::string icinga::to_string(const win32_error &e) {
+ std::ostringstream msgbuf;
+
+ const char * const *func = boost::get_error_info<boost::errinfo_api_function>(e);
+
+ if (func) {
+ msgbuf << "Function call '" << *func << "'";
+ } else {
+ msgbuf << "Function call";
+ }
+
+ const std::string *fname = boost::get_error_info<boost::errinfo_file_name>(e);
+
+ if (fname) {
+ msgbuf << " for file '" << *fname << "'";
+ }
+
+ msgbuf << " failed";
+
+ const int *errnum = boost::get_error_info<errinfo_win32_error>(e);
+
+ if (errnum) {
+ msgbuf << " with error code " << Utility::FormatErrorNumber(*errnum);
+ }
+
+ return msgbuf.str();
+}
+
+std::string icinga::to_string(const errinfo_win32_error& e)
+{
+ return "[errinfo_win32_error] = " + Utility::FormatErrorNumber(e.value()) + "\n";
+}
+#endif /* _WIN32 */
+
+std::string icinga::to_string(const errinfo_getaddrinfo_error& e)
+{
+ String msg;
+
+#ifdef _WIN32
+ msg = gai_strerrorA(e.value());
+#else /* _WIN32 */
+ msg = gai_strerror(e.value());
+#endif /* _WIN32 */
+
+ return "[errinfo_getaddrinfo_error] = " + String(msg) + "\n";
+}
+
+std::string icinga::to_string(const ContextTraceErrorInfo& e)
+{
+ std::ostringstream msgbuf;
+ msgbuf << "[Context] = " << e.value();
+ return msgbuf.str();
+}
+
+invalid_downtime_removal_error::invalid_downtime_removal_error(String message)
+ : m_Message(std::move(message))
+{ }
+
+invalid_downtime_removal_error::invalid_downtime_removal_error(const char *message)
+ : m_Message(message)
+{ }
+
+invalid_downtime_removal_error::~invalid_downtime_removal_error() noexcept
+{ }
+
+const char *invalid_downtime_removal_error::what() const noexcept
+{
+ return m_Message.CStr();
+}
diff --git a/lib/base/exception.hpp b/lib/base/exception.hpp
new file mode 100644
index 0000000..18dab65
--- /dev/null
+++ b/lib/base/exception.hpp
@@ -0,0 +1,166 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EXCEPTION_H
+#define EXCEPTION_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include "base/context.hpp"
+#include "base/debuginfo.hpp"
+#include "base/dictionary.hpp"
+#include "base/configobject.hpp"
+#include <boost/exception/errinfo_api_function.hpp>
+#include <boost/exception/errinfo_errno.hpp>
+#include <boost/exception/errinfo_file_name.hpp>
+#include <boost/exception/diagnostic_information.hpp>
+#include <boost/exception_ptr.hpp>
+#include <boost/stacktrace.hpp>
+
+#ifdef _WIN32
+# include <boost/algorithm/string/trim.hpp>
+#endif /* _WIN32 */
+
+namespace icinga
+{
+
+class user_error : virtual public std::exception, virtual public boost::exception
+{ };
+
+/*
+ * @ingroup base
+ */
+class ScriptError : virtual public user_error
+{
+public:
+ ScriptError(String message);
+ ScriptError(String message, DebugInfo di, bool incompleteExpr = false);
+
+ const char *what(void) const throw() final;
+
+ DebugInfo GetDebugInfo() const;
+ bool IsIncompleteExpression() const;
+
+ bool IsHandledByDebugger() const;
+ void SetHandledByDebugger(bool handled);
+
+private:
+ String m_Message;
+ DebugInfo m_DebugInfo;
+ bool m_IncompleteExpr;
+ bool m_HandledByDebugger;
+};
+
+/*
+ * @ingroup base
+ */
+class ValidationError : virtual public user_error
+{
+public:
+ ValidationError(const ConfigObject::Ptr& object, const std::vector<String>& attributePath, const String& message);
+ ~ValidationError() throw() override;
+
+ const char *what() const throw() override;
+
+ ConfigObject::Ptr GetObject() const;
+ std::vector<String> GetAttributePath() const;
+ String GetMessage() const;
+
+ void SetDebugHint(const Dictionary::Ptr& dhint);
+ Dictionary::Ptr GetDebugHint() const;
+
+private:
+ ConfigObject::Ptr m_Object;
+ std::vector<String> m_AttributePath;
+ String m_Message;
+ String m_What;
+ Dictionary::Ptr m_DebugHint;
+};
+
+boost::stacktrace::stacktrace *GetLastExceptionStack();
+void SetLastExceptionStack(const boost::stacktrace::stacktrace& trace);
+
+ContextTrace *GetLastExceptionContext();
+void SetLastExceptionContext(const ContextTrace& context);
+
+void RethrowUncaughtException();
+
+struct errinfo_stacktrace_;
+typedef boost::error_info<struct errinfo_stacktrace_, boost::stacktrace::stacktrace> StackTraceErrorInfo;
+
+std::string to_string(const StackTraceErrorInfo&);
+
+typedef boost::error_info<ContextTrace, ContextTrace> ContextTraceErrorInfo;
+
+std::string to_string(const ContextTraceErrorInfo& e);
+
+/**
+ * Generate diagnostic information about an exception
+ *
+ * The following information is gathered in the result:
+ * - Exception error message
+ * - Debug information about the Icinga config if the exception is a ValidationError
+ * - Stack trace
+ * - Context trace
+ *
+ * Each, stack trace and the context trace, are printed if the they were saved in the boost exception error
+ * information, are explicitly passed as a parameter, or were stored when the last exception was thrown. If multiple
+ * of these exist, the first one is used.
+ *
+ * @param ex exception to print diagnostic information about
+ * @param verbose if verbose is set, a stack trace is added
+ * @param stack optionally supply a stack trace
+ * @param context optionally supply a context trace
+ * @return string containing the aforementioned information
+ */
+String DiagnosticInformation(const std::exception& ex, bool verbose = true,
+ boost::stacktrace::stacktrace *stack = nullptr, ContextTrace *context = nullptr);
+String DiagnosticInformation(const boost::exception_ptr& eptr, bool verbose = true);
+
+class posix_error : virtual public std::exception, virtual public boost::exception {
+public:
+ ~posix_error() throw() override;
+
+ const char *what(void) const throw() final;
+
+private:
+ mutable char *m_Message{nullptr};
+};
+
+#ifdef _WIN32
+class win32_error : virtual public std::exception, virtual public boost::exception {
+public:
+ const char *what() const noexcept override;
+};
+
+std::string to_string(const win32_error& e);
+
+struct errinfo_win32_error_;
+typedef boost::error_info<struct errinfo_win32_error_, int> errinfo_win32_error;
+
+std::string to_string(const errinfo_win32_error& e);
+#endif /* _WIN32 */
+
+struct errinfo_getaddrinfo_error_;
+typedef boost::error_info<struct errinfo_getaddrinfo_error_, int> errinfo_getaddrinfo_error;
+
+std::string to_string(const errinfo_getaddrinfo_error& e);
+
+struct errinfo_message_;
+typedef boost::error_info<struct errinfo_message_, std::string> errinfo_message;
+
+class invalid_downtime_removal_error : virtual public std::exception, virtual public boost::exception {
+public:
+ explicit invalid_downtime_removal_error(String message);
+ explicit invalid_downtime_removal_error(const char* message);
+
+ ~invalid_downtime_removal_error() noexcept override;
+
+ const char *what() const noexcept final;
+
+private:
+ String m_Message;
+};
+
+}
+
+#endif /* EXCEPTION_H */
diff --git a/lib/base/fifo.cpp b/lib/base/fifo.cpp
new file mode 100644
index 0000000..8653f51
--- /dev/null
+++ b/lib/base/fifo.cpp
@@ -0,0 +1,124 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/fifo.hpp"
+
+using namespace icinga;
+
+/**
+ * Destructor for the FIFO class.
+ */
+FIFO::~FIFO()
+{
+ free(m_Buffer);
+}
+
+/**
+ * Resizes the FIFO's buffer so that it is at least newSize bytes long.
+ *
+ * @param newSize The minimum new size of the FIFO buffer.
+ */
+void FIFO::ResizeBuffer(size_t newSize, bool decrease)
+{
+ if (m_AllocSize >= newSize && !decrease)
+ return;
+
+ newSize = (newSize / FIFO::BlockSize + 1) * FIFO::BlockSize;
+
+ if (newSize == m_AllocSize)
+ return;
+
+ auto *newBuffer = static_cast<char *>(realloc(m_Buffer, newSize));
+
+ if (!newBuffer)
+ BOOST_THROW_EXCEPTION(std::bad_alloc());
+
+ m_Buffer = newBuffer;
+
+ m_AllocSize = newSize;
+}
+
+/**
+ * Optimizes memory usage of the FIFO buffer by reallocating
+ * and moving the buffer.
+ */
+void FIFO::Optimize()
+{
+ if (m_Offset > m_DataSize / 10 && m_Offset - m_DataSize > 1024) {
+ std::memmove(m_Buffer, m_Buffer + m_Offset, m_DataSize);
+ m_Offset = 0;
+
+ if (m_DataSize > 0)
+ ResizeBuffer(m_DataSize, true);
+
+ return;
+ }
+}
+
+size_t FIFO::Peek(void *buffer, size_t count, bool allow_partial)
+{
+ ASSERT(allow_partial);
+
+ if (count > m_DataSize)
+ count = m_DataSize;
+
+ if (buffer)
+ std::memcpy(buffer, m_Buffer + m_Offset, count);
+
+ return count;
+}
+
+/**
+ * Implements IOQueue::Read.
+ */
+size_t FIFO::Read(void *buffer, size_t count, bool allow_partial)
+{
+ ASSERT(allow_partial);
+
+ if (count > m_DataSize)
+ count = m_DataSize;
+
+ if (buffer)
+ std::memcpy(buffer, m_Buffer + m_Offset, count);
+
+ m_DataSize -= count;
+ m_Offset += count;
+
+ Optimize();
+
+ return count;
+}
+
+/**
+ * Implements IOQueue::Write.
+ */
+void FIFO::Write(const void *buffer, size_t count)
+{
+ ResizeBuffer(m_Offset + m_DataSize + count, false);
+ std::memcpy(m_Buffer + m_Offset + m_DataSize, buffer, count);
+ m_DataSize += count;
+
+ SignalDataAvailable();
+}
+
+void FIFO::Close()
+{ }
+
+bool FIFO::IsEof() const
+{
+ return false;
+}
+
+size_t FIFO::GetAvailableBytes() const
+{
+ return m_DataSize;
+}
+
+bool FIFO::SupportsWaiting() const
+{
+ return true;
+}
+
+bool FIFO::IsDataAvailable() const
+{
+ return m_DataSize > 0;
+}
diff --git a/lib/base/fifo.hpp b/lib/base/fifo.hpp
new file mode 100644
index 0000000..a8273c1
--- /dev/null
+++ b/lib/base/fifo.hpp
@@ -0,0 +1,48 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FIFO_H
+#define FIFO_H
+
+#include "base/i2-base.hpp"
+#include "base/stream.hpp"
+
+namespace icinga
+{
+
+/**
+ * A byte-based FIFO buffer.
+ *
+ * @ingroup base
+ */
+class FIFO final : public Stream
+{
+public:
+ DECLARE_PTR_TYPEDEFS(FIFO);
+
+ static const size_t BlockSize = 512;
+
+ ~FIFO() override;
+
+ size_t Peek(void *buffer, size_t count, bool allow_partial = false) override;
+ size_t Read(void *buffer, size_t count, bool allow_partial = false) override;
+ void Write(const void *buffer, size_t count) override;
+ void Close() override;
+ bool IsEof() const override;
+ bool SupportsWaiting() const override;
+ bool IsDataAvailable() const override;
+
+ size_t GetAvailableBytes() const;
+
+private:
+ char *m_Buffer{nullptr};
+ size_t m_DataSize{0};
+ size_t m_AllocSize{0};
+ size_t m_Offset{0};
+
+ void ResizeBuffer(size_t newSize, bool decrease);
+ void Optimize();
+};
+
+}
+
+#endif /* FIFO_H */
diff --git a/lib/base/filelogger.cpp b/lib/base/filelogger.cpp
new file mode 100644
index 0000000..c3da84a
--- /dev/null
+++ b/lib/base/filelogger.cpp
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/filelogger.hpp"
+#include "base/filelogger-ti.cpp"
+#include "base/configtype.hpp"
+#include "base/statsfunction.hpp"
+#include "base/application.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+REGISTER_TYPE(FileLogger);
+
+REGISTER_STATSFUNCTION(FileLogger, &FileLogger::StatsFunc);
+
+void FileLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const FileLogger::Ptr& filelogger : ConfigType::GetObjectsByType<FileLogger>()) {
+ nodes.emplace_back(filelogger->GetName(), 1); //add more stats
+ }
+
+ status->Set("filelogger", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * Constructor for the FileLogger class.
+ */
+void FileLogger::Start(bool runtimeCreated)
+{
+ ReopenLogFile();
+
+ Application::OnReopenLogs.connect([this]() { ReopenLogFile(); });
+
+ ObjectImpl<FileLogger>::Start(runtimeCreated);
+
+ Log(LogInformation, "FileLogger")
+ << "'" << GetName() << "' started.";
+}
+
+void FileLogger::ReopenLogFile()
+{
+ auto *stream = new std::ofstream();
+
+ String path = GetPath();
+
+ try {
+ stream->open(path.CStr(), std::fstream::app | std::fstream::out);
+
+ if (!stream->good())
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not open logfile '" + path + "'"));
+ } catch (...) {
+ delete stream;
+ throw;
+ }
+
+ BindStream(stream, true);
+}
diff --git a/lib/base/filelogger.hpp b/lib/base/filelogger.hpp
new file mode 100644
index 0000000..420337f
--- /dev/null
+++ b/lib/base/filelogger.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FILELOGGER_H
+#define FILELOGGER_H
+
+#include "base/i2-base.hpp"
+#include "base/filelogger-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * A logger that logs to a file.
+ *
+ * @ingroup base
+ */
+class FileLogger final : public ObjectImpl<FileLogger>
+{
+public:
+ DECLARE_OBJECT(FileLogger);
+ DECLARE_OBJECTNAME(FileLogger);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void Start(bool runtimeCreated) override;
+
+private:
+ void ReopenLogFile();
+};
+
+}
+
+#endif /* FILELOGGER_H */
diff --git a/lib/base/filelogger.ti b/lib/base/filelogger.ti
new file mode 100644
index 0000000..8af2498
--- /dev/null
+++ b/lib/base/filelogger.ti
@@ -0,0 +1,17 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/streamlogger.hpp"
+
+library base;
+
+namespace icinga
+{
+
+class FileLogger : StreamLogger
+{
+ activation_priority -100;
+
+ [config, required] String path;
+};
+
+}
diff --git a/lib/base/function-script.cpp b/lib/base/function-script.cpp
new file mode 100644
index 0000000..e59e84d
--- /dev/null
+++ b/lib/base/function-script.cpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+static Value FunctionCall(const std::vector<Value>& args)
+{
+ if (args.size() < 1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments for call()"));
+
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Function::Ptr self = static_cast<Function::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ std::vector<Value> uargs(args.begin() + 1, args.end());
+ return self->InvokeThis(args[0], uargs);
+}
+
+static Value FunctionCallV(const Value& thisArg, const Array::Ptr& args)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Function::Ptr self = static_cast<Function::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ std::vector<Value> uargs;
+
+ {
+ ObjectLock olock(args);
+ uargs = std::vector<Value>(args->Begin(), args->End());
+ }
+
+ return self->InvokeThis(thisArg, uargs);
+}
+
+
+Object::Ptr Function::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "call", new Function("Function#call", FunctionCall) },
+ { "callv", new Function("Function#callv", FunctionCallV) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/function.cpp b/lib/base/function.cpp
new file mode 100644
index 0000000..f9a261d
--- /dev/null
+++ b/lib/base/function.cpp
@@ -0,0 +1,37 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/function.hpp"
+#include "base/function-ti.cpp"
+#include "base/array.hpp"
+#include "base/scriptframe.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE_WITH_PROTOTYPE(Function, Function::GetPrototype());
+
+Function::Function(const String& name, Callback function, const std::vector<String>& args,
+ bool side_effect_free, bool deprecated)
+ : m_Callback(std::move(function))
+{
+ SetName(name, true);
+ SetSideEffectFree(side_effect_free, true);
+ SetDeprecated(deprecated, true);
+ SetArguments(Array::FromVector(args), true);
+}
+
+Value Function::Invoke(const std::vector<Value>& arguments)
+{
+ ScriptFrame frame(false);
+ return m_Callback(arguments);
+}
+
+Value Function::InvokeThis(const Value& otherThis, const std::vector<Value>& arguments)
+{
+ ScriptFrame frame(false, otherThis);
+ return m_Callback(arguments);
+}
+
+Object::Ptr Function::Clone() const
+{
+ return const_cast<Function *>(this);
+}
diff --git a/lib/base/function.hpp b/lib/base/function.hpp
new file mode 100644
index 0000000..d52a230
--- /dev/null
+++ b/lib/base/function.hpp
@@ -0,0 +1,89 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FUNCTION_H
+#define FUNCTION_H
+
+#include "base/i2-base.hpp"
+#include "base/function-ti.hpp"
+#include "base/value.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptglobal.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * A script function that can be used to execute a script task.
+ *
+ * @ingroup base
+ */
+class Function final : public ObjectImpl<Function>
+{
+public:
+ DECLARE_OBJECT(Function);
+
+ typedef std::function<Value (const std::vector<Value>& arguments)> Callback;
+
+ template<typename F>
+ Function(const String& name, F function, const std::vector<String>& args = std::vector<String>(),
+ bool side_effect_free = false, bool deprecated = false)
+ : Function(name, WrapFunction(function), args, side_effect_free, deprecated)
+ { }
+
+ Value Invoke(const std::vector<Value>& arguments = std::vector<Value>());
+ Value InvokeThis(const Value& otherThis, const std::vector<Value>& arguments = std::vector<Value>());
+
+ bool IsSideEffectFree() const
+ {
+ return GetSideEffectFree();
+ }
+
+ bool IsDeprecated() const
+ {
+ return GetDeprecated();
+ }
+
+ static Object::Ptr GetPrototype();
+
+ Object::Ptr Clone() const override;
+
+private:
+ Callback m_Callback;
+
+ Function(const String& name, Callback function, const std::vector<String>& args,
+ bool side_effect_free, bool deprecated);
+};
+
+/* Ensure that the priority is lower than the basic namespace initialization in scriptframe.cpp. */
+#define REGISTER_FUNCTION(ns, name, callback, args) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ Function::Ptr sf = new icinga::Function(#ns "#" #name, callback, String(args).Split(":"), false); \
+ Namespace::Ptr nsp = ScriptGlobal::Get(#ns); \
+ nsp->Set(#name, sf, true); \
+ }, InitializePriority::RegisterFunctions)
+
+#define REGISTER_SAFE_FUNCTION(ns, name, callback, args) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ Function::Ptr sf = new icinga::Function(#ns "#" #name, callback, String(args).Split(":"), true); \
+ Namespace::Ptr nsp = ScriptGlobal::Get(#ns); \
+ nsp->Set(#name, sf, true); \
+ }, InitializePriority::RegisterFunctions)
+
+#define REGISTER_FUNCTION_NONCONST(ns, name, callback, args) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ Function::Ptr sf = new icinga::Function(#ns "#" #name, callback, String(args).Split(":"), false); \
+ Namespace::Ptr nsp = ScriptGlobal::Get(#ns); \
+ nsp->Set(#name, sf, false); \
+ }, InitializePriority::RegisterFunctions)
+
+#define REGISTER_SAFE_FUNCTION_NONCONST(ns, name, callback, args) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ Function::Ptr sf = new icinga::Function(#ns "#" #name, callback, String(args).Split(":"), true); \
+ Namespace::Ptr nsp = ScriptGlobal::Get(#ns); \
+ nsp->SetAttribute(#name, sf, false); \
+ }, InitializePriority::RegisterFunctions)
+
+}
+
+#endif /* FUNCTION_H */
diff --git a/lib/base/function.ti b/lib/base/function.ti
new file mode 100644
index 0000000..f2623c1
--- /dev/null
+++ b/lib/base/function.ti
@@ -0,0 +1,18 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library base;
+
+namespace icinga
+{
+
+abstract class Function
+{
+ [config] String "name";
+ [config] bool side_effect_free;
+ [config] bool "deprecated";
+ [config] Array::Ptr arguments;
+};
+
+}
diff --git a/lib/base/functionwrapper.hpp b/lib/base/functionwrapper.hpp
new file mode 100644
index 0000000..57cf1cb
--- /dev/null
+++ b/lib/base/functionwrapper.hpp
@@ -0,0 +1,149 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FUNCTIONWRAPPER_H
+#define FUNCTIONWRAPPER_H
+
+#include "base/i2-base.hpp"
+#include "base/value.hpp"
+#include <boost/function_types/function_type.hpp>
+#include <boost/function_types/parameter_types.hpp>
+#include <boost/function_types/result_type.hpp>
+#include <boost/function_types/function_arity.hpp>
+#include <vector>
+
+namespace icinga
+{
+
+template<typename FuncType>
+typename std::enable_if<
+ std::is_class<FuncType>::value &&
+ std::is_same<typename boost::function_types::result_type<decltype(&FuncType::operator())>::type, Value>::value &&
+ boost::function_types::function_arity<decltype(&FuncType::operator())>::value == 2,
+ std::function<Value (const std::vector<Value>&)>>::type
+WrapFunction(FuncType function)
+{
+ static_assert(std::is_same<typename boost::mpl::at_c<typename boost::function_types::parameter_types<decltype(&FuncType::operator())>, 1>::type, const std::vector<Value>&>::value, "Argument type must be const std::vector<Value>");
+ return function;
+}
+
+inline std::function<Value (const std::vector<Value>&)> WrapFunction(void (*function)(const std::vector<Value>&))
+{
+ return [function](const std::vector<Value>& arguments) {
+ function(arguments);
+ return Empty;
+ };
+}
+
+template<typename Return>
+std::function<Value (const std::vector<Value>&)> WrapFunction(Return (*function)(const std::vector<Value>&))
+{
+ return [function](const std::vector<Value>& values) -> Value { return function(values); };
+}
+
+template <std::size_t... Indices>
+struct indices {
+ using next = indices<Indices..., sizeof...(Indices)>;
+};
+
+template <std::size_t N>
+struct build_indices {
+ using type = typename build_indices<N-1>::type::next;
+};
+
+template <>
+struct build_indices<0> {
+ using type = indices<>;
+};
+
+template <std::size_t N>
+using BuildIndices = typename build_indices<N>::type;
+
+struct UnpackCaller
+{
+private:
+ template <typename FuncType, size_t... I>
+ auto Invoke(FuncType f, const std::vector<Value>& args, indices<I...>) -> decltype(f(args[I]...))
+ {
+ return f(args[I]...);
+ }
+
+public:
+ template <typename FuncType, int Arity>
+ auto operator() (FuncType f, const std::vector<Value>& args) -> decltype(Invoke(f, args, BuildIndices<Arity>{}))
+ {
+ return Invoke(f, args, BuildIndices<Arity>{});
+ }
+};
+
+template<typename FuncType, int Arity, typename ReturnType>
+struct FunctionWrapper
+{
+ static Value Invoke(FuncType function, const std::vector<Value>& arguments)
+ {
+ return UnpackCaller().operator()<FuncType, Arity>(function, arguments);
+ }
+};
+
+template<typename FuncType, int Arity>
+struct FunctionWrapper<FuncType, Arity, void>
+{
+ static Value Invoke(FuncType function, const std::vector<Value>& arguments)
+ {
+ UnpackCaller().operator()<FuncType, Arity>(function, arguments);
+ return Empty;
+ }
+};
+
+template<typename FuncType>
+typename std::enable_if<
+ std::is_function<typename std::remove_pointer<FuncType>::type>::value && !std::is_same<FuncType, Value(*)(const std::vector<Value>&)>::value,
+ std::function<Value (const std::vector<Value>&)>>::type
+WrapFunction(FuncType function)
+{
+ return [function](const std::vector<Value>& arguments) {
+ constexpr size_t arity = boost::function_types::function_arity<typename std::remove_pointer<FuncType>::type>::value;
+
+ if (arity > 0) {
+ if (arguments.size() < arity)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments for function."));
+ else if (arguments.size() > arity)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too many arguments for function."));
+ }
+
+ using ReturnType = decltype(UnpackCaller().operator()<FuncType, arity>(*static_cast<FuncType *>(nullptr), std::vector<Value>()));
+
+ return FunctionWrapper<FuncType, arity, ReturnType>::Invoke(function, arguments);
+ };
+}
+
+template<typename FuncType>
+typename std::enable_if<
+ std::is_class<FuncType>::value &&
+ !(std::is_same<typename boost::function_types::result_type<decltype(&FuncType::operator())>::type, Value>::value &&
+ boost::function_types::function_arity<decltype(&FuncType::operator())>::value == 2),
+ std::function<Value (const std::vector<Value>&)>>::type
+WrapFunction(FuncType function)
+{
+ static_assert(!std::is_same<typename boost::mpl::at_c<typename boost::function_types::parameter_types<decltype(&FuncType::operator())>, 1>::type, const std::vector<Value>&>::value, "Argument type must be const std::vector<Value>");
+
+ using FuncTypeInvoker = decltype(&FuncType::operator());
+
+ return [function](const std::vector<Value>& arguments) {
+ constexpr size_t arity = boost::function_types::function_arity<FuncTypeInvoker>::value - 1;
+
+ if (arity > 0) {
+ if (arguments.size() < arity)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments for function."));
+ else if (arguments.size() > arity)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too many arguments for function."));
+ }
+
+ using ReturnType = decltype(UnpackCaller().operator()<FuncType, arity>(*static_cast<FuncType *>(nullptr), std::vector<Value>()));
+
+ return FunctionWrapper<FuncType, arity, ReturnType>::Invoke(function, arguments);
+ };
+}
+
+}
+
+#endif /* FUNCTIONWRAPPER_H */
diff --git a/lib/base/i2-base.hpp b/lib/base/i2-base.hpp
new file mode 100644
index 0000000..a7bfc6a
--- /dev/null
+++ b/lib/base/i2-base.hpp
@@ -0,0 +1,79 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2BASE_H
+#define I2BASE_H
+
+/**
+ * @mainpage Icinga Documentation
+ *
+ * Icinga implements a framework for run-time-loadable components which can
+ * pass messages between each other. These components can either be hosted in
+ * the same process or in several host processes (either on the same machine or
+ * on different machines).
+ *
+ * The framework's code critically depends on the following patterns:
+ *
+ * <list type="bullet">
+ * <item>Smart pointers
+ *
+ * The shared_ptr and weak_ptr template classes are used to simplify memory
+ * management and to avoid accidental memory leaks and use-after-free
+ * bugs.</item>
+ *
+ * <item>Observer pattern
+ *
+ * Framework classes expose events which other objects can subscribe to. This
+ * is used to decouple clients of a class from the class' internal
+ * implementation.</item>
+ * </list>
+ */
+
+/**
+ * @defgroup base Base class library
+ *
+ * The base class library implements commonly-used functionality like
+ * event handling for sockets and timers.
+ */
+
+#include <boost/config.hpp>
+
+#if defined(__clang__) && __cplusplus >= 201103L
+# undef BOOST_NO_CXX11_HDR_TUPLE
+#endif
+
+#ifdef _MSC_VER
+# pragma warning(disable:4251)
+# pragma warning(disable:4275)
+# pragma warning(disable:4345)
+#endif /* _MSC_VER */
+
+#include "config.h"
+
+#ifdef _WIN32
+# include "base/win32.hpp"
+#else
+# include "base/unix.hpp"
+#endif
+
+#include <cstdlib>
+#include <cstdarg>
+#include <cstdio>
+#include <cstring>
+#include <cerrno>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <signal.h>
+
+#include <exception>
+#include <stdexcept>
+
+#if defined(__APPLE__) && defined(__MACH__)
+# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
+#define BOOST_BIND_NO_PLACEHOLDERS
+
+#include <functional>
+
+#endif /* I2BASE_H */
diff --git a/lib/base/initialize.cpp b/lib/base/initialize.cpp
new file mode 100644
index 0000000..49b653f
--- /dev/null
+++ b/lib/base/initialize.cpp
@@ -0,0 +1,13 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/initialize.hpp"
+#include "base/loader.hpp"
+
+using namespace icinga;
+
+bool icinga::InitializeOnceHelper(const std::function<void()>& func, InitializePriority priority)
+{
+ Loader::AddDeferredInitializer(func, priority);
+ return true;
+}
+
diff --git a/lib/base/initialize.hpp b/lib/base/initialize.hpp
new file mode 100644
index 0000000..adc995f
--- /dev/null
+++ b/lib/base/initialize.hpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef INITIALIZE_H
+#define INITIALIZE_H
+
+#include "base/i2-base.hpp"
+#include <functional>
+
+namespace icinga
+{
+
+/**
+ * Priority values for use with the INITIALIZE_ONCE_WITH_PRIORITY macro.
+ *
+ * The values are given in the order of initialization.
+ */
+enum class InitializePriority {
+ CreateNamespaces,
+ InitIcingaApplication,
+ RegisterTypeType,
+ RegisterObjectType,
+ RegisterPrimitiveTypes,
+ RegisterBuiltinTypes,
+ RegisterFunctions,
+ RegisterTypes,
+ EvaluateConfigFragments,
+ Default,
+ FreezeNamespaces,
+};
+
+#define I2_TOKENPASTE(x, y) x ## y
+#define I2_TOKENPASTE2(x, y) I2_TOKENPASTE(x, y)
+
+#define I2_UNIQUE_NAME(prefix) I2_TOKENPASTE2(prefix, __COUNTER__)
+
+bool InitializeOnceHelper(const std::function<void()>& func, InitializePriority priority = InitializePriority::Default);
+
+#define INITIALIZE_ONCE(func) \
+ namespace { namespace I2_UNIQUE_NAME(io) { \
+ bool l_InitializeOnce(icinga::InitializeOnceHelper(func)); \
+ } }
+
+#define INITIALIZE_ONCE_WITH_PRIORITY(func, priority) \
+ namespace { namespace I2_UNIQUE_NAME(io) { \
+ bool l_InitializeOnce(icinga::InitializeOnceHelper(func, priority)); \
+ } }
+}
+
+#endif /* INITIALIZE_H */
diff --git a/lib/base/io-engine.cpp b/lib/base/io-engine.cpp
new file mode 100644
index 0000000..26125fe
--- /dev/null
+++ b/lib/base/io-engine.cpp
@@ -0,0 +1,155 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configuration.hpp"
+#include "base/exception.hpp"
+#include "base/io-engine.hpp"
+#include "base/lazy-init.hpp"
+#include "base/logger.hpp"
+#include <exception>
+#include <memory>
+#include <thread>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/asio/post.hpp>
+#include <boost/date_time/posix_time/ptime.hpp>
+#include <boost/system/error_code.hpp>
+
+using namespace icinga;
+
+CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc)
+ : m_Done(false)
+{
+ auto& ioEngine (IoEngine::Get());
+
+ for (;;) {
+ auto availableSlots (ioEngine.m_CpuBoundSemaphore.fetch_sub(1));
+
+ if (availableSlots < 1) {
+ ioEngine.m_CpuBoundSemaphore.fetch_add(1);
+ IoEngine::YieldCurrentCoroutine(yc);
+ continue;
+ }
+
+ break;
+ }
+}
+
+CpuBoundWork::~CpuBoundWork()
+{
+ if (!m_Done) {
+ IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1);
+ }
+}
+
+void CpuBoundWork::Done()
+{
+ if (!m_Done) {
+ IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1);
+
+ m_Done = true;
+ }
+}
+
+IoBoundWorkSlot::IoBoundWorkSlot(boost::asio::yield_context yc)
+ : yc(yc)
+{
+ IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1);
+}
+
+IoBoundWorkSlot::~IoBoundWorkSlot()
+{
+ auto& ioEngine (IoEngine::Get());
+
+ for (;;) {
+ auto availableSlots (ioEngine.m_CpuBoundSemaphore.fetch_sub(1));
+
+ if (availableSlots < 1) {
+ ioEngine.m_CpuBoundSemaphore.fetch_add(1);
+ IoEngine::YieldCurrentCoroutine(yc);
+ continue;
+ }
+
+ break;
+ }
+}
+
+LazyInit<std::unique_ptr<IoEngine>> IoEngine::m_Instance ([]() { return std::unique_ptr<IoEngine>(new IoEngine()); });
+
+IoEngine& IoEngine::Get()
+{
+ return *m_Instance.Get();
+}
+
+boost::asio::io_context& IoEngine::GetIoContext()
+{
+ return m_IoContext;
+}
+
+IoEngine::IoEngine() : m_IoContext(), m_KeepAlive(boost::asio::make_work_guard(m_IoContext)), m_Threads(decltype(m_Threads)::size_type(Configuration::Concurrency * 2u)), m_AlreadyExpiredTimer(m_IoContext)
+{
+ m_AlreadyExpiredTimer.expires_at(boost::posix_time::neg_infin);
+ m_CpuBoundSemaphore.store(Configuration::Concurrency * 3u / 2u);
+
+ for (auto& thread : m_Threads) {
+ thread = std::thread(&IoEngine::RunEventLoop, this);
+ }
+}
+
+IoEngine::~IoEngine()
+{
+ for (auto& thread : m_Threads) {
+ boost::asio::post(m_IoContext, []() {
+ throw TerminateIoThread();
+ });
+ }
+
+ for (auto& thread : m_Threads) {
+ thread.join();
+ }
+}
+
+void IoEngine::RunEventLoop()
+{
+ for (;;) {
+ try {
+ m_IoContext.run();
+
+ break;
+ } catch (const TerminateIoThread&) {
+ break;
+ } catch (const std::exception& e) {
+ Log(LogCritical, "IoEngine", "Exception during I/O operation!");
+ Log(LogDebug, "IoEngine") << "Exception during I/O operation: " << DiagnosticInformation(e);
+ }
+ }
+}
+
+AsioConditionVariable::AsioConditionVariable(boost::asio::io_context& io, bool init)
+ : m_Timer(io)
+{
+ m_Timer.expires_at(init ? boost::posix_time::neg_infin : boost::posix_time::pos_infin);
+}
+
+void AsioConditionVariable::Set()
+{
+ m_Timer.expires_at(boost::posix_time::neg_infin);
+}
+
+void AsioConditionVariable::Clear()
+{
+ m_Timer.expires_at(boost::posix_time::pos_infin);
+}
+
+void AsioConditionVariable::Wait(boost::asio::yield_context yc)
+{
+ boost::system::error_code ec;
+ m_Timer.async_wait(yc[ec]);
+}
+
+void Timeout::Cancel()
+{
+ m_Cancelled.store(true);
+
+ boost::system::error_code ec;
+ m_Timer.cancel(ec);
+}
diff --git a/lib/base/io-engine.hpp b/lib/base/io-engine.hpp
new file mode 100644
index 0000000..684d3ac
--- /dev/null
+++ b/lib/base/io-engine.hpp
@@ -0,0 +1,216 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef IO_ENGINE_H
+#define IO_ENGINE_H
+
+#include "base/exception.hpp"
+#include "base/lazy-init.hpp"
+#include "base/logger.hpp"
+#include "base/shared-object.hpp"
+#include <atomic>
+#include <exception>
+#include <memory>
+#include <thread>
+#include <utility>
+#include <vector>
+#include <stdexcept>
+#include <boost/exception/all.hpp>
+#include <boost/asio/deadline_timer.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/spawn.hpp>
+
+namespace icinga
+{
+
+/**
+ * Scope lock for CPU-bound work done in an I/O thread
+ *
+ * @ingroup base
+ */
+class CpuBoundWork
+{
+public:
+ CpuBoundWork(boost::asio::yield_context yc);
+ CpuBoundWork(const CpuBoundWork&) = delete;
+ CpuBoundWork(CpuBoundWork&&) = delete;
+ CpuBoundWork& operator=(const CpuBoundWork&) = delete;
+ CpuBoundWork& operator=(CpuBoundWork&&) = delete;
+ ~CpuBoundWork();
+
+ void Done();
+
+private:
+ bool m_Done;
+};
+
+/**
+ * Scope break for CPU-bound work done in an I/O thread
+ *
+ * @ingroup base
+ */
+class IoBoundWorkSlot
+{
+public:
+ IoBoundWorkSlot(boost::asio::yield_context yc);
+ IoBoundWorkSlot(const IoBoundWorkSlot&) = delete;
+ IoBoundWorkSlot(IoBoundWorkSlot&&) = delete;
+ IoBoundWorkSlot& operator=(const IoBoundWorkSlot&) = delete;
+ IoBoundWorkSlot& operator=(IoBoundWorkSlot&&) = delete;
+ ~IoBoundWorkSlot();
+
+private:
+ boost::asio::yield_context yc;
+};
+
+/**
+ * Async I/O engine
+ *
+ * @ingroup base
+ */
+class IoEngine
+{
+ friend CpuBoundWork;
+ friend IoBoundWorkSlot;
+
+public:
+ IoEngine(const IoEngine&) = delete;
+ IoEngine(IoEngine&&) = delete;
+ IoEngine& operator=(const IoEngine&) = delete;
+ IoEngine& operator=(IoEngine&&) = delete;
+ ~IoEngine();
+
+ static IoEngine& Get();
+
+ boost::asio::io_context& GetIoContext();
+
+ static inline size_t GetCoroutineStackSize() {
+#ifdef _WIN32
+ // Increase the stack size for Windows coroutines to prevent exception corruption.
+ // Rationale: Low cost Windows agent only & https://github.com/Icinga/icinga2/issues/7431
+ return 8 * 1024 * 1024;
+#else /* _WIN32 */
+ // Increase the stack size for Linux/Unix coroutines for many JSON objects on the stack.
+ // This may help mitigate possible stack overflows. https://github.com/Icinga/icinga2/issues/7532
+ return 256 * 1024;
+ //return boost::coroutines::stack_allocator::traits_type::default_size(); // Default 64 KB
+#endif /* _WIN32 */
+ }
+
+ template <typename Handler, typename Function>
+ static void SpawnCoroutine(Handler& h, Function f) {
+
+ boost::asio::spawn(h,
+ [f](boost::asio::yield_context yc) {
+
+ try {
+ f(yc);
+ } catch (const boost::coroutines::detail::forced_unwind &) {
+ // Required for proper stack unwinding when coroutines are destroyed.
+ // https://github.com/boostorg/coroutine/issues/39
+ throw;
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "IoEngine", "Exception in coroutine!");
+ Log(LogDebug, "IoEngine") << "Exception in coroutine: " << DiagnosticInformation(ex);
+ } catch (...) {
+ Log(LogCritical, "IoEngine", "Exception in coroutine!");
+ }
+ },
+ boost::coroutines::attributes(GetCoroutineStackSize()) // Set a pre-defined stack size.
+ );
+ }
+
+ static inline
+ void YieldCurrentCoroutine(boost::asio::yield_context yc)
+ {
+ Get().m_AlreadyExpiredTimer.async_wait(yc);
+ }
+
+private:
+ IoEngine();
+
+ void RunEventLoop();
+
+ static LazyInit<std::unique_ptr<IoEngine>> m_Instance;
+
+ boost::asio::io_context m_IoContext;
+ boost::asio::executor_work_guard<boost::asio::io_context::executor_type> m_KeepAlive;
+ std::vector<std::thread> m_Threads;
+ boost::asio::deadline_timer m_AlreadyExpiredTimer;
+ std::atomic_int_fast32_t m_CpuBoundSemaphore;
+};
+
+class TerminateIoThread : public std::exception
+{
+};
+
+/**
+ * Condition variable which doesn't block I/O threads
+ *
+ * @ingroup base
+ */
+class AsioConditionVariable
+{
+public:
+ AsioConditionVariable(boost::asio::io_context& io, bool init = false);
+
+ void Set();
+ void Clear();
+ void Wait(boost::asio::yield_context yc);
+
+private:
+ boost::asio::deadline_timer m_Timer;
+};
+
+/**
+ * I/O timeout emulator
+ *
+ * @ingroup base
+ */
+class Timeout : public SharedObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Timeout);
+
+ template<class Executor, class TimeoutFromNow, class OnTimeout>
+ Timeout(boost::asio::io_context& io, Executor& executor, TimeoutFromNow timeoutFromNow, OnTimeout onTimeout)
+ : m_Timer(io)
+ {
+ Ptr keepAlive (this);
+
+ m_Cancelled.store(false);
+ m_Timer.expires_from_now(std::move(timeoutFromNow));
+
+ IoEngine::SpawnCoroutine(executor, [this, keepAlive, onTimeout](boost::asio::yield_context yc) {
+ if (m_Cancelled.load()) {
+ return;
+ }
+
+ {
+ boost::system::error_code ec;
+
+ m_Timer.async_wait(yc[ec]);
+
+ if (ec) {
+ return;
+ }
+ }
+
+ if (m_Cancelled.load()) {
+ return;
+ }
+
+ auto f (onTimeout);
+ f(std::move(yc));
+ });
+ }
+
+ void Cancel();
+
+private:
+ boost::asio::deadline_timer m_Timer;
+ std::atomic<bool> m_Cancelled;
+};
+
+}
+
+#endif /* IO_ENGINE_H */
diff --git a/lib/base/journaldlogger.cpp b/lib/base/journaldlogger.cpp
new file mode 100644
index 0000000..92d6af7
--- /dev/null
+++ b/lib/base/journaldlogger.cpp
@@ -0,0 +1,87 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#include "base/i2-base.hpp"
+#if !defined(_WIN32) && defined(HAVE_SYSTEMD)
+#include "base/journaldlogger.hpp"
+#include "base/journaldlogger-ti.cpp"
+#include "base/configtype.hpp"
+#include "base/statsfunction.hpp"
+#include "base/sysloglogger.hpp"
+#include <systemd/sd-journal.h>
+
+using namespace icinga;
+
+REGISTER_TYPE(JournaldLogger);
+
+REGISTER_STATSFUNCTION(JournaldLogger, &JournaldLogger::StatsFunc);
+
+void JournaldLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const JournaldLogger::Ptr& journaldlogger : ConfigType::GetObjectsByType<JournaldLogger>()) {
+ nodes.emplace_back(journaldlogger->GetName(), 1); //add more stats
+ }
+
+ status->Set("journaldlogger", new Dictionary(std::move(nodes)));
+}
+
+void JournaldLogger::OnConfigLoaded()
+{
+ ObjectImpl<JournaldLogger>::OnConfigLoaded();
+ m_ConfiguredJournalFields.clear();
+ m_ConfiguredJournalFields.push_back(
+ String("SYSLOG_FACILITY=") + Value(SyslogHelper::FacilityToNumber(GetFacility())));
+ const String identifier = GetIdentifier();
+ if (!identifier.IsEmpty()) {
+ m_ConfiguredJournalFields.push_back(String("SYSLOG_IDENTIFIER=" + identifier));
+ }
+}
+
+void JournaldLogger::ValidateFacility(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<JournaldLogger>::ValidateFacility(lvalue, utils);
+ if (!SyslogHelper::ValidateFacility(lvalue()))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "facility" }, "Invalid facility specified."));
+}
+
+/**
+ * Processes a log entry and outputs it to journald.
+ *
+ * @param entry The log entry.
+ */
+void JournaldLogger::ProcessLogEntry(const LogEntry& entry)
+{
+ const std::vector<String> sdFields {
+ String("MESSAGE=") + entry.Message.GetData(),
+ String("PRIORITY=") + Value(SyslogHelper::SeverityToNumber(entry.Severity)),
+ String("ICINGA2_FACILITY=") + entry.Facility,
+ };
+ SystemdJournalSend(sdFields);
+}
+
+void JournaldLogger::Flush()
+{
+ /* Nothing to do here. */
+}
+
+void JournaldLogger::SystemdJournalSend(const std::vector<String>& varJournalFields) const
+{
+ struct iovec iovec[m_ConfiguredJournalFields.size() + varJournalFields.size()];
+ int iovecCount = 0;
+
+ for (const String& journalField: m_ConfiguredJournalFields) {
+ iovec[iovecCount] = IovecFromString(journalField);
+ iovecCount++;
+ }
+ for (const String& journalField: varJournalFields) {
+ iovec[iovecCount] = IovecFromString(journalField);
+ iovecCount++;
+ }
+ sd_journal_sendv(iovec, iovecCount);
+}
+
+struct iovec JournaldLogger::IovecFromString(const String& s) {
+ return { const_cast<char *>(s.CStr()), s.GetLength() };
+}
+#endif /* !_WIN32 && HAVE_SYSTEMD */
diff --git a/lib/base/journaldlogger.hpp b/lib/base/journaldlogger.hpp
new file mode 100644
index 0000000..373dd1a
--- /dev/null
+++ b/lib/base/journaldlogger.hpp
@@ -0,0 +1,44 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#ifndef JOURNALDLOGGER_H
+#define JOURNALDLOGGER_H
+
+#include "base/i2-base.hpp"
+#if !defined(_WIN32) && defined(HAVE_SYSTEMD)
+#include "base/journaldlogger-ti.hpp"
+#include <sys/uio.h>
+
+namespace icinga
+{
+
+/**
+ * A logger that logs to systemd journald.
+ *
+ * @ingroup base
+ */
+class JournaldLogger final : public ObjectImpl<JournaldLogger>
+{
+public:
+ DECLARE_OBJECT(JournaldLogger);
+ DECLARE_OBJECTNAME(JournaldLogger);
+
+ static void StaticInitialize();
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void OnConfigLoaded() override;
+ void ValidateFacility(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void SystemdJournalSend(const std::vector<String>& varJournalFields) const;
+ static struct iovec IovecFromString(const String& s);
+
+ std::vector<String> m_ConfiguredJournalFields;
+
+ void ProcessLogEntry(const LogEntry& entry) override;
+ void Flush() override;
+};
+
+}
+#endif /* !_WIN32 && HAVE_SYSTEMD */
+
+#endif /* JOURNALDLOGGER_H */
diff --git a/lib/base/journaldlogger.ti b/lib/base/journaldlogger.ti
new file mode 100644
index 0000000..88e9ca1
--- /dev/null
+++ b/lib/base/journaldlogger.ti
@@ -0,0 +1,21 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#include "base/logger.hpp"
+
+library base;
+
+namespace icinga
+{
+
+class JournaldLogger : Logger
+{
+ activation_priority -100;
+
+ [config] String facility {
+ default {{{ return "LOG_USER"; }}}
+ };
+
+ [config] String identifier;
+};
+
+}
diff --git a/lib/base/json-script.cpp b/lib/base/json-script.cpp
new file mode 100644
index 0000000..90595c8
--- /dev/null
+++ b/lib/base/json-script.cpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/initialize.hpp"
+#include "base/json.hpp"
+
+using namespace icinga;
+
+static String JsonEncodeShim(const Value& value)
+{
+ return JsonEncode(value);
+}
+
+INITIALIZE_ONCE([]() {
+ Namespace::Ptr jsonNS = new Namespace(true);
+
+ /* Methods */
+ jsonNS->Set("encode", new Function("Json#encode", JsonEncodeShim, { "value" }, true));
+ jsonNS->Set("decode", new Function("Json#decode", JsonDecode, { "value" }, true));
+
+ jsonNS->Freeze();
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+ systemNS->Set("Json", jsonNS, true);
+});
diff --git a/lib/base/json.cpp b/lib/base/json.cpp
new file mode 100644
index 0000000..5689330
--- /dev/null
+++ b/lib/base/json.cpp
@@ -0,0 +1,525 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/json.hpp"
+#include "base/debug.hpp"
+#include "base/namespace.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include <bitset>
+#include <boost/exception_ptr.hpp>
+#include <cstdint>
+#include <json.hpp>
+#include <stack>
+#include <utility>
+#include <vector>
+
+using namespace icinga;
+
+class JsonSax : public nlohmann::json_sax<nlohmann::json>
+{
+public:
+ bool null() override;
+ bool boolean(bool val) override;
+ bool number_integer(number_integer_t val) override;
+ bool number_unsigned(number_unsigned_t val) override;
+ bool number_float(number_float_t val, const string_t& s) override;
+ bool string(string_t& val) override;
+ bool binary(binary_t& val) override;
+ bool start_object(std::size_t elements) override;
+ bool key(string_t& val) override;
+ bool end_object() override;
+ bool start_array(std::size_t elements) override;
+ bool end_array() override;
+ bool parse_error(std::size_t position, const std::string& last_token, const nlohmann::detail::exception& ex) override;
+
+ Value GetResult();
+
+private:
+ Value m_Root;
+ std::stack<std::pair<Dictionary*, Array*>> m_CurrentSubtree;
+ String m_CurrentKey;
+
+ void FillCurrentTarget(Value value);
+};
+
+const char l_Null[] = "null";
+const char l_False[] = "false";
+const char l_True[] = "true";
+const char l_Indent[] = " ";
+
+// https://github.com/nlohmann/json/issues/1512
+template<bool prettyPrint>
+class JsonEncoder
+{
+public:
+ void Null();
+ void Boolean(bool value);
+ void NumberFloat(double value);
+ void Strng(String value);
+ void StartObject();
+ void Key(String value);
+ void EndObject();
+ void StartArray();
+ void EndArray();
+
+ String GetResult();
+
+private:
+ std::vector<char> m_Result;
+ String m_CurrentKey;
+ std::stack<std::bitset<2>> m_CurrentSubtree;
+
+ void AppendChar(char c);
+
+ template<class Iterator>
+ void AppendChars(Iterator begin, Iterator end);
+
+ void AppendJson(nlohmann::json json);
+
+ void BeforeItem();
+
+ void FinishContainer(char terminator);
+};
+
+template<bool prettyPrint>
+void Encode(JsonEncoder<prettyPrint>& stateMachine, const Value& value);
+
+template<bool prettyPrint>
+inline
+void EncodeNamespace(JsonEncoder<prettyPrint>& stateMachine, const Namespace::Ptr& ns)
+{
+ stateMachine.StartObject();
+
+ ObjectLock olock(ns);
+ for (const Namespace::Pair& kv : ns) {
+ stateMachine.Key(Utility::ValidateUTF8(kv.first));
+ Encode(stateMachine, kv.second.Val);
+ }
+
+ stateMachine.EndObject();
+}
+
+template<bool prettyPrint>
+inline
+void EncodeDictionary(JsonEncoder<prettyPrint>& stateMachine, const Dictionary::Ptr& dict)
+{
+ stateMachine.StartObject();
+
+ ObjectLock olock(dict);
+ for (const Dictionary::Pair& kv : dict) {
+ stateMachine.Key(Utility::ValidateUTF8(kv.first));
+ Encode(stateMachine, kv.second);
+ }
+
+ stateMachine.EndObject();
+}
+
+template<bool prettyPrint>
+inline
+void EncodeArray(JsonEncoder<prettyPrint>& stateMachine, const Array::Ptr& arr)
+{
+ stateMachine.StartArray();
+
+ ObjectLock olock(arr);
+ for (const Value& value : arr) {
+ Encode(stateMachine, value);
+ }
+
+ stateMachine.EndArray();
+}
+
+template<bool prettyPrint>
+void Encode(JsonEncoder<prettyPrint>& stateMachine, const Value& value)
+{
+ switch (value.GetType()) {
+ case ValueNumber:
+ stateMachine.NumberFloat(value.Get<double>());
+ break;
+
+ case ValueBoolean:
+ stateMachine.Boolean(value.ToBool());
+ break;
+
+ case ValueString:
+ stateMachine.Strng(Utility::ValidateUTF8(value.Get<String>()));
+ break;
+
+ case ValueObject:
+ {
+ const Object::Ptr& obj = value.Get<Object::Ptr>();
+
+ {
+ Namespace::Ptr ns = dynamic_pointer_cast<Namespace>(obj);
+ if (ns) {
+ EncodeNamespace(stateMachine, ns);
+ break;
+ }
+ }
+
+ {
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>(obj);
+ if (dict) {
+ EncodeDictionary(stateMachine, dict);
+ break;
+ }
+ }
+
+ {
+ Array::Ptr arr = dynamic_pointer_cast<Array>(obj);
+ if (arr) {
+ EncodeArray(stateMachine, arr);
+ break;
+ }
+ }
+
+ // obj is most likely a function => "Object of type 'Function'"
+ Encode(stateMachine, obj->ToString());
+ break;
+ }
+
+ case ValueEmpty:
+ stateMachine.Null();
+ break;
+
+ default:
+ VERIFY(!"Invalid variant type.");
+ }
+}
+
+String icinga::JsonEncode(const Value& value, bool pretty_print)
+{
+ if (pretty_print) {
+ JsonEncoder<true> stateMachine;
+
+ Encode(stateMachine, value);
+
+ return stateMachine.GetResult() + "\n";
+ } else {
+ JsonEncoder<false> stateMachine;
+
+ Encode(stateMachine, value);
+
+ return stateMachine.GetResult();
+ }
+}
+
+Value icinga::JsonDecode(const String& data)
+{
+ String sanitized (Utility::ValidateUTF8(data));
+
+ JsonSax stateMachine;
+
+ nlohmann::json::sax_parse(sanitized.Begin(), sanitized.End(), &stateMachine);
+
+ return stateMachine.GetResult();
+}
+
+inline
+bool JsonSax::null()
+{
+ FillCurrentTarget(Value());
+
+ return true;
+}
+
+inline
+bool JsonSax::boolean(bool val)
+{
+ FillCurrentTarget(val);
+
+ return true;
+}
+
+inline
+bool JsonSax::number_integer(JsonSax::number_integer_t val)
+{
+ FillCurrentTarget((double)val);
+
+ return true;
+}
+
+inline
+bool JsonSax::number_unsigned(JsonSax::number_unsigned_t val)
+{
+ FillCurrentTarget((double)val);
+
+ return true;
+}
+
+inline
+bool JsonSax::number_float(JsonSax::number_float_t val, const JsonSax::string_t&)
+{
+ FillCurrentTarget((double)val);
+
+ return true;
+}
+
+inline
+bool JsonSax::string(JsonSax::string_t& val)
+{
+ FillCurrentTarget(String(std::move(val)));
+
+ return true;
+}
+
+inline
+bool JsonSax::binary(JsonSax::binary_t& val)
+{
+ FillCurrentTarget(String(val.begin(), val.end()));
+
+ return true;
+}
+
+inline
+bool JsonSax::start_object(std::size_t)
+{
+ auto object (new Dictionary());
+
+ FillCurrentTarget(object);
+
+ m_CurrentSubtree.push({object, nullptr});
+
+ return true;
+}
+
+inline
+bool JsonSax::key(JsonSax::string_t& val)
+{
+ m_CurrentKey = String(std::move(val));
+
+ return true;
+}
+
+inline
+bool JsonSax::end_object()
+{
+ m_CurrentSubtree.pop();
+ m_CurrentKey = String();
+
+ return true;
+}
+
+inline
+bool JsonSax::start_array(std::size_t)
+{
+ auto array (new Array());
+
+ FillCurrentTarget(array);
+
+ m_CurrentSubtree.push({nullptr, array});
+
+ return true;
+}
+
+inline
+bool JsonSax::end_array()
+{
+ m_CurrentSubtree.pop();
+
+ return true;
+}
+
+inline
+bool JsonSax::parse_error(std::size_t, const std::string&, const nlohmann::detail::exception& ex)
+{
+ throw std::invalid_argument(ex.what());
+}
+
+inline
+Value JsonSax::GetResult()
+{
+ return m_Root;
+}
+
+inline
+void JsonSax::FillCurrentTarget(Value value)
+{
+ if (m_CurrentSubtree.empty()) {
+ m_Root = value;
+ } else {
+ auto& node (m_CurrentSubtree.top());
+
+ if (node.first) {
+ node.first->Set(m_CurrentKey, value);
+ } else {
+ node.second->Add(value);
+ }
+ }
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::Null()
+{
+ BeforeItem();
+ AppendChars((const char*)l_Null, (const char*)l_Null + 4);
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::Boolean(bool value)
+{
+ BeforeItem();
+
+ if (value) {
+ AppendChars((const char*)l_True, (const char*)l_True + 4);
+ } else {
+ AppendChars((const char*)l_False, (const char*)l_False + 5);
+ }
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::NumberFloat(double value)
+{
+ BeforeItem();
+
+ // Make sure 0.0 is serialized as 0, so e.g. Icinga DB can parse it as int.
+ if (value < 0) {
+ long long i = value;
+
+ if (i == value) {
+ AppendJson(i);
+ } else {
+ AppendJson(value);
+ }
+ } else {
+ unsigned long long i = value;
+
+ if (i == value) {
+ AppendJson(i);
+ } else {
+ AppendJson(value);
+ }
+ }
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::Strng(String value)
+{
+ BeforeItem();
+ AppendJson(std::move(value));
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::StartObject()
+{
+ BeforeItem();
+ AppendChar('{');
+
+ m_CurrentSubtree.push(2);
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::Key(String value)
+{
+ m_CurrentKey = std::move(value);
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::EndObject()
+{
+ FinishContainer('}');
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::StartArray()
+{
+ BeforeItem();
+ AppendChar('[');
+
+ m_CurrentSubtree.push(0);
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::EndArray()
+{
+ FinishContainer(']');
+}
+
+template<bool prettyPrint>
+inline
+String JsonEncoder<prettyPrint>::GetResult()
+{
+ return String(m_Result.begin(), m_Result.end());
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::AppendChar(char c)
+{
+ m_Result.emplace_back(c);
+}
+
+template<bool prettyPrint>
+template<class Iterator>
+inline
+void JsonEncoder<prettyPrint>::AppendChars(Iterator begin, Iterator end)
+{
+ m_Result.insert(m_Result.end(), begin, end);
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::AppendJson(nlohmann::json json)
+{
+ nlohmann::detail::serializer<nlohmann::json>(nlohmann::detail::output_adapter<char>(m_Result), ' ').dump(std::move(json), prettyPrint, true, 0);
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::BeforeItem()
+{
+ if (!m_CurrentSubtree.empty()) {
+ auto& node (m_CurrentSubtree.top());
+
+ if (node[0]) {
+ AppendChar(',');
+ } else {
+ node[0] = true;
+ }
+
+ if (prettyPrint) {
+ AppendChar('\n');
+
+ for (auto i (m_CurrentSubtree.size()); i; --i) {
+ AppendChars((const char*)l_Indent, (const char*)l_Indent + 4);
+ }
+ }
+
+ if (node[1]) {
+ AppendJson(std::move(m_CurrentKey));
+ AppendChar(':');
+
+ if (prettyPrint) {
+ AppendChar(' ');
+ }
+ }
+ }
+}
+
+template<bool prettyPrint>
+inline
+void JsonEncoder<prettyPrint>::FinishContainer(char terminator)
+{
+ if (prettyPrint && m_CurrentSubtree.top()[0]) {
+ AppendChar('\n');
+
+ for (auto i (m_CurrentSubtree.size() - 1u); i; --i) {
+ AppendChars((const char*)l_Indent, (const char*)l_Indent + 4);
+ }
+ }
+
+ AppendChar(terminator);
+
+ m_CurrentSubtree.pop();
+}
diff --git a/lib/base/json.hpp b/lib/base/json.hpp
new file mode 100644
index 0000000..df0ea18
--- /dev/null
+++ b/lib/base/json.hpp
@@ -0,0 +1,19 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef JSON_H
+#define JSON_H
+
+#include "base/i2-base.hpp"
+
+namespace icinga
+{
+
+class String;
+class Value;
+
+String JsonEncode(const Value& value, bool pretty_print = false);
+Value JsonDecode(const String& data);
+
+}
+
+#endif /* JSON_H */
diff --git a/lib/base/lazy-init.hpp b/lib/base/lazy-init.hpp
new file mode 100644
index 0000000..c1da2cd
--- /dev/null
+++ b/lib/base/lazy-init.hpp
@@ -0,0 +1,72 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LAZY_INIT
+#define LAZY_INIT
+
+#include <atomic>
+#include <functional>
+#include <mutex>
+#include <utility>
+
+namespace icinga
+{
+
+/**
+ * Lazy object initialization abstraction inspired from
+ * <https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1?view=netframework-4.7.2>.
+ *
+ * @ingroup base
+ */
+template<class T>
+class LazyInit
+{
+public:
+ inline
+ LazyInit(std::function<T()> initializer = []() { return T(); }) : m_Initializer(std::move(initializer))
+ {
+ m_Underlying.store(nullptr, std::memory_order_release);
+ }
+
+ LazyInit(const LazyInit&) = delete;
+ LazyInit(LazyInit&&) = delete;
+ LazyInit& operator=(const LazyInit&) = delete;
+ LazyInit& operator=(LazyInit&&) = delete;
+
+ inline
+ ~LazyInit()
+ {
+ auto ptr (m_Underlying.load(std::memory_order_acquire));
+
+ if (ptr != nullptr) {
+ delete ptr;
+ }
+ }
+
+ inline
+ T& Get()
+ {
+ auto ptr (m_Underlying.load(std::memory_order_acquire));
+
+ if (ptr == nullptr) {
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ ptr = m_Underlying.load(std::memory_order_acquire);
+
+ if (ptr == nullptr) {
+ ptr = new T(m_Initializer());
+ m_Underlying.store(ptr, std::memory_order_release);
+ }
+ }
+
+ return *ptr;
+ }
+
+private:
+ std::function<T()> m_Initializer;
+ std::mutex m_Mutex;
+ std::atomic<T*> m_Underlying;
+};
+
+}
+
+#endif /* LAZY_INIT */
diff --git a/lib/base/library.cpp b/lib/base/library.cpp
new file mode 100644
index 0000000..541ed74
--- /dev/null
+++ b/lib/base/library.cpp
@@ -0,0 +1,68 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/library.hpp"
+#include "base/loader.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/application.hpp"
+
+using namespace icinga;
+
+/**
+ * Loads the specified library.
+ *
+ * @param name The name of the library.
+ */
+Library::Library(const String& name)
+{
+ String path;
+#if defined(_WIN32)
+ path = name + ".dll";
+#elif defined(__APPLE__)
+ path = "lib" + name + "." + Application::GetAppSpecVersion() + ".dylib";
+#else /* __APPLE__ */
+ path = "lib" + name + ".so." + Application::GetAppSpecVersion();
+#endif /* _WIN32 */
+
+ Log(LogNotice, "Library")
+ << "Loading library '" << path << "'";
+
+#ifdef _WIN32
+ HMODULE hModule = LoadLibrary(path.CStr());
+
+ if (!hModule) {
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("LoadLibrary")
+ << errinfo_win32_error(GetLastError())
+ << boost::errinfo_file_name(path));
+ }
+#else /* _WIN32 */
+ void *hModule = dlopen(path.CStr(), RTLD_NOW | RTLD_GLOBAL);
+
+ if (!hModule) {
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not load library '" + path + "': " + dlerror()));
+ }
+#endif /* _WIN32 */
+
+ Loader::ExecuteDeferredInitializers();
+
+ m_Handle.reset(new LibraryHandle(hModule), [](LibraryHandle *handle) {
+#ifdef _WIN32
+ FreeLibrary(*handle);
+#else /* _WIN32 */
+ dlclose(*handle);
+#endif /* _WIN32 */
+ });
+}
+
+void *Library::GetSymbolAddress(const String& name) const
+{
+ if (!m_Handle)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid library handle"));
+
+#ifdef _WIN32
+ return GetProcAddress(*m_Handle.get(), name.CStr());
+#else /* _WIN32 */
+ return dlsym(*m_Handle.get(), name.CStr());
+#endif /* _WIN32 */
+}
diff --git a/lib/base/library.hpp b/lib/base/library.hpp
new file mode 100644
index 0000000..6bd2065
--- /dev/null
+++ b/lib/base/library.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LIBRARY_H
+#define LIBRARY_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include <memory>
+
+namespace icinga
+{
+
+#ifndef _WIN32
+typedef void *LibraryHandle;
+#else /* _WIN32 */
+typedef HMODULE LibraryHandle;
+#endif /* _WIN32 */
+
+class Library
+{
+public:
+ Library() = default;
+ Library(const String& name);
+
+ void *GetSymbolAddress(const String& name) const;
+
+ template<typename T>
+ T GetSymbolAddress(const String& name) const
+ {
+ static_assert(!std::is_same<T, void *>::value, "T must not be void *");
+
+ return reinterpret_cast<T>(GetSymbolAddress(name));
+ }
+
+private:
+ std::shared_ptr<LibraryHandle> m_Handle;
+};
+
+}
+
+#endif /* LIBRARY_H */
diff --git a/lib/base/loader.cpp b/lib/base/loader.cpp
new file mode 100644
index 0000000..a4364de
--- /dev/null
+++ b/lib/base/loader.cpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/loader.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/application.hpp"
+
+using namespace icinga;
+
+boost::thread_specific_ptr<Loader::DeferredInitializerPriorityQueue>& Loader::GetDeferredInitializers()
+{
+ static boost::thread_specific_ptr<DeferredInitializerPriorityQueue> initializers;
+ return initializers;
+}
+
+void Loader::ExecuteDeferredInitializers()
+{
+ auto& initializers = GetDeferredInitializers();
+ if (!initializers.get())
+ return;
+
+ while (!initializers->empty()) {
+ DeferredInitializer initializer = initializers->top();
+ initializers->pop();
+ initializer();
+ }
+}
+
+void Loader::AddDeferredInitializer(const std::function<void()>& callback, InitializePriority priority)
+{
+ auto& initializers = GetDeferredInitializers();
+ if (!initializers.get()) {
+ initializers.reset(new Loader::DeferredInitializerPriorityQueue());
+ }
+
+ initializers->push(DeferredInitializer(callback, priority));
+}
+
diff --git a/lib/base/loader.hpp b/lib/base/loader.hpp
new file mode 100644
index 0000000..f1c7759
--- /dev/null
+++ b/lib/base/loader.hpp
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LOADER_H
+#define LOADER_H
+
+#include "base/i2-base.hpp"
+#include "base/initialize.hpp"
+#include "base/string.hpp"
+#include <boost/thread/tss.hpp>
+#include <queue>
+
+namespace icinga
+{
+
+struct DeferredInitializer
+{
+public:
+ DeferredInitializer(std::function<void ()> callback, InitializePriority priority)
+ : m_Callback(std::move(callback)), m_Priority(priority)
+ { }
+
+ bool operator>(const DeferredInitializer& other) const
+ {
+ return m_Priority > other.m_Priority;
+ }
+
+ void operator()()
+ {
+ m_Callback();
+ }
+
+private:
+ std::function<void ()> m_Callback;
+ InitializePriority m_Priority;
+};
+
+/**
+ * Loader helper functions.
+ *
+ * @ingroup base
+ */
+class Loader
+{
+public:
+ static void AddDeferredInitializer(const std::function<void ()>& callback, InitializePriority priority = InitializePriority::Default);
+ static void ExecuteDeferredInitializers();
+
+private:
+ Loader();
+
+ // Deferred initializers are run in the order of the definition of their enum values.
+ // Therefore, initializers that should be run first have lower enum values and
+ // the order of the std::priority_queue has to be reversed using std::greater.
+ using DeferredInitializerPriorityQueue = std::priority_queue<DeferredInitializer, std::vector<DeferredInitializer>, std::greater<>>;
+
+ static boost::thread_specific_ptr<DeferredInitializerPriorityQueue>& GetDeferredInitializers();
+};
+
+}
+
+#endif /* LOADER_H */
diff --git a/lib/base/logger.cpp b/lib/base/logger.cpp
new file mode 100644
index 0000000..38a2c67
--- /dev/null
+++ b/lib/base/logger.cpp
@@ -0,0 +1,326 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/logger.hpp"
+#include "base/logger-ti.cpp"
+#include "base/application.hpp"
+#include "base/streamlogger.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/objectlock.hpp"
+#include "base/context.hpp"
+#include "base/scriptglobal.hpp"
+#ifdef _WIN32
+#include "base/windowseventloglogger.hpp"
+#endif /* _WIN32 */
+#include <algorithm>
+#include <iostream>
+#include <utility>
+
+using namespace icinga;
+
+template Log& Log::operator<<(const Value&);
+template Log& Log::operator<<(const String&);
+template Log& Log::operator<<(const std::string&);
+template Log& Log::operator<<(const bool&);
+template Log& Log::operator<<(const unsigned int&);
+template Log& Log::operator<<(const int&);
+template Log& Log::operator<<(const unsigned long&);
+template Log& Log::operator<<(const long&);
+template Log& Log::operator<<(const double&);
+
+REGISTER_TYPE(Logger);
+
+std::set<Logger::Ptr> Logger::m_Loggers;
+std::mutex Logger::m_Mutex;
+bool Logger::m_ConsoleLogEnabled = true;
+std::atomic<bool> Logger::m_EarlyLoggingEnabled (true);
+bool Logger::m_TimestampEnabled = true;
+LogSeverity Logger::m_ConsoleLogSeverity = LogInformation;
+std::mutex Logger::m_UpdateMinLogSeverityMutex;
+Atomic<LogSeverity> Logger::m_MinLogSeverity (LogDebug);
+
+INITIALIZE_ONCE([]() {
+ ScriptGlobal::Set("System.LogDebug", LogDebug);
+ ScriptGlobal::Set("System.LogNotice", LogNotice);
+ ScriptGlobal::Set("System.LogInformation", LogInformation);
+ ScriptGlobal::Set("System.LogWarning", LogWarning);
+ ScriptGlobal::Set("System.LogCritical", LogCritical);
+});
+
+/**
+ * Constructor for the Logger class.
+ */
+void Logger::Start(bool runtimeCreated)
+{
+ ObjectImpl<Logger>::Start(runtimeCreated);
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Loggers.insert(this);
+ }
+
+ UpdateMinLogSeverity();
+}
+
+void Logger::Stop(bool runtimeRemoved)
+{
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Loggers.erase(this);
+ }
+
+ UpdateMinLogSeverity();
+
+ ObjectImpl<Logger>::Stop(runtimeRemoved);
+}
+
+std::set<Logger::Ptr> Logger::GetLoggers()
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ return m_Loggers;
+}
+
+/**
+ * Retrieves the minimum severity for this logger.
+ *
+ * @returns The minimum severity.
+ */
+LogSeverity Logger::GetMinSeverity() const
+{
+ String severity = GetSeverity();
+ if (severity.IsEmpty())
+ return LogInformation;
+ else {
+ LogSeverity ls = LogInformation;
+
+ try {
+ ls = Logger::StringToSeverity(severity);
+ } catch (const std::exception&) { /* use the default level */ }
+
+ return ls;
+ }
+}
+
+/**
+ * Converts a severity enum value to a string.
+ *
+ * @param severity The severity value.
+ */
+String Logger::SeverityToString(LogSeverity severity)
+{
+ switch (severity) {
+ case LogDebug:
+ return "debug";
+ case LogNotice:
+ return "notice";
+ case LogInformation:
+ return "information";
+ case LogWarning:
+ return "warning";
+ case LogCritical:
+ return "critical";
+ default:
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid severity."));
+ }
+}
+
+/**
+ * Converts a string to a severity enum value.
+ *
+ * @param severity The severity.
+ */
+LogSeverity Logger::StringToSeverity(const String& severity)
+{
+ if (severity == "debug")
+ return LogDebug;
+ else if (severity == "notice")
+ return LogNotice;
+ else if (severity == "information")
+ return LogInformation;
+ else if (severity == "warning")
+ return LogWarning;
+ else if (severity == "critical")
+ return LogCritical;
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid severity: " + severity));
+}
+
+void Logger::DisableConsoleLog()
+{
+ m_ConsoleLogEnabled = false;
+
+ UpdateMinLogSeverity();
+}
+
+void Logger::EnableConsoleLog()
+{
+ m_ConsoleLogEnabled = true;
+
+ UpdateMinLogSeverity();
+}
+
+bool Logger::IsConsoleLogEnabled()
+{
+ return m_ConsoleLogEnabled;
+}
+
+void Logger::SetConsoleLogSeverity(LogSeverity logSeverity)
+{
+ m_ConsoleLogSeverity = logSeverity;
+}
+
+LogSeverity Logger::GetConsoleLogSeverity()
+{
+ return m_ConsoleLogSeverity;
+}
+
+void Logger::DisableEarlyLogging() {
+ m_EarlyLoggingEnabled = false;
+
+ UpdateMinLogSeverity();
+}
+
+bool Logger::IsEarlyLoggingEnabled() {
+ return m_EarlyLoggingEnabled;
+}
+
+void Logger::DisableTimestamp()
+{
+ m_TimestampEnabled = false;
+}
+
+void Logger::EnableTimestamp()
+{
+ m_TimestampEnabled = true;
+}
+
+bool Logger::IsTimestampEnabled()
+{
+ return m_TimestampEnabled;
+}
+
+void Logger::SetSeverity(const String& value, bool suppress_events, const Value& cookie)
+{
+ ObjectImpl<Logger>::SetSeverity(value, suppress_events, cookie);
+
+ UpdateMinLogSeverity();
+}
+
+void Logger::ValidateSeverity(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Logger>::ValidateSeverity(lvalue, utils);
+
+ try {
+ StringToSeverity(lvalue());
+ } catch (...) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "severity" }, "Invalid severity specified: " + lvalue()));
+ }
+}
+
+void Logger::UpdateMinLogSeverity()
+{
+ std::unique_lock<std::mutex> lock (m_UpdateMinLogSeverityMutex);
+ auto result (LogNothing);
+
+ for (auto& logger : Logger::GetLoggers()) {
+ ObjectLock llock (logger);
+
+ if (logger->IsActive()) {
+ result = std::min(result, logger->GetMinSeverity());
+ }
+ }
+
+ if (Logger::IsConsoleLogEnabled()) {
+ result = std::min(result, Logger::GetConsoleLogSeverity());
+ }
+
+#ifdef _WIN32
+ if (Logger::IsEarlyLoggingEnabled()) {
+ result = std::min(result, LogCritical);
+ }
+#endif /* _WIN32 */
+
+ m_MinLogSeverity.store(result);
+}
+
+Log::Log(LogSeverity severity, String facility, const String& message)
+ : Log(severity, std::move(facility))
+{
+ if (!m_IsNoOp) {
+ m_Buffer << message;
+ }
+}
+
+Log::Log(LogSeverity severity, String facility)
+ : m_Severity(severity), m_Facility(std::move(facility)), m_IsNoOp(severity < Logger::GetMinLogSeverity())
+{ }
+
+/**
+ * Writes the message to the application's log.
+ */
+Log::~Log()
+{
+ if (m_IsNoOp) {
+ return;
+ }
+
+ LogEntry entry;
+ entry.Timestamp = Utility::GetTime();
+ entry.Severity = m_Severity;
+ entry.Facility = m_Facility;
+
+ {
+ auto msg (m_Buffer.str());
+ msg.erase(msg.find_last_not_of("\n") + 1u);
+
+ entry.Message = std::move(msg);
+ }
+
+ if (m_Severity >= LogWarning) {
+ ContextTrace context;
+
+ if (context.GetLength() > 0) {
+ std::ostringstream trace;
+ trace << context;
+ entry.Message += "\nContext:" + trace.str();
+ }
+ }
+
+ for (const Logger::Ptr& logger : Logger::GetLoggers()) {
+ ObjectLock llock(logger);
+
+ if (!logger->IsActive())
+ continue;
+
+ if (entry.Severity >= logger->GetMinSeverity())
+ logger->ProcessLogEntry(entry);
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ /* Always flush, don't depend on the timer. Enable this for development sprints on Linux/macOS only. Windows crashes. */
+ //logger->Flush();
+#endif /* I2_DEBUG */
+ }
+
+ if (Logger::IsConsoleLogEnabled() && entry.Severity >= Logger::GetConsoleLogSeverity()) {
+ StreamLogger::ProcessLogEntry(std::cout, entry);
+
+ /* "Console" might be a pipe/socket (systemd, daemontools, docker, ...),
+ * then cout will not flush lines automatically. */
+ std::cout << std::flush;
+ }
+
+#ifdef _WIN32
+ if (Logger::IsEarlyLoggingEnabled() && entry.Severity >= LogCritical) {
+ WindowsEventLogLogger::WriteToWindowsEventLog(entry);
+ }
+#endif /* _WIN32 */
+}
+
+Log& Log::operator<<(const char *val)
+{
+ if (!m_IsNoOp) {
+ m_Buffer << val;
+ }
+
+ return *this;
+}
diff --git a/lib/base/logger.hpp b/lib/base/logger.hpp
new file mode 100644
index 0000000..10e0872
--- /dev/null
+++ b/lib/base/logger.hpp
@@ -0,0 +1,149 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LOGGER_H
+#define LOGGER_H
+
+#include "base/atomic.hpp"
+#include "base/i2-base.hpp"
+#include "base/logger-ti.hpp"
+#include <set>
+#include <sstream>
+
+namespace icinga
+{
+
+/**
+ * Log severity.
+ *
+ * @ingroup base
+ */
+enum LogSeverity
+{
+ LogDebug,
+ LogNotice,
+ LogInformation,
+ LogWarning,
+ LogCritical,
+
+ // Just for internal comparision
+ LogNothing,
+};
+
+/**
+ * A log entry.
+ *
+ * @ingroup base
+ */
+struct LogEntry {
+ double Timestamp; /**< The timestamp when this log entry was created. */
+ LogSeverity Severity; /**< The severity of this log entry. */
+ String Facility; /**< The facility this log entry belongs to. */
+ String Message; /**< The log entry's message. */
+};
+
+/**
+ * A log provider.
+ *
+ * @ingroup base
+ */
+class Logger : public ObjectImpl<Logger>
+{
+public:
+ DECLARE_OBJECT(Logger);
+
+ static String SeverityToString(LogSeverity severity);
+ static LogSeverity StringToSeverity(const String& severity);
+
+ LogSeverity GetMinSeverity() const;
+
+ /**
+ * Processes the log entry and writes it to the log that is
+ * represented by this ILogger object.
+ *
+ * @param entry The log entry that is to be processed.
+ */
+ virtual void ProcessLogEntry(const LogEntry& entry) = 0;
+
+ virtual void Flush() = 0;
+
+ static std::set<Logger::Ptr> GetLoggers();
+
+ static void DisableConsoleLog();
+ static void EnableConsoleLog();
+ static bool IsConsoleLogEnabled();
+ static void DisableEarlyLogging();
+ static bool IsEarlyLoggingEnabled();
+ static void DisableTimestamp();
+ static void EnableTimestamp();
+ static bool IsTimestampEnabled();
+
+ static void SetConsoleLogSeverity(LogSeverity logSeverity);
+ static LogSeverity GetConsoleLogSeverity();
+
+ static inline
+ LogSeverity GetMinLogSeverity()
+ {
+ return m_MinLogSeverity.load();
+ }
+
+ void SetSeverity(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+ void ValidateSeverity(const Lazy<String>& lvalue, const ValidationUtils& utils) final;
+
+protected:
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ static void UpdateMinLogSeverity();
+
+ static std::mutex m_Mutex;
+ static std::set<Logger::Ptr> m_Loggers;
+ static bool m_ConsoleLogEnabled;
+ static std::atomic<bool> m_EarlyLoggingEnabled;
+ static bool m_TimestampEnabled;
+ static LogSeverity m_ConsoleLogSeverity;
+ static std::mutex m_UpdateMinLogSeverityMutex;
+ static Atomic<LogSeverity> m_MinLogSeverity;
+};
+
+class Log
+{
+public:
+ Log() = delete;
+ Log(const Log& other) = delete;
+ Log& operator=(const Log& rhs) = delete;
+
+ Log(LogSeverity severity, String facility, const String& message);
+ Log(LogSeverity severity, String facility);
+
+ ~Log();
+
+ template<typename T>
+ Log& operator<<(const T& val)
+ {
+ m_Buffer << val;
+ return *this;
+ }
+
+ Log& operator<<(const char *val);
+
+private:
+ LogSeverity m_Severity;
+ String m_Facility;
+ std::ostringstream m_Buffer;
+ bool m_IsNoOp;
+};
+
+extern template Log& Log::operator<<(const Value&);
+extern template Log& Log::operator<<(const String&);
+extern template Log& Log::operator<<(const std::string&);
+extern template Log& Log::operator<<(const bool&);
+extern template Log& Log::operator<<(const unsigned int&);
+extern template Log& Log::operator<<(const int&);
+extern template Log& Log::operator<<(const unsigned long&);
+extern template Log& Log::operator<<(const long&);
+extern template Log& Log::operator<<(const double&);
+
+}
+
+#endif /* LOGGER_H */
diff --git a/lib/base/logger.ti b/lib/base/logger.ti
new file mode 100644
index 0000000..44226ce
--- /dev/null
+++ b/lib/base/logger.ti
@@ -0,0 +1,17 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library base;
+
+namespace icinga
+{
+
+abstract class Logger : ConfigObject
+{
+ [config, virtual] String severity {
+ default {{{ return "information"; }}}
+ };
+};
+
+}
diff --git a/lib/base/math-script.cpp b/lib/base/math-script.cpp
new file mode 100644
index 0000000..6cd7b0e
--- /dev/null
+++ b/lib/base/math-script.cpp
@@ -0,0 +1,184 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/initialize.hpp"
+#include "base/namespace.hpp"
+#include <boost/math/special_functions/round.hpp>
+#include <cmath>
+
+using namespace icinga;
+
+static double MathAbs(double x)
+{
+ return std::fabs(x);
+}
+
+static double MathAcos(double x)
+{
+ return std::acos(x);
+}
+
+static double MathAsin(double x)
+{
+ return std::asin(x);
+}
+
+static double MathAtan(double x)
+{
+ return std::atan(x);
+}
+
+static double MathAtan2(double y, double x)
+{
+ return std::atan2(y, x);
+}
+
+static double MathCeil(double x)
+{
+ return std::ceil(x);
+}
+
+static double MathCos(double x)
+{
+ return std::cos(x);
+}
+
+static double MathExp(double x)
+{
+ return std::exp(x);
+}
+
+static double MathFloor(double x)
+{
+ return std::floor(x);
+}
+
+static double MathLog(double x)
+{
+ return std::log(x);
+}
+
+static Value MathMax(const std::vector<Value>& args)
+{
+ bool first = true;
+ Value result = -INFINITY;
+
+ for (const Value& arg : args) {
+ if (first || arg > result) {
+ first = false;
+ result = arg;
+ }
+ }
+
+ return result;
+}
+
+static Value MathMin(const std::vector<Value>& args)
+{
+ bool first = true;
+ Value result = INFINITY;
+
+ for (const Value& arg : args) {
+ if (first || arg < result) {
+ first = false;
+ result = arg;
+ }
+ }
+
+ return result;
+}
+
+static double MathPow(double x, double y)
+{
+ return std::pow(x, y);
+}
+
+static double MathRandom()
+{
+ return (double)std::rand() / RAND_MAX;
+}
+
+static double MathRound(double x)
+{
+ return boost::math::round(x);
+}
+
+static double MathSin(double x)
+{
+ return std::sin(x);
+}
+
+static double MathSqrt(double x)
+{
+ return std::sqrt(x);
+}
+
+static double MathTan(double x)
+{
+ return std::tan(x);
+}
+
+static bool MathIsnan(double x)
+{
+ return boost::math::isnan(x);
+}
+
+static bool MathIsinf(double x)
+{
+ return boost::math::isinf(x);
+}
+
+static double MathSign(double x)
+{
+ if (x > 0)
+ return 1;
+ else if (x < 0)
+ return -1;
+ else
+ return 0;
+}
+
+INITIALIZE_ONCE([]() {
+ Namespace::Ptr mathNS = new Namespace(true);
+
+ /* Constants */
+ mathNS->Set("E", 2.71828182845904523536);
+ mathNS->Set("LN2", 0.693147180559945309417);
+ mathNS->Set("LN10", 2.30258509299404568402);
+ mathNS->Set("LOG2E", 1.44269504088896340736);
+ mathNS->Set("LOG10E", 0.434294481903251827651);
+ mathNS->Set("PI", 3.14159265358979323846);
+ mathNS->Set("SQRT1_2", 0.707106781186547524401);
+ mathNS->Set("SQRT2", 1.41421356237309504880);
+
+ /* Methods */
+ mathNS->Set("abs", new Function("Math#abs", MathAbs, { "x" }, true));
+ mathNS->Set("acos", new Function("Math#acos", MathAcos, { "x" }, true));
+ mathNS->Set("asin", new Function("Math#asin", MathAsin, { "x" }, true));
+ mathNS->Set("atan", new Function("Math#atan", MathAtan, { "x" }, true));
+ mathNS->Set("atan2", new Function("Math#atan2", MathAtan2, { "x", "y" }, true));
+ mathNS->Set("ceil", new Function("Math#ceil", MathCeil, { "x" }, true));
+ mathNS->Set("cos", new Function("Math#cos", MathCos, { "x" }, true));
+ mathNS->Set("exp", new Function("Math#exp", MathExp, { "x" }, true));
+ mathNS->Set("floor", new Function("Math#floor", MathFloor, { "x" }, true));
+ mathNS->Set("log", new Function("Math#log", MathLog, { "x" }, true));
+ mathNS->Set("max", new Function("Math#max", MathMax, {}, true));
+ mathNS->Set("min", new Function("Math#min", MathMin, {}, true));
+ mathNS->Set("pow", new Function("Math#pow", MathPow, { "x", "y" }, true));
+ mathNS->Set("random", new Function("Math#random", MathRandom, {}, true));
+ mathNS->Set("round", new Function("Math#round", MathRound, { "x" }, true));
+ mathNS->Set("sin", new Function("Math#sin", MathSin, { "x" }, true));
+ mathNS->Set("sqrt", new Function("Math#sqrt", MathSqrt, { "x" }, true));
+ mathNS->Set("tan", new Function("Math#tan", MathTan, { "x" }, true));
+ mathNS->Set("isnan", new Function("Math#isnan", MathIsnan, { "x" }, true));
+ mathNS->Set("isinf", new Function("Math#isinf", MathIsinf, { "x" }, true));
+ mathNS->Set("sign", new Function("Math#sign", MathSign, { "x" }, true));
+
+ mathNS->Freeze();
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+ systemNS->Set("Math", mathNS, true);
+});
diff --git a/lib/base/namespace-script.cpp b/lib/base/namespace-script.cpp
new file mode 100644
index 0000000..deaae7d
--- /dev/null
+++ b/lib/base/namespace-script.cpp
@@ -0,0 +1,84 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/namespace.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/array.hpp"
+
+using namespace icinga;
+
+static void NamespaceSet(const String& key, const Value& value)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Namespace::Ptr self = static_cast<Namespace::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Set(key, value);
+}
+
+static Value NamespaceGet(const String& key)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Namespace::Ptr self = static_cast<Namespace::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Get(key);
+}
+
+static void NamespaceRemove(const String& key)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Namespace::Ptr self = static_cast<Namespace::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Remove(key);
+}
+
+static bool NamespaceContains(const String& key)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Namespace::Ptr self = static_cast<Namespace::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Contains(key);
+}
+
+static Array::Ptr NamespaceKeys()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Namespace::Ptr self = static_cast<Namespace::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ ArrayData keys;
+ ObjectLock olock(self);
+ for (const Namespace::Pair& kv : self) {
+ keys.push_back(kv.first);
+ }
+ return new Array(std::move(keys));
+}
+
+static Array::Ptr NamespaceValues()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Namespace::Ptr self = static_cast<Namespace::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ ArrayData values;
+ ObjectLock olock(self);
+ for (const Namespace::Pair& kv : self) {
+ values.push_back(kv.second.Val);
+ }
+ return new Array(std::move(values));
+}
+
+Object::Ptr Namespace::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "set", new Function("Namespace#set", NamespaceSet, { "key", "value" }) },
+ { "get", new Function("Namespace#get", NamespaceGet, { "key" }) },
+ { "remove", new Function("Namespace#remove", NamespaceRemove, { "key" }) },
+ { "contains", new Function("Namespace#contains", NamespaceContains, { "key" }, true) },
+ { "keys", new Function("Namespace#keys", NamespaceKeys, {}, true) },
+ { "values", new Function("Namespace#values", NamespaceValues, {}, true) },
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/namespace.cpp b/lib/base/namespace.cpp
new file mode 100644
index 0000000..4c5f4f6
--- /dev/null
+++ b/lib/base/namespace.cpp
@@ -0,0 +1,189 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/namespace.hpp"
+#include "base/objectlock.hpp"
+#include "base/debug.hpp"
+#include "base/primitivetype.hpp"
+#include "base/debuginfo.hpp"
+#include "base/exception.hpp"
+#include <sstream>
+
+using namespace icinga;
+
+template class std::map<icinga::String, std::shared_ptr<icinga::NamespaceValue> >;
+
+REGISTER_PRIMITIVE_TYPE(Namespace, Object, Namespace::GetPrototype());
+
+/**
+ * Creates a new namespace.
+ *
+ * @param constValues If true, all values inserted into the namespace are treated as constants and can't be updated.
+ */
+Namespace::Namespace(bool constValues)
+ : m_ConstValues(constValues), m_Frozen(false)
+{ }
+
+Value Namespace::Get(const String& field) const
+{
+ Value value;
+ if (!Get(field, &value))
+ BOOST_THROW_EXCEPTION(ScriptError("Namespace does not contain field '" + field + "'"));
+ return value;
+}
+
+bool Namespace::Get(const String& field, Value *value) const
+{
+ auto lock(ReadLockUnlessFrozen());
+
+ auto nsVal = m_Data.find(field);
+
+ if (nsVal == m_Data.end()) {
+ return false;
+ }
+
+ *value = nsVal->second.Val;
+ return true;
+}
+
+void Namespace::Set(const String& field, const Value& value, bool isConst, const DebugInfo& debugInfo)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen) {
+ BOOST_THROW_EXCEPTION(ScriptError("Namespace is read-only and must not be modified.", debugInfo));
+ }
+
+ std::unique_lock<decltype(m_DataMutex)> dlock (m_DataMutex);
+
+ auto nsVal = m_Data.find(field);
+
+ if (nsVal == m_Data.end()) {
+ m_Data[field] = NamespaceValue{value, isConst || m_ConstValues};
+ } else {
+ if (nsVal->second.Const) {
+ BOOST_THROW_EXCEPTION(ScriptError("Constant must not be modified.", debugInfo));
+ }
+
+ nsVal->second.Val = value;
+ }
+}
+
+/**
+ * Returns the number of elements in the namespace.
+ *
+ * @returns Number of elements.
+ */
+size_t Namespace::GetLength() const
+{
+ auto lock(ReadLockUnlessFrozen());
+
+ return m_Data.size();
+}
+
+bool Namespace::Contains(const String& field) const
+{
+ auto lock (ReadLockUnlessFrozen());
+
+ return m_Data.find(field) != m_Data.end();
+}
+
+void Namespace::Remove(const String& field)
+{
+ ObjectLock olock(this);
+
+ if (m_Frozen) {
+ BOOST_THROW_EXCEPTION(ScriptError("Namespace is read-only and must not be modified."));
+ }
+
+ std::unique_lock<decltype(m_DataMutex)> dlock (m_DataMutex);
+
+ auto it = m_Data.find(field);
+
+ if (it == m_Data.end()) {
+ return;
+ }
+
+ if (it->second.Const) {
+ BOOST_THROW_EXCEPTION(ScriptError("Constants must not be removed."));
+ }
+
+ m_Data.erase(it);
+}
+
+/**
+ * Freeze the namespace, preventing further updates.
+ *
+ * This only prevents inserting, replacing or deleting values from the namespace. This operation has no effect on
+ * objects referenced by the values, these remain mutable if they were before.
+ */
+void Namespace::Freeze() {
+ ObjectLock olock(this);
+
+ m_Frozen = true;
+}
+
+std::shared_lock<std::shared_timed_mutex> Namespace::ReadLockUnlessFrozen() const
+{
+ if (m_Frozen.load(std::memory_order_relaxed)) {
+ return std::shared_lock<std::shared_timed_mutex>();
+ } else {
+ return std::shared_lock<std::shared_timed_mutex>(m_DataMutex);
+ }
+}
+
+Value Namespace::GetFieldByName(const String& field, bool, const DebugInfo& debugInfo) const
+{
+ auto lock (ReadLockUnlessFrozen());
+
+ auto nsVal = m_Data.find(field);
+
+ if (nsVal != m_Data.end())
+ return nsVal->second.Val;
+ else
+ return GetPrototypeField(const_cast<Namespace *>(this), field, false, debugInfo); /* Ignore indexer not found errors similar to the Dictionary class. */
+}
+
+void Namespace::SetFieldByName(const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo)
+{
+ // The override frozen parameter is mandated by the interface but ignored here. If the namespace is frozen, this
+ // disables locking for read operations, so it must not be modified again to ensure the consistency of the internal
+ // data structures.
+ (void) overrideFrozen;
+
+ Set(field, value, false, debugInfo);
+}
+
+bool Namespace::HasOwnField(const String& field) const
+{
+ return Contains(field);
+}
+
+bool Namespace::GetOwnField(const String& field, Value *result) const
+{
+ return Get(field, result);
+}
+
+Namespace::Iterator Namespace::Begin()
+{
+ ASSERT(OwnsLock());
+
+ return m_Data.begin();
+}
+
+Namespace::Iterator Namespace::End()
+{
+ ASSERT(OwnsLock());
+
+ return m_Data.end();
+}
+
+Namespace::Iterator icinga::begin(const Namespace::Ptr& x)
+{
+ return x->Begin();
+}
+
+Namespace::Iterator icinga::end(const Namespace::Ptr& x)
+{
+ return x->End();
+}
+
diff --git a/lib/base/namespace.hpp b/lib/base/namespace.hpp
new file mode 100644
index 0000000..94f2055
--- /dev/null
+++ b/lib/base/namespace.hpp
@@ -0,0 +1,105 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NAMESPACE_H
+#define NAMESPACE_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+#include "base/shared-object.hpp"
+#include "base/value.hpp"
+#include "base/debuginfo.hpp"
+#include <atomic>
+#include <map>
+#include <vector>
+#include <memory>
+#include <shared_mutex>
+
+namespace icinga
+{
+
+struct NamespaceValue
+{
+ Value Val;
+ bool Const;
+};
+
+
+/**
+ * A namespace.
+ *
+ * ## External Locking
+ *
+ * Synchronization is handled internally, so almost all functions are safe for concurrent use without external locking.
+ * The only exception to this are functions returning an iterator. To use these, the caller has to acquire an ObjectLock
+ * on the namespace. The iterators only remain valid for as long as that ObjectLock is held. Note that this also
+ * includes range-based for loops.
+ *
+ * If consistency across multiple operations is required, an ObjectLock must also be acquired to prevent concurrent
+ * modifications.
+ *
+ * ## Internal Locking
+ *
+ * Two mutex objects are involved in locking a namespace: the recursive mutex inherited from the Object class that is
+ * acquired and released using the ObjectLock class and the m_DataMutex shared mutex contained directly in the
+ * Namespace class. The ObjectLock is used to synchronize multiple write operations against each other. The shared mutex
+ * is only used to ensure the consistency of the m_Data data structure.
+ *
+ * Read operations must acquire a shared lock on m_DataMutex. This prevents concurrent writes to that data structure
+ * but still allows concurrent reads.
+ *
+ * Write operations must first obtain an ObjectLock and then a shared lock on m_DataMutex. This order is important for
+ * preventing deadlocks. The ObjectLock prevents concurrent write operations while the shared lock prevents concurrent
+ * read operations.
+ *
+ * External read access to iterators is synchronized by the caller holding an ObjectLock. This ensures no concurrent
+ * write operations as these require the ObjectLock but still allows concurrent reads as m_DataMutex is not locked.
+ *
+ * @ingroup base
+ */
+class Namespace final : public Object
+{
+public:
+ DECLARE_OBJECT(Namespace);
+
+ typedef std::map<String, NamespaceValue>::iterator Iterator;
+
+ typedef std::map<String, NamespaceValue>::value_type Pair;
+
+ explicit Namespace(bool constValues = false);
+
+ Value Get(const String& field) const;
+ bool Get(const String& field, Value *value) const;
+ void Set(const String& field, const Value& value, bool isConst = false, const DebugInfo& debugInfo = DebugInfo());
+ bool Contains(const String& field) const;
+ void Remove(const String& field);
+ void Freeze();
+
+ Iterator Begin();
+ Iterator End();
+
+ size_t GetLength() const;
+
+ Value GetFieldByName(const String& field, bool sandboxed, const DebugInfo& debugInfo) const override;
+ void SetFieldByName(const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) override;
+ bool HasOwnField(const String& field) const override;
+ bool GetOwnField(const String& field, Value *result) const override;
+
+ static Object::Ptr GetPrototype();
+
+private:
+ std::shared_lock<std::shared_timed_mutex> ReadLockUnlessFrozen() const;
+
+ std::map<String, NamespaceValue> m_Data;
+ mutable std::shared_timed_mutex m_DataMutex;
+ bool m_ConstValues;
+ std::atomic<bool> m_Frozen;
+};
+
+Namespace::Iterator begin(const Namespace::Ptr& x);
+Namespace::Iterator end(const Namespace::Ptr& x);
+
+}
+
+extern template class std::map<icinga::String, std::shared_ptr<icinga::NamespaceValue> >;
+
+#endif /* NAMESPACE_H */
diff --git a/lib/base/netstring.cpp b/lib/base/netstring.cpp
new file mode 100644
index 0000000..60f08c2
--- /dev/null
+++ b/lib/base/netstring.cpp
@@ -0,0 +1,334 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/netstring.hpp"
+#include "base/debug.hpp"
+#include "base/tlsstream.hpp"
+#include <cstdint>
+#include <memory>
+#include <sstream>
+#include <utility>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/read.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/asio/write.hpp>
+
+using namespace icinga;
+
+/**
+ * Reads data from a stream in netstring format.
+ *
+ * @param stream The stream to read from.
+ * @param[out] str The String that has been read from the IOQueue.
+ * @returns true if a complete String was read from the IOQueue, false otherwise.
+ * @exception invalid_argument The input stream is invalid.
+ * @see https://github.com/PeterScott/netstring-c/blob/master/netstring.c
+ */
+StreamReadStatus NetString::ReadStringFromStream(const Stream::Ptr& stream, String *str, StreamReadContext& context,
+ bool may_wait, ssize_t maxMessageLength)
+{
+ if (context.Eof)
+ return StatusEof;
+
+ if (context.MustRead) {
+ if (!context.FillFromStream(stream, may_wait)) {
+ context.Eof = true;
+ return StatusEof;
+ }
+
+ context.MustRead = false;
+ }
+
+ size_t header_length = 0;
+
+ for (size_t i = 0; i < context.Size; i++) {
+ if (context.Buffer[i] == ':') {
+ header_length = i;
+
+ /* make sure there's a header */
+ if (header_length == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (no length specifier)"));
+
+ break;
+ } else if (i > 16)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (missing :)"));
+ }
+
+ if (header_length == 0) {
+ context.MustRead = true;
+ return StatusNeedData;
+ }
+
+ /* no leading zeros allowed */
+ if (context.Buffer[0] == '0' && isdigit(context.Buffer[1]))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (leading zero)"));
+
+ size_t len, i;
+
+ len = 0;
+ for (i = 0; i < header_length && isdigit(context.Buffer[i]); i++) {
+ /* length specifier must have at most 9 characters */
+ if (i >= 9)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Length specifier must not exceed 9 characters"));
+
+ len = len * 10 + (context.Buffer[i] - '0');
+ }
+
+ /* read the whole message */
+ size_t data_length = len + 1;
+
+ if (maxMessageLength >= 0 && data_length > (size_t)maxMessageLength) {
+ std::stringstream errorMessage;
+ errorMessage << "Max data length exceeded: " << (maxMessageLength / 1024) << " KB";
+
+ BOOST_THROW_EXCEPTION(std::invalid_argument(errorMessage.str()));
+ }
+
+ char *data = context.Buffer + header_length + 1;
+
+ if (context.Size < header_length + 1 + data_length) {
+ context.MustRead = true;
+ return StatusNeedData;
+ }
+
+ if (data[len] != ',')
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (missing ,)"));
+
+ *str = String(&data[0], &data[len]);
+
+ context.DropData(header_length + 1 + len + 1);
+
+ return StatusNewItem;
+}
+
+/**
+ * Writes data into a stream using the netstring format and returns bytes written.
+ *
+ * @param stream The stream.
+ * @param str The String that is to be written.
+ *
+ * @return The amount of bytes written.
+ */
+size_t NetString::WriteStringToStream(const Stream::Ptr& stream, const String& str)
+{
+ std::ostringstream msgbuf;
+ WriteStringToStream(msgbuf, str);
+
+ String msg = msgbuf.str();
+ stream->Write(msg.CStr(), msg.GetLength());
+ return msg.GetLength();
+}
+
+/**
+ * Reads data from a stream in netstring format.
+ *
+ * @param stream The stream to read from.
+ * @returns The String that has been read from the IOQueue.
+ * @exception invalid_argument The input stream is invalid.
+ * @see https://github.com/PeterScott/netstring-c/blob/master/netstring.c
+ */
+String NetString::ReadStringFromStream(const Shared<AsioTlsStream>::Ptr& stream,
+ ssize_t maxMessageLength)
+{
+ namespace asio = boost::asio;
+
+ size_t len = 0;
+ bool leadingZero = false;
+
+ for (uint_fast8_t readBytes = 0;; ++readBytes) {
+ char byte = 0;
+
+ {
+ asio::mutable_buffer byteBuf (&byte, 1);
+ asio::read(*stream, byteBuf);
+ }
+
+ if (isdigit(byte)) {
+ if (readBytes == 9) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Length specifier must not exceed 9 characters"));
+ }
+
+ if (leadingZero) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (leading zero)"));
+ }
+
+ len = len * 10u + size_t(byte - '0');
+
+ if (!readBytes && byte == '0') {
+ leadingZero = true;
+ }
+ } else if (byte == ':') {
+ if (!readBytes) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (no length specifier)"));
+ }
+
+ break;
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (missing :)"));
+ }
+ }
+
+ if (maxMessageLength >= 0 && len > maxMessageLength) {
+ std::stringstream errorMessage;
+ errorMessage << "Max data length exceeded: " << (maxMessageLength / 1024) << " KB";
+
+ BOOST_THROW_EXCEPTION(std::invalid_argument(errorMessage.str()));
+ }
+
+ String payload;
+
+ if (len) {
+ payload.Append(len, 0);
+
+ asio::mutable_buffer payloadBuf (&*payload.Begin(), payload.GetLength());
+ asio::read(*stream, payloadBuf);
+ }
+
+ char trailer = 0;
+
+ {
+ asio::mutable_buffer trailerBuf (&trailer, 1);
+ asio::read(*stream, trailerBuf);
+ }
+
+ if (trailer != ',') {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (missing ,)"));
+ }
+
+ return payload;
+}
+
+/**
+ * Reads data from a stream in netstring format.
+ *
+ * @param stream The stream to read from.
+ * @returns The String that has been read from the IOQueue.
+ * @exception invalid_argument The input stream is invalid.
+ * @see https://github.com/PeterScott/netstring-c/blob/master/netstring.c
+ */
+String NetString::ReadStringFromStream(const Shared<AsioTlsStream>::Ptr& stream,
+ boost::asio::yield_context yc, ssize_t maxMessageLength)
+{
+ namespace asio = boost::asio;
+
+ size_t len = 0;
+ bool leadingZero = false;
+
+ for (uint_fast8_t readBytes = 0;; ++readBytes) {
+ char byte = 0;
+
+ {
+ asio::mutable_buffer byteBuf (&byte, 1);
+ asio::async_read(*stream, byteBuf, yc);
+ }
+
+ if (isdigit(byte)) {
+ if (readBytes == 9) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Length specifier must not exceed 9 characters"));
+ }
+
+ if (leadingZero) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (leading zero)"));
+ }
+
+ len = len * 10u + size_t(byte - '0');
+
+ if (!readBytes && byte == '0') {
+ leadingZero = true;
+ }
+ } else if (byte == ':') {
+ if (!readBytes) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (no length specifier)"));
+ }
+
+ break;
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (missing :)"));
+ }
+ }
+
+ if (maxMessageLength >= 0 && len > maxMessageLength) {
+ std::stringstream errorMessage;
+ errorMessage << "Max data length exceeded: " << (maxMessageLength / 1024) << " KB";
+
+ BOOST_THROW_EXCEPTION(std::invalid_argument(errorMessage.str()));
+ }
+
+ String payload;
+
+ if (len) {
+ payload.Append(len, 0);
+
+ asio::mutable_buffer payloadBuf (&*payload.Begin(), payload.GetLength());
+ asio::async_read(*stream, payloadBuf, yc);
+ }
+
+ char trailer = 0;
+
+ {
+ asio::mutable_buffer trailerBuf (&trailer, 1);
+ asio::async_read(*stream, trailerBuf, yc);
+ }
+
+ if (trailer != ',') {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (missing ,)"));
+ }
+
+ return payload;
+}
+
+/**
+ * Writes data into a stream using the netstring format and returns bytes written.
+ *
+ * @param stream The stream.
+ * @param str The String that is to be written.
+ *
+ * @return The amount of bytes written.
+ */
+size_t NetString::WriteStringToStream(const Shared<AsioTlsStream>::Ptr& stream, const String& str)
+{
+ namespace asio = boost::asio;
+
+ std::ostringstream msgbuf;
+ WriteStringToStream(msgbuf, str);
+
+ String msg = msgbuf.str();
+ asio::const_buffer msgBuf (msg.CStr(), msg.GetLength());
+
+ asio::write(*stream, msgBuf);
+
+ return msg.GetLength();
+}
+
+/**
+ * Writes data into a stream using the netstring format and returns bytes written.
+ *
+ * @param stream The stream.
+ * @param str The String that is to be written.
+ *
+ * @return The amount of bytes written.
+ */
+size_t NetString::WriteStringToStream(const Shared<AsioTlsStream>::Ptr& stream, const String& str, boost::asio::yield_context yc)
+{
+ namespace asio = boost::asio;
+
+ std::ostringstream msgbuf;
+ WriteStringToStream(msgbuf, str);
+
+ String msg = msgbuf.str();
+ asio::const_buffer msgBuf (msg.CStr(), msg.GetLength());
+
+ asio::async_write(*stream, msgBuf, yc);
+
+ return msg.GetLength();
+}
+
+/**
+ * Writes data into a stream using the netstring format.
+ *
+ * @param stream The stream.
+ * @param str The String that is to be written.
+ */
+void NetString::WriteStringToStream(std::ostream& stream, const String& str)
+{
+ stream << str.GetLength() << ":" << str << ",";
+}
diff --git a/lib/base/netstring.hpp b/lib/base/netstring.hpp
new file mode 100644
index 0000000..e5ec051
--- /dev/null
+++ b/lib/base/netstring.hpp
@@ -0,0 +1,43 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NETSTRING_H
+#define NETSTRING_H
+
+#include "base/i2-base.hpp"
+#include "base/stream.hpp"
+#include "base/tlsstream.hpp"
+#include <memory>
+#include <boost/asio/spawn.hpp>
+
+namespace icinga
+{
+
+class String;
+
+/**
+ * Helper functions for reading/writing messages in the netstring format.
+ *
+ * @see https://cr.yp.to/proto/netstrings.txt
+ *
+ * @ingroup base
+ */
+class NetString
+{
+public:
+ static StreamReadStatus ReadStringFromStream(const Stream::Ptr& stream, String *message, StreamReadContext& context,
+ bool may_wait = false, ssize_t maxMessageLength = -1);
+ static String ReadStringFromStream(const Shared<AsioTlsStream>::Ptr& stream, ssize_t maxMessageLength = -1);
+ static String ReadStringFromStream(const Shared<AsioTlsStream>::Ptr& stream,
+ boost::asio::yield_context yc, ssize_t maxMessageLength = -1);
+ static size_t WriteStringToStream(const Stream::Ptr& stream, const String& message);
+ static size_t WriteStringToStream(const Shared<AsioTlsStream>::Ptr& stream, const String& message);
+ static size_t WriteStringToStream(const Shared<AsioTlsStream>::Ptr& stream, const String& message, boost::asio::yield_context yc);
+ static void WriteStringToStream(std::ostream& stream, const String& message);
+
+private:
+ NetString();
+};
+
+}
+
+#endif /* NETSTRING_H */
diff --git a/lib/base/networkstream.cpp b/lib/base/networkstream.cpp
new file mode 100644
index 0000000..57da507
--- /dev/null
+++ b/lib/base/networkstream.cpp
@@ -0,0 +1,81 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/networkstream.hpp"
+
+using namespace icinga;
+
+NetworkStream::NetworkStream(Socket::Ptr socket)
+ : m_Socket(std::move(socket)), m_Eof(false)
+{ }
+
+void NetworkStream::Close()
+{
+ Stream::Close();
+
+ m_Socket->Close();
+}
+
+/**
+ * Reads data from the stream.
+ *
+ * @param buffer The buffer where data should be stored. May be nullptr if you're
+ * not actually interested in the data.
+ * @param count The number of bytes to read from the queue.
+ * @returns The number of bytes actually read.
+ */
+size_t NetworkStream::Read(void *buffer, size_t count, bool allow_partial)
+{
+ size_t rc;
+
+ ASSERT(allow_partial);
+
+ if (m_Eof)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Tried to read from closed socket."));
+
+ try {
+ rc = m_Socket->Read(buffer, count);
+ } catch (...) {
+ m_Eof = true;
+
+ throw;
+ }
+
+ if (rc == 0)
+ m_Eof = true;
+
+ return rc;
+}
+
+/**
+ * Writes data to the stream.
+ *
+ * @param buffer The data that is to be written.
+ * @param count The number of bytes to write.
+ * @returns The number of bytes written
+ */
+void NetworkStream::Write(const void *buffer, size_t count)
+{
+ size_t rc;
+
+ if (m_Eof)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Tried to write to closed socket."));
+
+ try {
+ rc = m_Socket->Write(buffer, count);
+ } catch (...) {
+ m_Eof = true;
+
+ throw;
+ }
+
+ if (rc < count) {
+ m_Eof = true;
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Short write for socket."));
+ }
+}
+
+bool NetworkStream::IsEof() const
+{
+ return m_Eof;
+}
diff --git a/lib/base/networkstream.hpp b/lib/base/networkstream.hpp
new file mode 100644
index 0000000..453d7ad
--- /dev/null
+++ b/lib/base/networkstream.hpp
@@ -0,0 +1,39 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NETWORKSTREAM_H
+#define NETWORKSTREAM_H
+
+#include "base/i2-base.hpp"
+#include "base/stream.hpp"
+#include "base/socket.hpp"
+
+namespace icinga
+{
+
+/**
+ * A network stream. DEPRECATED - Use Boost ASIO instead.
+ *
+ * @ingroup base
+ */
+class NetworkStream final : public Stream
+{
+public:
+ DECLARE_PTR_TYPEDEFS(NetworkStream);
+
+ NetworkStream(Socket::Ptr socket);
+
+ size_t Read(void *buffer, size_t count, bool allow_partial = false) override;
+ void Write(const void *buffer, size_t count) override;
+
+ void Close() override;
+
+ bool IsEof() const override;
+
+private:
+ Socket::Ptr m_Socket;
+ bool m_Eof;
+};
+
+}
+
+#endif /* NETWORKSTREAM_H */
diff --git a/lib/base/number-script.cpp b/lib/base/number-script.cpp
new file mode 100644
index 0000000..0dcaca5
--- /dev/null
+++ b/lib/base/number-script.cpp
@@ -0,0 +1,25 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/number.hpp"
+#include "base/convert.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+
+using namespace icinga;
+
+static String NumberToString()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ return vframe->Self;
+}
+
+Object::Ptr Number::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "to_string", new Function("Number#to_string", NumberToString, {}, true) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/number.cpp b/lib/base/number.cpp
new file mode 100644
index 0000000..a336519
--- /dev/null
+++ b/lib/base/number.cpp
@@ -0,0 +1,9 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/number.hpp"
+#include "base/primitivetype.hpp"
+
+using namespace icinga;
+
+REGISTER_BUILTIN_TYPE(Number, Number::GetPrototype());
+
diff --git a/lib/base/number.hpp b/lib/base/number.hpp
new file mode 100644
index 0000000..dd5196f
--- /dev/null
+++ b/lib/base/number.hpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NUMBER_H
+#define NUMBER_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+
+namespace icinga {
+
+class Value;
+
+/**
+ * Number class.
+ */
+class Number
+{
+public:
+ static Object::Ptr GetPrototype();
+
+private:
+ Number();
+};
+
+}
+
+#endif /* NUMBER_H */
diff --git a/lib/base/object-packer.cpp b/lib/base/object-packer.cpp
new file mode 100644
index 0000000..123ddad
--- /dev/null
+++ b/lib/base/object-packer.cpp
@@ -0,0 +1,246 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/object-packer.hpp"
+#include "base/debug.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include <algorithm>
+#include <climits>
+#include <cstdint>
+#include <string>
+#include <utility>
+
+using namespace icinga;
+
+union EndiannessDetector
+{
+ EndiannessDetector()
+ {
+ i = 1;
+ }
+
+ int i;
+ char buf[sizeof(int)];
+};
+
+static const EndiannessDetector l_EndiannessDetector;
+
+// Assumption: The compiler will optimize (away) if/else statements using this.
+#define MACHINE_LITTLE_ENDIAN (l_EndiannessDetector.buf[0])
+
+static void PackAny(const Value& value, std::string& builder);
+
+/**
+ * std::swap() seems not to work
+ */
+static inline void SwapBytes(char& a, char& b)
+{
+ char c = a;
+ a = b;
+ b = c;
+}
+
+#if CHAR_MIN != 0
+union CharU2SConverter
+{
+ CharU2SConverter()
+ {
+ s = 0;
+ }
+
+ unsigned char u;
+ signed char s;
+};
+#endif
+
+/**
+ * Avoid implementation-defined overflows during unsigned to signed casts
+ */
+static inline char UIntToByte(unsigned i)
+{
+#if CHAR_MIN == 0
+ return i;
+#else
+ CharU2SConverter converter;
+
+ converter.u = i;
+ return converter.s;
+#endif
+}
+
+/**
+ * Append the given int as big-endian 64-bit unsigned int
+ */
+static inline void PackUInt64BE(uint_least64_t i, std::string& builder)
+{
+ char buf[8] = {
+ UIntToByte(i >> 56u),
+ UIntToByte((i >> 48u) & 255u),
+ UIntToByte((i >> 40u) & 255u),
+ UIntToByte((i >> 32u) & 255u),
+ UIntToByte((i >> 24u) & 255u),
+ UIntToByte((i >> 16u) & 255u),
+ UIntToByte((i >> 8u) & 255u),
+ UIntToByte(i & 255u)
+ };
+
+ builder.append((char*)buf, 8);
+}
+
+union Double2BytesConverter
+{
+ Double2BytesConverter()
+ {
+ buf[0] = 0;
+ buf[1] = 0;
+ buf[2] = 0;
+ buf[3] = 0;
+ buf[4] = 0;
+ buf[5] = 0;
+ buf[6] = 0;
+ buf[7] = 0;
+ }
+
+ double f;
+ char buf[8];
+};
+
+/**
+ * Append the given double as big-endian IEEE 754 binary64
+ */
+static inline void PackFloat64BE(double f, std::string& builder)
+{
+ Double2BytesConverter converter;
+
+ converter.f = f;
+
+ if (MACHINE_LITTLE_ENDIAN) {
+ SwapBytes(converter.buf[0], converter.buf[7]);
+ SwapBytes(converter.buf[1], converter.buf[6]);
+ SwapBytes(converter.buf[2], converter.buf[5]);
+ SwapBytes(converter.buf[3], converter.buf[4]);
+ }
+
+ builder.append((char*)converter.buf, 8);
+}
+
+/**
+ * Append the given string's length (BE uint64) and the string itself
+ */
+static inline void PackString(const String& string, std::string& builder)
+{
+ PackUInt64BE(string.GetLength(), builder);
+ builder += string.GetData();
+}
+
+/**
+ * Append the given array
+ */
+static inline void PackArray(const Array::Ptr& arr, std::string& builder)
+{
+ ObjectLock olock(arr);
+
+ builder += '\5';
+ PackUInt64BE(arr->GetLength(), builder);
+
+ for (const Value& value : arr) {
+ PackAny(value, builder);
+ }
+}
+
+/**
+ * Append the given dictionary
+ */
+static inline void PackDictionary(const Dictionary::Ptr& dict, std::string& builder)
+{
+ ObjectLock olock(dict);
+
+ builder += '\6';
+ PackUInt64BE(dict->GetLength(), builder);
+
+ for (const Dictionary::Pair& kv : dict) {
+ PackString(kv.first, builder);
+ PackAny(kv.second, builder);
+ }
+}
+
+/**
+ * Append any JSON-encodable value
+ */
+static void PackAny(const Value& value, std::string& builder)
+{
+ switch (value.GetType()) {
+ case ValueString:
+ builder += '\4';
+ PackString(value.Get<String>(), builder);
+ break;
+
+ case ValueNumber:
+ builder += '\3';
+ PackFloat64BE(value.Get<double>(), builder);
+ break;
+
+ case ValueBoolean:
+ builder += (value.ToBool() ? '\2' : '\1');
+ break;
+
+ case ValueEmpty:
+ builder += '\0';
+ break;
+
+ case ValueObject:
+ {
+ const Object::Ptr& obj = value.Get<Object::Ptr>();
+
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>(obj);
+ if (dict) {
+ PackDictionary(dict, builder);
+ break;
+ }
+
+ Array::Ptr arr = dynamic_pointer_cast<Array>(obj);
+ if (arr) {
+ PackArray(arr, builder);
+ break;
+ }
+ }
+
+ builder += '\0';
+ break;
+
+ default:
+ VERIFY(!"Invalid variant type.");
+ }
+}
+
+/**
+ * Pack any JSON-encodable value to a BSON-similar structure suitable for consistent hashing
+ *
+ * Spec:
+ * null: 0x00
+ * false: 0x01
+ * true: 0x02
+ * number: 0x03 (ieee754_binary64_bigendian)payload
+ * string: 0x04 (uint64_bigendian)payload.length (char[])payload
+ * array: 0x05 (uint64_bigendian)payload.length (any[])payload
+ * object: 0x06 (uint64_bigendian)payload.length (keyvalue[])payload.sort()
+ *
+ * any: null|false|true|number|string|array|object
+ * keyvalue: (uint64_bigendian)key.length (char[])key (any)value
+ *
+ * Assumptions:
+ * - double is IEEE 754 binary64
+ * - all int types (signed and unsigned) and all float types share the same endianness
+ * - char is exactly 8 bits wide and one char is exactly one byte affected by the machine endianness
+ * - all input strings, arrays and dictionaries are at most 2^64-1 long
+ *
+ * If not, this function will silently produce invalid results.
+ */
+String icinga::PackObject(const Value& value)
+{
+ std::string builder;
+ PackAny(value, builder);
+
+ return std::move(builder);
+}
diff --git a/lib/base/object-packer.hpp b/lib/base/object-packer.hpp
new file mode 100644
index 0000000..00f7b99
--- /dev/null
+++ b/lib/base/object-packer.hpp
@@ -0,0 +1,18 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECT_PACKER
+#define OBJECT_PACKER
+
+#include "base/i2-base.hpp"
+
+namespace icinga
+{
+
+class String;
+class Value;
+
+String PackObject(const Value& value);
+
+}
+
+#endif /* OBJECT_PACKER */
diff --git a/lib/base/object-script.cpp b/lib/base/object-script.cpp
new file mode 100644
index 0000000..fff7df0
--- /dev/null
+++ b/lib/base/object-script.cpp
@@ -0,0 +1,45 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/object.hpp"
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+
+using namespace icinga;
+
+static String ObjectToString()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Object::Ptr self = static_cast<Object::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->ToString();
+}
+
+static void ObjectNotifyAttribute(const String& attribute)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Object::Ptr self = static_cast<Object::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->NotifyField(self->GetReflectionType()->GetFieldId(attribute));
+}
+
+static Object::Ptr ObjectClone()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Object::Ptr self = static_cast<Object::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Clone();
+}
+
+Object::Ptr Object::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "to_string", new Function("Object#to_string", ObjectToString, {}, true) },
+ { "notify_attribute", new Function("Object#notify_attribute", ObjectNotifyAttribute, { "attribute" }, false) },
+ { "clone", new Function("Object#clone", ObjectClone, {}, true) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/object.cpp b/lib/base/object.cpp
new file mode 100644
index 0000000..92a43b9
--- /dev/null
+++ b/lib/base/object.cpp
@@ -0,0 +1,275 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/object.hpp"
+#include "base/value.hpp"
+#include "base/dictionary.hpp"
+#include "base/primitivetype.hpp"
+#include "base/utility.hpp"
+#include "base/timer.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include <boost/lexical_cast.hpp>
+#include <boost/thread/recursive_mutex.hpp>
+#include <thread>
+
+using namespace icinga;
+
+DEFINE_TYPE_INSTANCE(Object);
+
+#ifdef I2_LEAK_DEBUG
+static std::mutex l_ObjectCountLock;
+static std::map<String, int> l_ObjectCounts;
+static Timer::Ptr l_ObjectCountTimer;
+#endif /* I2_LEAK_DEBUG */
+
+/**
+ * Constructor for the Object class.
+ */
+Object::Object()
+{
+ m_References.store(0);
+
+#ifdef I2_DEBUG
+ m_LockOwner.store(decltype(m_LockOwner.load())());
+#endif /* I2_DEBUG */
+}
+
+/**
+ * Destructor for the Object class.
+ */
+Object::~Object()
+{
+}
+
+/**
+ * Returns a string representation for the object.
+ */
+String Object::ToString() const
+{
+ return "Object of type '" + GetReflectionType()->GetName() + "'";
+}
+
+#ifdef I2_DEBUG
+/**
+ * Checks if the calling thread owns the lock on this object.
+ *
+ * @returns True if the calling thread owns the lock, false otherwise.
+ */
+bool Object::OwnsLock() const
+{
+ return m_LockOwner.load() == std::this_thread::get_id();
+}
+#endif /* I2_DEBUG */
+
+void Object::SetField(int id, const Value&, bool, const Value&)
+{
+ if (id == 0)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Type field cannot be set."));
+ else
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid field ID."));
+}
+
+Value Object::GetField(int id) const
+{
+ if (id == 0)
+ return GetReflectionType()->GetName();
+ else
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid field ID."));
+}
+
+bool Object::HasOwnField(const String& field) const
+{
+ Type::Ptr type = GetReflectionType();
+
+ if (!type)
+ return false;
+
+ return type->GetFieldId(field) != -1;
+}
+
+bool Object::GetOwnField(const String& field, Value *result) const
+{
+ Type::Ptr type = GetReflectionType();
+
+ if (!type)
+ return false;
+
+ int tid = type->GetFieldId(field);
+
+ if (tid == -1)
+ return false;
+
+ *result = GetField(tid);
+ return true;
+}
+
+Value Object::GetFieldByName(const String& field, bool sandboxed, const DebugInfo& debugInfo) const
+{
+ Type::Ptr type = GetReflectionType();
+
+ if (!type)
+ return Empty;
+
+ int fid = type->GetFieldId(field);
+
+ if (fid == -1)
+ return GetPrototypeField(const_cast<Object *>(this), field, true, debugInfo);
+
+ if (sandboxed) {
+ Field fieldInfo = type->GetFieldInfo(fid);
+
+ if (fieldInfo.Attributes & FANoUserView)
+ BOOST_THROW_EXCEPTION(ScriptError("Accessing the field '" + field + "' for type '" + type->GetName() + "' is not allowed in sandbox mode.", debugInfo));
+ }
+
+ return GetField(fid);
+}
+
+void Object::SetFieldByName(const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo)
+{
+ Type::Ptr type = GetReflectionType();
+
+ if (!type)
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot set field on object.", debugInfo));
+
+ int fid = type->GetFieldId(field);
+
+ if (fid == -1)
+ BOOST_THROW_EXCEPTION(ScriptError("Attribute '" + field + "' does not exist.", debugInfo));
+
+ try {
+ SetField(fid, value);
+ } catch (const boost::bad_lexical_cast&) {
+ Field fieldInfo = type->GetFieldInfo(fid);
+ Type::Ptr ftype = Type::GetByName(fieldInfo.TypeName);
+ BOOST_THROW_EXCEPTION(ScriptError("Attribute '" + field + "' cannot be set to value of type '" + value.GetTypeName() + "', expected '" + ftype->GetName() + "'", debugInfo));
+ } catch (const std::bad_cast&) {
+ Field fieldInfo = type->GetFieldInfo(fid);
+ Type::Ptr ftype = Type::GetByName(fieldInfo.TypeName);
+ BOOST_THROW_EXCEPTION(ScriptError("Attribute '" + field + "' cannot be set to value of type '" + value.GetTypeName() + "', expected '" + ftype->GetName() + "'", debugInfo));
+ }
+}
+
+void Object::Validate(int types, const ValidationUtils& utils)
+{
+ /* Nothing to do here. */
+}
+
+void Object::ValidateField(int id, const Lazy<Value>& lvalue, const ValidationUtils& utils)
+{
+ /* Nothing to do here. */
+}
+
+void Object::NotifyField(int id, const Value& cookie)
+{
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid field ID."));
+}
+
+Object::Ptr Object::NavigateField(int id) const
+{
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid field ID."));
+}
+
+Object::Ptr Object::Clone() const
+{
+ BOOST_THROW_EXCEPTION(std::runtime_error("Object cannot be cloned."));
+}
+
+Type::Ptr Object::GetReflectionType() const
+{
+ return Object::TypeInstance;
+}
+
+Value icinga::GetPrototypeField(const Value& context, const String& field, bool not_found_error, const DebugInfo& debugInfo)
+{
+ Type::Ptr ctype = context.GetReflectionType();
+ Type::Ptr type = ctype;
+
+ do {
+ Object::Ptr object = type->GetPrototype();
+
+ if (object && object->HasOwnField(field))
+ return object->GetFieldByName(field, false, debugInfo);
+
+ type = type->GetBaseType();
+ } while (type);
+
+ if (not_found_error)
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid field access (for value of type '" + ctype->GetName() + "'): '" + field + "'", debugInfo));
+ else
+ return Empty;
+}
+
+#ifdef I2_LEAK_DEBUG
+void icinga::TypeAddObject(Object *object)
+{
+ std::unique_lock<std::mutex> lock(l_ObjectCountLock);
+ String typeName = Utility::GetTypeName(typeid(*object));
+ l_ObjectCounts[typeName]++;
+}
+
+void icinga::TypeRemoveObject(Object *object)
+{
+ std::unique_lock<std::mutex> lock(l_ObjectCountLock);
+ String typeName = Utility::GetTypeName(typeid(*object));
+ l_ObjectCounts[typeName]--;
+}
+
+static void TypeInfoTimerHandler()
+{
+ std::unique_lock<std::mutex> lock(l_ObjectCountLock);
+
+ typedef std::map<String, int>::value_type kv_pair;
+ for (kv_pair& kv : l_ObjectCounts) {
+ if (kv.second == 0)
+ continue;
+
+ Log(LogInformation, "TypeInfo")
+ << kv.second << " " << kv.first << " objects";
+
+ kv.second = 0;
+ }
+}
+
+INITIALIZE_ONCE([]() {
+ l_ObjectCountTimer = Timer::Create();
+ l_ObjectCountTimer->SetInterval(10);
+ l_ObjectCountTimer->OnTimerExpired.connect([](const Timer * const&) { TypeInfoTimerHandler(); });
+ l_ObjectCountTimer->Start();
+});
+#endif /* I2_LEAK_DEBUG */
+
+void icinga::intrusive_ptr_add_ref(Object *object)
+{
+#ifdef I2_LEAK_DEBUG
+ if (object->m_References.fetch_add(1) == 0u)
+ TypeAddObject(object);
+#else /* I2_LEAK_DEBUG */
+ object->m_References.fetch_add(1);
+#endif /* I2_LEAK_DEBUG */
+}
+
+void icinga::intrusive_ptr_release(Object *object)
+{
+ auto previous (object->m_References.fetch_sub(1));
+
+ if (previous == 1u) {
+#ifdef I2_LEAK_DEBUG
+ TypeRemoveObject(object);
+#endif /* I2_LEAK_DEBUG */
+
+ delete object;
+ }
+}
+
+void icinga::DefaultObjectFactoryCheckArgs(const std::vector<Value>& args)
+{
+ if (!args.empty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Constructor does not take any arguments."));
+}
+
+void icinga::RequireNotNullInternal(const intrusive_ptr<Object>& object, const char *description)
+{
+ if (!object)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Pointer must not be null: " + String(description)));
+}
diff --git a/lib/base/object.hpp b/lib/base/object.hpp
new file mode 100644
index 0000000..5a90cfa
--- /dev/null
+++ b/lib/base/object.hpp
@@ -0,0 +1,225 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECT_H
+#define OBJECT_H
+
+#include "base/i2-base.hpp"
+#include "base/debug.hpp"
+#include <boost/smart_ptr/intrusive_ptr.hpp>
+#include <atomic>
+#include <cstddef>
+#include <cstdint>
+#include <mutex>
+#include <thread>
+#include <vector>
+
+using boost::intrusive_ptr;
+using boost::dynamic_pointer_cast;
+using boost::static_pointer_cast;
+
+namespace icinga
+{
+
+class Value;
+class Object;
+class Type;
+class String;
+struct DebugInfo;
+class ValidationUtils;
+
+extern Value Empty;
+
+#define DECLARE_PTR_TYPEDEFS(klass) \
+ typedef intrusive_ptr<klass> Ptr
+
+#define IMPL_TYPE_LOOKUP_SUPER() \
+
+#define IMPL_TYPE_LOOKUP() \
+ static intrusive_ptr<Type> TypeInstance; \
+ virtual intrusive_ptr<Type> GetReflectionType() const override \
+ { \
+ return TypeInstance; \
+ }
+
+#define DECLARE_OBJECT(klass) \
+ DECLARE_PTR_TYPEDEFS(klass); \
+ IMPL_TYPE_LOOKUP();
+
+#define REQUIRE_NOT_NULL(ptr) RequireNotNullInternal(ptr, #ptr)
+
+void RequireNotNullInternal(const intrusive_ptr<Object>& object, const char *description);
+
+void DefaultObjectFactoryCheckArgs(const std::vector<Value>& args);
+
+template<typename T>
+intrusive_ptr<Object> DefaultObjectFactory(const std::vector<Value>& args)
+{
+ DefaultObjectFactoryCheckArgs(args);
+
+ return new T();
+}
+
+template<typename T>
+intrusive_ptr<Object> DefaultObjectFactoryVA(const std::vector<Value>& args)
+{
+ return new T(args);
+}
+
+typedef intrusive_ptr<Object> (*ObjectFactory)(const std::vector<Value>&);
+
+template<typename T, bool VA>
+struct TypeHelper
+{
+};
+
+template<typename T>
+struct TypeHelper<T, false>
+{
+ static ObjectFactory GetFactory()
+ {
+ return DefaultObjectFactory<T>;
+ }
+};
+
+template<typename T>
+struct TypeHelper<T, true>
+{
+ static ObjectFactory GetFactory()
+ {
+ return DefaultObjectFactoryVA<T>;
+ }
+};
+
+template<typename T>
+struct Lazy
+{
+ using Accessor = std::function<T ()>;
+
+ explicit Lazy(T value)
+ : m_Cached(true), m_Value(value)
+ { }
+
+ explicit Lazy(Accessor accessor)
+ : m_Accessor(accessor)
+ { }
+
+ template<typename U>
+ explicit Lazy(const Lazy<U>& other)
+ {
+ if (other.m_Cached) {
+ m_Accessor = Accessor();
+ m_Value = static_cast<T>(other.m_Value);
+ m_Cached = true;
+ } else {
+ auto accessor = other.m_Accessor;
+ m_Accessor = [accessor]() { return static_cast<T>(accessor()); };
+ m_Cached = false;
+ }
+ }
+
+ template<typename U>
+ operator Lazy<U>() const
+ {
+ if (m_Cached)
+ return Lazy<U>(static_cast<U>(m_Value));
+ else {
+ Accessor accessor = m_Accessor;
+ return Lazy<U>(static_cast<typename Lazy<U>::Accessor>([accessor]() { return static_cast<U>(accessor()); }));
+ }
+ }
+
+ const T& operator()() const
+ {
+ if (!m_Cached) {
+ m_Value = m_Accessor();
+ m_Cached = true;
+ }
+
+ return m_Value;
+ }
+
+private:
+ Accessor m_Accessor;
+ mutable bool m_Cached{false};
+ mutable T m_Value;
+
+ template<typename U>
+ friend struct Lazy;
+};
+
+/**
+ * Base class for all heap-allocated objects. At least one of its methods
+ * has to be virtual for RTTI to work.
+ *
+ * @ingroup base
+ */
+class Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Object);
+
+ Object();
+ virtual ~Object();
+
+ virtual String ToString() const;
+
+ virtual intrusive_ptr<Type> GetReflectionType() const;
+
+ virtual void Validate(int types, const ValidationUtils& utils);
+
+ virtual void SetField(int id, const Value& value, bool suppress_events = false, const Value& cookie = Empty);
+ virtual Value GetField(int id) const;
+ virtual Value GetFieldByName(const String& field, bool sandboxed, const DebugInfo& debugInfo) const;
+ virtual void SetFieldByName(const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo);
+ virtual bool HasOwnField(const String& field) const;
+ virtual bool GetOwnField(const String& field, Value *result) const;
+ virtual void ValidateField(int id, const Lazy<Value>& lvalue, const ValidationUtils& utils);
+ virtual void NotifyField(int id, const Value& cookie = Empty);
+ virtual Object::Ptr NavigateField(int id) const;
+
+#ifdef I2_DEBUG
+ bool OwnsLock() const;
+#endif /* I2_DEBUG */
+
+ static Object::Ptr GetPrototype();
+
+ virtual Object::Ptr Clone() const;
+
+ static intrusive_ptr<Type> TypeInstance;
+
+private:
+ Object(const Object& other) = delete;
+ Object& operator=(const Object& rhs) = delete;
+
+ std::atomic<uint_fast64_t> m_References;
+ mutable std::recursive_mutex m_Mutex;
+
+#ifdef I2_DEBUG
+ mutable std::atomic<std::thread::id> m_LockOwner;
+ mutable size_t m_LockCount = 0;
+#endif /* I2_DEBUG */
+
+ friend struct ObjectLock;
+
+ friend void intrusive_ptr_add_ref(Object *object);
+ friend void intrusive_ptr_release(Object *object);
+};
+
+Value GetPrototypeField(const Value& context, const String& field, bool not_found_error, const DebugInfo& debugInfo);
+
+void TypeAddObject(Object *object);
+void TypeRemoveObject(Object *object);
+
+void intrusive_ptr_add_ref(Object *object);
+void intrusive_ptr_release(Object *object);
+
+template<typename T>
+class ObjectImpl
+{
+};
+
+}
+
+#endif /* OBJECT_H */
+
+#include "base/type.hpp"
diff --git a/lib/base/objectlock.cpp b/lib/base/objectlock.cpp
new file mode 100644
index 0000000..fc0c7c6
--- /dev/null
+++ b/lib/base/objectlock.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/objectlock.hpp"
+#include <thread>
+
+using namespace icinga;
+
+#define I2MUTEX_UNLOCKED 0
+#define I2MUTEX_LOCKED 1
+
+ObjectLock::~ObjectLock()
+{
+ Unlock();
+}
+
+ObjectLock::ObjectLock(const Object::Ptr& object)
+ : ObjectLock(object.get())
+{
+}
+
+ObjectLock::ObjectLock(const Object *object)
+ : m_Object(object), m_Locked(false)
+{
+ if (m_Object)
+ Lock();
+}
+
+void ObjectLock::Lock()
+{
+ ASSERT(!m_Locked && m_Object);
+
+ m_Object->m_Mutex.lock();
+
+ m_Locked = true;
+
+#ifdef I2_DEBUG
+ if (++m_Object->m_LockCount == 1u) {
+ m_Object->m_LockOwner.store(std::this_thread::get_id());
+ }
+#endif /* I2_DEBUG */
+}
+
+void ObjectLock::Unlock()
+{
+#ifdef I2_DEBUG
+ if (m_Locked && !--m_Object->m_LockCount) {
+ m_Object->m_LockOwner.store(decltype(m_Object->m_LockOwner.load())());
+ }
+#endif /* I2_DEBUG */
+
+ if (m_Locked) {
+ m_Object->m_Mutex.unlock();
+ m_Locked = false;
+ }
+}
diff --git a/lib/base/objectlock.hpp b/lib/base/objectlock.hpp
new file mode 100644
index 0000000..8e98641
--- /dev/null
+++ b/lib/base/objectlock.hpp
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTLOCK_H
+#define OBJECTLOCK_H
+
+#include "base/object.hpp"
+
+namespace icinga
+{
+
+/**
+ * A scoped lock for Objects.
+ */
+struct ObjectLock
+{
+public:
+ ObjectLock(const Object::Ptr& object);
+ ObjectLock(const Object *object);
+
+ ObjectLock(const ObjectLock&) = delete;
+ ObjectLock& operator=(const ObjectLock&) = delete;
+
+ ~ObjectLock();
+
+ void Lock();
+ void Unlock();
+
+private:
+ const Object *m_Object{nullptr};
+ bool m_Locked{false};
+};
+
+}
+
+#endif /* OBJECTLOCK_H */
diff --git a/lib/base/objecttype.cpp b/lib/base/objecttype.cpp
new file mode 100644
index 0000000..b871555
--- /dev/null
+++ b/lib/base/objecttype.cpp
@@ -0,0 +1,57 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/objecttype.hpp"
+#include "base/initialize.hpp"
+#include <boost/throw_exception.hpp>
+
+using namespace icinga;
+
+/* Ensure that the priority is lower than the basic namespace initialization in scriptframe.cpp. */
+INITIALIZE_ONCE_WITH_PRIORITY([]() {
+ Type::Ptr type = new ObjectType();
+ type->SetPrototype(Object::GetPrototype());
+ Type::Register(type);
+ Object::TypeInstance = type;
+}, InitializePriority::RegisterObjectType);
+
+String ObjectType::GetName() const
+{
+ return "Object";
+}
+
+Type::Ptr ObjectType::GetBaseType() const
+{
+ return nullptr;
+}
+
+int ObjectType::GetAttributes() const
+{
+ return 0;
+}
+
+int ObjectType::GetFieldId(const String& name) const
+{
+ if (name == "type")
+ return 0;
+ else
+ return -1;
+}
+
+Field ObjectType::GetFieldInfo(int id) const
+{
+ if (id == 0)
+ return {1, "String", "type", nullptr, nullptr, 0, 0};
+ else
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid field ID."));
+}
+
+int ObjectType::GetFieldCount() const
+{
+ return 1;
+}
+
+ObjectFactory ObjectType::GetFactory() const
+{
+ return DefaultObjectFactory<Object>;
+}
+
diff --git a/lib/base/objecttype.hpp b/lib/base/objecttype.hpp
new file mode 100644
index 0000000..0db715e
--- /dev/null
+++ b/lib/base/objecttype.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTTYPE_H
+#define OBJECTTYPE_H
+
+#include "base/i2-base.hpp"
+#include "base/type.hpp"
+#include "base/initialize.hpp"
+
+namespace icinga
+{
+
+class ObjectType final : public Type
+{
+public:
+ String GetName() const override;
+ Type::Ptr GetBaseType() const override;
+ int GetAttributes() const override;
+ int GetFieldId(const String& name) const override;
+ Field GetFieldInfo(int id) const override;
+ int GetFieldCount() const override;
+
+protected:
+ ObjectFactory GetFactory() const override;
+};
+
+}
+
+#endif /* OBJECTTYPE_H */
diff --git a/lib/base/perfdatavalue.cpp b/lib/base/perfdatavalue.cpp
new file mode 100644
index 0000000..60a39e4
--- /dev/null
+++ b/lib/base/perfdatavalue.cpp
@@ -0,0 +1,395 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/perfdatavalue.hpp"
+#include "base/perfdatavalue-ti.cpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include "base/function.hpp"
+#include <boost/algorithm/string.hpp>
+#include <cmath>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(PerfdataValue);
+REGISTER_FUNCTION(System, parse_performance_data, PerfdataValue::Parse, "perfdata");
+
+struct UoM
+{
+ double Factor;
+ const char* Out;
+};
+
+typedef std::unordered_map<std::string /* in */, UoM> UoMs;
+typedef std::unordered_multimap<std::string /* in */, UoM> DupUoMs;
+
+static const UoMs l_CsUoMs (([]() -> UoMs {
+ DupUoMs uoms ({
+ // Misc:
+ { "", { 1, "" } },
+ { "%", { 1, "percent" } },
+ { "c", { 1, "" } },
+ { "C", { 1, "degrees-celsius" } }
+ });
+
+ {
+ // Data (rate):
+
+ struct { const char* Char; int Power; } prefixes[] = {
+ { "k", 1 }, { "K", 1 },
+ { "m", 2 }, { "M", 2 },
+ { "g", 3 }, { "G", 3 },
+ { "t", 4 }, { "T", 4 },
+ { "p", 5 }, { "P", 5 },
+ { "e", 6 }, { "E", 6 },
+ { "z", 7 }, { "Z", 7 },
+ { "y", 8 }, { "Y", 8 }
+ };
+
+ struct { const char* Char; double Factor; } siIecs[] = {
+ { "", 1000 },
+ { "i", 1024 }, { "I", 1024 }
+ };
+
+ struct { const char *In, *Out; } bases[] = {
+ { "b", "bits" },
+ { "B", "bytes" }
+ };
+
+ for (auto base : bases) {
+ uoms.emplace(base.In, UoM{1, base.Out});
+ }
+
+ for (auto prefix : prefixes) {
+ for (auto siIec : siIecs) {
+ auto factor (pow(siIec.Factor, prefix.Power));
+
+ for (auto base : bases) {
+ uoms.emplace(
+ std::string(prefix.Char) + siIec.Char + base.In,
+ UoM{factor, base.Out}
+ );
+ }
+ }
+ }
+ }
+
+ {
+ // Energy:
+
+ struct { const char* Char; int Power; } prefixes[] = {
+ { "n", -3 }, { "N", -3 },
+ { "u", -2 }, { "U", -2 },
+ { "m", -1 },
+ { "", 0 },
+ { "k", 1 }, { "K", 1 },
+ { "M", 2 },
+ { "g", 3 }, { "G", 3 },
+ { "t", 4 }, { "T", 4 },
+ { "p", 5 }, { "P", 5 },
+ { "e", 6 }, { "E", 6 },
+ { "z", 7 }, { "Z", 7 },
+ { "y", 8 }, { "Y", 8 }
+ };
+
+ {
+ struct { const char* Ins[2]; const char* Out; } bases[] = {
+ { { "a", "A" }, "amperes" },
+ { { "o", "O" }, "ohms" },
+ { { "v", "V" }, "volts" },
+ { { "w", "W" }, "watts" }
+ };
+
+ for (auto prefix : prefixes) {
+ auto factor (pow(1000.0, prefix.Power));
+
+ for (auto base : bases) {
+ for (auto b : base.Ins) {
+ uoms.emplace(std::string(prefix.Char) + b, UoM{factor, base.Out});
+ }
+ }
+ }
+ }
+
+ struct { const char* Char; double Factor; } suffixes[] = {
+ { "s", 1 }, { "S", 1 },
+ { "m", 60 }, { "M", 60 },
+ { "h", 60 * 60 }, { "H", 60 * 60 }
+ };
+
+ struct { const char* Ins[2]; double Factor; const char* Out; } bases[] = {
+ { { "a", "A" }, 1, "ampere-seconds" },
+ { { "w", "W" }, 60 * 60, "watt-hours" }
+ };
+
+ for (auto prefix : prefixes) {
+ auto factor (pow(1000.0, prefix.Power));
+
+ for (auto suffix : suffixes) {
+ auto timeFactor (factor * suffix.Factor);
+
+ for (auto& base : bases) {
+ auto baseFactor (timeFactor / base.Factor);
+
+ for (auto b : base.Ins) {
+ uoms.emplace(
+ std::string(prefix.Char) + b + suffix.Char,
+ UoM{baseFactor, base.Out}
+ );
+ }
+ }
+ }
+ }
+ }
+
+ UoMs uniqUoms;
+
+ for (auto& uom : uoms) {
+ if (!uniqUoms.emplace(uom).second) {
+ throw std::logic_error("Duplicate case-sensitive UoM detected: " + uom.first);
+ }
+ }
+
+ return uniqUoms;
+})());
+
+static const UoMs l_CiUoMs (([]() -> UoMs {
+ DupUoMs uoms ({
+ // Time:
+ { "ns", { 1.0 / 1000 / 1000 / 1000, "seconds" } },
+ { "us", { 1.0 / 1000 / 1000, "seconds" } },
+ { "ms", { 1.0 / 1000, "seconds" } },
+ { "s", { 1, "seconds" } },
+ { "m", { 60, "seconds" } },
+ { "h", { 60 * 60, "seconds" } },
+ { "d", { 60 * 60 * 24, "seconds" } },
+
+ // Mass:
+ { "ng", { 1.0 / 1000 / 1000 / 1000, "grams" } },
+ { "ug", { 1.0 / 1000 / 1000, "grams" } },
+ { "mg", { 1.0 / 1000, "grams" } },
+ { "g", { 1, "grams" } },
+ { "kg", { 1000, "grams" } },
+ { "t", { 1000 * 1000, "grams" } },
+
+ // Volume:
+ { "ml", { 1.0 / 1000, "liters" } },
+ { "l", { 1, "liters" } },
+ { "hl", { 100, "liters" } },
+
+ // Misc:
+ { "packets", { 1, "packets" } },
+ { "lm", { 1, "lumens" } },
+ { "dbm", { 1, "decibel-milliwatts" } },
+ { "f", { 1, "degrees-fahrenheit" } },
+ { "k", { 1, "degrees-kelvin" } }
+ });
+
+ UoMs uniqUoms;
+
+ for (auto& uom : uoms) {
+ if (!uniqUoms.emplace(uom).second) {
+ throw std::logic_error("Duplicate case-insensitive UoM detected: " + uom.first);
+ }
+ }
+
+ for (auto& uom : l_CsUoMs) {
+ auto input (uom.first);
+ boost::algorithm::to_lower(input);
+
+ auto pos (uoms.find(input));
+
+ if (pos != uoms.end()) {
+ throw std::logic_error("Duplicate case-sensitive/case-insensitive UoM detected: " + pos->first);
+ }
+ }
+
+ return uniqUoms;
+})());
+
+PerfdataValue::PerfdataValue(const String& label, double value, bool counter,
+ const String& unit, const Value& warn, const Value& crit, const Value& min,
+ const Value& max)
+{
+ SetLabel(label, true);
+ SetValue(value, true);
+ SetCounter(counter, true);
+ SetUnit(unit, true);
+ SetWarn(warn, true);
+ SetCrit(crit, true);
+ SetMin(min, true);
+ SetMax(max, true);
+}
+
+PerfdataValue::Ptr PerfdataValue::Parse(const String& perfdata)
+{
+ size_t eqp = perfdata.FindLastOf('=');
+
+ if (eqp == String::NPos)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid performance data value: " + perfdata));
+
+ String label = perfdata.SubStr(0, eqp);
+
+ if (label.GetLength() > 2 && label[0] == '\'' && label[label.GetLength() - 1] == '\'')
+ label = label.SubStr(1, label.GetLength() - 2);
+
+ size_t spq = perfdata.FindFirstOf(' ', eqp);
+
+ if (spq == String::NPos)
+ spq = perfdata.GetLength();
+
+ String valueStr = perfdata.SubStr(eqp + 1, spq - eqp - 1);
+ std::vector<String> tokens = valueStr.Split(";");
+
+ if (valueStr.FindFirstOf(',') != String::NPos || tokens.empty()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid performance data value: " + perfdata));
+ }
+
+ // Find the position where to split value and unit. Possible values of tokens[0] include:
+ // "1000", "1.0", "1.", "-.1", "+1", "1e10", "1GB", "1e10GB", "1e10EB", "1E10EB", "1.5GB", "1.GB", "+1.E-1EW"
+ // Consider everything up to and including the last digit or decimal point as part of the value.
+ size_t pos = tokens[0].FindLastOf("0123456789.");
+ if (pos != String::NPos) {
+ pos++;
+ }
+
+ double value = Convert::ToDouble(tokens[0].SubStr(0, pos));
+
+ bool counter = false;
+ String unit;
+ Value warn, crit, min, max;
+
+ if (pos != String::NPos)
+ unit = tokens[0].SubStr(pos, String::NPos);
+
+ double base;
+
+ {
+ auto uom (l_CsUoMs.find(unit.GetData()));
+
+ if (uom == l_CsUoMs.end()) {
+ auto ciUnit (unit.ToLower());
+ auto uom (l_CiUoMs.find(ciUnit.GetData()));
+
+ if (uom == l_CiUoMs.end()) {
+ Log(LogDebug, "PerfdataValue")
+ << "Invalid performance data unit: " << unit;
+
+ unit = "";
+ base = 1.0;
+ } else {
+ unit = uom->second.Out;
+ base = uom->second.Factor;
+ }
+ } else {
+ unit = uom->second.Out;
+ base = uom->second.Factor;
+ }
+ }
+
+ if (unit == "c") {
+ counter = true;
+ }
+
+ warn = ParseWarnCritMinMaxToken(tokens, 1, "warning");
+ crit = ParseWarnCritMinMaxToken(tokens, 2, "critical");
+ min = ParseWarnCritMinMaxToken(tokens, 3, "minimum");
+ max = ParseWarnCritMinMaxToken(tokens, 4, "maximum");
+
+ value = value * base;
+
+ if (!warn.IsEmpty())
+ warn = warn * base;
+
+ if (!crit.IsEmpty())
+ crit = crit * base;
+
+ if (!min.IsEmpty())
+ min = min * base;
+
+ if (!max.IsEmpty())
+ max = max * base;
+
+ return new PerfdataValue(label, value, counter, unit, warn, crit, min, max);
+}
+
+static const std::unordered_map<std::string, const char*> l_FormatUoMs ({
+ { "ampere-seconds", "As" },
+ { "amperes", "A" },
+ { "bits", "b" },
+ { "bytes", "B" },
+ { "decibel-milliwatts", "dBm" },
+ { "degrees-celsius", "C" },
+ { "degrees-fahrenheit", "F" },
+ { "degrees-kelvin", "K" },
+ { "grams", "g" },
+ { "liters", "l" },
+ { "lumens", "lm" },
+ { "ohms", "O" },
+ { "percent", "%" },
+ { "seconds", "s" },
+ { "volts", "V" },
+ { "watt-hours", "Wh" },
+ { "watts", "W" }
+});
+
+String PerfdataValue::Format() const
+{
+ std::ostringstream result;
+
+ if (GetLabel().FindFirstOf(" ") != String::NPos)
+ result << "'" << GetLabel() << "'";
+ else
+ result << GetLabel();
+
+ result << "=" << Convert::ToString(GetValue());
+
+ String unit;
+
+ if (GetCounter()) {
+ unit = "c";
+ } else {
+ auto myUnit (GetUnit());
+ auto uom (l_FormatUoMs.find(myUnit.GetData()));
+
+ if (uom != l_FormatUoMs.end()) {
+ unit = uom->second;
+ }
+ }
+
+ result << unit;
+
+ if (!GetWarn().IsEmpty()) {
+ result << ";" << Convert::ToString(GetWarn());
+
+ if (!GetCrit().IsEmpty()) {
+ result << ";" << Convert::ToString(GetCrit());
+
+ if (!GetMin().IsEmpty()) {
+ result << ";" << Convert::ToString(GetMin());
+
+ if (!GetMax().IsEmpty()) {
+ result << ";" << Convert::ToString(GetMax());
+ }
+ }
+ }
+ }
+
+ return result.str();
+}
+
+Value PerfdataValue::ParseWarnCritMinMaxToken(const std::vector<String>& tokens, std::vector<String>::size_type index, const String& description)
+{
+ if (tokens.size() > index && tokens[index] != "U" && tokens[index] != "" && tokens[index].FindFirstNotOf("+-0123456789.eE") == String::NPos)
+ return Convert::ToDouble(tokens[index]);
+ else {
+ if (tokens.size() > index && tokens[index] != "")
+ Log(LogDebug, "PerfdataValue")
+ << "Ignoring unsupported perfdata " << description << " range, value: '" << tokens[index] << "'.";
+ return Empty;
+ }
+}
diff --git a/lib/base/perfdatavalue.hpp b/lib/base/perfdatavalue.hpp
new file mode 100644
index 0000000..05b2c34
--- /dev/null
+++ b/lib/base/perfdatavalue.hpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PERFDATAVALUE_H
+#define PERFDATAVALUE_H
+
+#include "base/i2-base.hpp"
+#include "base/perfdatavalue-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * A performance data value.
+ *
+ * @ingroup base
+ */
+class PerfdataValue final : public ObjectImpl<PerfdataValue>
+{
+public:
+ DECLARE_OBJECT(PerfdataValue);
+
+ PerfdataValue() = default;
+
+ PerfdataValue(const String& label, double value, bool counter = false, const String& unit = "",
+ const Value& warn = Empty, const Value& crit = Empty,
+ const Value& min = Empty, const Value& max = Empty);
+
+ static PerfdataValue::Ptr Parse(const String& perfdata);
+ String Format() const;
+
+private:
+ static Value ParseWarnCritMinMaxToken(const std::vector<String>& tokens,
+ std::vector<String>::size_type index, const String& description);
+};
+
+}
+
+#endif /* PERFDATA_VALUE */
diff --git a/lib/base/perfdatavalue.ti b/lib/base/perfdatavalue.ti
new file mode 100644
index 0000000..b2692e9
--- /dev/null
+++ b/lib/base/perfdatavalue.ti
@@ -0,0 +1,20 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+library base;
+
+namespace icinga
+{
+
+class PerfdataValue
+{
+ [state] String label;
+ [state] double value;
+ [state] bool counter;
+ [state] String unit;
+ [state] Value crit;
+ [state] Value warn;
+ [state] Value min;
+ [state] Value max;
+};
+
+}
diff --git a/lib/base/primitivetype.cpp b/lib/base/primitivetype.cpp
new file mode 100644
index 0000000..10286c7
--- /dev/null
+++ b/lib/base/primitivetype.cpp
@@ -0,0 +1,64 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/primitivetype.hpp"
+#include "base/dictionary.hpp"
+
+using namespace icinga;
+
+PrimitiveType::PrimitiveType(String name, String base, const ObjectFactory& factory)
+ : m_Name(std::move(name)), m_Base(std::move(base)), m_Factory(factory)
+{ }
+
+String PrimitiveType::GetName() const
+{
+ return m_Name;
+}
+
+Type::Ptr PrimitiveType::GetBaseType() const
+{
+ if (m_Base == "None")
+ return nullptr;
+ else
+ return Type::GetByName(m_Base);
+}
+
+int PrimitiveType::GetAttributes() const
+{
+ return 0;
+}
+
+int PrimitiveType::GetFieldId(const String& name) const
+{
+ Type::Ptr base = GetBaseType();
+
+ if (base)
+ return base->GetFieldId(name);
+ else
+ return -1;
+}
+
+Field PrimitiveType::GetFieldInfo(int id) const
+{
+ Type::Ptr base = GetBaseType();
+
+ if (base)
+ return base->GetFieldInfo(id);
+ else
+ throw std::runtime_error("Invalid field ID.");
+}
+
+int PrimitiveType::GetFieldCount() const
+{
+ Type::Ptr base = GetBaseType();
+
+ if (base)
+ return Object::TypeInstance->GetFieldCount();
+ else
+ return 0;
+}
+
+ObjectFactory PrimitiveType::GetFactory() const
+{
+ return m_Factory;
+}
+
diff --git a/lib/base/primitivetype.hpp b/lib/base/primitivetype.hpp
new file mode 100644
index 0000000..439e20f
--- /dev/null
+++ b/lib/base/primitivetype.hpp
@@ -0,0 +1,62 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PRIMITIVETYPE_H
+#define PRIMITIVETYPE_H
+
+#include "base/i2-base.hpp"
+#include "base/type.hpp"
+#include "base/initialize.hpp"
+
+namespace icinga
+{
+
+class PrimitiveType final : public Type
+{
+public:
+ PrimitiveType(String name, String base, const ObjectFactory& factory = ObjectFactory());
+
+ String GetName() const override;
+ Type::Ptr GetBaseType() const override;
+ int GetAttributes() const override;
+ int GetFieldId(const String& name) const override;
+ Field GetFieldInfo(int id) const override;
+ int GetFieldCount() const override;
+
+protected:
+ ObjectFactory GetFactory() const override;
+
+private:
+ String m_Name;
+ String m_Base;
+ ObjectFactory m_Factory;
+};
+
+/* Ensure that the priority is lower than the basic namespace initialization in scriptframe.cpp. */
+#define REGISTER_BUILTIN_TYPE(type, prototype) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ icinga::Type::Ptr t = new PrimitiveType(#type, "None"); \
+ t->SetPrototype(prototype); \
+ icinga::Type::Register(t); \
+ }, InitializePriority::RegisterBuiltinTypes)
+
+#define REGISTER_PRIMITIVE_TYPE_FACTORY(type, base, prototype, factory) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ icinga::Type::Ptr t = new PrimitiveType(#type, #base, factory); \
+ t->SetPrototype(prototype); \
+ icinga::Type::Register(t); \
+ type::TypeInstance = t; \
+ }, InitializePriority::RegisterPrimitiveTypes); \
+ DEFINE_TYPE_INSTANCE(type)
+
+#define REGISTER_PRIMITIVE_TYPE(type, base, prototype) \
+ REGISTER_PRIMITIVE_TYPE_FACTORY(type, base, prototype, DefaultObjectFactory<type>)
+
+#define REGISTER_PRIMITIVE_TYPE_VA(type, base, prototype) \
+ REGISTER_PRIMITIVE_TYPE_FACTORY(type, base, prototype, DefaultObjectFactoryVA<type>)
+
+#define REGISTER_PRIMITIVE_TYPE_NOINST(type, base, prototype) \
+ REGISTER_PRIMITIVE_TYPE_FACTORY(type, base, prototype, nullptr)
+
+}
+
+#endif /* PRIMITIVETYPE_H */
diff --git a/lib/base/process.cpp b/lib/base/process.cpp
new file mode 100644
index 0000000..d4246a6
--- /dev/null
+++ b/lib/base/process.cpp
@@ -0,0 +1,1207 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/process.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/initialize.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/json.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/thread/once.hpp>
+#include <thread>
+#include <iostream>
+
+#ifndef _WIN32
+# include <execvpe.h>
+# include <poll.h>
+# include <string.h>
+
+# ifndef __APPLE__
+extern char **environ;
+# else /* __APPLE__ */
+# include <crt_externs.h>
+# define environ (*_NSGetEnviron())
+# endif /* __APPLE__ */
+#endif /* _WIN32 */
+
+using namespace icinga;
+
+#define IOTHREADS 4
+
+static std::mutex l_ProcessMutex[IOTHREADS];
+static std::map<Process::ProcessHandle, Process::Ptr> l_Processes[IOTHREADS];
+#ifdef _WIN32
+static HANDLE l_Events[IOTHREADS];
+#else /* _WIN32 */
+static int l_EventFDs[IOTHREADS][2];
+static std::map<Process::ConsoleHandle, Process::ProcessHandle> l_FDs[IOTHREADS];
+
+static std::mutex l_ProcessControlMutex;
+static int l_ProcessControlFD = -1;
+static pid_t l_ProcessControlPID;
+#endif /* _WIN32 */
+static boost::once_flag l_ProcessOnceFlag = BOOST_ONCE_INIT;
+static boost::once_flag l_SpawnHelperOnceFlag = BOOST_ONCE_INIT;
+
+Process::Process(Process::Arguments arguments, Dictionary::Ptr extraEnvironment)
+ : m_Arguments(std::move(arguments)), m_ExtraEnvironment(std::move(extraEnvironment)),
+ m_Timeout(600)
+#ifdef _WIN32
+ , m_ReadPending(false), m_ReadFailed(false), m_Overlapped()
+#else /* _WIN32 */
+ , m_SentSigterm(false)
+#endif /* _WIN32 */
+ , m_AdjustPriority(false), m_ResultAvailable(false)
+{
+#ifdef _WIN32
+ m_Overlapped.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
+#endif /* _WIN32 */
+}
+
+Process::~Process()
+{
+#ifdef _WIN32
+ CloseHandle(m_Overlapped.hEvent);
+#endif /* _WIN32 */
+}
+
+#ifndef _WIN32
+static Value ProcessSpawnImpl(struct msghdr *msgh, const Dictionary::Ptr& request)
+{
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(msgh);
+
+ if (cmsg == nullptr || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_len != CMSG_LEN(sizeof(int) * 3)) {
+ std::cerr << "Invalid 'spawn' request: FDs missing" << std::endl;
+ return Empty;
+ }
+
+ auto *fds = (int *)CMSG_DATA(cmsg);
+
+ Array::Ptr arguments = request->Get("arguments");
+ Dictionary::Ptr extraEnvironment = request->Get("extraEnvironment");
+ bool adjustPriority = request->Get("adjustPriority");
+
+ // build argv
+ auto **argv = new char *[arguments->GetLength() + 1];
+
+ for (unsigned int i = 0; i < arguments->GetLength(); i++) {
+ String arg = arguments->Get(i);
+ argv[i] = strdup(arg.CStr());
+ }
+
+ argv[arguments->GetLength()] = nullptr;
+
+ // build envp
+ int envc = 0;
+
+ /* count existing environment variables */
+ while (environ[envc])
+ envc++;
+
+ auto **envp = new char *[envc + (extraEnvironment ? extraEnvironment->GetLength() : 0) + 2];
+ const char* lcnumeric = "LC_NUMERIC=";
+ const char* notifySocket = "NOTIFY_SOCKET=";
+ int j = 0;
+
+ for (int i = 0; i < envc; i++) {
+ if (strncmp(environ[i], lcnumeric, strlen(lcnumeric)) == 0) {
+ continue;
+ }
+
+ if (strncmp(environ[i], notifySocket, strlen(notifySocket)) == 0) {
+ continue;
+ }
+
+ envp[j] = strdup(environ[i]);
+ ++j;
+ }
+
+ if (extraEnvironment) {
+ ObjectLock olock(extraEnvironment);
+
+ for (const Dictionary::Pair& kv : extraEnvironment) {
+ String skv = kv.first + "=" + Convert::ToString(kv.second);
+ envp[j] = strdup(skv.CStr());
+ j++;
+ }
+ }
+
+ envp[j] = strdup("LC_NUMERIC=C");
+ envp[j + 1] = nullptr;
+
+ extraEnvironment.reset();
+
+ pid_t pid = fork();
+
+ int errorCode = 0;
+
+ if (pid < 0)
+ errorCode = errno;
+
+ if (pid == 0) {
+ // child process
+
+ (void)close(l_ProcessControlFD);
+
+ if (setsid() < 0) {
+ perror("setsid() failed");
+ _exit(128);
+ }
+
+ if (dup2(fds[0], STDIN_FILENO) < 0 || dup2(fds[1], STDOUT_FILENO) < 0 || dup2(fds[2], STDERR_FILENO) < 0) {
+ perror("dup2() failed");
+ _exit(128);
+ }
+
+ (void)close(fds[0]);
+ (void)close(fds[1]);
+ (void)close(fds[2]);
+
+#ifdef HAVE_NICE
+ if (adjustPriority) {
+ // Cheating the compiler on "warning: ignoring return value of 'int nice(int)', declared with attribute warn_unused_result [-Wunused-result]".
+ auto x (nice(5));
+ (void)x;
+ }
+#endif /* HAVE_NICE */
+
+ sigset_t mask;
+ sigemptyset(&mask);
+ sigprocmask(SIG_SETMASK, &mask, nullptr);
+
+ if (icinga2_execvpe(argv[0], argv, envp) < 0) {
+ char errmsg[512];
+ strcpy(errmsg, "execvpe(");
+ strncat(errmsg, argv[0], sizeof(errmsg) - strlen(errmsg) - 1);
+ strncat(errmsg, ") failed", sizeof(errmsg) - strlen(errmsg) - 1);
+ errmsg[sizeof(errmsg) - 1] = '\0';
+ perror(errmsg);
+ }
+
+ _exit(128);
+ }
+
+ (void)close(fds[0]);
+ (void)close(fds[1]);
+ (void)close(fds[2]);
+
+ // free arguments
+ for (int i = 0; argv[i]; i++)
+ free(argv[i]);
+
+ delete[] argv;
+
+ // free environment
+ for (int i = 0; envp[i]; i++)
+ free(envp[i]);
+
+ delete[] envp;
+
+ Dictionary::Ptr response = new Dictionary({
+ { "rc", pid },
+ { "errno", errorCode }
+ });
+
+ return response;
+}
+
+static Value ProcessKillImpl(struct msghdr *msgh, const Dictionary::Ptr& request)
+{
+ pid_t pid = request->Get("pid");
+ int signum = request->Get("signum");
+
+ errno = 0;
+ kill(pid, signum);
+ int error = errno;
+
+ Dictionary::Ptr response = new Dictionary({
+ { "errno", error }
+ });
+
+ return response;
+}
+
+static Value ProcessWaitPIDImpl(struct msghdr *msgh, const Dictionary::Ptr& request)
+{
+ pid_t pid = request->Get("pid");
+
+ int status;
+ int rc = waitpid(pid, &status, 0);
+
+ Dictionary::Ptr response = new Dictionary({
+ { "status", status },
+ { "rc", rc }
+ });
+
+ return response;
+}
+
+static void ProcessHandler()
+{
+ sigset_t mask;
+ sigfillset(&mask);
+ sigprocmask(SIG_SETMASK, &mask, nullptr);
+
+ Utility::CloseAllFDs({0, 1, 2, l_ProcessControlFD});
+
+ for (;;) {
+ size_t length;
+
+ struct msghdr msg;
+ memset(&msg, 0, sizeof(msg));
+
+ struct iovec io;
+ io.iov_base = &length;
+ io.iov_len = sizeof(length);
+
+ msg.msg_iov = &io;
+ msg.msg_iovlen = 1;
+
+ char cbuf[4096];
+ msg.msg_control = cbuf;
+ msg.msg_controllen = sizeof(cbuf);
+
+ int rc = recvmsg(l_ProcessControlFD, &msg, 0);
+
+ if (rc <= 0) {
+ if (rc < 0 && (errno == EINTR || errno == EAGAIN))
+ continue;
+
+ break;
+ }
+
+ auto *mbuf = new char[length];
+
+ size_t count = 0;
+ while (count < length) {
+ rc = recv(l_ProcessControlFD, mbuf + count, length - count, 0);
+
+ if (rc <= 0) {
+ if (rc < 0 && (errno == EINTR || errno == EAGAIN))
+ continue;
+
+ delete [] mbuf;
+
+ _exit(0);
+ }
+
+ count += rc;
+
+ if (rc == 0)
+ break;
+ }
+
+ String jrequest = String(mbuf, mbuf + count);
+
+ delete [] mbuf;
+
+ Dictionary::Ptr request = JsonDecode(jrequest);
+
+ String command = request->Get("command");
+
+ Value response;
+
+ if (command == "spawn")
+ response = ProcessSpawnImpl(&msg, request);
+ else if (command == "waitpid")
+ response = ProcessWaitPIDImpl(&msg, request);
+ else if (command == "kill")
+ response = ProcessKillImpl(&msg, request);
+ else
+ response = Empty;
+
+ String jresponse = JsonEncode(response);
+
+ if (send(l_ProcessControlFD, jresponse.CStr(), jresponse.GetLength(), 0) < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("send")
+ << boost::errinfo_errno(errno));
+ }
+ }
+
+ _exit(0);
+}
+
+static void StartSpawnProcessHelper()
+{
+ if (l_ProcessControlFD != -1) {
+ (void)close(l_ProcessControlFD);
+
+ int status;
+ (void)waitpid(l_ProcessControlPID, &status, 0);
+ }
+
+ int controlFDs[2];
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, controlFDs) < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("socketpair")
+ << boost::errinfo_errno(errno));
+ }
+
+ pid_t pid = fork();
+
+ if (pid < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("fork")
+ << boost::errinfo_errno(errno));
+ }
+
+ if (pid == 0) {
+ (void)close(controlFDs[1]);
+
+ l_ProcessControlFD = controlFDs[0];
+
+ ProcessHandler();
+
+ _exit(1);
+ }
+
+ (void)close(controlFDs[0]);
+
+ l_ProcessControlFD = controlFDs[1];
+ l_ProcessControlPID = pid;
+}
+
+static pid_t ProcessSpawn(const std::vector<String>& arguments, const Dictionary::Ptr& extraEnvironment, bool adjustPriority, int fds[3])
+{
+ Dictionary::Ptr request = new Dictionary({
+ { "command", "spawn" },
+ { "arguments", Array::FromVector(arguments) },
+ { "extraEnvironment", extraEnvironment },
+ { "adjustPriority", adjustPriority }
+ });
+
+ String jrequest = JsonEncode(request);
+ size_t length = jrequest.GetLength();
+
+ std::unique_lock<std::mutex> lock(l_ProcessControlMutex);
+
+ struct msghdr msg;
+ memset(&msg, 0, sizeof(msg));
+
+ struct iovec io;
+ io.iov_base = &length;
+ io.iov_len = sizeof(length);
+
+ msg.msg_iov = &io;
+ msg.msg_iovlen = 1;
+
+ char cbuf[CMSG_SPACE(sizeof(int) * 3)];
+ msg.msg_control = cbuf;
+ msg.msg_controllen = sizeof(cbuf);
+
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int) * 3);
+
+ memcpy(CMSG_DATA(cmsg), fds, sizeof(int) * 3);
+
+ msg.msg_controllen = cmsg->cmsg_len;
+
+ do {
+ while (sendmsg(l_ProcessControlFD, &msg, 0) < 0) {
+ StartSpawnProcessHelper();
+ }
+ } while (send(l_ProcessControlFD, jrequest.CStr(), jrequest.GetLength(), 0) < 0);
+
+ char buf[4096];
+
+ ssize_t rc = recv(l_ProcessControlFD, buf, sizeof(buf), 0);
+
+ if (rc <= 0)
+ return -1;
+
+ String jresponse = String(buf, buf + rc);
+
+ Dictionary::Ptr response = JsonDecode(jresponse);
+
+ if (response->Get("rc") == -1)
+ errno = response->Get("errno");
+
+ return response->Get("rc");
+}
+
+static int ProcessKill(pid_t pid, int signum)
+{
+ Dictionary::Ptr request = new Dictionary({
+ { "command", "kill" },
+ { "pid", pid },
+ { "signum", signum }
+ });
+
+ String jrequest = JsonEncode(request);
+ size_t length = jrequest.GetLength();
+
+ std::unique_lock<std::mutex> lock(l_ProcessControlMutex);
+
+ do {
+ while (send(l_ProcessControlFD, &length, sizeof(length), 0) < 0) {
+ StartSpawnProcessHelper();
+ }
+ } while (send(l_ProcessControlFD, jrequest.CStr(), jrequest.GetLength(), 0) < 0);
+
+ char buf[4096];
+
+ ssize_t rc = recv(l_ProcessControlFD, buf, sizeof(buf), 0);
+
+ if (rc <= 0)
+ return -1;
+
+ String jresponse = String(buf, buf + rc);
+
+ Dictionary::Ptr response = JsonDecode(jresponse);
+ return response->Get("errno");
+}
+
+static int ProcessWaitPID(pid_t pid, int *status)
+{
+ Dictionary::Ptr request = new Dictionary({
+ { "command", "waitpid" },
+ { "pid", pid }
+ });
+
+ String jrequest = JsonEncode(request);
+ size_t length = jrequest.GetLength();
+
+ std::unique_lock<std::mutex> lock(l_ProcessControlMutex);
+
+ do {
+ while (send(l_ProcessControlFD, &length, sizeof(length), 0) < 0) {
+ StartSpawnProcessHelper();
+ }
+ } while (send(l_ProcessControlFD, jrequest.CStr(), jrequest.GetLength(), 0) < 0);
+
+ char buf[4096];
+
+ ssize_t rc = recv(l_ProcessControlFD, buf, sizeof(buf), 0);
+
+ if (rc <= 0)
+ return -1;
+
+ String jresponse = String(buf, buf + rc);
+
+ Dictionary::Ptr response = JsonDecode(jresponse);
+ *status = response->Get("status");
+ return response->Get("rc");
+}
+
+void Process::InitializeSpawnHelper()
+{
+ if (l_ProcessControlFD == -1)
+ StartSpawnProcessHelper();
+}
+#endif /* _WIN32 */
+
+static void InitializeProcess()
+{
+#ifdef _WIN32
+ for (auto& event : l_Events) {
+ event = CreateEvent(nullptr, TRUE, FALSE, nullptr);
+ }
+#else /* _WIN32 */
+ for (auto& eventFD : l_EventFDs) {
+# ifdef HAVE_PIPE2
+ if (pipe2(eventFD, O_CLOEXEC) < 0) {
+ if (errno == ENOSYS) {
+# endif /* HAVE_PIPE2 */
+ if (pipe(eventFD) < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("pipe")
+ << boost::errinfo_errno(errno));
+ }
+
+ Utility::SetCloExec(eventFD[0]);
+ Utility::SetCloExec(eventFD[1]);
+# ifdef HAVE_PIPE2
+ } else {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("pipe2")
+ << boost::errinfo_errno(errno));
+ }
+ }
+# endif /* HAVE_PIPE2 */
+ }
+#endif /* _WIN32 */
+}
+
+INITIALIZE_ONCE(InitializeProcess);
+
+void Process::ThreadInitialize()
+{
+ /* Note to self: Make sure this runs _after_ we've daemonized. */
+ for (int tid = 0; tid < IOTHREADS; tid++) {
+ std::thread t([tid]() { IOThreadProc(tid); });
+ t.detach();
+ }
+}
+
+Process::Arguments Process::PrepareCommand(const Value& command)
+{
+#ifdef _WIN32
+ String args;
+#else /* _WIN32 */
+ std::vector<String> args;
+#endif /* _WIN32 */
+
+ if (command.IsObjectType<Array>()) {
+ Array::Ptr arguments = command;
+
+ ObjectLock olock(arguments);
+ for (const Value& argument : arguments) {
+#ifdef _WIN32
+ if (args != "")
+ args += " ";
+
+ args += Utility::EscapeCreateProcessArg(argument);
+#else /* _WIN32 */
+ args.push_back(argument);
+#endif /* _WIN32 */
+ }
+
+ return args;
+ }
+
+#ifdef _WIN32
+ return command;
+#else /* _WIN32 */
+ return { "sh", "-c", command };
+#endif
+}
+
+void Process::SetTimeout(double timeout)
+{
+ m_Timeout = timeout;
+}
+
+double Process::GetTimeout() const
+{
+ return m_Timeout;
+}
+
+void Process::SetAdjustPriority(bool adjust)
+{
+ m_AdjustPriority = adjust;
+}
+
+bool Process::GetAdjustPriority() const
+{
+ return m_AdjustPriority;
+}
+
+void Process::IOThreadProc(int tid)
+{
+#ifdef _WIN32
+ HANDLE *handles = nullptr;
+ HANDLE *fhandles = nullptr;
+#else /* _WIN32 */
+ pollfd *pfds = nullptr;
+#endif /* _WIN32 */
+ int count = 0;
+ double now;
+
+ Utility::SetThreadName("ProcessIO");
+
+ for (;;) {
+ double timeout = -1;
+
+ now = Utility::GetTime();
+
+ {
+ std::unique_lock<std::mutex> lock(l_ProcessMutex[tid]);
+
+ count = 1 + l_Processes[tid].size();
+#ifdef _WIN32
+ handles = reinterpret_cast<HANDLE *>(realloc(handles, sizeof(HANDLE) * count));
+ fhandles = reinterpret_cast<HANDLE *>(realloc(fhandles, sizeof(HANDLE) * count));
+
+ fhandles[0] = l_Events[tid];
+
+#else /* _WIN32 */
+ pfds = reinterpret_cast<pollfd *>(realloc(pfds, sizeof(pollfd) * count));
+
+ pfds[0].fd = l_EventFDs[tid][0];
+ pfds[0].events = POLLIN;
+ pfds[0].revents = 0;
+#endif /* _WIN32 */
+
+ int i = 1;
+ typedef std::pair<ProcessHandle, Process::Ptr> kv_pair;
+ for (const kv_pair& kv : l_Processes[tid]) {
+ const Process::Ptr& process = kv.second;
+#ifdef _WIN32
+ handles[i] = kv.first;
+
+ if (!process->m_ReadPending) {
+ process->m_ReadPending = true;
+
+ BOOL res = ReadFile(process->m_FD, process->m_ReadBuffer, sizeof(process->m_ReadBuffer), 0, &process->m_Overlapped);
+ if (res || GetLastError() != ERROR_IO_PENDING) {
+ process->m_ReadFailed = !res;
+ SetEvent(process->m_Overlapped.hEvent);
+ }
+ }
+
+ fhandles[i] = process->m_Overlapped.hEvent;
+#else /* _WIN32 */
+ pfds[i].fd = process->m_FD;
+ pfds[i].events = POLLIN;
+ pfds[i].revents = 0;
+#endif /* _WIN32 */
+
+ if (process->m_Timeout != 0) {
+ double delta = process->GetNextTimeout() - (now - process->m_Result.ExecutionStart);
+
+ if (timeout == -1 || delta < timeout)
+ timeout = delta;
+ }
+
+ i++;
+ }
+ }
+
+ if (timeout < 0.01)
+ timeout = 0.5;
+
+ timeout *= 1000;
+
+#ifdef _WIN32
+ DWORD rc = WaitForMultipleObjects(count, fhandles, FALSE, timeout == -1 ? INFINITE : static_cast<DWORD>(timeout));
+#else /* _WIN32 */
+ int rc = poll(pfds, count, timeout);
+
+ if (rc < 0)
+ continue;
+#endif /* _WIN32 */
+
+ now = Utility::GetTime();
+
+ {
+ std::unique_lock<std::mutex> lock(l_ProcessMutex[tid]);
+
+#ifdef _WIN32
+ if (rc == WAIT_OBJECT_0)
+ ResetEvent(l_Events[tid]);
+#else /* _WIN32 */
+ if (pfds[0].revents & (POLLIN | POLLHUP | POLLERR)) {
+ char buffer[512];
+ if (read(l_EventFDs[tid][0], buffer, sizeof(buffer)) < 0)
+ Log(LogCritical, "base", "Read from event FD failed.");
+ }
+#endif /* _WIN32 */
+
+ for (int i = 1; i < count; i++) {
+#ifdef _WIN32
+ auto it = l_Processes[tid].find(handles[i]);
+#else /* _WIN32 */
+ auto it2 = l_FDs[tid].find(pfds[i].fd);
+
+ if (it2 == l_FDs[tid].end())
+ continue; /* This should never happen. */
+
+ auto it = l_Processes[tid].find(it2->second);
+#endif /* _WIN32 */
+
+ if (it == l_Processes[tid].end())
+ continue; /* This should never happen. */
+
+ bool is_timeout = false;
+
+ if (it->second->m_Timeout != 0) {
+ double timeout = it->second->m_Result.ExecutionStart + it->second->GetNextTimeout();
+
+ if (timeout < now)
+ is_timeout = true;
+ }
+
+#ifdef _WIN32
+ if (rc == WAIT_OBJECT_0 + i || is_timeout) {
+#else /* _WIN32 */
+ if (pfds[i].revents & (POLLIN | POLLHUP | POLLERR) || is_timeout) {
+#endif /* _WIN32 */
+ if (!it->second->DoEvents()) {
+#ifdef _WIN32
+ CloseHandle(it->first);
+ CloseHandle(it->second->m_FD);
+#else /* _WIN32 */
+ l_FDs[tid].erase(it->second->m_FD);
+ (void)close(it->second->m_FD);
+#endif /* _WIN32 */
+ l_Processes[tid].erase(it);
+ }
+ }
+ }
+ }
+ }
+}
+
+String Process::PrettyPrintArguments(const Process::Arguments& arguments)
+{
+#ifdef _WIN32
+ return "'" + arguments + "'";
+#else /* _WIN32 */
+ return "'" + boost::algorithm::join(arguments, "' '") + "'";
+#endif /* _WIN32 */
+}
+
+#ifdef _WIN32
+static BOOL CreatePipeOverlapped(HANDLE *outReadPipe, HANDLE *outWritePipe,
+ SECURITY_ATTRIBUTES *securityAttributes, DWORD size, DWORD readMode, DWORD writeMode)
+{
+ static LONG pipeIndex = 0;
+
+ if (size == 0)
+ size = 8192;
+
+ LONG currentIndex = InterlockedIncrement(&pipeIndex);
+
+ char pipeName[128];
+ sprintf(pipeName, "\\\\.\\Pipe\\OverlappedPipe.%d.%d", (int)GetCurrentProcessId(), (int)currentIndex);
+
+ *outReadPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_INBOUND | readMode,
+ PIPE_TYPE_BYTE | PIPE_WAIT, 1, size, size, 60 * 1000, securityAttributes);
+
+ if (*outReadPipe == INVALID_HANDLE_VALUE)
+ return FALSE;
+
+ *outWritePipe = CreateFile(pipeName, GENERIC_WRITE, 0, securityAttributes, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | writeMode, nullptr);
+
+ if (*outWritePipe == INVALID_HANDLE_VALUE) {
+ DWORD error = GetLastError();
+ CloseHandle(*outReadPipe);
+ SetLastError(error);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+#endif /* _WIN32 */
+
+void Process::Run(const std::function<void(const ProcessResult&)>& callback)
+{
+#ifndef _WIN32
+ boost::call_once(l_SpawnHelperOnceFlag, &Process::InitializeSpawnHelper);
+#endif /* _WIN32 */
+ boost::call_once(l_ProcessOnceFlag, &Process::ThreadInitialize);
+
+ m_Result.ExecutionStart = Utility::GetTime();
+
+#ifdef _WIN32
+ SECURITY_ATTRIBUTES sa = {};
+ sa.nLength = sizeof(sa);
+ sa.bInheritHandle = TRUE;
+
+ HANDLE outReadPipe, outWritePipe;
+ if (!CreatePipeOverlapped(&outReadPipe, &outWritePipe, &sa, 0, FILE_FLAG_OVERLAPPED, 0))
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("CreatePipe")
+ << errinfo_win32_error(GetLastError()));
+
+ if (!SetHandleInformation(outReadPipe, HANDLE_FLAG_INHERIT, 0))
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("SetHandleInformation")
+ << errinfo_win32_error(GetLastError()));
+
+ HANDLE outWritePipeDup;
+ if (!DuplicateHandle(GetCurrentProcess(), outWritePipe, GetCurrentProcess(),
+ &outWritePipeDup, 0, TRUE, DUPLICATE_SAME_ACCESS))
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("DuplicateHandle")
+ << errinfo_win32_error(GetLastError()));
+
+/* LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
+ SIZE_T cbSize;
+
+ if (!InitializeProcThreadAttributeList(nullptr, 1, 0, &cbSize) && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("InitializeProcThreadAttributeList")
+ << errinfo_win32_error(GetLastError()));
+
+ lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(new char[cbSize]);
+
+ if (!InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &cbSize))
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("InitializeProcThreadAttributeList")
+ << errinfo_win32_error(GetLastError()));
+
+ HANDLE rgHandles[3];
+ rgHandles[0] = outWritePipe;
+ rgHandles[1] = outWritePipeDup;
+ rgHandles[2] = GetStdHandle(STD_INPUT_HANDLE);
+
+ if (!UpdateProcThreadAttribute(lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+ rgHandles, sizeof(rgHandles), nullptr, nullptr))
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("UpdateProcThreadAttribute")
+ << errinfo_win32_error(GetLastError()));
+*/
+
+ STARTUPINFOEX si = {};
+ si.StartupInfo.cb = sizeof(si);
+ si.StartupInfo.hStdError = outWritePipe;
+ si.StartupInfo.hStdOutput = outWritePipeDup;
+ si.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
+ si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
+// si.lpAttributeList = lpAttributeList;
+
+ PROCESS_INFORMATION pi;
+
+ char *args = new char[m_Arguments.GetLength() + 1];
+ strncpy(args, m_Arguments.CStr(), m_Arguments.GetLength() + 1);
+ args[m_Arguments.GetLength()] = '\0';
+
+ LPCH pEnvironment = GetEnvironmentStrings();
+ size_t ioffset = 0, offset = 0;
+
+ char *envp = nullptr;
+
+ for (;;) {
+ size_t len = strlen(pEnvironment + ioffset);
+
+ if (len == 0)
+ break;
+
+ char *eqp = strchr(pEnvironment + ioffset, '=');
+ if (eqp && m_ExtraEnvironment && m_ExtraEnvironment->Contains(String(pEnvironment + ioffset, eqp))) {
+ ioffset += len + 1;
+ continue;
+ }
+
+ envp = static_cast<char *>(realloc(envp, offset + len + 1));
+
+ if (!envp)
+ BOOST_THROW_EXCEPTION(std::bad_alloc());
+
+ strcpy(envp + offset, pEnvironment + ioffset);
+ offset += len + 1;
+ ioffset += len + 1;
+ }
+
+ FreeEnvironmentStrings(pEnvironment);
+
+ if (m_ExtraEnvironment) {
+ ObjectLock olock(m_ExtraEnvironment);
+
+ for (const Dictionary::Pair& kv : m_ExtraEnvironment) {
+ String skv = kv.first + "=" + Convert::ToString(kv.second);
+
+ envp = static_cast<char *>(realloc(envp, offset + skv.GetLength() + 1));
+
+ if (!envp)
+ BOOST_THROW_EXCEPTION(std::bad_alloc());
+
+ strcpy(envp + offset, skv.CStr());
+ offset += skv.GetLength() + 1;
+ }
+ }
+
+ envp = static_cast<char *>(realloc(envp, offset + 1));
+
+ if (!envp)
+ BOOST_THROW_EXCEPTION(std::bad_alloc());
+
+ envp[offset] = '\0';
+
+ if (!CreateProcess(nullptr, args, nullptr, nullptr, TRUE,
+ 0 /*EXTENDED_STARTUPINFO_PRESENT*/, envp, nullptr, &si.StartupInfo, &pi)) {
+ DWORD error = GetLastError();
+ CloseHandle(outWritePipe);
+ CloseHandle(outWritePipeDup);
+ free(envp);
+/* DeleteProcThreadAttributeList(lpAttributeList);
+ delete [] reinterpret_cast<char *>(lpAttributeList); */
+
+ m_Result.PID = 0;
+ m_Result.ExecutionEnd = Utility::GetTime();
+ m_Result.ExitStatus = 127;
+ m_Result.Output = "Command " + String(args) + " failed to execute: " + Utility::FormatErrorNumber(error);
+
+ delete [] args;
+
+ if (callback) {
+ /*
+ * Explicitly use Process::Ptr to keep the reference counted while the
+ * callback is active and making it crash safe
+ */
+ Process::Ptr process(this);
+ Utility::QueueAsyncCallback([this, process, callback]() { callback(m_Result); });
+ }
+
+ return;
+ }
+
+ delete [] args;
+ free(envp);
+/* DeleteProcThreadAttributeList(lpAttributeList);
+ delete [] reinterpret_cast<char *>(lpAttributeList); */
+
+ CloseHandle(outWritePipe);
+ CloseHandle(outWritePipeDup);
+ CloseHandle(pi.hThread);
+
+ m_Process = pi.hProcess;
+ m_FD = outReadPipe;
+ m_PID = pi.dwProcessId;
+
+ Log(LogNotice, "Process")
+ << "Running command " << PrettyPrintArguments(m_Arguments) << ": PID " << m_PID;
+
+#else /* _WIN32 */
+ int outfds[2];
+
+#ifdef HAVE_PIPE2
+ if (pipe2(outfds, O_CLOEXEC) < 0) {
+ if (errno == ENOSYS) {
+#endif /* HAVE_PIPE2 */
+ if (pipe(outfds) < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("pipe")
+ << boost::errinfo_errno(errno));
+ }
+
+ Utility::SetCloExec(outfds[0]);
+ Utility::SetCloExec(outfds[1]);
+#ifdef HAVE_PIPE2
+ } else {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("pipe2")
+ << boost::errinfo_errno(errno));
+ }
+ }
+#endif /* HAVE_PIPE2 */
+
+ int fds[3];
+ fds[0] = STDIN_FILENO;
+ fds[1] = outfds[1];
+ fds[2] = outfds[1];
+
+ m_Process = ProcessSpawn(m_Arguments, m_ExtraEnvironment, m_AdjustPriority, fds);
+ m_PID = m_Process;
+
+ if (m_PID == -1) {
+ m_OutputStream << "Fork failed with error code " << errno << " (" << Utility::FormatErrorNumber(errno) << ")";
+ Log(LogCritical, "Process", m_OutputStream.str());
+ }
+
+ Log(LogNotice, "Process")
+ << "Running command " << PrettyPrintArguments(m_Arguments) << ": PID " << m_PID;
+
+ (void)close(outfds[1]);
+
+ Utility::SetNonBlocking(outfds[0]);
+
+ m_FD = outfds[0];
+#endif /* _WIN32 */
+
+ m_Callback = callback;
+
+ int tid = GetTID();
+
+ {
+ std::unique_lock<std::mutex> lock(l_ProcessMutex[tid]);
+ l_Processes[tid][m_Process] = this;
+#ifndef _WIN32
+ l_FDs[tid][m_FD] = m_Process;
+#endif /* _WIN32 */
+ }
+
+#ifdef _WIN32
+ SetEvent(l_Events[tid]);
+#else /* _WIN32 */
+ if (write(l_EventFDs[tid][1], "T", 1) < 0 && errno != EINTR && errno != EAGAIN)
+ Log(LogCritical, "base", "Write to event FD failed.");
+#endif /* _WIN32 */
+}
+
+const ProcessResult& Process::WaitForResult() {
+ std::unique_lock<std::mutex> lock(m_ResultMutex);
+ m_ResultCondition.wait(lock, [this]{ return m_ResultAvailable; });
+ return m_Result;
+}
+
+bool Process::DoEvents()
+{
+ bool is_timeout = false;
+#ifndef _WIN32
+ bool could_not_kill = false;
+#endif /* _WIN32 */
+
+ if (m_Timeout != 0) {
+ auto now (Utility::GetTime());
+
+#ifndef _WIN32
+ {
+ auto timeout (GetNextTimeout());
+ auto deadline (m_Result.ExecutionStart + timeout);
+
+ if (deadline < now && !m_SentSigterm) {
+ Log(LogWarning, "Process")
+ << "Terminating process " << m_PID << " (" << PrettyPrintArguments(m_Arguments)
+ << ") after timeout of " << timeout << " seconds";
+
+ m_OutputStream << "<Timeout exceeded.>";
+
+ int error = ProcessKill(m_Process, SIGTERM);
+ if (error) {
+ Log(LogWarning, "Process")
+ << "Couldn't terminate the process " << m_PID << " (" << PrettyPrintArguments(m_Arguments)
+ << "): [errno " << error << "] " << strerror(error);
+ }
+
+ m_SentSigterm = true;
+ }
+ }
+#endif /* _WIN32 */
+
+ auto timeout (GetNextTimeout());
+ auto deadline (m_Result.ExecutionStart + timeout);
+
+ if (deadline < now) {
+ Log(LogWarning, "Process")
+ << "Killing process group " << m_PID << " (" << PrettyPrintArguments(m_Arguments)
+ << ") after timeout of " << timeout << " seconds";
+
+#ifdef _WIN32
+ m_OutputStream << "<Timeout exceeded.>";
+ TerminateProcess(m_Process, 3);
+#else /* _WIN32 */
+ int error = ProcessKill(-m_Process, SIGKILL);
+ if (error) {
+ Log(LogWarning, "Process")
+ << "Couldn't kill the process group " << m_PID << " (" << PrettyPrintArguments(m_Arguments)
+ << "): [errno " << error << "] " << strerror(error);
+ could_not_kill = true;
+ }
+#endif /* _WIN32 */
+
+ is_timeout = true;
+ }
+ }
+
+ if (!is_timeout) {
+#ifdef _WIN32
+ m_ReadPending = false;
+
+ DWORD rc;
+ if (!m_ReadFailed && GetOverlappedResult(m_FD, &m_Overlapped, &rc, TRUE) && rc > 0) {
+ m_OutputStream.write(m_ReadBuffer, rc);
+ return true;
+ }
+#else /* _WIN32 */
+ char buffer[512];
+ for (;;) {
+ int rc = read(m_FD, buffer, sizeof(buffer));
+
+ if (rc < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ return true;
+
+ if (rc > 0) {
+ m_OutputStream.write(buffer, rc);
+ continue;
+ }
+
+ break;
+ }
+#endif /* _WIN32 */
+ }
+
+ String output = m_OutputStream.str();
+
+#ifdef _WIN32
+ WaitForSingleObject(m_Process, INFINITE);
+
+ DWORD exitcode;
+ GetExitCodeProcess(m_Process, &exitcode);
+
+ Log(LogNotice, "Process")
+ << "PID " << m_PID << " (" << PrettyPrintArguments(m_Arguments) << ") terminated with exit code " << exitcode;
+#else /* _WIN32 */
+ int status, exitcode;
+ if (could_not_kill || m_PID == -1) {
+ exitcode = 128;
+ } else if (ProcessWaitPID(m_Process, &status) != m_Process) {
+ exitcode = 128;
+
+ Log(LogWarning, "Process")
+ << "PID " << m_PID << " (" << PrettyPrintArguments(m_Arguments) << ") died mysteriously: waitpid failed";
+ } else if (WIFEXITED(status)) {
+ exitcode = WEXITSTATUS(status);
+
+ Log msg(LogNotice, "Process");
+ msg << "PID " << m_PID << " (" << PrettyPrintArguments(m_Arguments)
+ << ") terminated with exit code " << exitcode;
+
+ if (m_SentSigterm) {
+ exitcode = 128;
+ msg << " after sending SIGTERM";
+ }
+ } else if (WIFSIGNALED(status)) {
+ int signum = WTERMSIG(status);
+ const char *zsigname = strsignal(signum);
+
+ String signame = Convert::ToString(signum);
+
+ if (zsigname) {
+ signame += " (";
+ signame += zsigname;
+ signame += ")";
+ }
+
+ Log(LogWarning, "Process")
+ << "PID " << m_PID << " was terminated by signal " << signame;
+
+ std::ostringstream outputbuf;
+ outputbuf << "<Terminated by signal " << signame << ".>";
+ output = output + outputbuf.str();
+ exitcode = 128;
+ } else {
+ exitcode = 128;
+ }
+#endif /* _WIN32 */
+
+ {
+ std::lock_guard<std::mutex> lock(m_ResultMutex);
+ m_Result.PID = m_PID;
+ m_Result.ExecutionEnd = Utility::GetTime();
+ m_Result.ExitStatus = exitcode;
+ m_Result.Output = output;
+ m_ResultAvailable = true;
+ }
+ m_ResultCondition.notify_all();
+
+ if (m_Callback) {
+ /*
+ * Explicitly use Process::Ptr to keep the reference counted while the
+ * callback is active and making it crash safe
+ */
+ Process::Ptr process(this);
+ Utility::QueueAsyncCallback([this, process]() { m_Callback(m_Result); });
+ }
+
+ return false;
+}
+
+pid_t Process::GetPID() const
+{
+ return m_PID;
+}
+
+
+int Process::GetTID() const
+{
+ return (reinterpret_cast<uintptr_t>(this) / sizeof(void *)) % IOTHREADS;
+}
+
+double Process::GetNextTimeout() const
+{
+#ifdef _WIN32
+ return m_Timeout;
+#else /* _WIN32 */
+ return m_SentSigterm ? m_Timeout * 1.1 : m_Timeout;
+#endif /* _WIN32 */
+}
diff --git a/lib/base/process.hpp b/lib/base/process.hpp
new file mode 100644
index 0000000..d83ba6e
--- /dev/null
+++ b/lib/base/process.hpp
@@ -0,0 +1,117 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PROCESS_H
+#define PROCESS_H
+
+#include "base/i2-base.hpp"
+#include "base/dictionary.hpp"
+#include <iosfwd>
+#include <deque>
+#include <vector>
+#include <sstream>
+#include <mutex>
+#include <condition_variable>
+
+namespace icinga
+{
+
+/**
+ * The result of a Process task.
+ *
+ * @ingroup base
+ */
+struct ProcessResult
+{
+ pid_t PID;
+ double ExecutionStart;
+ double ExecutionEnd;
+ long ExitStatus;
+ String Output;
+};
+
+/**
+ * A process task. Executes an external application and returns the exit
+ * code and console output.
+ *
+ * @ingroup base
+ */
+class Process final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Process);
+
+#ifdef _WIN32
+ typedef String Arguments;
+ typedef HANDLE ProcessHandle;
+ typedef HANDLE ConsoleHandle;
+#else /* _WIN32 */
+ typedef std::vector<String> Arguments;
+ typedef pid_t ProcessHandle;
+ typedef int ConsoleHandle;
+#endif /* _WIN32 */
+
+ static const std::deque<Process::Ptr>::size_type MaxTasksPerThread = 512;
+
+ Process(Arguments arguments, Dictionary::Ptr extraEnvironment = nullptr);
+ ~Process() override;
+
+ void SetTimeout(double timeout);
+ double GetTimeout() const;
+
+ void SetAdjustPriority(bool adjust);
+ bool GetAdjustPriority() const;
+
+ void Run(const std::function<void (const ProcessResult&)>& callback = std::function<void (const ProcessResult&)>());
+
+ const ProcessResult& WaitForResult();
+
+ pid_t GetPID() const;
+
+ static Arguments PrepareCommand(const Value& command);
+
+ static void ThreadInitialize();
+
+ static String PrettyPrintArguments(const Arguments& arguments);
+
+#ifndef _WIN32
+ static void InitializeSpawnHelper();
+#endif /* _WIN32 */
+
+private:
+ Arguments m_Arguments;
+ Dictionary::Ptr m_ExtraEnvironment;
+
+ double m_Timeout;
+#ifndef _WIN32
+ bool m_SentSigterm;
+#endif /* _WIN32 */
+
+ bool m_AdjustPriority;
+
+ ProcessHandle m_Process;
+ pid_t m_PID;
+ ConsoleHandle m_FD;
+
+#ifdef _WIN32
+ bool m_ReadPending;
+ bool m_ReadFailed;
+ OVERLAPPED m_Overlapped;
+ char m_ReadBuffer[1024];
+#endif /* _WIN32 */
+
+ std::ostringstream m_OutputStream;
+ std::function<void (const ProcessResult&)> m_Callback;
+ ProcessResult m_Result;
+ bool m_ResultAvailable;
+ std::mutex m_ResultMutex;
+ std::condition_variable m_ResultCondition;
+
+ static void IOThreadProc(int tid);
+ bool DoEvents();
+ int GetTID() const;
+ double GetNextTimeout() const;
+};
+
+}
+
+#endif /* PROCESS_H */
diff --git a/lib/base/reference-script.cpp b/lib/base/reference-script.cpp
new file mode 100644
index 0000000..9408245
--- /dev/null
+++ b/lib/base/reference-script.cpp
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/reference.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+static void ReferenceSet(const Value& value)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Reference::Ptr self = static_cast<Reference::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ self->Set(value);
+}
+
+static Value ReferenceGet()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Reference::Ptr self = static_cast<Reference::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+ return self->Get();
+}
+
+Object::Ptr Reference::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "set", new Function("Reference#set", ReferenceSet, { "value" }) },
+ { "get", new Function("Reference#get", ReferenceGet, {}, true) },
+ });
+
+ return prototype;
+}
diff --git a/lib/base/reference.cpp b/lib/base/reference.cpp
new file mode 100644
index 0000000..b0104af
--- /dev/null
+++ b/lib/base/reference.cpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/reference.hpp"
+#include "base/debug.hpp"
+#include "base/primitivetype.hpp"
+#include "base/dictionary.hpp"
+#include "base/configwriter.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_PRIMITIVE_TYPE_NOINST(Reference, Object, Reference::GetPrototype());
+
+Reference::Reference(const Object::Ptr& parent, const String& index)
+ : m_Parent(parent), m_Index(index)
+{
+}
+
+Value Reference::Get() const
+{
+ return m_Parent->GetFieldByName(m_Index, true, DebugInfo());
+}
+
+void Reference::Set(const Value& value)
+{
+ m_Parent->SetFieldByName(m_Index, value, false, DebugInfo());
+}
+
+Object::Ptr Reference::GetParent() const
+{
+ return m_Parent;
+}
+
+String Reference::GetIndex() const
+{
+ return m_Index;
+}
diff --git a/lib/base/reference.hpp b/lib/base/reference.hpp
new file mode 100644
index 0000000..30faabe
--- /dev/null
+++ b/lib/base/reference.hpp
@@ -0,0 +1,40 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef REFERENCE_H
+#define REFERENCE_H
+
+#include "base/i2-base.hpp"
+#include "base/objectlock.hpp"
+#include "base/value.hpp"
+
+namespace icinga
+{
+
+/**
+ * A reference.
+ *
+ * @ingroup base
+ */
+class Reference final : public Object
+{
+public:
+ DECLARE_OBJECT(Reference);
+
+ Reference(const Object::Ptr& parent, const String& index);
+
+ Value Get() const;
+ void Set(const Value& value);
+
+ Object::Ptr GetParent() const;
+ String GetIndex() const;
+
+ static Object::Ptr GetPrototype();
+
+private:
+ Object::Ptr m_Parent;
+ String m_Index;
+};
+
+}
+
+#endif /* REFERENCE_H */
diff --git a/lib/base/registry.hpp b/lib/base/registry.hpp
new file mode 100644
index 0000000..c13f7e1
--- /dev/null
+++ b/lib/base/registry.hpp
@@ -0,0 +1,121 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef REGISTRY_H
+#define REGISTRY_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include <boost/signals2.hpp>
+#include <map>
+#include <mutex>
+
+namespace icinga
+{
+
+/**
+ * A registry.
+ *
+ * @ingroup base
+ */
+template<typename U, typename T>
+class Registry
+{
+public:
+ typedef std::map<String, T> ItemMap;
+
+ void RegisterIfNew(const String& name, const T& item)
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ if (m_Items.find(name) != m_Items.end())
+ return;
+
+ RegisterInternal(name, item, lock);
+ }
+
+ void Register(const String& name, const T& item)
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ RegisterInternal(name, item, lock);
+ }
+
+ void Unregister(const String& name)
+ {
+ size_t erased;
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ erased = m_Items.erase(name);
+ }
+
+ if (erased > 0)
+ OnUnregistered(name);
+ }
+
+ void Clear()
+ {
+ typename Registry<U, T>::ItemMap items;
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ items = m_Items;
+ }
+
+ for (const auto& kv : items) {
+ OnUnregistered(kv.first);
+ }
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Items.clear();
+ }
+ }
+
+ T GetItem(const String& name) const
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ auto it = m_Items.find(name);
+
+ if (it == m_Items.end())
+ return T();
+
+ return it->second;
+ }
+
+ ItemMap GetItems() const
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_Items; /* Makes a copy of the map. */
+ }
+
+ boost::signals2::signal<void (const String&, const T&)> OnRegistered;
+ boost::signals2::signal<void (const String&)> OnUnregistered;
+
+private:
+ mutable std::mutex m_Mutex;
+ typename Registry<U, T>::ItemMap m_Items;
+
+ void RegisterInternal(const String& name, const T& item, std::unique_lock<std::mutex>& lock)
+ {
+ bool old_item = false;
+
+ if (m_Items.erase(name) > 0)
+ old_item = true;
+
+ m_Items[name] = item;
+
+ lock.unlock();
+
+ if (old_item)
+ OnUnregistered(name);
+
+ OnRegistered(name, item);
+ }
+};
+
+}
+
+#endif /* REGISTRY_H */
diff --git a/lib/base/ringbuffer.cpp b/lib/base/ringbuffer.cpp
new file mode 100644
index 0000000..52e2ae5
--- /dev/null
+++ b/lib/base/ringbuffer.cpp
@@ -0,0 +1,91 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/ringbuffer.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include <algorithm>
+
+using namespace icinga;
+
+RingBuffer::RingBuffer(RingBuffer::SizeType slots)
+ : m_Slots(slots, 0), m_TimeValue(0), m_InsertedValues(0)
+{ }
+
+RingBuffer::SizeType RingBuffer::GetLength() const
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ return m_Slots.size();
+}
+
+void RingBuffer::InsertValue(RingBuffer::SizeType tv, int num)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ InsertValueUnlocked(tv, num);
+}
+
+void RingBuffer::InsertValueUnlocked(RingBuffer::SizeType tv, int num)
+{
+ RingBuffer::SizeType offsetTarget = tv % m_Slots.size();
+
+ if (m_TimeValue == 0)
+ m_InsertedValues = 1;
+
+ if (tv > m_TimeValue) {
+ RingBuffer::SizeType offset = m_TimeValue % m_Slots.size();
+
+ /* walk towards the target offset, resetting slots to 0 */
+ while (offset != offsetTarget) {
+ offset++;
+
+ if (offset >= m_Slots.size())
+ offset = 0;
+
+ m_Slots[offset] = 0;
+
+ if (m_TimeValue != 0 && m_InsertedValues < m_Slots.size())
+ m_InsertedValues++;
+ }
+
+ m_TimeValue = tv;
+ }
+
+ m_Slots[offsetTarget] += num;
+}
+
+int RingBuffer::UpdateAndGetValues(RingBuffer::SizeType tv, RingBuffer::SizeType span)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return UpdateAndGetValuesUnlocked(tv, span);
+}
+
+int RingBuffer::UpdateAndGetValuesUnlocked(RingBuffer::SizeType tv, RingBuffer::SizeType span)
+{
+ InsertValueUnlocked(tv, 0);
+
+ if (span > m_Slots.size())
+ span = m_Slots.size();
+
+ int off = m_TimeValue % m_Slots.size();
+ int sum = 0;
+ while (span > 0) {
+ sum += m_Slots[off];
+
+ if (off == 0)
+ off = m_Slots.size();
+
+ off--;
+ span--;
+ }
+
+ return sum;
+}
+
+double RingBuffer::CalculateRate(RingBuffer::SizeType tv, RingBuffer::SizeType span)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ int sum = UpdateAndGetValuesUnlocked(tv, span);
+ return sum / static_cast<double>(std::min(span, m_InsertedValues));
+}
diff --git a/lib/base/ringbuffer.hpp b/lib/base/ringbuffer.hpp
new file mode 100644
index 0000000..9fbef53
--- /dev/null
+++ b/lib/base/ringbuffer.hpp
@@ -0,0 +1,45 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef RINGBUFFER_H
+#define RINGBUFFER_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+#include <vector>
+#include <mutex>
+
+namespace icinga
+{
+
+/**
+ * A ring buffer that holds a pre-defined number of integers.
+ *
+ * @ingroup base
+ */
+class RingBuffer final
+{
+public:
+ DECLARE_PTR_TYPEDEFS(RingBuffer);
+
+ typedef std::vector<int>::size_type SizeType;
+
+ RingBuffer(SizeType slots);
+
+ SizeType GetLength() const;
+ void InsertValue(SizeType tv, int num);
+ int UpdateAndGetValues(SizeType tv, SizeType span);
+ double CalculateRate(SizeType tv, SizeType span);
+
+private:
+ mutable std::mutex m_Mutex;
+ std::vector<int> m_Slots;
+ SizeType m_TimeValue;
+ SizeType m_InsertedValues;
+
+ void InsertValueUnlocked(SizeType tv, int num);
+ int UpdateAndGetValuesUnlocked(SizeType tv, SizeType span);
+};
+
+}
+
+#endif /* RINGBUFFER_H */
diff --git a/lib/base/scriptframe.cpp b/lib/base/scriptframe.cpp
new file mode 100644
index 0000000..7a7f44c
--- /dev/null
+++ b/lib/base/scriptframe.cpp
@@ -0,0 +1,130 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/scriptframe.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/namespace.hpp"
+#include "base/exception.hpp"
+#include "base/configuration.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+boost::thread_specific_ptr<std::stack<ScriptFrame *> > ScriptFrame::m_ScriptFrames;
+
+static Namespace::Ptr l_SystemNS, l_StatsNS;
+
+/* Ensure that this gets called with highest priority
+ * and wins against other static initializers in lib/icinga, etc.
+ * LTO-enabled builds will cause trouble otherwise, see GH #6575.
+ */
+INITIALIZE_ONCE_WITH_PRIORITY([]() {
+ Namespace::Ptr globalNS = ScriptGlobal::GetGlobals();
+
+ l_SystemNS = new Namespace(true);
+ l_SystemNS->Set("PlatformKernel", Utility::GetPlatformKernel());
+ l_SystemNS->Set("PlatformKernelVersion", Utility::GetPlatformKernelVersion());
+ l_SystemNS->Set("PlatformName", Utility::GetPlatformName());
+ l_SystemNS->Set("PlatformVersion", Utility::GetPlatformVersion());
+ l_SystemNS->Set("PlatformArchitecture", Utility::GetPlatformArchitecture());
+ l_SystemNS->Set("BuildHostName", ICINGA_BUILD_HOST_NAME);
+ l_SystemNS->Set("BuildCompilerName", ICINGA_BUILD_COMPILER_NAME);
+ l_SystemNS->Set("BuildCompilerVersion", ICINGA_BUILD_COMPILER_VERSION);
+ globalNS->Set("System", l_SystemNS, true);
+
+ l_SystemNS->Set("Configuration", new Configuration());
+
+ l_StatsNS = new Namespace(true);
+ globalNS->Set("StatsFunctions", l_StatsNS, true);
+
+ globalNS->Set("Internal", new Namespace(), true);
+}, InitializePriority::CreateNamespaces);
+
+INITIALIZE_ONCE_WITH_PRIORITY([]() {
+ l_SystemNS->Freeze();
+ l_StatsNS->Freeze();
+}, InitializePriority::FreezeNamespaces);
+
+ScriptFrame::ScriptFrame(bool allocLocals)
+ : Locals(allocLocals ? new Dictionary() : nullptr), Self(ScriptGlobal::GetGlobals()), Sandboxed(false), Depth(0)
+{
+ InitializeFrame();
+}
+
+ScriptFrame::ScriptFrame(bool allocLocals, Value self)
+ : Locals(allocLocals ? new Dictionary() : nullptr), Self(std::move(self)), Sandboxed(false), Depth(0)
+{
+ InitializeFrame();
+}
+
+void ScriptFrame::InitializeFrame()
+{
+ std::stack<ScriptFrame *> *frames = m_ScriptFrames.get();
+
+ if (frames && !frames->empty()) {
+ ScriptFrame *frame = frames->top();
+
+ Sandboxed = frame->Sandboxed;
+ }
+
+ PushFrame(this);
+}
+
+ScriptFrame::~ScriptFrame()
+{
+ ScriptFrame *frame = PopFrame();
+ ASSERT(frame == this);
+
+#ifndef I2_DEBUG
+ (void)frame;
+#endif /* I2_DEBUG */
+}
+
+void ScriptFrame::IncreaseStackDepth()
+{
+ if (Depth + 1 > 300)
+ BOOST_THROW_EXCEPTION(ScriptError("Stack overflow while evaluating expression: Recursion level too deep."));
+
+ Depth++;
+}
+
+void ScriptFrame::DecreaseStackDepth()
+{
+ Depth--;
+}
+
+ScriptFrame *ScriptFrame::GetCurrentFrame()
+{
+ std::stack<ScriptFrame *> *frames = m_ScriptFrames.get();
+
+ ASSERT(!frames->empty());
+ return frames->top();
+}
+
+ScriptFrame *ScriptFrame::PopFrame()
+{
+ std::stack<ScriptFrame *> *frames = m_ScriptFrames.get();
+
+ ASSERT(!frames->empty());
+
+ ScriptFrame *frame = frames->top();
+ frames->pop();
+
+ return frame;
+}
+
+void ScriptFrame::PushFrame(ScriptFrame *frame)
+{
+ std::stack<ScriptFrame *> *frames = m_ScriptFrames.get();
+
+ if (!frames) {
+ frames = new std::stack<ScriptFrame *>();
+ m_ScriptFrames.reset(frames);
+ }
+
+ if (!frames->empty()) {
+ ScriptFrame *parent = frames->top();
+ frame->Depth += parent->Depth;
+ }
+
+ frames->push(frame);
+}
diff --git a/lib/base/scriptframe.hpp b/lib/base/scriptframe.hpp
new file mode 100644
index 0000000..18e23ef
--- /dev/null
+++ b/lib/base/scriptframe.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SCRIPTFRAME_H
+#define SCRIPTFRAME_H
+
+#include "base/i2-base.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include <boost/thread/tss.hpp>
+#include <stack>
+
+namespace icinga
+{
+
+struct ScriptFrame
+{
+ Dictionary::Ptr Locals;
+ Value Self;
+ bool Sandboxed;
+ int Depth;
+
+ ScriptFrame(bool allocLocals);
+ ScriptFrame(bool allocLocals, Value self);
+ ~ScriptFrame();
+
+ void IncreaseStackDepth();
+ void DecreaseStackDepth();
+
+ static ScriptFrame *GetCurrentFrame();
+
+private:
+ static boost::thread_specific_ptr<std::stack<ScriptFrame *> > m_ScriptFrames;
+
+ static void PushFrame(ScriptFrame *frame);
+ static ScriptFrame *PopFrame();
+
+ void InitializeFrame();
+};
+
+}
+
+#endif /* SCRIPTFRAME_H */
diff --git a/lib/base/scriptglobal.cpp b/lib/base/scriptglobal.cpp
new file mode 100644
index 0000000..e85e9ec
--- /dev/null
+++ b/lib/base/scriptglobal.cpp
@@ -0,0 +1,110 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/atomic-file.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/singleton.hpp"
+#include "base/logger.hpp"
+#include "base/stdiostream.hpp"
+#include "base/netstring.hpp"
+#include "base/json.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+#include "base/namespace.hpp"
+#include "base/utility.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+Namespace::Ptr ScriptGlobal::m_Globals = new Namespace();
+
+Value ScriptGlobal::Get(const String& name, const Value *defaultValue)
+{
+ Value result;
+
+ if (!m_Globals->Get(name, &result)) {
+ if (defaultValue)
+ return *defaultValue;
+
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Tried to access undefined script variable '" + name + "'"));
+ }
+
+ return result;
+}
+
+void ScriptGlobal::Set(const String& name, const Value& value)
+{
+ std::vector<String> tokens = name.Split(".");
+
+ if (tokens.empty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Name must not be empty"));
+
+ {
+ ObjectLock olock(m_Globals);
+
+ Namespace::Ptr parent = m_Globals;
+
+ for (std::vector<String>::size_type i = 0; i < tokens.size(); i++) {
+ const String& token = tokens[i];
+
+ if (i + 1 != tokens.size()) {
+ Value vparent;
+
+ if (!parent->Get(token, &vparent)) {
+ Namespace::Ptr dict = new Namespace();
+ parent->Set(token, dict);
+ parent = dict;
+ } else {
+ parent = vparent;
+ }
+ }
+ }
+
+ parent->Set(tokens[tokens.size() - 1], value);
+ }
+}
+
+void ScriptGlobal::SetConst(const String& name, const Value& value)
+{
+ GetGlobals()->Set(name, value, true);
+}
+
+bool ScriptGlobal::Exists(const String& name)
+{
+ return m_Globals->Contains(name);
+}
+
+Namespace::Ptr ScriptGlobal::GetGlobals()
+{
+ return m_Globals;
+}
+
+void ScriptGlobal::WriteToFile(const String& filename)
+{
+ Log(LogInformation, "ScriptGlobal")
+ << "Dumping variables to file '" << filename << "'";
+
+ AtomicFile fp (filename, 0600);
+ StdioStream::Ptr sfp = new StdioStream(&fp, false);
+
+ ObjectLock olock(m_Globals);
+ for (const Namespace::Pair& kv : m_Globals) {
+ Value value = kv.second.Val;
+
+ if (value.IsObject())
+ value = Convert::ToString(value);
+
+ Dictionary::Ptr persistentVariable = new Dictionary({
+ { "name", kv.first },
+ { "value", value }
+ });
+
+ String json = JsonEncode(persistentVariable);
+
+ NetString::WriteStringToStream(sfp, json);
+ }
+
+ sfp->Close();
+ fp.Commit();
+}
+
diff --git a/lib/base/scriptglobal.hpp b/lib/base/scriptglobal.hpp
new file mode 100644
index 0000000..f349b7b
--- /dev/null
+++ b/lib/base/scriptglobal.hpp
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SCRIPTGLOBAL_H
+#define SCRIPTGLOBAL_H
+
+#include "base/i2-base.hpp"
+#include "base/namespace.hpp"
+
+namespace icinga
+{
+
+/**
+ * Global script variables.
+ *
+ * @ingroup base
+ */
+class ScriptGlobal
+{
+public:
+ static Value Get(const String& name, const Value *defaultValue = nullptr);
+ static void Set(const String& name, const Value& value);
+ static void SetConst(const String& name, const Value& value);
+ static bool Exists(const String& name);
+
+ static void WriteToFile(const String& filename);
+
+ static Namespace::Ptr GetGlobals();
+
+private:
+ static Namespace::Ptr m_Globals;
+};
+
+}
+
+#endif /* SCRIPTGLOBAL_H */
diff --git a/lib/base/scriptutils.cpp b/lib/base/scriptutils.cpp
new file mode 100644
index 0000000..7fe856d
--- /dev/null
+++ b/lib/base/scriptutils.cpp
@@ -0,0 +1,570 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/scriptutils.hpp"
+#include "base/function.hpp"
+#include "base/scriptframe.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/configtype.hpp"
+#include "base/application.hpp"
+#include "base/dependencygraph.hpp"
+#include "base/initialize.hpp"
+#include "base/namespace.hpp"
+#include "config/configitem.hpp"
+#include <boost/regex.hpp>
+#include <algorithm>
+#include <set>
+#ifdef _WIN32
+#include <msi.h>
+#endif /* _WIN32 */
+
+using namespace icinga;
+
+REGISTER_SAFE_FUNCTION(System, regex, &ScriptUtils::Regex, "pattern:text:mode");
+REGISTER_SAFE_FUNCTION(System, match, &ScriptUtils::Match, "pattern:text:mode");
+REGISTER_SAFE_FUNCTION(System, cidr_match, &ScriptUtils::CidrMatch, "pattern:ip:mode");
+REGISTER_SAFE_FUNCTION(System, len, &ScriptUtils::Len, "value");
+REGISTER_SAFE_FUNCTION(System, union, &ScriptUtils::Union, "");
+REGISTER_SAFE_FUNCTION(System, intersection, &ScriptUtils::Intersection, "");
+REGISTER_FUNCTION(System, log, &ScriptUtils::Log, "severity:facility:value");
+REGISTER_FUNCTION(System, range, &ScriptUtils::Range, "start:end:increment");
+REGISTER_FUNCTION(System, exit, &Application::Exit, "status");
+REGISTER_SAFE_FUNCTION(System, typeof, &ScriptUtils::TypeOf, "value");
+REGISTER_SAFE_FUNCTION(System, keys, &ScriptUtils::Keys, "value");
+REGISTER_SAFE_FUNCTION(System, random, &Utility::Random, "");
+REGISTER_SAFE_FUNCTION(System, get_template, &ScriptUtils::GetTemplate, "type:name");
+REGISTER_SAFE_FUNCTION(System, get_templates, &ScriptUtils::GetTemplates, "type");
+REGISTER_SAFE_FUNCTION(System, get_object, &ScriptUtils::GetObject, "type:name");
+REGISTER_SAFE_FUNCTION(System, get_objects, &ScriptUtils::GetObjects, "type");
+REGISTER_FUNCTION(System, assert, &ScriptUtils::Assert, "value");
+REGISTER_SAFE_FUNCTION(System, string, &ScriptUtils::CastString, "value");
+REGISTER_SAFE_FUNCTION(System, number, &ScriptUtils::CastNumber, "value");
+REGISTER_SAFE_FUNCTION(System, bool, &ScriptUtils::CastBool, "value");
+REGISTER_SAFE_FUNCTION(System, get_time, &Utility::GetTime, "");
+REGISTER_SAFE_FUNCTION(System, basename, &Utility::BaseName, "path");
+REGISTER_SAFE_FUNCTION(System, dirname, &Utility::DirName, "path");
+REGISTER_SAFE_FUNCTION(System, getenv, &ScriptUtils::GetEnv, "value");
+REGISTER_SAFE_FUNCTION(System, msi_get_component_path, &ScriptUtils::MsiGetComponentPathShim, "component");
+REGISTER_SAFE_FUNCTION(System, track_parents, &ScriptUtils::TrackParents, "child");
+REGISTER_SAFE_FUNCTION(System, escape_shell_cmd, &Utility::EscapeShellCmd, "cmd");
+REGISTER_SAFE_FUNCTION(System, escape_shell_arg, &Utility::EscapeShellArg, "arg");
+#ifdef _WIN32
+REGISTER_SAFE_FUNCTION(System, escape_create_process_arg, &Utility::EscapeCreateProcessArg, "arg");
+#endif /* _WIN32 */
+REGISTER_FUNCTION(System, ptr, &ScriptUtils::Ptr, "object");
+REGISTER_FUNCTION(System, sleep, &Utility::Sleep, "interval");
+REGISTER_FUNCTION(System, path_exists, &Utility::PathExists, "path");
+REGISTER_FUNCTION(System, glob, &ScriptUtils::Glob, "pathspec:callback:type");
+REGISTER_FUNCTION(System, glob_recursive, &ScriptUtils::GlobRecursive, "pathspec:callback:type");
+
+INITIALIZE_ONCE(&ScriptUtils::StaticInitialize);
+
+enum MatchType
+{
+ MatchAll,
+ MatchAny
+};
+
+void ScriptUtils::StaticInitialize()
+{
+ ScriptGlobal::Set("System.MatchAll", MatchAll);
+ ScriptGlobal::Set("System.MatchAny", MatchAny);
+
+ ScriptGlobal::Set("System.GlobFile", GlobFile);
+ ScriptGlobal::Set("System.GlobDirectory", GlobDirectory);
+}
+
+String ScriptUtils::CastString(const Value& value)
+{
+ return value;
+}
+
+double ScriptUtils::CastNumber(const Value& value)
+{
+ return value;
+}
+
+bool ScriptUtils::CastBool(const Value& value)
+{
+ return value.ToBool();
+}
+
+bool ScriptUtils::Regex(const std::vector<Value>& args)
+{
+ if (args.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Regular expression and text must be specified for regex()."));
+
+ String pattern = args[0];
+ const Value& argTexts = args[1];
+
+ if (argTexts.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionaries are not supported by regex()."));
+
+ MatchType mode;
+
+ if (args.size() > 2)
+ mode = static_cast<MatchType>(static_cast<int>(args[2]));
+ else
+ mode = MatchAll;
+
+ boost::regex expr(pattern.GetData());
+
+ Array::Ptr texts;
+
+ if (argTexts.IsObject())
+ texts = argTexts;
+
+ if (texts) {
+ ObjectLock olock(texts);
+
+ if (texts->GetLength() == 0)
+ return false;
+
+ for (const String& text : texts) {
+ bool res = false;
+ try {
+ boost::smatch what;
+ res = boost::regex_search(text.GetData(), what, expr);
+ } catch (boost::exception&) {
+ res = false; /* exception means something went terribly wrong */
+ }
+
+ if (mode == MatchAny && res)
+ return true;
+ else if (mode == MatchAll && !res)
+ return false;
+ }
+
+ /* MatchAny: Nothing matched. MatchAll: Everything matched. */
+ return mode == MatchAll;
+ } else {
+ String text = argTexts;
+ boost::smatch what;
+ return boost::regex_search(text.GetData(), what, expr);
+ }
+}
+
+bool ScriptUtils::Match(const std::vector<Value>& args)
+{
+ if (args.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Pattern and text must be specified for match()."));
+
+ String pattern = args[0];
+ const Value& argTexts = args[1];
+
+ if (argTexts.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionaries are not supported by match()."));
+
+ MatchType mode;
+
+ if (args.size() > 2)
+ mode = static_cast<MatchType>(static_cast<int>(args[2]));
+ else
+ mode = MatchAll;
+
+ Array::Ptr texts;
+
+ if (argTexts.IsObject())
+ texts = argTexts;
+
+ if (texts) {
+ ObjectLock olock(texts);
+
+ if (texts->GetLength() == 0)
+ return false;
+
+ for (const String& text : texts) {
+ bool res = Utility::Match(pattern, text);
+
+ if (mode == MatchAny && res)
+ return true;
+ else if (mode == MatchAll && !res)
+ return false;
+ }
+
+ /* MatchAny: Nothing matched. MatchAll: Everything matched. */
+ return mode == MatchAll;
+ } else {
+ String text = argTexts;
+ return Utility::Match(pattern, argTexts);
+ }
+}
+
+bool ScriptUtils::CidrMatch(const std::vector<Value>& args)
+{
+ if (args.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("CIDR and IP address must be specified for cidr_match()."));
+
+ String pattern = args[0];
+ const Value& argIps = args[1];
+
+ if (argIps.IsObjectType<Dictionary>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionaries are not supported by cidr_match()."));
+
+ MatchType mode;
+
+ if (args.size() > 2)
+ mode = static_cast<MatchType>(static_cast<int>(args[2]));
+ else
+ mode = MatchAll;
+
+ Array::Ptr ips;
+
+ if (argIps.IsObject())
+ ips = argIps;
+
+ if (ips) {
+ ObjectLock olock(ips);
+
+ if (ips->GetLength() == 0)
+ return false;
+
+ for (const String& ip : ips) {
+ bool res = Utility::CidrMatch(pattern, ip);
+
+ if (mode == MatchAny && res)
+ return true;
+ else if (mode == MatchAll && !res)
+ return false;
+ }
+
+ /* MatchAny: Nothing matched. MatchAll: Everything matched. */
+ return mode == MatchAll;
+ } else {
+ String ip = argIps;
+ return Utility::CidrMatch(pattern, ip);
+ }
+}
+
+double ScriptUtils::Len(const Value& value)
+{
+ if (value.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = value;
+ return dict->GetLength();
+ } else if (value.IsObjectType<Array>()) {
+ Array::Ptr array = value;
+ return array->GetLength();
+ } else if (value.IsString()) {
+ return Convert::ToString(value).GetLength();
+ } else {
+ return 0;
+ }
+}
+
+Array::Ptr ScriptUtils::Union(const std::vector<Value>& arguments)
+{
+ std::set<Value> values;
+
+ for (const Value& varr : arguments) {
+ Array::Ptr arr = varr;
+
+ if (arr) {
+ ObjectLock olock(arr);
+ for (const Value& value : arr) {
+ values.insert(value);
+ }
+ }
+ }
+
+ return Array::FromSet(values);
+}
+
+Array::Ptr ScriptUtils::Intersection(const std::vector<Value>& arguments)
+{
+ if (arguments.size() == 0)
+ return new Array();
+
+ Array::Ptr result = new Array();
+
+ Array::Ptr arg1 = arguments[0];
+
+ if (!arg1)
+ return result;
+
+ Array::Ptr arr1 = arg1->ShallowClone();
+
+ for (std::vector<Value>::size_type i = 1; i < arguments.size(); i++) {
+ {
+ ObjectLock olock(arr1);
+ std::sort(arr1->Begin(), arr1->End());
+ }
+
+ Array::Ptr arg2 = arguments[i];
+
+ if (!arg2)
+ return result;
+
+ Array::Ptr arr2 = arg2->ShallowClone();
+ {
+ ObjectLock olock(arr2);
+ std::sort(arr2->Begin(), arr2->End());
+ }
+
+ result->Resize(std::max(arr1->GetLength(), arr2->GetLength()));
+ Array::SizeType len;
+ {
+ ObjectLock olock(arr1), xlock(arr2), ylock(result);
+ auto it = std::set_intersection(arr1->Begin(), arr1->End(), arr2->Begin(), arr2->End(), result->Begin());
+ len = it - result->Begin();
+ }
+ result->Resize(len);
+ arr1 = result;
+ }
+
+ return result;
+}
+
+void ScriptUtils::Log(const std::vector<Value>& arguments)
+{
+ if (arguments.size() != 1 && arguments.size() != 3)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid number of arguments for log()"));
+
+ LogSeverity severity;
+ String facility;
+ Value message;
+
+ if (arguments.size() == 1) {
+ severity = LogInformation;
+ facility = "config";
+ message = arguments[0];
+ } else {
+ auto sval = static_cast<int>(arguments[0]);
+ severity = static_cast<LogSeverity>(sval);
+ facility = arguments[1];
+ message = arguments[2];
+ }
+
+ if (message.IsString() || (!message.IsObjectType<Array>() && !message.IsObjectType<Dictionary>()))
+ ::Log(severity, facility, message);
+ else
+ ::Log(severity, facility, JsonEncode(message));
+}
+
+Array::Ptr ScriptUtils::Range(const std::vector<Value>& arguments)
+{
+ double start, end, increment;
+
+ switch (arguments.size()) {
+ case 1:
+ start = 0;
+ end = arguments[0];
+ increment = 1;
+ break;
+ case 2:
+ start = arguments[0];
+ end = arguments[1];
+ increment = 1;
+ break;
+ case 3:
+ start = arguments[0];
+ end = arguments[1];
+ increment = arguments[2];
+ break;
+ default:
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid number of arguments for range()"));
+ }
+
+ ArrayData result;
+
+ if ((start < end && increment <= 0) ||
+ (start > end && increment >= 0))
+ return new Array();
+
+ for (double i = start; (increment > 0 ? i < end : i > end); i += increment)
+ result.push_back(i);
+
+ return new Array(std::move(result));
+}
+
+Type::Ptr ScriptUtils::TypeOf(const Value& value)
+{
+ return value.GetReflectionType();
+}
+
+Array::Ptr ScriptUtils::Keys(const Object::Ptr& obj)
+{
+ ArrayData result;
+
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>(obj);
+
+ if (dict) {
+ ObjectLock olock(dict);
+ for (const Dictionary::Pair& kv : dict) {
+ result.push_back(kv.first);
+ }
+ }
+
+ Namespace::Ptr ns = dynamic_pointer_cast<Namespace>(obj);
+
+ if (ns) {
+ ObjectLock olock(ns);
+ for (const Namespace::Pair& kv : ns) {
+ result.push_back(kv.first);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+static Dictionary::Ptr GetTargetForTemplate(const ConfigItem::Ptr& item)
+{
+ DebugInfo di = item->GetDebugInfo();
+
+ return new Dictionary({
+ { "name", item->GetName() },
+ { "type", item->GetType()->GetName() },
+ { "location", new Dictionary({
+ { "path", di.Path },
+ { "first_line", di.FirstLine },
+ { "first_column", di.FirstColumn },
+ { "last_line", di.LastLine },
+ { "last_column", di.LastColumn }
+ }) }
+ });
+}
+
+Dictionary::Ptr ScriptUtils::GetTemplate(const Value& vtype, const String& name)
+{
+ Type::Ptr ptype;
+
+ if (vtype.IsObjectType<Type>())
+ ptype = vtype;
+ else
+ ptype = Type::GetByName(vtype);
+
+ ConfigItem::Ptr item = ConfigItem::GetByTypeAndName(ptype, name);
+
+ if (!item || !item->IsAbstract())
+ return nullptr;
+
+ DebugInfo di = item->GetDebugInfo();
+
+ return GetTargetForTemplate(item);
+}
+
+Array::Ptr ScriptUtils::GetTemplates(const Type::Ptr& type)
+{
+ if (!type)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid type: Must not be null"));
+
+ ArrayData result;
+
+ for (const ConfigItem::Ptr& item : ConfigItem::GetItems(type)) {
+ if (item->IsAbstract())
+ result.push_back(GetTargetForTemplate(item));
+ }
+
+ return new Array(std::move(result));
+}
+
+ConfigObject::Ptr ScriptUtils::GetObject(const Value& vtype, const String& name)
+{
+ Type::Ptr ptype;
+
+ if (vtype.IsObjectType<Type>())
+ ptype = vtype;
+ else
+ ptype = Type::GetByName(vtype);
+
+ auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (!ctype)
+ return nullptr;
+
+ return ctype->GetObject(name);
+}
+
+Array::Ptr ScriptUtils::GetObjects(const Type::Ptr& type)
+{
+ if (!type)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid type: Must not be null"));
+
+ auto *ctype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!ctype)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid type: Type must inherit from 'ConfigObject'"));
+
+ ArrayData result;
+
+ for (const ConfigObject::Ptr& object : ctype->GetObjects())
+ result.push_back(object);
+
+ return new Array(std::move(result));
+}
+
+void ScriptUtils::Assert(const Value& arg)
+{
+ if (!arg.ToBool())
+ BOOST_THROW_EXCEPTION(std::runtime_error("Assertion failed"));
+}
+
+String ScriptUtils::MsiGetComponentPathShim(const String& component)
+{
+#ifdef _WIN32
+ TCHAR productCode[39];
+ if (MsiGetProductCode(component.CStr(), productCode) != ERROR_SUCCESS)
+ return "";
+ TCHAR path[2048];
+ DWORD szPath = sizeof(path);
+ path[0] = '\0';
+ MsiGetComponentPath(productCode, component.CStr(), path, &szPath);
+ return path;
+#else /* _WIN32 */
+ return String();
+#endif /* _WIN32 */
+}
+
+Array::Ptr ScriptUtils::TrackParents(const Object::Ptr& child)
+{
+ return Array::FromVector(DependencyGraph::GetParents(child));
+}
+
+double ScriptUtils::Ptr(const Object::Ptr& object)
+{
+ return reinterpret_cast<intptr_t>(object.get());
+}
+
+Value ScriptUtils::Glob(const std::vector<Value>& args)
+{
+ if (args.size() < 1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Path must be specified."));
+
+ String pathSpec = args[0];
+ int type = GlobFile | GlobDirectory;
+
+ if (args.size() > 1)
+ type = args[1];
+
+ std::vector<String> paths;
+ Utility::Glob(pathSpec, [&paths](const String& path) { paths.push_back(path); }, type);
+
+ return Array::FromVector(paths);
+}
+
+Value ScriptUtils::GlobRecursive(const std::vector<Value>& args)
+{
+ if (args.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Path and pattern must be specified."));
+
+ String path = args[0];
+ String pattern = args[1];
+
+ int type = GlobFile | GlobDirectory;
+
+ if (args.size() > 2)
+ type = args[2];
+
+ std::vector<String> paths;
+ Utility::GlobRecursive(path, pattern, [&paths](const String& newPath) { paths.push_back(newPath); }, type);
+
+ return Array::FromVector(paths);
+}
+
+String ScriptUtils::GetEnv(const String& key)
+{
+ return Utility::GetFromEnvironment(key);
+}
diff --git a/lib/base/scriptutils.hpp b/lib/base/scriptutils.hpp
new file mode 100644
index 0000000..7bd3e8b
--- /dev/null
+++ b/lib/base/scriptutils.hpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SCRIPTUTILS_H
+#define SCRIPTUTILS_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include "base/array.hpp"
+#include "base/dictionary.hpp"
+#include "base/type.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup base
+ */
+class ScriptUtils
+{
+public:
+ static void StaticInitialize();
+ static String CastString(const Value& value);
+ static double CastNumber(const Value& value);
+ static bool CastBool(const Value& value);
+ static bool Regex(const std::vector<Value>& args);
+ static bool Match(const std::vector<Value>& args);
+ static bool CidrMatch(const std::vector<Value>& args);
+ static double Len(const Value& value);
+ static Array::Ptr Union(const std::vector<Value>& arguments);
+ static Array::Ptr Intersection(const std::vector<Value>& arguments);
+ static void Log(const std::vector<Value>& arguments);
+ static Array::Ptr Range(const std::vector<Value>& arguments);
+ static Type::Ptr TypeOf(const Value& value);
+ static Array::Ptr Keys(const Object::Ptr& obj);
+ static Dictionary::Ptr GetTemplate(const Value& vtype, const String& name);
+ static Array::Ptr GetTemplates(const Type::Ptr& type);
+ static ConfigObject::Ptr GetObject(const Value& type, const String& name);
+ static Array::Ptr GetObjects(const Type::Ptr& type);
+ static void Assert(const Value& arg);
+ static String MsiGetComponentPathShim(const String& component);
+ static Array::Ptr TrackParents(const Object::Ptr& parent);
+ static double Ptr(const Object::Ptr& object);
+ static Value Glob(const std::vector<Value>& args);
+ static Value GlobRecursive(const std::vector<Value>& args);
+ static String GetEnv(const String& key);
+
+private:
+ ScriptUtils();
+};
+
+}
+
+#endif /* SCRIPTUTILS_H */
diff --git a/lib/base/serializer.cpp b/lib/base/serializer.cpp
new file mode 100644
index 0000000..b8b140a
--- /dev/null
+++ b/lib/base/serializer.cpp
@@ -0,0 +1,331 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/serializer.hpp"
+#include "base/type.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include "base/namespace.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <deque>
+#include <utility>
+
+using namespace icinga;
+
+struct SerializeStackEntry
+{
+ String Name;
+ Value Val;
+};
+
+CircularReferenceError::CircularReferenceError(String message, std::vector<String> path)
+ : m_Message(message), m_Path(path)
+{ }
+
+const char *CircularReferenceError::what(void) const throw()
+{
+ return m_Message.CStr();
+}
+
+std::vector<String> CircularReferenceError::GetPath() const
+{
+ return m_Path;
+}
+
+struct SerializeStack
+{
+ std::deque<SerializeStackEntry> Entries;
+
+ inline void Push(const String& name, const Value& val)
+ {
+ Object::Ptr obj;
+
+ if (val.IsObject())
+ obj = val;
+
+ if (obj) {
+ for (const auto& entry : Entries) {
+ if (entry.Val == obj) {
+ std::vector<String> path;
+ for (const auto& entry : Entries)
+ path.push_back(entry.Name);
+ path.push_back(name);
+ BOOST_THROW_EXCEPTION(CircularReferenceError("Cannot serialize object which recursively refers to itself. Attribute path which leads to the cycle: " + boost::algorithm::join(path, " -> "), path));
+ }
+ }
+ }
+
+ Entries.push_back({ name, obj });
+ }
+
+ inline void Pop()
+ {
+ Entries.pop_back();
+ }
+};
+
+static Value SerializeInternal(const Value& value, int attributeTypes, SerializeStack& stack, bool dryRun);
+
+static Array::Ptr SerializeArray(const Array::Ptr& input, int attributeTypes, SerializeStack& stack, bool dryRun)
+{
+ ArrayData result;
+
+ if (!dryRun) {
+ result.reserve(input->GetLength());
+ }
+
+ ObjectLock olock(input);
+
+ int index = 0;
+
+ for (const Value& value : input) {
+ stack.Push(Convert::ToString(index), value);
+
+ auto serialized (SerializeInternal(value, attributeTypes, stack, dryRun));
+
+ if (!dryRun) {
+ result.emplace_back(std::move(serialized));
+ }
+
+ stack.Pop();
+ index++;
+ }
+
+ return dryRun ? nullptr : new Array(std::move(result));
+}
+
+static Dictionary::Ptr SerializeDictionary(const Dictionary::Ptr& input, int attributeTypes, SerializeStack& stack, bool dryRun)
+{
+ DictionaryData result;
+
+ if (!dryRun) {
+ result.reserve(input->GetLength());
+ }
+
+ ObjectLock olock(input);
+
+ for (const Dictionary::Pair& kv : input) {
+ stack.Push(kv.first, kv.second);
+
+ auto serialized (SerializeInternal(kv.second, attributeTypes, stack, dryRun));
+
+ if (!dryRun) {
+ result.emplace_back(kv.first, std::move(serialized));
+ }
+
+ stack.Pop();
+ }
+
+ return dryRun ? nullptr : new Dictionary(std::move(result));
+}
+
+static Dictionary::Ptr SerializeNamespace(const Namespace::Ptr& input, int attributeTypes, SerializeStack& stack, bool dryRun)
+{
+ DictionaryData result;
+
+ if (!dryRun) {
+ result.reserve(input->GetLength());
+ }
+
+ ObjectLock olock(input);
+
+ for (const Namespace::Pair& kv : input) {
+ Value val = kv.second.Val;
+ stack.Push(kv.first, val);
+
+ auto serialized (SerializeInternal(val, attributeTypes, stack, dryRun));
+
+ if (!dryRun) {
+ result.emplace_back(kv.first, std::move(serialized));
+ }
+
+ stack.Pop();
+ }
+
+ return dryRun ? nullptr : new Dictionary(std::move(result));
+}
+
+static Object::Ptr SerializeObject(const Object::Ptr& input, int attributeTypes, SerializeStack& stack, bool dryRun)
+{
+ Type::Ptr type = input->GetReflectionType();
+
+ if (!type)
+ return nullptr;
+
+ DictionaryData fields;
+
+ if (!dryRun) {
+ fields.reserve(type->GetFieldCount() + 1);
+ }
+
+ ObjectLock olock(input);
+
+ for (int i = 0; i < type->GetFieldCount(); i++) {
+ Field field = type->GetFieldInfo(i);
+
+ if (attributeTypes != 0 && (field.Attributes & attributeTypes) == 0)
+ continue;
+
+ if (strcmp(field.Name, "type") == 0)
+ continue;
+
+ Value value = input->GetField(i);
+ stack.Push(field.Name, value);
+
+ auto serialized (SerializeInternal(value, attributeTypes, stack, dryRun));
+
+ if (!dryRun) {
+ fields.emplace_back(field.Name, std::move(serialized));
+ }
+
+ stack.Pop();
+ }
+
+ if (!dryRun) {
+ fields.emplace_back("type", type->GetName());
+ }
+
+ return dryRun ? nullptr : new Dictionary(std::move(fields));
+}
+
+static Array::Ptr DeserializeArray(const Array::Ptr& input, bool safe_mode, int attributeTypes)
+{
+ ArrayData result;
+
+ result.reserve(input->GetLength());
+
+ ObjectLock olock(input);
+
+ for (const Value& value : input) {
+ result.emplace_back(Deserialize(value, safe_mode, attributeTypes));
+ }
+
+ return new Array(std::move(result));
+}
+
+static Dictionary::Ptr DeserializeDictionary(const Dictionary::Ptr& input, bool safe_mode, int attributeTypes)
+{
+ DictionaryData result;
+
+ result.reserve(input->GetLength());
+
+ ObjectLock olock(input);
+
+ for (const Dictionary::Pair& kv : input) {
+ result.emplace_back(kv.first, Deserialize(kv.second, safe_mode, attributeTypes));
+ }
+
+ return new Dictionary(std::move(result));
+}
+
+static Object::Ptr DeserializeObject(const Object::Ptr& object, const Dictionary::Ptr& input, bool safe_mode, int attributeTypes)
+{
+ if (!object && safe_mode)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Tried to instantiate object while safe mode is enabled."));
+
+ Type::Ptr type;
+
+ if (object)
+ type = object->GetReflectionType();
+ else
+ type = Type::GetByName(input->Get("type"));
+
+ if (!type)
+ return object;
+
+ Object::Ptr instance;
+
+ if (object)
+ instance = object;
+ else
+ instance = type->Instantiate(std::vector<Value>());
+
+ ObjectLock olock(input);
+ for (const Dictionary::Pair& kv : input) {
+ if (kv.first.IsEmpty())
+ continue;
+
+ int fid = type->GetFieldId(kv.first);
+
+ if (fid < 0)
+ continue;
+
+ Field field = type->GetFieldInfo(fid);
+
+ if ((field.Attributes & attributeTypes) == 0)
+ continue;
+
+ try {
+ instance->SetField(fid, Deserialize(kv.second, safe_mode, attributeTypes), true);
+ } catch (const std::exception&) {
+ instance->SetField(fid, Empty);
+ }
+ }
+
+ return instance;
+}
+
+static Value SerializeInternal(const Value& value, int attributeTypes, SerializeStack& stack, bool dryRun)
+{
+ if (!value.IsObject())
+ return dryRun ? Empty : value;
+
+ Object::Ptr input = value;
+
+ Array::Ptr array = dynamic_pointer_cast<Array>(input);
+
+ if (array)
+ return SerializeArray(array, attributeTypes, stack, dryRun);
+
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>(input);
+
+ if (dict)
+ return SerializeDictionary(dict, attributeTypes, stack, dryRun);
+
+ Namespace::Ptr ns = dynamic_pointer_cast<Namespace>(input);
+
+ if (ns)
+ return SerializeNamespace(ns, attributeTypes, stack, dryRun);
+
+ return SerializeObject(input, attributeTypes, stack, dryRun);
+}
+
+void icinga::AssertNoCircularReferences(const Value& value)
+{
+ SerializeStack stack;
+ SerializeInternal(value, FAConfig, stack, true);
+}
+
+Value icinga::Serialize(const Value& value, int attributeTypes)
+{
+ SerializeStack stack;
+ return SerializeInternal(value, attributeTypes, stack, false);
+}
+
+Value icinga::Deserialize(const Value& value, bool safe_mode, int attributeTypes)
+{
+ return Deserialize(nullptr, value, safe_mode, attributeTypes);
+}
+
+Value icinga::Deserialize(const Object::Ptr& object, const Value& value, bool safe_mode, int attributeTypes)
+{
+ if (!value.IsObject())
+ return value;
+
+ Object::Ptr input = value;
+
+ Array::Ptr array = dynamic_pointer_cast<Array>(input);
+
+ if (array)
+ return DeserializeArray(array, safe_mode, attributeTypes);
+
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>(input);
+
+ ASSERT(dict);
+
+ if ((safe_mode && !object) || !dict->Contains("type"))
+ return DeserializeDictionary(dict, safe_mode, attributeTypes);
+ else
+ return DeserializeObject(object, dict, safe_mode, attributeTypes);
+}
diff --git a/lib/base/serializer.hpp b/lib/base/serializer.hpp
new file mode 100644
index 0000000..f055b3b
--- /dev/null
+++ b/lib/base/serializer.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERIALIZER_H
+#define SERIALIZER_H
+
+#include "base/i2-base.hpp"
+#include "base/type.hpp"
+#include "base/value.hpp"
+#include "base/exception.hpp"
+
+namespace icinga
+{
+
+class CircularReferenceError : virtual public user_error
+{
+public:
+ CircularReferenceError(String message, std::vector<String> path);
+
+ const char *what(void) const throw() final;
+ std::vector<String> GetPath() const;
+
+private:
+ String m_Message;
+ std::vector<String> m_Path;
+};
+
+void AssertNoCircularReferences(const Value& value);
+Value Serialize(const Value& value, int attributeTypes = FAState);
+Value Deserialize(const Value& value, bool safe_mode = false, int attributeTypes = FAState);
+Value Deserialize(const Object::Ptr& object, const Value& value, bool safe_mode = false, int attributeTypes = FAState);
+
+}
+
+#endif /* SERIALIZER_H */
diff --git a/lib/base/shared-memory.hpp b/lib/base/shared-memory.hpp
new file mode 100644
index 0000000..dd350c8
--- /dev/null
+++ b/lib/base/shared-memory.hpp
@@ -0,0 +1,45 @@
+/* Icinga 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+#pragma once
+
+#include <boost/interprocess/anonymous_shared_memory.hpp>
+#include <utility>
+
+namespace icinga
+{
+
+/**
+ * Type-safe memory shared across fork(2).
+ *
+ * @ingroup base
+ */
+template<class T>
+class SharedMemory
+{
+public:
+ template<class... Args>
+ SharedMemory(Args&&... args) : m_Memory(boost::interprocess::anonymous_shared_memory(sizeof(T)))
+ {
+ new(GetAddress()) T(std::forward<Args>(args)...);
+ }
+
+ SharedMemory(const SharedMemory&) = delete;
+ SharedMemory(SharedMemory&&) = delete;
+ SharedMemory& operator=(const SharedMemory&) = delete;
+ SharedMemory& operator=(SharedMemory&&) = delete;
+
+ inline T& Get() const
+ {
+ return *GetAddress();
+ }
+
+private:
+ inline T* GetAddress() const
+ {
+ return (T*)m_Memory.get_address();
+ }
+
+ boost::interprocess::mapped_region m_Memory;
+};
+
+}
diff --git a/lib/base/shared-object.hpp b/lib/base/shared-object.hpp
new file mode 100644
index 0000000..58636dc
--- /dev/null
+++ b/lib/base/shared-object.hpp
@@ -0,0 +1,73 @@
+/* Icinga 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+#ifndef SHARED_OBJECT_H
+#define SHARED_OBJECT_H
+
+#include "base/atomic.hpp"
+#include "base/object.hpp"
+#include <cstdint>
+
+namespace icinga
+{
+
+class SharedObject;
+
+inline void intrusive_ptr_add_ref(SharedObject *object);
+inline void intrusive_ptr_release(SharedObject *object);
+
+/**
+ * Seamless and polymorphistic base for any class to create shared pointers of.
+ * Saves a memory allocation compared to std::shared_ptr.
+ *
+ * @ingroup base
+ */
+class SharedObject
+{
+ friend void intrusive_ptr_add_ref(SharedObject *object);
+ friend void intrusive_ptr_release(SharedObject *object);
+
+protected:
+ inline SharedObject() : m_References(0)
+ {
+ }
+
+ inline SharedObject(const SharedObject&) : SharedObject()
+ {
+ }
+
+ inline SharedObject(SharedObject&&) : SharedObject()
+ {
+ }
+
+ inline SharedObject& operator=(const SharedObject&)
+ {
+ return *this;
+ }
+
+ inline SharedObject& operator=(SharedObject&&)
+ {
+ return *this;
+ }
+
+ inline virtual
+ ~SharedObject() = default;
+
+private:
+ Atomic<uint_fast64_t> m_References;
+};
+
+inline void intrusive_ptr_add_ref(SharedObject *object)
+{
+ object->m_References.fetch_add(1);
+}
+
+inline void intrusive_ptr_release(SharedObject *object)
+{
+ if (object->m_References.fetch_sub(1) == 1u) {
+ delete object;
+ }
+}
+
+}
+
+#endif /* SHARED_OBJECT_H */
diff --git a/lib/base/shared.hpp b/lib/base/shared.hpp
new file mode 100644
index 0000000..63b35cb
--- /dev/null
+++ b/lib/base/shared.hpp
@@ -0,0 +1,101 @@
+/* Icinga 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+#ifndef SHARED_H
+#define SHARED_H
+
+#include "base/atomic.hpp"
+#include <boost/smart_ptr/intrusive_ptr.hpp>
+#include <cstdint>
+#include <utility>
+
+namespace icinga
+{
+
+template<class T>
+class Shared;
+
+template<class T>
+inline void intrusive_ptr_add_ref(Shared<T> *object)
+{
+ object->m_References.fetch_add(1);
+}
+
+template<class T>
+inline void intrusive_ptr_release(Shared<T> *object)
+{
+ if (object->m_References.fetch_sub(1) == 1u) {
+ delete object;
+ }
+}
+
+/**
+ * Seamless wrapper for any class to create shared pointers of.
+ * Saves a memory allocation compared to std::shared_ptr.
+ *
+ * @ingroup base
+ */
+template<class T>
+class Shared : public T
+{
+ friend void intrusive_ptr_add_ref<>(Shared<T> *object);
+ friend void intrusive_ptr_release<>(Shared<T> *object);
+
+public:
+ typedef boost::intrusive_ptr<Shared> Ptr;
+
+ /**
+ * Like std::make_shared, but for this class.
+ *
+ * @param args Constructor arguments
+ *
+ * @return Ptr
+ */
+ template<class... Args>
+ static inline
+ Ptr Make(Args&&... args)
+ {
+ return new Shared(std::forward<Args>(args)...);
+ }
+
+ inline Shared(const Shared& origin) : Shared((const T&)origin)
+ {
+ }
+
+ inline Shared(Shared&& origin) : Shared((T&&)origin)
+ {
+ }
+
+ template<class... Args>
+ inline Shared(Args&&... args) : T(std::forward<Args>(args)...), m_References(0)
+ {
+ }
+
+ inline Shared& operator=(const Shared& rhs)
+ {
+ return operator=((const T&)rhs);
+ }
+
+ inline Shared& operator=(Shared&& rhs)
+ {
+ return operator=((T&&)rhs);
+ }
+
+ inline Shared& operator=(const T& rhs)
+ {
+ T::operator=(rhs);
+ return *this;
+ }
+
+ inline Shared& operator=(T&& rhs)
+ {
+ T::operator=(std::move(rhs));
+ return *this;
+ }
+
+private:
+ Atomic<uint_fast64_t> m_References;
+};
+
+}
+
+#endif /* SHARED_H */
diff --git a/lib/base/singleton.hpp b/lib/base/singleton.hpp
new file mode 100644
index 0000000..77511c0
--- /dev/null
+++ b/lib/base/singleton.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SINGLETON_H
+#define SINGLETON_H
+
+#include "base/i2-base.hpp"
+
+namespace icinga
+{
+
+/**
+ * A singleton.
+ *
+ * @ingroup base
+ */
+template<typename T>
+class Singleton
+{
+public:
+ static T *GetInstance()
+ {
+ static T instance;
+ return &instance;
+ }
+};
+
+}
+
+#endif /* SINGLETON_H */
diff --git a/lib/base/socket.cpp b/lib/base/socket.cpp
new file mode 100644
index 0000000..4c967de
--- /dev/null
+++ b/lib/base/socket.cpp
@@ -0,0 +1,430 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/socket.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include <sstream>
+#include <iostream>
+#include <boost/exception/errinfo_api_function.hpp>
+#include <boost/exception/errinfo_errno.hpp>
+#include <socketpair.h>
+
+#ifndef _WIN32
+# include <poll.h>
+#endif /* _WIN32 */
+
+using namespace icinga;
+
+/**
+ * Constructor for the Socket class.
+ */
+Socket::Socket(SOCKET fd)
+{
+ SetFD(fd);
+}
+
+/**
+ * Destructor for the Socket class.
+ */
+Socket::~Socket()
+{
+ Close();
+}
+
+/**
+ * Sets the file descriptor for this socket object.
+ *
+ * @param fd The file descriptor.
+ */
+void Socket::SetFD(SOCKET fd)
+{
+ if (fd != INVALID_SOCKET) {
+#ifndef _WIN32
+ /* mark the socket as close-on-exec */
+ Utility::SetCloExec(fd);
+#endif /* _WIN32 */
+ }
+
+ ObjectLock olock(this);
+ m_FD = fd;
+}
+
+/**
+ * Retrieves the file descriptor for this socket object.
+ *
+ * @returns The file descriptor.
+ */
+SOCKET Socket::GetFD() const
+{
+ ObjectLock olock(this);
+
+ return m_FD;
+}
+
+/**
+ * Closes the socket.
+ */
+void Socket::Close()
+{
+ ObjectLock olock(this);
+
+ if (m_FD != INVALID_SOCKET) {
+ closesocket(m_FD);
+ m_FD = INVALID_SOCKET;
+ }
+}
+
+/**
+ * Retrieves the last error that occurred for the socket.
+ *
+ * @returns An error code.
+ */
+int Socket::GetError() const
+{
+ int opt;
+ socklen_t optlen = sizeof(opt);
+
+ int rc = getsockopt(GetFD(), SOL_SOCKET, SO_ERROR, (char *)&opt, &optlen);
+
+ if (rc >= 0)
+ return opt;
+
+ return 0;
+}
+
+/**
+ * Formats a sockaddr in a human-readable way.
+ *
+ * @returns A pair of host and service.
+ */
+String Socket::GetHumanReadableAddress(const std::pair<String, String>& socketDetails)
+{
+ std::ostringstream s;
+ s << "[" << socketDetails.first << "]:" << socketDetails.second;
+ return s.str();
+}
+
+/**
+ * Returns host and service as pair.
+ *
+ * @returns A pair with host and service.
+ */
+std::pair<String, String> Socket::GetDetailsFromSockaddr(sockaddr *address, socklen_t len)
+{
+ char host[NI_MAXHOST];
+ char service[NI_MAXSERV];
+
+ if (getnameinfo(address, len, host, sizeof(host), service,
+ sizeof(service), NI_NUMERICHOST | NI_NUMERICSERV) < 0) {
+#ifndef _WIN32
+ Log(LogCritical, "Socket")
+ << "getnameinfo() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("getnameinfo")
+ << boost::errinfo_errno(errno));
+#else /* _WIN32 */
+ Log(LogCritical, "Socket")
+ << "getnameinfo() failed with error code " << WSAGetLastError() << ", \"" << Utility::FormatErrorNumber(WSAGetLastError()) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("getnameinfo")
+ << errinfo_win32_error(WSAGetLastError()));
+#endif /* _WIN32 */
+ }
+
+ return std::make_pair(host, service);
+}
+
+/**
+ * Returns a pair describing the local host and service of the socket.
+ *
+ * @returns A pair describing the local host and service.
+ */
+std::pair<String, String> Socket::GetClientAddressDetails()
+{
+ std::unique_lock<std::mutex> lock(m_SocketMutex);
+
+ sockaddr_storage sin;
+ socklen_t len = sizeof(sin);
+
+ if (getsockname(GetFD(), (sockaddr *)&sin, &len) < 0) {
+#ifndef _WIN32
+ Log(LogCritical, "Socket")
+ << "getsockname() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("getsockname")
+ << boost::errinfo_errno(errno));
+#else /* _WIN32 */
+ Log(LogCritical, "Socket")
+ << "getsockname() failed with error code " << WSAGetLastError() << ", \"" << Utility::FormatErrorNumber(WSAGetLastError()) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("getsockname")
+ << errinfo_win32_error(WSAGetLastError()));
+#endif /* _WIN32 */
+ }
+
+ std::pair<String, String> details;
+ try {
+ details = GetDetailsFromSockaddr((sockaddr *)&sin, len);
+ } catch (const std::exception&) {
+ /* already logged */
+ }
+
+ return details;
+}
+
+/**
+ * Returns a String describing the local address of the socket.
+ *
+ * @returns A String describing the local address.
+ */
+String Socket::GetClientAddress()
+{
+ return GetHumanReadableAddress(GetClientAddressDetails());
+}
+
+/**
+ * Returns a pair describing the peer host and service of the socket.
+ *
+ * @returns A pair describing the peer host and service.
+ */
+std::pair<String, String> Socket::GetPeerAddressDetails()
+{
+ std::unique_lock<std::mutex> lock(m_SocketMutex);
+
+ sockaddr_storage sin;
+ socklen_t len = sizeof(sin);
+
+ if (getpeername(GetFD(), (sockaddr *)&sin, &len) < 0) {
+#ifndef _WIN32
+ Log(LogCritical, "Socket")
+ << "getpeername() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("getpeername")
+ << boost::errinfo_errno(errno));
+#else /* _WIN32 */
+ Log(LogCritical, "Socket")
+ << "getpeername() failed with error code " << WSAGetLastError() << ", \"" << Utility::FormatErrorNumber(WSAGetLastError()) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("getpeername")
+ << errinfo_win32_error(WSAGetLastError()));
+#endif /* _WIN32 */
+ }
+
+ std::pair<String, String> details;
+ try {
+ details = GetDetailsFromSockaddr((sockaddr *)&sin, len);
+ } catch (const std::exception&) {
+ /* already logged */
+ }
+
+ return details;
+}
+
+/**
+ * Returns a String describing the peer address of the socket.
+ *
+ * @returns A String describing the peer address.
+ */
+String Socket::GetPeerAddress()
+{
+ return GetHumanReadableAddress(GetPeerAddressDetails());
+}
+
+/**
+ * Starts listening for incoming client connections.
+ */
+void Socket::Listen()
+{
+ if (listen(GetFD(), SOMAXCONN) < 0) {
+#ifndef _WIN32
+ Log(LogCritical, "Socket")
+ << "listen() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("listen")
+ << boost::errinfo_errno(errno));
+#else /* _WIN32 */
+ Log(LogCritical, "Socket")
+ << "listen() failed with error code " << WSAGetLastError() << ", \"" << Utility::FormatErrorNumber(WSAGetLastError()) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("listen")
+ << errinfo_win32_error(WSAGetLastError()));
+#endif /* _WIN32 */
+ }
+}
+
+/**
+ * Sends data for the socket.
+ */
+size_t Socket::Write(const void *buffer, size_t count)
+{
+ int rc;
+
+#ifndef _WIN32
+ rc = write(GetFD(), (const char *)buffer, count);
+#else /* _WIN32 */
+ rc = send(GetFD(), (const char *)buffer, count, 0);
+#endif /* _WIN32 */
+
+ if (rc < 0) {
+#ifndef _WIN32
+ Log(LogCritical, "Socket")
+ << "send() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("send")
+ << boost::errinfo_errno(errno));
+#else /* _WIN32 */
+ Log(LogCritical, "Socket")
+ << "send() failed with error code " << WSAGetLastError() << ", \"" << Utility::FormatErrorNumber(WSAGetLastError()) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("send")
+ << errinfo_win32_error(WSAGetLastError()));
+#endif /* _WIN32 */
+ }
+
+ return rc;
+}
+
+/**
+ * Processes data that can be written for this socket.
+ */
+size_t Socket::Read(void *buffer, size_t count)
+{
+ int rc;
+
+#ifndef _WIN32
+ rc = read(GetFD(), (char *)buffer, count);
+#else /* _WIN32 */
+ rc = recv(GetFD(), (char *)buffer, count, 0);
+#endif /* _WIN32 */
+
+ if (rc < 0) {
+#ifndef _WIN32
+ Log(LogCritical, "Socket")
+ << "recv() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("recv")
+ << boost::errinfo_errno(errno));
+#else /* _WIN32 */
+ Log(LogCritical, "Socket")
+ << "recv() failed with error code " << WSAGetLastError() << ", \"" << Utility::FormatErrorNumber(WSAGetLastError()) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("recv")
+ << errinfo_win32_error(WSAGetLastError()));
+#endif /* _WIN32 */
+ }
+
+ return rc;
+}
+
+/**
+ * Accepts a new client and creates a new client object for it.
+ */
+Socket::Ptr Socket::Accept()
+{
+ sockaddr_storage addr;
+ socklen_t addrlen = sizeof(addr);
+
+ SOCKET fd = accept(GetFD(), (sockaddr *)&addr, &addrlen);
+
+#ifndef _WIN32
+ if (fd < 0) {
+ Log(LogCritical, "Socket")
+ << "accept() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("accept")
+ << boost::errinfo_errno(errno));
+ }
+#else /* _WIN32 */
+ if (fd == INVALID_SOCKET) {
+ Log(LogCritical, "Socket")
+ << "accept() failed with error code " << WSAGetLastError() << ", \"" << Utility::FormatErrorNumber(WSAGetLastError()) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("accept")
+ << errinfo_win32_error(WSAGetLastError()));
+ }
+#endif /* _WIN32 */
+
+ return new Socket(fd);
+}
+
+bool Socket::Poll(bool read, bool write, struct timeval *timeout)
+{
+ int rc;
+
+#ifdef _WIN32
+ fd_set readfds, writefds, exceptfds;
+
+ FD_ZERO(&readfds);
+ if (read)
+ FD_SET(GetFD(), &readfds);
+
+ FD_ZERO(&writefds);
+ if (write)
+ FD_SET(GetFD(), &writefds);
+
+ FD_ZERO(&exceptfds);
+ FD_SET(GetFD(), &exceptfds);
+
+ rc = select(GetFD() + 1, &readfds, &writefds, &exceptfds, timeout);
+
+ if (rc < 0) {
+ Log(LogCritical, "Socket")
+ << "select() failed with error code " << WSAGetLastError() << ", \"" << Utility::FormatErrorNumber(WSAGetLastError()) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("select")
+ << errinfo_win32_error(WSAGetLastError()));
+ }
+#else /* _WIN32 */
+ pollfd pfd;
+ pfd.fd = GetFD();
+ pfd.events = (read ? POLLIN : 0) | (write ? POLLOUT : 0);
+ pfd.revents = 0;
+
+ rc = poll(&pfd, 1, timeout ? (timeout->tv_sec + 1000 + timeout->tv_usec / 1000) : -1);
+
+ if (rc < 0) {
+ Log(LogCritical, "Socket")
+ << "poll() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("poll")
+ << boost::errinfo_errno(errno));
+ }
+#endif /* _WIN32 */
+
+ return (rc != 0);
+}
+
+void Socket::MakeNonBlocking()
+{
+#ifdef _WIN32
+ Utility::SetNonBlockingSocket(GetFD());
+#else /* _WIN32 */
+ Utility::SetNonBlocking(GetFD());
+#endif /* _WIN32 */
+}
+
+void Socket::SocketPair(SOCKET s[2])
+{
+ if (dumb_socketpair(s, 0) < 0)
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("socketpair")
+ << boost::errinfo_errno(errno));
+}
diff --git a/lib/base/socket.hpp b/lib/base/socket.hpp
new file mode 100644
index 0000000..f7acf7f
--- /dev/null
+++ b/lib/base/socket.hpp
@@ -0,0 +1,66 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SOCKET_H
+#define SOCKET_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+#include <mutex>
+
+namespace icinga
+{
+
+/**
+ * Base class for connection-oriented sockets.
+ *
+ * @ingroup base
+ */
+class Socket : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Socket);
+
+ Socket() = default;
+ Socket(SOCKET fd);
+ ~Socket() override;
+
+ SOCKET GetFD() const;
+
+ void Close();
+
+ std::pair<String, String> GetClientAddressDetails();
+ String GetClientAddress();
+ std::pair<String, String> GetPeerAddressDetails();
+ String GetPeerAddress();
+
+ size_t Read(void *buffer, size_t size);
+ size_t Write(const void *buffer, size_t size);
+
+ void Listen();
+ Socket::Ptr Accept();
+
+ bool Poll(bool read, bool write, struct timeval *timeout = nullptr);
+
+ void MakeNonBlocking();
+
+ static void SocketPair(SOCKET s[2]);
+
+protected:
+ void SetFD(SOCKET fd);
+
+ int GetError() const;
+
+ mutable std::mutex m_SocketMutex;
+
+private:
+ SOCKET m_FD{INVALID_SOCKET}; /**< The socket descriptor. */
+
+ static std::pair<String, String> GetDetailsFromSockaddr(sockaddr *address, socklen_t len);
+ static String GetHumanReadableAddress(const std::pair<String, String>& socketDetails);
+};
+
+class socket_error : virtual public std::exception, virtual public boost::exception { };
+
+}
+
+#endif /* SOCKET_H */
diff --git a/lib/base/stacktrace.cpp b/lib/base/stacktrace.cpp
new file mode 100644
index 0000000..e3f15ce
--- /dev/null
+++ b/lib/base/stacktrace.cpp
@@ -0,0 +1,43 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#include <base/i2-base.hpp>
+#include "base/stacktrace.hpp"
+#include <iostream>
+#include <iomanip>
+#include <vector>
+
+#ifdef HAVE_BACKTRACE_SYMBOLS
+# include <execinfo.h>
+#endif /* HAVE_BACKTRACE_SYMBOLS */
+
+using namespace icinga;
+
+std::ostream &icinga::operator<<(std::ostream &os, const StackTraceFormatter &f)
+{
+ /* In most cases, this operator<< just relies on the operator<< for the `boost::stacktrace::stacktrace` wrapped in
+ * the `StackTraceFormatter`. But as this operator turned out to not work properly on some platforms, there is a
+ * fallback implementation that can be enabled using the `-DICINGA2_STACKTRACE_USE_BACKTRACE_SYMBOLS` flag at
+ * compile time. This will then switch to `backtrace_symbols()` from `<execinfo.h>` instead of the implementation
+ * provided by Boost.
+ */
+
+ const boost::stacktrace::stacktrace &stack = f.m_Stack;
+
+#ifdef ICINGA2_STACKTRACE_USE_BACKTRACE_SYMBOLS
+ std::vector<void *> addrs;
+ addrs.reserve(stack.size());
+ std::transform(stack.begin(), stack.end(), std::back_inserter(addrs), [](const boost::stacktrace::frame &f) {
+ return const_cast<void *>(f.address());
+ });
+
+ char **symbols = backtrace_symbols(addrs.data(), addrs.size());
+ for (size_t i = 0; i < addrs.size(); i++) {
+ os << std::setw(2) << i << "# " << symbols[i] << std::endl;
+ }
+ std::free(symbols);
+#else /* ICINGA2_STACKTRACE_USE_BACKTRACE_SYMBOLS */
+ os << stack;
+#endif /* ICINGA2_STACKTRACE_USE_BACKTRACE_SYMBOLS */
+
+ return os;
+}
diff --git a/lib/base/stacktrace.hpp b/lib/base/stacktrace.hpp
new file mode 100644
index 0000000..b4a9765
--- /dev/null
+++ b/lib/base/stacktrace.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#ifndef STACKTRACE_H
+#define STACKTRACE_H
+
+#include <boost/stacktrace.hpp>
+
+namespace icinga
+{
+
+/**
+ * Formatter for `boost::stacktrace::stacktrace` objects
+ *
+ * This class wraps `boost::stacktrace::stacktrace` objects and provides an operator<<
+ * for printing them to an `std::ostream` in a custom format.
+ */
+class StackTraceFormatter {
+public:
+ StackTraceFormatter(const boost::stacktrace::stacktrace &stack) : m_Stack(stack) {}
+
+private:
+ const boost::stacktrace::stacktrace &m_Stack;
+
+ friend std::ostream &operator<<(std::ostream &os, const StackTraceFormatter &f);
+};
+
+std::ostream& operator<<(std::ostream& os, const StackTraceFormatter &f);
+
+}
+
+#endif /* STACKTRACE_H */
diff --git a/lib/base/statsfunction.hpp b/lib/base/statsfunction.hpp
new file mode 100644
index 0000000..ecac33c
--- /dev/null
+++ b/lib/base/statsfunction.hpp
@@ -0,0 +1,17 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STATSFUNCTION_H
+#define STATSFUNCTION_H
+
+#include "base/i2-base.hpp"
+#include "base/function.hpp"
+
+namespace icinga
+{
+
+#define REGISTER_STATSFUNCTION(name, callback) \
+ REGISTER_FUNCTION(StatsFunctions, name, callback, "status:perfdata")
+
+}
+
+#endif /* STATSFUNCTION_H */
diff --git a/lib/base/stdiostream.cpp b/lib/base/stdiostream.cpp
new file mode 100644
index 0000000..449036f
--- /dev/null
+++ b/lib/base/stdiostream.cpp
@@ -0,0 +1,57 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/stdiostream.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+/**
+ * Constructor for the StdioStream class.
+ *
+ * @param innerStream The inner stream.
+ * @param ownsStream Whether the new object owns the inner stream. If true
+ * the stream's destructor deletes the inner stream.
+ */
+StdioStream::StdioStream(std::iostream *innerStream, bool ownsStream)
+ : m_InnerStream(innerStream), m_OwnsStream(ownsStream)
+{ }
+
+StdioStream::~StdioStream()
+{
+ Close();
+}
+
+size_t StdioStream::Read(void *buffer, size_t size, bool allow_partial)
+{
+ ObjectLock olock(this);
+
+ m_InnerStream->read(static_cast<char *>(buffer), size);
+ return m_InnerStream->gcount();
+}
+
+void StdioStream::Write(const void *buffer, size_t size)
+{
+ ObjectLock olock(this);
+
+ m_InnerStream->write(static_cast<const char *>(buffer), size);
+}
+
+void StdioStream::Close()
+{
+ Stream::Close();
+
+ if (m_OwnsStream) {
+ delete m_InnerStream;
+ m_OwnsStream = false;
+ }
+}
+
+bool StdioStream::IsDataAvailable() const
+{
+ return !IsEof();
+}
+
+bool StdioStream::IsEof() const
+{
+ return !m_InnerStream->good();
+}
diff --git a/lib/base/stdiostream.hpp b/lib/base/stdiostream.hpp
new file mode 100644
index 0000000..b305c7f
--- /dev/null
+++ b/lib/base/stdiostream.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STDIOSTREAM_H
+#define STDIOSTREAM_H
+
+#include "base/i2-base.hpp"
+#include "base/stream.hpp"
+#include <iosfwd>
+#include <iostream>
+
+namespace icinga {
+
+class StdioStream final : public Stream
+{
+public:
+ DECLARE_PTR_TYPEDEFS(StdioStream);
+
+ StdioStream(std::iostream *innerStream, bool ownsStream);
+ ~StdioStream() override;
+
+ size_t Read(void *buffer, size_t size, bool allow_partial = false) override;
+ void Write(const void *buffer, size_t size) override;
+
+ void Close() override;
+
+ bool IsDataAvailable() const override;
+ bool IsEof() const override;
+
+private:
+ std::iostream *m_InnerStream;
+ bool m_OwnsStream;
+};
+
+}
+
+#endif /* STDIOSTREAM_H */
diff --git a/lib/base/stream.cpp b/lib/base/stream.cpp
new file mode 100644
index 0000000..e558385
--- /dev/null
+++ b/lib/base/stream.cpp
@@ -0,0 +1,149 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/stream.hpp"
+#include <boost/algorithm/string/trim.hpp>
+#include <chrono>
+
+using namespace icinga;
+
+void Stream::RegisterDataHandler(const std::function<void(const Stream::Ptr&)>& handler)
+{
+ if (SupportsWaiting())
+ OnDataAvailable.connect(handler);
+ else
+ BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support waiting."));
+}
+
+bool Stream::SupportsWaiting() const
+{
+ return false;
+}
+
+bool Stream::IsDataAvailable() const
+{
+ return false;
+}
+
+void Stream::Shutdown()
+{
+ BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support Shutdown()."));
+}
+
+size_t Stream::Peek(void *buffer, size_t count, bool allow_partial)
+{
+ BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support Peek()."));
+}
+
+void Stream::SignalDataAvailable()
+{
+ OnDataAvailable(this);
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_CV.notify_all();
+ }
+}
+
+bool Stream::WaitForData()
+{
+ if (!SupportsWaiting())
+ BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support waiting."));
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ while (!IsDataAvailable() && !IsEof())
+ m_CV.wait(lock);
+
+ return IsDataAvailable() || IsEof();
+}
+
+bool Stream::WaitForData(int timeout)
+{
+ namespace ch = std::chrono;
+
+ if (!SupportsWaiting())
+ BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support waiting."));
+
+ if (timeout < 0)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Timeout can't be negative"));
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_CV.wait_for(lock, ch::duration<int>(timeout), [this]() { return IsDataAvailable() || IsEof(); });
+}
+
+void Stream::Close()
+{
+ OnDataAvailable.disconnect_all_slots();
+
+ /* Force signals2 to remove the slots, see https://stackoverflow.com/questions/2049291/force-deletion-of-slot-in-boostsignals2
+ * for details. */
+ OnDataAvailable.connect([](const Stream::Ptr&) { });
+}
+
+StreamReadStatus Stream::ReadLine(String *line, StreamReadContext& context, bool may_wait)
+{
+ if (context.Eof)
+ return StatusEof;
+
+ if (context.MustRead) {
+ if (!context.FillFromStream(this, may_wait)) {
+ context.Eof = true;
+
+ *line = String(context.Buffer, &(context.Buffer[context.Size]));
+ boost::algorithm::trim_right(*line);
+
+ return StatusNewItem;
+ }
+ }
+
+ for (size_t i = 0; i < context.Size; i++) {
+ if (context.Buffer[i] == '\n') {
+ *line = String(context.Buffer, context.Buffer + i);
+ boost::algorithm::trim_right(*line);
+
+ context.DropData(i + 1u);
+
+ context.MustRead = !context.Size;
+ return StatusNewItem;
+ }
+ }
+
+ context.MustRead = true;
+ return StatusNeedData;
+}
+
+bool StreamReadContext::FillFromStream(const Stream::Ptr& stream, bool may_wait)
+{
+ if (may_wait && stream->SupportsWaiting())
+ stream->WaitForData();
+
+ size_t count = 0;
+
+ do {
+ Buffer = (char *)realloc(Buffer, Size + 4096);
+
+ if (!Buffer)
+ throw std::bad_alloc();
+
+ if (stream->IsEof())
+ break;
+
+ size_t rc = stream->Read(Buffer + Size, 4096, true);
+
+ Size += rc;
+ count += rc;
+ } while (count < 64 * 1024 && stream->IsDataAvailable());
+
+ if (count == 0 && stream->IsEof())
+ return false;
+ else
+ return true;
+}
+
+void StreamReadContext::DropData(size_t count)
+{
+ ASSERT(count <= Size);
+ memmove(Buffer, Buffer + count, Size - count);
+ Size -= count;
+}
diff --git a/lib/base/stream.hpp b/lib/base/stream.hpp
new file mode 100644
index 0000000..6bc8fed
--- /dev/null
+++ b/lib/base/stream.hpp
@@ -0,0 +1,133 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STREAM_H
+#define STREAM_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+#include <boost/signals2.hpp>
+#include <condition_variable>
+#include <mutex>
+
+namespace icinga
+{
+
+class String;
+class Stream;
+
+enum ConnectionRole
+{
+ RoleClient,
+ RoleServer
+};
+
+struct StreamReadContext
+{
+ ~StreamReadContext()
+ {
+ free(Buffer);
+ }
+
+ bool FillFromStream(const intrusive_ptr<Stream>& stream, bool may_wait);
+ void DropData(size_t count);
+
+ char *Buffer{nullptr};
+ size_t Size{0};
+ bool MustRead{true};
+ bool Eof{false};
+};
+
+enum StreamReadStatus
+{
+ StatusNewItem,
+ StatusNeedData,
+ StatusEof
+};
+
+/**
+ * A stream.
+ *
+ * @ingroup base
+ */
+class Stream : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Stream);
+
+ /**
+ * Reads data from the stream without removing it from the stream buffer.
+ *
+ * @param buffer The buffer where data should be stored. May be nullptr if you're
+ * not actually interested in the data.
+ * @param count The number of bytes to read from the queue.
+ * @param allow_partial Whether to allow partial reads.
+ * @returns The number of bytes actually read.
+ */
+ virtual size_t Peek(void *buffer, size_t count, bool allow_partial = false);
+
+ /**
+ * Reads data from the stream.
+ *
+ * @param buffer The buffer where data should be stored. May be nullptr if you're
+ * not actually interested in the data.
+ * @param count The number of bytes to read from the queue.
+ * @param allow_partial Whether to allow partial reads.
+ * @returns The number of bytes actually read.
+ */
+ virtual size_t Read(void *buffer, size_t count, bool allow_partial = false) = 0;
+
+ /**
+ * Writes data to the stream.
+ *
+ * @param buffer The data that is to be written.
+ * @param count The number of bytes to write.
+ * @returns The number of bytes written
+ */
+ virtual void Write(const void *buffer, size_t count) = 0;
+
+ /**
+ * Causes the stream to be closed (via Close()) once all pending data has been
+ * written.
+ */
+ virtual void Shutdown();
+
+ /**
+ * Closes the stream and releases resources.
+ */
+ virtual void Close();
+
+ /**
+ * Checks whether we've reached the end-of-file condition.
+ *
+ * @returns true if EOF.
+ */
+ virtual bool IsEof() const = 0;
+
+ /**
+ * Waits until data can be read from the stream.
+ * Optionally with a timeout.
+ */
+ bool WaitForData();
+ bool WaitForData(int timeout);
+
+ virtual bool SupportsWaiting() const;
+
+ virtual bool IsDataAvailable() const;
+
+ void RegisterDataHandler(const std::function<void(const Stream::Ptr&)>& handler);
+
+ StreamReadStatus ReadLine(String *line, StreamReadContext& context, bool may_wait = false);
+
+protected:
+ void SignalDataAvailable();
+
+private:
+ boost::signals2::signal<void(const Stream::Ptr&)> OnDataAvailable;
+
+ std::mutex m_Mutex;
+ std::condition_variable m_CV;
+};
+
+}
+
+#endif /* STREAM_H */
diff --git a/lib/base/streamlogger.cpp b/lib/base/streamlogger.cpp
new file mode 100644
index 0000000..162b9c3
--- /dev/null
+++ b/lib/base/streamlogger.cpp
@@ -0,0 +1,119 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/streamlogger.hpp"
+#include "base/streamlogger-ti.cpp"
+#include "base/utility.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include <iostream>
+
+using namespace icinga;
+
+REGISTER_TYPE(StreamLogger);
+
+std::mutex StreamLogger::m_Mutex;
+
+void StreamLogger::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<StreamLogger>::Stop(runtimeRemoved);
+
+ // make sure we flush the log data on shutdown, even if we don't call the destructor
+ if (m_Stream)
+ m_Stream->flush();
+}
+
+/**
+ * Destructor for the StreamLogger class.
+ */
+StreamLogger::~StreamLogger()
+{
+ if (m_FlushLogTimer)
+ m_FlushLogTimer->Stop(true);
+
+ if (m_Stream && m_OwnsStream)
+ delete m_Stream;
+}
+
+void StreamLogger::FlushLogTimerHandler()
+{
+ Flush();
+}
+
+void StreamLogger::Flush()
+{
+ ObjectLock oLock (this);
+
+ if (m_Stream)
+ m_Stream->flush();
+}
+
+void StreamLogger::BindStream(std::ostream *stream, bool ownsStream)
+{
+ ObjectLock olock(this);
+
+ if (m_Stream && m_OwnsStream)
+ delete m_Stream;
+
+ m_Stream = stream;
+ m_OwnsStream = ownsStream;
+
+ if (!m_FlushLogTimer) {
+ m_FlushLogTimer = Timer::Create();
+ m_FlushLogTimer->SetInterval(1);
+ m_FlushLogTimer->OnTimerExpired.connect([this](const Timer * const&) { FlushLogTimerHandler(); });
+ m_FlushLogTimer->Start();
+ }
+}
+
+/**
+ * Processes a log entry and outputs it to a stream.
+ *
+ * @param stream The output stream.
+ * @param entry The log entry.
+ */
+void StreamLogger::ProcessLogEntry(std::ostream& stream, const LogEntry& entry)
+{
+ String timestamp = Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", entry.Timestamp);
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ if (Logger::IsTimestampEnabled())
+ stream << "[" << timestamp << "] ";
+
+ int color;
+
+ switch (entry.Severity) {
+ case LogDebug:
+ color = Console_ForegroundCyan;
+ break;
+ case LogNotice:
+ color = Console_ForegroundBlue;
+ break;
+ case LogInformation:
+ color = Console_ForegroundGreen;
+ break;
+ case LogWarning:
+ color = Console_ForegroundYellow | Console_Bold;
+ break;
+ case LogCritical:
+ color = Console_ForegroundRed | Console_Bold;
+ break;
+ default:
+ return;
+ }
+
+ stream << ConsoleColorTag(color);
+ stream << Logger::SeverityToString(entry.Severity);
+ stream << ConsoleColorTag(Console_Normal);
+ stream << "/" << entry.Facility << ": " << entry.Message << "\n";
+}
+
+/**
+ * Processes a log entry and outputs it to a stream.
+ *
+ * @param entry The log entry.
+ */
+void StreamLogger::ProcessLogEntry(const LogEntry& entry)
+{
+ ProcessLogEntry(*m_Stream, entry);
+}
diff --git a/lib/base/streamlogger.hpp b/lib/base/streamlogger.hpp
new file mode 100644
index 0000000..8cbe313
--- /dev/null
+++ b/lib/base/streamlogger.hpp
@@ -0,0 +1,47 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STREAMLOGGER_H
+#define STREAMLOGGER_H
+
+#include "base/i2-base.hpp"
+#include "base/streamlogger-ti.hpp"
+#include "base/timer.hpp"
+#include <iosfwd>
+
+namespace icinga
+{
+
+/**
+ * A logger that logs to an iostream.
+ *
+ * @ingroup base
+ */
+class StreamLogger : public ObjectImpl<StreamLogger>
+{
+public:
+ DECLARE_OBJECT(StreamLogger);
+
+ void Stop(bool runtimeRemoved) override;
+ ~StreamLogger() override;
+
+ void BindStream(std::ostream *stream, bool ownsStream);
+
+ static void ProcessLogEntry(std::ostream& stream, const LogEntry& entry);
+
+protected:
+ void ProcessLogEntry(const LogEntry& entry) final;
+ void Flush() final;
+
+private:
+ static std::mutex m_Mutex;
+ std::ostream *m_Stream{nullptr};
+ bool m_OwnsStream{false};
+
+ Timer::Ptr m_FlushLogTimer;
+
+ void FlushLogTimerHandler();
+};
+
+}
+
+#endif /* STREAMLOGGER_H */
diff --git a/lib/base/streamlogger.ti b/lib/base/streamlogger.ti
new file mode 100644
index 0000000..6dc36e0
--- /dev/null
+++ b/lib/base/streamlogger.ti
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/logger.hpp"
+
+library base;
+
+namespace icinga
+{
+
+abstract class StreamLogger : Logger
+{
+};
+
+}
diff --git a/lib/base/string-script.cpp b/lib/base/string-script.cpp
new file mode 100644
index 0000000..323f99c
--- /dev/null
+++ b/lib/base/string-script.cpp
@@ -0,0 +1,138 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/object.hpp"
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string.hpp>
+
+using namespace icinga;
+
+static int StringLen()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+ return self.GetLength();
+}
+
+static String StringToString()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ return vframe->Self;
+}
+
+static String StringSubstr(const std::vector<Value>& args)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+
+ if (args.empty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments"));
+
+ if (static_cast<double>(args[0]) < 0 || static_cast<double>(args[0]) >= self.GetLength())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("String index is out of range"));
+
+ if (args.size() > 1)
+ return self.SubStr(args[0], args[1]);
+ else
+ return self.SubStr(args[0]);
+}
+
+static String StringUpper()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+ return boost::to_upper_copy(self);
+}
+
+static String StringLower()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+ return boost::to_lower_copy(self);
+}
+
+static Array::Ptr StringSplit(const String& delims)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+ std::vector<String> tokens = self.Split(delims.CStr());
+
+ return Array::FromVector(tokens);
+}
+
+static int StringFind(const std::vector<Value>& args)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+
+ if (args.empty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments"));
+
+ String::SizeType result;
+
+ if (args.size() > 1) {
+ if (static_cast<double>(args[1]) < 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("String index is out of range"));
+
+ result = self.Find(args[0], args[1]);
+ } else
+ result = self.Find(args[0]);
+
+ if (result == String::NPos)
+ return -1;
+ else
+ return result;
+}
+
+static bool StringContains(const String& str)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+ return self.Contains(str);
+}
+
+static Value StringReplace(const String& search, const String& replacement)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+
+ boost::algorithm::replace_all(self, search, replacement);
+ return self;
+}
+
+static String StringReverse()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+ return self.Reverse();
+}
+
+static String StringTrim()
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ String self = vframe->Self;
+ return self.Trim();
+}
+
+Object::Ptr String::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "len", new Function("String#len", StringLen, {}, true) },
+ { "to_string", new Function("String#to_string", StringToString, {}, true) },
+ { "substr", new Function("String#substr", StringSubstr, { "start", "len" }, true) },
+ { "upper", new Function("String#upper", StringUpper, {}, true) },
+ { "lower", new Function("String#lower", StringLower, {}, true) },
+ { "split", new Function("String#split", StringSplit, { "delims" }, true) },
+ { "find", new Function("String#find", StringFind, { "str", "start" }, true) },
+ { "contains", new Function("String#contains", StringContains, { "str" }, true) },
+ { "replace", new Function("String#replace", StringReplace, { "search", "replacement" }, true) },
+ { "reverse", new Function("String#reverse", StringReverse, {}, true) },
+ { "trim", new Function("String#trim", StringTrim, {}, true) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/string.cpp b/lib/base/string.cpp
new file mode 100644
index 0000000..3c440cd
--- /dev/null
+++ b/lib/base/string.cpp
@@ -0,0 +1,468 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/string.hpp"
+#include "base/value.hpp"
+#include "base/primitivetype.hpp"
+#include "base/dictionary.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/algorithm/string/split.hpp>
+#include <ostream>
+
+using namespace icinga;
+
+template class std::vector<String>;
+
+REGISTER_BUILTIN_TYPE(String, String::GetPrototype());
+
+const String::SizeType String::NPos = std::string::npos;
+
+String::String(const char *data)
+ : m_Data(data)
+{ }
+
+String::String(std::string data)
+ : m_Data(std::move(data))
+{ }
+
+String::String(String::SizeType n, char c)
+ : m_Data(n, c)
+{ }
+
+String::String(const String& other)
+ : m_Data(other)
+{ }
+
+String::String(String&& other)
+ : m_Data(std::move(other.m_Data))
+{ }
+
+#ifndef _MSC_VER
+String::String(Value&& other)
+{
+ *this = std::move(other);
+}
+#endif /* _MSC_VER */
+
+String& String::operator=(Value&& other)
+{
+ if (other.IsString())
+ m_Data = std::move(other.Get<String>());
+ else
+ *this = static_cast<String>(other);
+
+ return *this;
+}
+
+String& String::operator+=(const Value& rhs)
+{
+ m_Data += static_cast<String>(rhs);
+ return *this;
+}
+
+String& String::operator=(const String& rhs)
+{
+ m_Data = rhs.m_Data;
+ return *this;
+}
+
+String& String::operator=(String&& rhs)
+{
+ m_Data = std::move(rhs.m_Data);
+ return *this;
+}
+
+String& String::operator=(const std::string& rhs)
+{
+ m_Data = rhs;
+ return *this;
+}
+
+String& String::operator=(const char *rhs)
+{
+ m_Data = rhs;
+ return *this;
+}
+
+const char& String::operator[](String::SizeType pos) const
+{
+ return m_Data[pos];
+}
+
+char& String::operator[](String::SizeType pos)
+{
+ return m_Data[pos];
+}
+
+String& String::operator+=(const String& rhs)
+{
+ m_Data += rhs.m_Data;
+ return *this;
+}
+
+String& String::operator+=(const char *rhs)
+{
+ m_Data += rhs;
+ return *this;
+}
+
+String& String::operator+=(char rhs)
+{
+ m_Data += rhs;
+ return *this;
+}
+
+bool String::IsEmpty() const
+{
+ return m_Data.empty();
+}
+
+bool String::operator<(const String& rhs) const
+{
+ return m_Data < rhs.m_Data;
+}
+
+String::operator const std::string&() const
+{
+ return m_Data;
+}
+
+/**
+ * Conversion function to boost::beast::string_view.
+ *
+ * This allows using String as the value for HTTP headers in boost::beast::http::basic_fields::set.
+ *
+ * @return A boost::beast::string_view representing this string.
+ */
+String::operator boost::beast::string_view() const
+{
+ return boost::beast::string_view(m_Data);
+}
+
+const char *String::CStr() const
+{
+ return m_Data.c_str();
+}
+
+void String::Clear()
+{
+ m_Data.clear();
+}
+
+String::SizeType String::GetLength() const
+{
+ return m_Data.size();
+}
+
+std::string& String::GetData()
+{
+ return m_Data;
+}
+
+const std::string& String::GetData() const
+{
+ return m_Data;
+}
+
+String::SizeType String::Find(const String& str, String::SizeType pos) const
+{
+ return m_Data.find(str, pos);
+}
+
+String::SizeType String::RFind(const String& str, String::SizeType pos) const
+{
+ return m_Data.rfind(str, pos);
+}
+
+String::SizeType String::FindFirstOf(const char *s, String::SizeType pos) const
+{
+ return m_Data.find_first_of(s, pos);
+}
+
+String::SizeType String::FindFirstOf(char ch, String::SizeType pos) const
+{
+ return m_Data.find_first_of(ch, pos);
+}
+
+String::SizeType String::FindFirstNotOf(const char *s, String::SizeType pos) const
+{
+ return m_Data.find_first_not_of(s, pos);
+}
+
+String::SizeType String::FindFirstNotOf(char ch, String::SizeType pos) const
+{
+ return m_Data.find_first_not_of(ch, pos);
+}
+
+String::SizeType String::FindLastOf(const char *s, String::SizeType pos) const
+{
+ return m_Data.find_last_of(s, pos);
+}
+
+String::SizeType String::FindLastOf(char ch, String::SizeType pos) const
+{
+ return m_Data.find_last_of(ch, pos);
+}
+
+String String::SubStr(String::SizeType first, String::SizeType len) const
+{
+ return m_Data.substr(first, len);
+}
+
+std::vector<String> String::Split(const char *separators) const
+{
+ std::vector<String> result;
+ boost::algorithm::split(result, m_Data, boost::is_any_of(separators));
+ return result;
+}
+
+void String::Replace(String::SizeType first, String::SizeType second, const String& str)
+{
+ m_Data.replace(first, second, str);
+}
+
+String String::Trim() const
+{
+ String t = m_Data;
+ boost::algorithm::trim(t);
+ return t;
+}
+
+String String::ToLower() const
+{
+ String t = m_Data;
+ boost::algorithm::to_lower(t);
+ return t;
+}
+
+String String::ToUpper() const
+{
+ String t = m_Data;
+ boost::algorithm::to_upper(t);
+ return t;
+}
+
+String String::Reverse() const
+{
+ String t = m_Data;
+ std::reverse(t.m_Data.begin(), t.m_Data.end());
+ return t;
+}
+
+void String::Append(int count, char ch)
+{
+ m_Data.append(count, ch);
+}
+
+bool String::Contains(const String& str) const
+{
+ return (m_Data.find(str) != std::string::npos);
+}
+
+void String::swap(String& str)
+{
+ m_Data.swap(str.m_Data);
+}
+
+String::Iterator String::erase(String::Iterator first, String::Iterator last)
+{
+ return m_Data.erase(first, last);
+}
+
+String::Iterator String::Begin()
+{
+ return m_Data.begin();
+}
+
+String::ConstIterator String::Begin() const
+{
+ return m_Data.begin();
+}
+
+String::Iterator String::End()
+{
+ return m_Data.end();
+}
+
+String::ConstIterator String::End() const
+{
+ return m_Data.end();
+}
+
+String::ReverseIterator String::RBegin()
+{
+ return m_Data.rbegin();
+}
+
+String::ConstReverseIterator String::RBegin() const
+{
+ return m_Data.rbegin();
+}
+
+String::ReverseIterator String::REnd()
+{
+ return m_Data.rend();
+}
+
+String::ConstReverseIterator String::REnd() const
+{
+ return m_Data.rend();
+}
+
+std::ostream& icinga::operator<<(std::ostream& stream, const String& str)
+{
+ stream << str.GetData();
+ return stream;
+}
+
+std::istream& icinga::operator>>(std::istream& stream, String& str)
+{
+ std::string tstr;
+ stream >> tstr;
+ str = tstr;
+ return stream;
+}
+
+String icinga::operator+(const String& lhs, const String& rhs)
+{
+ return lhs.GetData() + rhs.GetData();
+}
+
+String icinga::operator+(const String& lhs, const char *rhs)
+{
+ return lhs.GetData() + rhs;
+}
+
+String icinga::operator+(const char *lhs, const String& rhs)
+{
+ return lhs + rhs.GetData();
+}
+
+bool icinga::operator==(const String& lhs, const String& rhs)
+{
+ return lhs.GetData() == rhs.GetData();
+}
+
+bool icinga::operator==(const String& lhs, const char *rhs)
+{
+ return lhs.GetData() == rhs;
+}
+
+bool icinga::operator==(const char *lhs, const String& rhs)
+{
+ return lhs == rhs.GetData();
+}
+
+bool icinga::operator<(const String& lhs, const char *rhs)
+{
+ return lhs.GetData() < rhs;
+}
+
+bool icinga::operator<(const char *lhs, const String& rhs)
+{
+ return lhs < rhs.GetData();
+}
+
+bool icinga::operator>(const String& lhs, const String& rhs)
+{
+ return lhs.GetData() > rhs.GetData();
+}
+
+bool icinga::operator>(const String& lhs, const char *rhs)
+{
+ return lhs.GetData() > rhs;
+}
+
+bool icinga::operator>(const char *lhs, const String& rhs)
+{
+ return lhs > rhs.GetData();
+}
+
+bool icinga::operator<=(const String& lhs, const String& rhs)
+{
+ return lhs.GetData() <= rhs.GetData();
+}
+
+bool icinga::operator<=(const String& lhs, const char *rhs)
+{
+ return lhs.GetData() <= rhs;
+}
+
+bool icinga::operator<=(const char *lhs, const String& rhs)
+{
+ return lhs <= rhs.GetData();
+}
+
+bool icinga::operator>=(const String& lhs, const String& rhs)
+{
+ return lhs.GetData() >= rhs.GetData();
+}
+
+bool icinga::operator>=(const String& lhs, const char *rhs)
+{
+ return lhs.GetData() >= rhs;
+}
+
+bool icinga::operator>=(const char *lhs, const String& rhs)
+{
+ return lhs >= rhs.GetData();
+}
+
+bool icinga::operator!=(const String& lhs, const String& rhs)
+{
+ return lhs.GetData() != rhs.GetData();
+}
+
+bool icinga::operator!=(const String& lhs, const char *rhs)
+{
+ return lhs.GetData() != rhs;
+}
+
+bool icinga::operator!=(const char *lhs, const String& rhs)
+{
+ return lhs != rhs.GetData();
+}
+
+String::Iterator icinga::begin(String& x)
+{
+ return x.Begin();
+}
+
+String::ConstIterator icinga::begin(const String& x)
+{
+ return x.Begin();
+}
+
+String::Iterator icinga::end(String& x)
+{
+ return x.End();
+}
+
+String::ConstIterator icinga::end(const String& x)
+{
+ return x.End();
+}
+String::Iterator icinga::range_begin(String& x)
+{
+ return x.Begin();
+}
+
+String::ConstIterator icinga::range_begin(const String& x)
+{
+ return x.Begin();
+}
+
+String::Iterator icinga::range_end(String& x)
+{
+ return x.End();
+}
+
+String::ConstIterator icinga::range_end(const String& x)
+{
+ return x.End();
+}
+
+std::size_t std::hash<String>::operator()(const String& s) const noexcept
+{
+ return std::hash<std::string>{}(s.GetData());
+}
diff --git a/lib/base/string.hpp b/lib/base/string.hpp
new file mode 100644
index 0000000..0eb08b5
--- /dev/null
+++ b/lib/base/string.hpp
@@ -0,0 +1,208 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STRING_H
+#define STRING_H
+
+#include "base/i2-base.hpp"
+#include "base/object.hpp"
+#include <boost/beast/core.hpp>
+#include <boost/range/iterator.hpp>
+#include <boost/utility/string_view.hpp>
+#include <functional>
+#include <string>
+#include <iosfwd>
+
+namespace icinga {
+
+class Value;
+
+/**
+ * String class.
+ *
+ * Rationale for having this: The std::string class has an ambiguous assignment
+ * operator when used in conjunction with the Value class.
+ */
+class String
+{
+public:
+ typedef std::string::iterator Iterator;
+ typedef std::string::const_iterator ConstIterator;
+
+ typedef std::string::iterator iterator;
+ typedef std::string::const_iterator const_iterator;
+
+ typedef std::string::reverse_iterator ReverseIterator;
+ typedef std::string::const_reverse_iterator ConstReverseIterator;
+
+ typedef std::string::reverse_iterator reverse_iterator;
+ typedef std::string::const_reverse_iterator const_reverse_iterator;
+
+ typedef std::string::size_type SizeType;
+
+ String() = default;
+ String(const char *data);
+ String(std::string data);
+ String(String::SizeType n, char c);
+ String(const String& other);
+ String(String&& other);
+
+#ifndef _MSC_VER
+ String(Value&& other);
+#endif /* _MSC_VER */
+
+ template<typename InputIterator>
+ String(InputIterator begin, InputIterator end)
+ : m_Data(begin, end)
+ { }
+
+ String& operator=(const String& rhs);
+ String& operator=(String&& rhs);
+ String& operator=(Value&& rhs);
+ String& operator=(const std::string& rhs);
+ String& operator=(const char *rhs);
+
+ const char& operator[](SizeType pos) const;
+ char& operator[](SizeType pos);
+
+ String& operator+=(const String& rhs);
+ String& operator+=(const char *rhs);
+ String& operator+=(const Value& rhs);
+ String& operator+=(char rhs);
+
+ bool IsEmpty() const;
+
+ bool operator<(const String& rhs) const;
+
+ operator const std::string&() const;
+ operator boost::beast::string_view() const;
+
+ const char *CStr() const;
+
+ void Clear();
+
+ SizeType GetLength() const;
+
+ std::string& GetData();
+ const std::string& GetData() const;
+
+ SizeType Find(const String& str, SizeType pos = 0) const;
+ SizeType RFind(const String& str, SizeType pos = NPos) const;
+ SizeType FindFirstOf(const char *s, SizeType pos = 0) const;
+ SizeType FindFirstOf(char ch, SizeType pos = 0) const;
+ SizeType FindFirstNotOf(const char *s, SizeType pos = 0) const;
+ SizeType FindFirstNotOf(char ch, SizeType pos = 0) const;
+ SizeType FindLastOf(const char *s, SizeType pos = NPos) const;
+ SizeType FindLastOf(char ch, SizeType pos = NPos) const;
+
+ String SubStr(SizeType first, SizeType len = NPos) const;
+
+ std::vector<String> Split(const char *separators) const;
+
+ void Replace(SizeType first, SizeType second, const String& str);
+
+ String Trim() const;
+
+ String ToLower() const;
+
+ String ToUpper() const;
+
+ String Reverse() const;
+
+ void Append(int count, char ch);
+
+ bool Contains(const String& str) const;
+
+ void swap(String& str);
+
+ Iterator erase(Iterator first, Iterator last);
+
+ template<typename InputIterator>
+ void insert(Iterator p, InputIterator first, InputIterator last)
+ {
+ m_Data.insert(p, first, last);
+ }
+
+ Iterator Begin();
+ ConstIterator Begin() const;
+ Iterator End();
+ ConstIterator End() const;
+ ReverseIterator RBegin();
+ ConstReverseIterator RBegin() const;
+ ReverseIterator REnd();
+ ConstReverseIterator REnd() const;
+
+ static const SizeType NPos;
+
+ static Object::Ptr GetPrototype();
+
+private:
+ std::string m_Data;
+};
+
+std::ostream& operator<<(std::ostream& stream, const String& str);
+std::istream& operator>>(std::istream& stream, String& str);
+
+String operator+(const String& lhs, const String& rhs);
+String operator+(const String& lhs, const char *rhs);
+String operator+(const char *lhs, const String& rhs);
+
+bool operator==(const String& lhs, const String& rhs);
+bool operator==(const String& lhs, const char *rhs);
+bool operator==(const char *lhs, const String& rhs);
+
+bool operator<(const String& lhs, const char *rhs);
+bool operator<(const char *lhs, const String& rhs);
+
+bool operator>(const String& lhs, const String& rhs);
+bool operator>(const String& lhs, const char *rhs);
+bool operator>(const char *lhs, const String& rhs);
+
+bool operator<=(const String& lhs, const String& rhs);
+bool operator<=(const String& lhs, const char *rhs);
+bool operator<=(const char *lhs, const String& rhs);
+
+bool operator>=(const String& lhs, const String& rhs);
+bool operator>=(const String& lhs, const char *rhs);
+bool operator>=(const char *lhs, const String& rhs);
+
+bool operator!=(const String& lhs, const String& rhs);
+bool operator!=(const String& lhs, const char *rhs);
+bool operator!=(const char *lhs, const String& rhs);
+
+String::Iterator begin(String& x);
+String::ConstIterator begin(const String& x);
+String::Iterator end(String& x);
+String::ConstIterator end(const String& x);
+String::Iterator range_begin(String& x);
+String::ConstIterator range_begin(const String& x);
+String::Iterator range_end(String& x);
+String::ConstIterator range_end(const String& x);
+
+}
+
+template<>
+struct std::hash<icinga::String>
+{
+ std::size_t operator()(const icinga::String& s) const noexcept;
+};
+
+extern template class std::vector<icinga::String>;
+
+namespace boost
+{
+
+template<>
+struct range_mutable_iterator<icinga::String>
+{
+ typedef icinga::String::Iterator type;
+};
+
+template<>
+struct range_const_iterator<icinga::String>
+{
+ typedef icinga::String::ConstIterator type;
+};
+
+}
+
+#endif /* STRING_H */
diff --git a/lib/base/sysloglogger.cpp b/lib/base/sysloglogger.cpp
new file mode 100644
index 0000000..fc2ec09
--- /dev/null
+++ b/lib/base/sysloglogger.cpp
@@ -0,0 +1,144 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+#include "base/sysloglogger.hpp"
+#include "base/sysloglogger-ti.cpp"
+#include "base/configtype.hpp"
+#include "base/statsfunction.hpp"
+#include <syslog.h>
+
+using namespace icinga;
+
+REGISTER_TYPE(SyslogLogger);
+
+REGISTER_STATSFUNCTION(SyslogLogger, &SyslogLogger::StatsFunc);
+
+INITIALIZE_ONCE(&SyslogHelper::StaticInitialize);
+
+std::map<String, int> SyslogHelper::m_FacilityMap;
+
+void SyslogHelper::StaticInitialize()
+{
+ ScriptGlobal::Set("System.FacilityAuth", "LOG_AUTH");
+ ScriptGlobal::Set("System.FacilityAuthPriv", "LOG_AUTHPRIV");
+ ScriptGlobal::Set("System.FacilityCron", "LOG_CRON");
+ ScriptGlobal::Set("System.FacilityDaemon", "LOG_DAEMON");
+ ScriptGlobal::Set("System.FacilityFtp", "LOG_FTP");
+ ScriptGlobal::Set("System.FacilityKern", "LOG_KERN");
+ ScriptGlobal::Set("System.FacilityLocal0", "LOG_LOCAL0");
+ ScriptGlobal::Set("System.FacilityLocal1", "LOG_LOCAL1");
+ ScriptGlobal::Set("System.FacilityLocal2", "LOG_LOCAL2");
+ ScriptGlobal::Set("System.FacilityLocal3", "LOG_LOCAL3");
+ ScriptGlobal::Set("System.FacilityLocal4", "LOG_LOCAL4");
+ ScriptGlobal::Set("System.FacilityLocal5", "LOG_LOCAL5");
+ ScriptGlobal::Set("System.FacilityLocal6", "LOG_LOCAL6");
+ ScriptGlobal::Set("System.FacilityLocal7", "LOG_LOCAL7");
+ ScriptGlobal::Set("System.FacilityLpr", "LOG_LPR");
+ ScriptGlobal::Set("System.FacilityMail", "LOG_MAIL");
+ ScriptGlobal::Set("System.FacilityNews", "LOG_NEWS");
+ ScriptGlobal::Set("System.FacilitySyslog", "LOG_SYSLOG");
+ ScriptGlobal::Set("System.FacilityUser", "LOG_USER");
+ ScriptGlobal::Set("System.FacilityUucp", "LOG_UUCP");
+
+ m_FacilityMap["LOG_AUTH"] = LOG_AUTH;
+ m_FacilityMap["LOG_AUTHPRIV"] = LOG_AUTHPRIV;
+ m_FacilityMap["LOG_CRON"] = LOG_CRON;
+ m_FacilityMap["LOG_DAEMON"] = LOG_DAEMON;
+#ifdef LOG_FTP
+ m_FacilityMap["LOG_FTP"] = LOG_FTP;
+#endif /* LOG_FTP */
+ m_FacilityMap["LOG_KERN"] = LOG_KERN;
+ m_FacilityMap["LOG_LOCAL0"] = LOG_LOCAL0;
+ m_FacilityMap["LOG_LOCAL1"] = LOG_LOCAL1;
+ m_FacilityMap["LOG_LOCAL2"] = LOG_LOCAL2;
+ m_FacilityMap["LOG_LOCAL3"] = LOG_LOCAL3;
+ m_FacilityMap["LOG_LOCAL4"] = LOG_LOCAL4;
+ m_FacilityMap["LOG_LOCAL5"] = LOG_LOCAL5;
+ m_FacilityMap["LOG_LOCAL6"] = LOG_LOCAL6;
+ m_FacilityMap["LOG_LOCAL7"] = LOG_LOCAL7;
+ m_FacilityMap["LOG_LPR"] = LOG_LPR;
+ m_FacilityMap["LOG_MAIL"] = LOG_MAIL;
+ m_FacilityMap["LOG_NEWS"] = LOG_NEWS;
+ m_FacilityMap["LOG_SYSLOG"] = LOG_SYSLOG;
+ m_FacilityMap["LOG_USER"] = LOG_USER;
+ m_FacilityMap["LOG_UUCP"] = LOG_UUCP;
+}
+
+bool SyslogHelper::ValidateFacility(const String& facility)
+{
+ if (m_FacilityMap.find(facility) == m_FacilityMap.end()) {
+ try {
+ Convert::ToLong(facility);
+ } catch (const std::exception&) {
+ return false;
+ }
+ }
+ return true;
+}
+
+int SyslogHelper::SeverityToNumber(LogSeverity severity)
+{
+ switch (severity) {
+ case LogDebug:
+ return LOG_DEBUG;
+ case LogNotice:
+ return LOG_NOTICE;
+ case LogWarning:
+ return LOG_WARNING;
+ case LogCritical:
+ return LOG_CRIT;
+ case LogInformation:
+ default:
+ return LOG_INFO;
+ }
+}
+
+int SyslogHelper::FacilityToNumber(const String& facility)
+{
+ auto it = m_FacilityMap.find(facility);
+ if (it != m_FacilityMap.end())
+ return it->second;
+ else
+ return Convert::ToLong(facility);
+}
+
+void SyslogLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const SyslogLogger::Ptr& sysloglogger : ConfigType::GetObjectsByType<SyslogLogger>()) {
+ nodes.emplace_back(sysloglogger->GetName(), 1); //add more stats
+ }
+
+ status->Set("sysloglogger", new Dictionary(std::move(nodes)));
+}
+
+void SyslogLogger::OnConfigLoaded()
+{
+ ObjectImpl<SyslogLogger>::OnConfigLoaded();
+ m_Facility = SyslogHelper::FacilityToNumber(GetFacility());
+}
+
+void SyslogLogger::ValidateFacility(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<SyslogLogger>::ValidateFacility(lvalue, utils);
+ if (!SyslogHelper::ValidateFacility(lvalue()))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "facility" }, "Invalid facility specified."));
+}
+
+/**
+ * Processes a log entry and outputs it to syslog.
+ *
+ * @param entry The log entry.
+ */
+void SyslogLogger::ProcessLogEntry(const LogEntry& entry)
+{
+ syslog(SyslogHelper::SeverityToNumber(entry.Severity) | m_Facility,
+ "%s", entry.Message.CStr());
+}
+
+void SyslogLogger::Flush()
+{
+ /* Nothing to do here. */
+}
+#endif /* _WIN32 */
diff --git a/lib/base/sysloglogger.hpp b/lib/base/sysloglogger.hpp
new file mode 100644
index 0000000..d1d6859
--- /dev/null
+++ b/lib/base/sysloglogger.hpp
@@ -0,0 +1,56 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SYSLOGLOGGER_H
+#define SYSLOGLOGGER_H
+
+#ifndef _WIN32
+#include "base/i2-base.hpp"
+#include "base/sysloglogger-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * Helper class to handle syslog facility strings and numbers.
+ *
+ * @ingroup base
+ */
+class SyslogHelper final
+{
+public:
+ static void StaticInitialize();
+ static bool ValidateFacility(const String& facility);
+ static int SeverityToNumber(LogSeverity severity);
+ static int FacilityToNumber(const String& facility);
+
+private:
+ static std::map<String, int> m_FacilityMap;
+};
+
+/**
+ * A logger that logs to syslog.
+ *
+ * @ingroup base
+ */
+class SyslogLogger final : public ObjectImpl<SyslogLogger>
+{
+public:
+ DECLARE_OBJECT(SyslogLogger);
+ DECLARE_OBJECTNAME(SyslogLogger);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void OnConfigLoaded() override;
+ void ValidateFacility(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ int m_Facility;
+
+ void ProcessLogEntry(const LogEntry& entry) override;
+ void Flush() override;
+};
+
+}
+#endif /* _WIN32 */
+
+#endif /* SYSLOGLOGGER_H */
diff --git a/lib/base/sysloglogger.ti b/lib/base/sysloglogger.ti
new file mode 100644
index 0000000..8f34359
--- /dev/null
+++ b/lib/base/sysloglogger.ti
@@ -0,0 +1,19 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/logger.hpp"
+
+library base;
+
+namespace icinga
+{
+
+class SyslogLogger : Logger
+{
+ activation_priority -100;
+
+ [config] String facility {
+ default {{{ return "LOG_USER"; }}}
+ };
+};
+
+}
diff --git a/lib/base/tcpsocket.cpp b/lib/base/tcpsocket.cpp
new file mode 100644
index 0000000..a9390e5
--- /dev/null
+++ b/lib/base/tcpsocket.cpp
@@ -0,0 +1,211 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/tcpsocket.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include <boost/exception/errinfo_api_function.hpp>
+#include <boost/exception/errinfo_errno.hpp>
+#include <iostream>
+
+using namespace icinga;
+
+/**
+ * Creates a socket and binds it to the specified service.
+ *
+ * @param service The service.
+ * @param family The address family for the socket.
+ */
+void TcpSocket::Bind(const String& service, int family)
+{
+ Bind(String(), service, family);
+}
+
+/**
+ * Creates a socket and binds it to the specified node and service.
+ *
+ * @param node The node.
+ * @param service The service.
+ * @param family The address family for the socket.
+ */
+void TcpSocket::Bind(const String& node, const String& service, int family)
+{
+ addrinfo hints;
+ addrinfo *result;
+ int error;
+ const char *func;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = family;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_TCP;
+ hints.ai_flags = AI_PASSIVE;
+
+ int rc = getaddrinfo(node.IsEmpty() ? nullptr : node.CStr(),
+ service.CStr(), &hints, &result);
+
+ if (rc != 0) {
+ Log(LogCritical, "TcpSocket")
+ << "getaddrinfo() failed with error code " << rc << ", \"" << gai_strerror(rc) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("getaddrinfo")
+ << errinfo_getaddrinfo_error(rc));
+ }
+
+ int fd = INVALID_SOCKET;
+
+ for (addrinfo *info = result; info != nullptr; info = info->ai_next) {
+ fd = socket(info->ai_family, info->ai_socktype, info->ai_protocol);
+
+ if (fd == INVALID_SOCKET) {
+#ifdef _WIN32
+ error = WSAGetLastError();
+#else /* _WIN32 */
+ error = errno;
+#endif /* _WIN32 */
+ func = "socket";
+
+ continue;
+ }
+
+ const int optFalse = 0;
+ setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char *>(&optFalse), sizeof(optFalse));
+
+ const int optTrue = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char *>(&optTrue), sizeof(optTrue));
+#ifdef SO_REUSEPORT
+ setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<const char *>(&optTrue), sizeof(optTrue));
+#endif /* SO_REUSEPORT */
+
+ int rc = bind(fd, info->ai_addr, info->ai_addrlen);
+
+ if (rc < 0) {
+#ifdef _WIN32
+ error = WSAGetLastError();
+#else /* _WIN32 */
+ error = errno;
+#endif /* _WIN32 */
+ func = "bind";
+
+ closesocket(fd);
+
+ continue;
+ }
+
+ SetFD(fd);
+
+ break;
+ }
+
+ freeaddrinfo(result);
+
+ if (GetFD() == INVALID_SOCKET) {
+ Log(LogCritical, "TcpSocket")
+ << "Invalid socket: " << Utility::FormatErrorNumber(error);
+
+#ifndef _WIN32
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function(func)
+ << boost::errinfo_errno(error));
+#else /* _WIN32 */
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function(func)
+ << errinfo_win32_error(error));
+#endif /* _WIN32 */
+ }
+}
+
+/**
+ * Creates a socket and connects to the specified node and service.
+ *
+ * @param node The node.
+ * @param service The service.
+ */
+void TcpSocket::Connect(const String& node, const String& service)
+{
+ addrinfo hints;
+ addrinfo *result;
+ int error;
+ const char *func;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_TCP;
+
+ int rc = getaddrinfo(node.CStr(), service.CStr(), &hints, &result);
+
+ if (rc != 0) {
+ Log(LogCritical, "TcpSocket")
+ << "getaddrinfo() failed with error code " << rc << ", \"" << gai_strerror(rc) << "\"";
+
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function("getaddrinfo")
+ << errinfo_getaddrinfo_error(rc));
+ }
+
+ SOCKET fd = INVALID_SOCKET;
+
+ for (addrinfo *info = result; info != nullptr; info = info->ai_next) {
+ fd = socket(info->ai_family, info->ai_socktype, info->ai_protocol);
+
+ if (fd == INVALID_SOCKET) {
+#ifdef _WIN32
+ error = WSAGetLastError();
+#else /* _WIN32 */
+ error = errno;
+#endif /* _WIN32 */
+ func = "socket";
+
+ continue;
+ }
+
+ const int optTrue = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast<const char *>(&optTrue), sizeof(optTrue)) != 0) {
+#ifdef _WIN32
+ error = WSAGetLastError();
+#else /* _WIN32 */
+ error = errno;
+#endif /* _WIN32 */
+ Log(LogWarning, "TcpSocket")
+ << "setsockopt() unable to enable TCP keep-alives with error code " << rc;
+ }
+
+ rc = connect(fd, info->ai_addr, info->ai_addrlen);
+
+ if (rc < 0) {
+#ifdef _WIN32
+ error = WSAGetLastError();
+#else /* _WIN32 */
+ error = errno;
+#endif /* _WIN32 */
+ func = "connect";
+
+ closesocket(fd);
+
+ continue;
+ }
+
+ SetFD(fd);
+
+ break;
+ }
+
+ freeaddrinfo(result);
+
+ if (GetFD() == INVALID_SOCKET) {
+ Log(LogCritical, "TcpSocket")
+ << "Invalid socket: " << Utility::FormatErrorNumber(error);
+
+#ifndef _WIN32
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function(func)
+ << boost::errinfo_errno(error));
+#else /* _WIN32 */
+ BOOST_THROW_EXCEPTION(socket_error()
+ << boost::errinfo_api_function(func)
+ << errinfo_win32_error(error));
+#endif /* _WIN32 */
+ }
+}
diff --git a/lib/base/tcpsocket.hpp b/lib/base/tcpsocket.hpp
new file mode 100644
index 0000000..471ad8d
--- /dev/null
+++ b/lib/base/tcpsocket.hpp
@@ -0,0 +1,102 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TCPSOCKET_H
+#define TCPSOCKET_H
+
+#include "base/i2-base.hpp"
+#include "base/io-engine.hpp"
+#include "base/socket.hpp"
+#include <boost/asio/error.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/system/system_error.hpp>
+
+namespace icinga
+{
+
+/**
+ * A TCP socket. DEPRECATED - Use Boost ASIO instead.
+ *
+ * @ingroup base
+ */
+class TcpSocket final : public Socket
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TcpSocket);
+
+ void Bind(const String& service, int family);
+ void Bind(const String& node, const String& service, int family);
+
+ void Connect(const String& node, const String& service);
+};
+
+/**
+ * TCP Connect based on Boost ASIO.
+ *
+ * @ingroup base
+ */
+template<class Socket>
+void Connect(Socket& socket, const String& node, const String& service)
+{
+ using boost::asio::ip::tcp;
+
+ tcp::resolver resolver (IoEngine::Get().GetIoContext());
+ tcp::resolver::query query (node, service);
+ auto result (resolver.resolve(query));
+ auto current (result.begin());
+
+ for (;;) {
+ try {
+ socket.open(current->endpoint().protocol());
+ socket.set_option(tcp::socket::keep_alive(true));
+ socket.connect(current->endpoint());
+
+ break;
+ } catch (const std::exception& ex) {
+ auto se (dynamic_cast<const boost::system::system_error*>(&ex));
+
+ if (se && se->code() == boost::asio::error::operation_aborted || ++current == result.end()) {
+ throw;
+ }
+
+ if (socket.is_open()) {
+ socket.close();
+ }
+ }
+ }
+}
+
+template<class Socket>
+void Connect(Socket& socket, const String& node, const String& service, boost::asio::yield_context yc)
+{
+ using boost::asio::ip::tcp;
+
+ tcp::resolver resolver (IoEngine::Get().GetIoContext());
+ tcp::resolver::query query (node, service);
+ auto result (resolver.async_resolve(query, yc));
+ auto current (result.begin());
+
+ for (;;) {
+ try {
+ socket.open(current->endpoint().protocol());
+ socket.set_option(tcp::socket::keep_alive(true));
+ socket.async_connect(current->endpoint(), yc);
+
+ break;
+ } catch (const std::exception& ex) {
+ auto se (dynamic_cast<const boost::system::system_error*>(&ex));
+
+ if (se && se->code() == boost::asio::error::operation_aborted || ++current == result.end()) {
+ throw;
+ }
+
+ if (socket.is_open()) {
+ socket.close();
+ }
+ }
+ }
+}
+
+}
+
+#endif /* TCPSOCKET_H */
diff --git a/lib/base/threadpool.cpp b/lib/base/threadpool.cpp
new file mode 100644
index 0000000..dc76e7b
--- /dev/null
+++ b/lib/base/threadpool.cpp
@@ -0,0 +1,51 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/threadpool.hpp"
+#include <boost/thread/locks.hpp>
+
+using namespace icinga;
+
+ThreadPool::ThreadPool() : m_Pending(0)
+{
+ Start();
+}
+
+ThreadPool::~ThreadPool()
+{
+ Stop();
+}
+
+void ThreadPool::Start()
+{
+ boost::unique_lock<decltype(m_Mutex)> lock (m_Mutex);
+
+ if (!m_Pool) {
+ InitializePool();
+ }
+}
+
+void ThreadPool::InitializePool()
+{
+ m_Pool = decltype(m_Pool)(new boost::asio::thread_pool(Configuration::Concurrency * 2u));
+}
+
+void ThreadPool::Stop()
+{
+ boost::unique_lock<decltype(m_Mutex)> lock (m_Mutex);
+
+ if (m_Pool) {
+ m_Pool->join();
+ m_Pool = nullptr;
+ }
+}
+
+void ThreadPool::Restart()
+{
+ boost::unique_lock<decltype(m_Mutex)> lock (m_Mutex);
+
+ if (m_Pool) {
+ m_Pool->join();
+ }
+
+ InitializePool();
+}
diff --git a/lib/base/threadpool.hpp b/lib/base/threadpool.hpp
new file mode 100644
index 0000000..d30fa69
--- /dev/null
+++ b/lib/base/threadpool.hpp
@@ -0,0 +1,101 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef THREADPOOL_H
+#define THREADPOOL_H
+
+#include "base/atomic.hpp"
+#include "base/configuration.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include <cstddef>
+#include <exception>
+#include <functional>
+#include <memory>
+#include <thread>
+#include <boost/asio/post.hpp>
+#include <boost/asio/thread_pool.hpp>
+#include <boost/thread/locks.hpp>
+#include <boost/thread/shared_mutex.hpp>
+#include <cstdint>
+
+namespace icinga
+{
+
+enum SchedulerPolicy
+{
+ DefaultScheduler,
+ LowLatencyScheduler
+};
+
+/**
+ * A thread pool.
+ *
+ * @ingroup base
+ */
+class ThreadPool
+{
+public:
+ typedef std::function<void ()> WorkFunction;
+
+ ThreadPool();
+ ~ThreadPool();
+
+ void Start();
+ void Stop();
+ void Restart();
+
+ /**
+ * Appends a work item to the work queue. Work items will be processed in FIFO order.
+ *
+ * @param callback The callback function for the work item.
+ * @returns true if the item was queued, false otherwise.
+ */
+ template<class T>
+ bool Post(T callback, SchedulerPolicy)
+ {
+ boost::shared_lock<decltype(m_Mutex)> lock (m_Mutex);
+
+ if (m_Pool) {
+ m_Pending.fetch_add(1);
+
+ boost::asio::post(*m_Pool, [this, callback]() {
+ m_Pending.fetch_sub(1);
+
+ try {
+ callback();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ThreadPool")
+ << "Exception thrown in event handler:\n"
+ << DiagnosticInformation(ex);
+ } catch (...) {
+ Log(LogCritical, "ThreadPool", "Exception of unknown type thrown in event handler.");
+ }
+ });
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the amount of queued tasks not started yet.
+ *
+ * @returns amount of queued tasks.
+ */
+ inline uint_fast64_t GetPending()
+ {
+ return m_Pending.load();
+ }
+
+private:
+ boost::shared_mutex m_Mutex;
+ std::unique_ptr<boost::asio::thread_pool> m_Pool;
+ Atomic<uint_fast64_t> m_Pending;
+
+ void InitializePool();
+};
+
+}
+
+#endif /* THREADPOOL_H */
diff --git a/lib/base/timer.cpp b/lib/base/timer.cpp
new file mode 100644
index 0000000..ffe1c39
--- /dev/null
+++ b/lib/base/timer.cpp
@@ -0,0 +1,354 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/defer.hpp"
+#include "base/timer.hpp"
+#include "base/debug.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include <boost/multi_index_container.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index/key_extractors.hpp>
+#include <chrono>
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+#include <utility>
+
+using namespace icinga;
+
+namespace icinga {
+
+class TimerHolder {
+public:
+ TimerHolder(Timer *timer)
+ : m_Timer(timer)
+ { }
+
+ inline Timer *GetObject() const
+ {
+ return m_Timer;
+ }
+
+ inline double GetNextUnlocked() const
+ {
+ return m_Timer->m_Next;
+ }
+
+ operator Timer *() const
+ {
+ return m_Timer;
+ }
+
+private:
+ Timer *m_Timer;
+};
+
+}
+
+typedef boost::multi_index_container<
+ TimerHolder,
+ boost::multi_index::indexed_by<
+ boost::multi_index::ordered_unique<boost::multi_index::const_mem_fun<TimerHolder, Timer *, &TimerHolder::GetObject> >,
+ boost::multi_index::ordered_non_unique<boost::multi_index::const_mem_fun<TimerHolder, double, &TimerHolder::GetNextUnlocked> >
+ >
+> TimerSet;
+
+static std::mutex l_TimerMutex;
+static std::condition_variable l_TimerCV;
+static std::thread l_TimerThread;
+static bool l_StopTimerThread;
+static TimerSet l_Timers;
+static int l_AliveTimers = 0;
+
+static Defer l_ShutdownTimersCleanlyOnExit (&Timer::Uninitialize);
+
+Timer::Ptr Timer::Create()
+{
+ Ptr t (new Timer());
+
+ t->m_Self = t;
+
+ return t;
+}
+
+/**
+ * Destructor for the Timer class.
+ */
+Timer::~Timer()
+{
+ Stop(true);
+}
+
+void Timer::Initialize()
+{
+ std::unique_lock<std::mutex> lock(l_TimerMutex);
+
+ if (l_AliveTimers > 0) {
+ InitializeThread();
+ }
+}
+
+void Timer::Uninitialize()
+{
+ std::unique_lock<std::mutex> lock(l_TimerMutex);
+
+ if (l_AliveTimers > 0) {
+ UninitializeThread();
+ }
+}
+
+void Timer::InitializeThread()
+{
+ l_StopTimerThread = false;
+ l_TimerThread = std::thread(&Timer::TimerThreadProc);
+}
+
+void Timer::UninitializeThread()
+{
+ {
+ l_StopTimerThread = true;
+ l_TimerCV.notify_all();
+ }
+
+ l_TimerMutex.unlock();
+
+ if (l_TimerThread.joinable())
+ l_TimerThread.join();
+
+ l_TimerMutex.lock();
+}
+
+/**
+ * Calls this timer.
+ */
+void Timer::Call()
+{
+ try {
+ OnTimerExpired(this);
+ } catch (...) {
+ InternalReschedule(true);
+
+ throw;
+ }
+
+ InternalReschedule(true);
+}
+
+/**
+ * Sets the interval for this timer.
+ *
+ * @param interval The new interval.
+ */
+void Timer::SetInterval(double interval)
+{
+ std::unique_lock<std::mutex> lock(l_TimerMutex);
+ m_Interval = interval;
+}
+
+/**
+ * Retrieves the interval for this timer.
+ *
+ * @returns The interval.
+ */
+double Timer::GetInterval() const
+{
+ std::unique_lock<std::mutex> lock(l_TimerMutex);
+ return m_Interval;
+}
+
+/**
+ * Registers the timer and starts processing events for it.
+ */
+void Timer::Start()
+{
+ std::unique_lock<std::mutex> lock(l_TimerMutex);
+
+ if (!m_Started && ++l_AliveTimers == 1) {
+ InitializeThread();
+ }
+
+ m_Started = true;
+
+ InternalRescheduleUnlocked(false, m_Interval > 0 ? -1 : m_Next);
+}
+
+/**
+ * Unregisters the timer and stops processing events for it.
+ */
+void Timer::Stop(bool wait)
+{
+ if (l_StopTimerThread)
+ return;
+
+ std::unique_lock<std::mutex> lock(l_TimerMutex);
+
+ if (m_Started && --l_AliveTimers == 0) {
+ UninitializeThread();
+ }
+
+ m_Started = false;
+ l_Timers.erase(this);
+
+ /* Notify the worker thread that we've disabled a timer. */
+ l_TimerCV.notify_all();
+
+ while (wait && m_Running)
+ l_TimerCV.wait(lock);
+}
+
+void Timer::Reschedule(double next)
+{
+ InternalReschedule(false, next);
+}
+
+void Timer::InternalReschedule(bool completed, double next)
+{
+ std::unique_lock<std::mutex> lock (l_TimerMutex);
+
+ InternalRescheduleUnlocked(completed, next);
+}
+
+/**
+ * Reschedules this timer.
+ *
+ * @param completed Whether the timer has just completed its callback.
+ * @param next The time when this timer should be called again. Use -1 to let
+ * the timer figure out a suitable time based on the interval.
+ */
+void Timer::InternalRescheduleUnlocked(bool completed, double next)
+{
+ if (completed)
+ m_Running = false;
+
+ if (next < 0) {
+ /* Don't schedule the next call if this is not a periodic timer. */
+ if (m_Interval <= 0)
+ return;
+
+ next = Utility::GetTime() + m_Interval;
+ }
+
+ m_Next = next;
+
+ if (m_Started && !m_Running) {
+ /* Remove and re-add the timer to update the index. */
+ l_Timers.erase(this);
+ l_Timers.insert(this);
+
+ /* Notify the worker that we've rescheduled a timer. */
+ l_TimerCV.notify_all();
+ }
+}
+
+/**
+ * Retrieves when the timer is next due.
+ *
+ * @returns The timestamp.
+ */
+double Timer::GetNext() const
+{
+ std::unique_lock<std::mutex> lock(l_TimerMutex);
+ return m_Next;
+}
+
+/**
+ * Adjusts all periodic timers by adding the specified amount of time to their
+ * next scheduled timestamp.
+ *
+ * @param adjustment The adjustment.
+ */
+void Timer::AdjustTimers(double adjustment)
+{
+ std::unique_lock<std::mutex> lock(l_TimerMutex);
+
+ double now = Utility::GetTime();
+
+ typedef boost::multi_index::nth_index<TimerSet, 1>::type TimerView;
+ TimerView& idx = boost::get<1>(l_Timers);
+
+ std::vector<Timer *> timers;
+
+ for (Timer *timer : idx) {
+ /* Don't schedule the next call if this is not a periodic timer. */
+ if (timer->m_Interval <= 0) {
+ continue;
+ }
+
+ if (std::fabs(now - (timer->m_Next + adjustment)) <
+ std::fabs(now - timer->m_Next)) {
+ timer->m_Next += adjustment;
+ timers.push_back(timer);
+ }
+ }
+
+ for (Timer *timer : timers) {
+ l_Timers.erase(timer);
+ l_Timers.insert(timer);
+ }
+
+ /* Notify the worker that we've rescheduled some timers. */
+ l_TimerCV.notify_all();
+}
+
+/**
+ * Worker thread proc for Timer objects.
+ */
+void Timer::TimerThreadProc()
+{
+ namespace ch = std::chrono;
+
+ Log(LogDebug, "Timer", "TimerThreadProc started.");
+
+ Utility::SetThreadName("Timer Thread");
+
+ std::unique_lock<std::mutex> lock (l_TimerMutex);
+
+ for (;;) {
+ typedef boost::multi_index::nth_index<TimerSet, 1>::type NextTimerView;
+ NextTimerView& idx = boost::get<1>(l_Timers);
+
+ /* Wait until there is at least one timer. */
+ while (idx.empty() && !l_StopTimerThread)
+ l_TimerCV.wait(lock);
+
+ if (l_StopTimerThread)
+ break;
+
+ auto it = idx.begin();
+
+ // timer->~Timer() may be called at any moment (if the last
+ // smart pointer gets destroyed) or even already waiting for
+ // l_TimerMutex (before doing anything else) which we have
+ // locked at the moment. Until our unlock using *timer is safe.
+ Timer *timer = *it;
+
+ ch::time_point<ch::system_clock, ch::duration<double>> next (ch::duration<double>(timer->m_Next));
+
+ if (next - ch::system_clock::now() > ch::duration<double>(0.01)) {
+ /* Wait for the next timer. */
+ l_TimerCV.wait_until(lock, next);
+
+ continue;
+ }
+
+ /* Remove the timer from the list so it doesn't get called again
+ * until the current call is completed. */
+ l_Timers.erase(timer);
+
+ auto keepAlive (timer->m_Self.lock());
+
+ if (!keepAlive) {
+ // The last std::shared_ptr is gone, let ~Timer() proceed
+ continue;
+ }
+
+ timer->m_Running = true;
+
+ lock.unlock();
+
+ /* Asynchronously call the timer. */
+ Utility::QueueAsyncCallback([timer=std::move(keepAlive)]() { timer->Call(); });
+
+ lock.lock();
+ }
+}
diff --git a/lib/base/timer.hpp b/lib/base/timer.hpp
new file mode 100644
index 0000000..db0f0b7
--- /dev/null
+++ b/lib/base/timer.hpp
@@ -0,0 +1,65 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TIMER_H
+#define TIMER_H
+
+#include "base/i2-base.hpp"
+#include <boost/signals2.hpp>
+#include <memory>
+
+namespace icinga {
+
+class TimerHolder;
+
+/**
+ * A timer that periodically triggers an event.
+ *
+ * @ingroup base
+ */
+class Timer final
+{
+public:
+ typedef std::shared_ptr<Timer> Ptr;
+
+ static Ptr Create();
+
+ ~Timer();
+
+ static void Initialize();
+ static void Uninitialize();
+ static void InitializeThread();
+ static void UninitializeThread();
+
+ void SetInterval(double interval);
+ double GetInterval() const;
+
+ static void AdjustTimers(double adjustment);
+
+ void Start();
+ void Stop(bool wait = false);
+
+ void Reschedule(double next = -1);
+ double GetNext() const;
+
+ boost::signals2::signal<void(const Timer * const&)> OnTimerExpired;
+
+private:
+ double m_Interval{0}; /**< The interval of the timer. */
+ double m_Next{0}; /**< When the next event should happen. */
+ bool m_Started{false}; /**< Whether the timer is enabled. */
+ bool m_Running{false}; /**< Whether the timer proc is currently running. */
+ std::weak_ptr<Timer> m_Self;
+
+ Timer() = default;
+ void Call();
+ void InternalReschedule(bool completed, double next = -1);
+ void InternalRescheduleUnlocked(bool completed, double next = -1);
+
+ static void TimerThreadProc();
+
+ friend class TimerHolder;
+};
+
+}
+
+#endif /* TIMER_H */
diff --git a/lib/base/tlsstream.cpp b/lib/base/tlsstream.cpp
new file mode 100644
index 0000000..db54c91
--- /dev/null
+++ b/lib/base/tlsstream.cpp
@@ -0,0 +1,71 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/tlsstream.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include "base/configuration.hpp"
+#include "base/convert.hpp"
+#include <boost/asio/ssl/context.hpp>
+#include <boost/asio/ssl/verify_context.hpp>
+#include <boost/asio/ssl/verify_mode.hpp>
+#include <iostream>
+#include <openssl/ssl.h>
+#include <openssl/tls1.h>
+#include <openssl/x509.h>
+#include <sstream>
+
+using namespace icinga;
+
+bool UnbufferedAsioTlsStream::IsVerifyOK() const
+{
+ return m_VerifyOK;
+}
+
+String UnbufferedAsioTlsStream::GetVerifyError() const
+{
+ return m_VerifyError;
+}
+
+std::shared_ptr<X509> UnbufferedAsioTlsStream::GetPeerCertificate()
+{
+ return std::shared_ptr<X509>(SSL_get_peer_certificate(native_handle()), X509_free);
+}
+
+void UnbufferedAsioTlsStream::BeforeHandshake(handshake_type type)
+{
+ namespace ssl = boost::asio::ssl;
+
+ if (!m_Hostname.IsEmpty()) {
+ X509_VERIFY_PARAM_set1_host(SSL_get0_param(native_handle()), m_Hostname.CStr(), m_Hostname.GetLength());
+ }
+
+ set_verify_mode(ssl::verify_peer | ssl::verify_client_once);
+
+ set_verify_callback([this](bool preverified, ssl::verify_context& ctx) {
+ if (!preverified) {
+ m_VerifyOK = false;
+
+ std::ostringstream msgbuf;
+ int err = X509_STORE_CTX_get_error(ctx.native_handle());
+
+ msgbuf << "code " << err << ": " << X509_verify_cert_error_string(err);
+ m_VerifyError = msgbuf.str();
+ }
+
+ return true;
+ });
+
+#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
+ if (type == client && !m_Hostname.IsEmpty()) {
+ String environmentName = Application::GetAppEnvironment();
+ String serverName = m_Hostname;
+
+ if (!environmentName.IsEmpty())
+ serverName += ":" + environmentName;
+
+ SSL_set_tlsext_host_name(native_handle(), serverName.CStr());
+ }
+#endif /* SSL_CTRL_SET_TLSEXT_HOSTNAME */
+}
diff --git a/lib/base/tlsstream.hpp b/lib/base/tlsstream.hpp
new file mode 100644
index 0000000..f6e5209
--- /dev/null
+++ b/lib/base/tlsstream.hpp
@@ -0,0 +1,129 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TLSSTREAM_H
+#define TLSSTREAM_H
+
+#include "base/i2-base.hpp"
+#include "base/shared.hpp"
+#include "base/socket.hpp"
+#include "base/stream.hpp"
+#include "base/tlsutility.hpp"
+#include "base/fifo.hpp"
+#include "base/utility.hpp"
+#include <atomic>
+#include <memory>
+#include <utility>
+#include <boost/asio/buffered_stream.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/asio/ssl/stream.hpp>
+
+namespace icinga
+{
+
+template<class ARS>
+class SeenStream : public ARS
+{
+public:
+ template<class... Args>
+ SeenStream(Args&&... args) : ARS(std::forward<Args>(args)...)
+ {
+ m_Seen.store(nullptr);
+ }
+
+ template<class... Args>
+ auto async_read_some(Args&&... args) -> decltype(((ARS*)nullptr)->async_read_some(std::forward<Args>(args)...))
+ {
+ {
+ auto seen (m_Seen.load());
+
+ if (seen) {
+ *seen = Utility::GetTime();
+ }
+ }
+
+ return ((ARS*)this)->async_read_some(std::forward<Args>(args)...);
+ }
+
+ inline void SetSeen(double* seen)
+ {
+ m_Seen.store(seen);
+ }
+
+private:
+ std::atomic<double*> m_Seen;
+};
+
+struct UnbufferedAsioTlsStreamParams
+{
+ boost::asio::io_context& IoContext;
+ boost::asio::ssl::context& SslContext;
+ const String& Hostname;
+};
+
+typedef SeenStream<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> AsioTcpTlsStream;
+
+class UnbufferedAsioTlsStream : public AsioTcpTlsStream
+{
+public:
+ inline
+ UnbufferedAsioTlsStream(UnbufferedAsioTlsStreamParams& init)
+ : AsioTcpTlsStream(init.IoContext, init.SslContext), m_VerifyOK(true), m_Hostname(init.Hostname)
+ {
+ }
+
+ bool IsVerifyOK() const;
+ String GetVerifyError() const;
+ std::shared_ptr<X509> GetPeerCertificate();
+
+ template<class... Args>
+ inline
+ auto async_handshake(handshake_type type, Args&&... args) -> decltype(((AsioTcpTlsStream*)nullptr)->async_handshake(type, std::forward<Args>(args)...))
+ {
+ BeforeHandshake(type);
+
+ return AsioTcpTlsStream::async_handshake(type, std::forward<Args>(args)...);
+ }
+
+ template<class... Args>
+ inline
+ auto handshake(handshake_type type, Args&&... args) -> decltype(((AsioTcpTlsStream*)nullptr)->handshake(type, std::forward<Args>(args)...))
+ {
+ BeforeHandshake(type);
+
+ return AsioTcpTlsStream::handshake(type, std::forward<Args>(args)...);
+ }
+
+private:
+ bool m_VerifyOK;
+ String m_VerifyError;
+ String m_Hostname;
+
+ void BeforeHandshake(handshake_type type);
+};
+
+class AsioTlsStream : public boost::asio::buffered_stream<UnbufferedAsioTlsStream>
+{
+public:
+ inline
+ AsioTlsStream(boost::asio::io_context& ioContext, boost::asio::ssl::context& sslContext, const String& hostname = String())
+ : AsioTlsStream(UnbufferedAsioTlsStreamParams{ioContext, sslContext, hostname})
+ {
+ }
+
+private:
+ inline
+ AsioTlsStream(UnbufferedAsioTlsStreamParams init)
+ : buffered_stream(init)
+ {
+ }
+};
+
+typedef boost::asio::buffered_stream<boost::asio::ip::tcp::socket> AsioTcpStream;
+typedef std::pair<Shared<AsioTlsStream>::Ptr, Shared<AsioTcpStream>::Ptr> OptionalTlsStream;
+
+}
+
+#endif /* TLSSTREAM_H */
diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp
new file mode 100644
index 0000000..2e1b90a
--- /dev/null
+++ b/lib/base/tlsutility.cpp
@@ -0,0 +1,1086 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/tlsutility.hpp"
+#include "base/convert.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/application.hpp"
+#include "base/exception.hpp"
+#include <boost/asio/ssl/context.hpp>
+#include <openssl/opensslv.h>
+#include <openssl/crypto.h>
+#include <openssl/ssl.h>
+#include <openssl/ssl3.h>
+#include <fstream>
+
+namespace icinga
+{
+
+static bool l_SSLInitialized = false;
+static std::mutex *l_Mutexes;
+static std::mutex l_RandomMutex;
+
+String GetOpenSSLVersion()
+{
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+ return OpenSSL_version(OPENSSL_VERSION);
+#else /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
+ return SSLeay_version(SSLEAY_VERSION);
+#endif /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
+}
+
+#ifdef CRYPTO_LOCK
+static void OpenSSLLockingCallback(int mode, int type, const char *, int)
+{
+ if (mode & CRYPTO_LOCK)
+ l_Mutexes[type].lock();
+ else
+ l_Mutexes[type].unlock();
+}
+
+static unsigned long OpenSSLIDCallback()
+{
+#ifdef _WIN32
+ return (unsigned long)GetCurrentThreadId();
+#else /* _WIN32 */
+ return (unsigned long)pthread_self();
+#endif /* _WIN32 */
+}
+#endif /* CRYPTO_LOCK */
+
+/**
+ * Initializes the OpenSSL library.
+ */
+void InitializeOpenSSL()
+{
+ if (l_SSLInitialized)
+ return;
+
+ SSL_library_init();
+ SSL_load_error_strings();
+
+ SSL_COMP_get_compression_methods();
+
+#ifdef CRYPTO_LOCK
+ l_Mutexes = new std::mutex[CRYPTO_num_locks()];
+ CRYPTO_set_locking_callback(&OpenSSLLockingCallback);
+ CRYPTO_set_id_callback(&OpenSSLIDCallback);
+#endif /* CRYPTO_LOCK */
+
+ l_SSLInitialized = true;
+}
+
+static void InitSslContext(const Shared<boost::asio::ssl::context>::Ptr& context, const String& pubkey, const String& privkey, const String& cakey)
+{
+ char errbuf[256];
+
+ // Enforce TLS v1.2 as minimum
+ context->set_options(
+ boost::asio::ssl::context::default_workarounds |
+ boost::asio::ssl::context::no_compression |
+ boost::asio::ssl::context::no_sslv2 |
+ boost::asio::ssl::context::no_sslv3 |
+ boost::asio::ssl::context::no_tlsv1 |
+ boost::asio::ssl::context::no_tlsv1_1
+ );
+
+ // Custom TLS flags
+ SSL_CTX *sslContext = context->native_handle();
+
+ long flags = SSL_CTX_get_options(sslContext);
+
+ flags |= SSL_OP_CIPHER_SERVER_PREFERENCE;
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+ SSL_CTX_set_info_callback(sslContext, [](const SSL* ssl, int where, int) {
+ if (where & SSL_CB_HANDSHAKE_DONE) {
+ ssl->s3->flags |= SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS;
+ }
+ });
+#else /* OPENSSL_VERSION_NUMBER < 0x10100000L */
+ flags |= SSL_OP_NO_RENEGOTIATION;
+#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
+
+ SSL_CTX_set_options(sslContext, flags);
+
+ SSL_CTX_set_mode(sslContext, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ SSL_CTX_set_session_id_context(sslContext, (const unsigned char *)"Icinga 2", 8);
+
+ // Explicitly load ECC ciphers, required on el7 - https://github.com/Icinga/icinga2/issues/7247
+ // SSL_CTX_set_ecdh_auto is deprecated and removed in OpenSSL 1.1.x - https://github.com/openssl/openssl/issues/1437
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+# ifdef SSL_CTX_set_ecdh_auto
+ SSL_CTX_set_ecdh_auto(sslContext, 1);
+# endif /* SSL_CTX_set_ecdh_auto */
+#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+ // The built-in DH parameters have to be enabled explicitly to allow the use of ciphers that use a DHE key exchange.
+ // SSL_CTX_set_dh_auto is only documented in OpenSSL starting from version 3.0.0 but was already added in 1.1.0.
+ // https://github.com/openssl/openssl/commit/09599b52d4e295c380512ba39958a11994d63401
+ // https://github.com/openssl/openssl/commit/0437309fdf544492e272943e892523653df2f189
+ SSL_CTX_set_dh_auto(sslContext, 1);
+#endif /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
+
+ if (!pubkey.IsEmpty()) {
+ if (!SSL_CTX_use_certificate_chain_file(sslContext, pubkey.CStr())) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error with public key file '" << pubkey << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SSL_CTX_use_certificate_chain_file")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(pubkey));
+ }
+ }
+
+ if (!privkey.IsEmpty()) {
+ if (!SSL_CTX_use_PrivateKey_file(sslContext, privkey.CStr(), SSL_FILETYPE_PEM)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error with private key file '" << privkey << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SSL_CTX_use_PrivateKey_file")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(privkey));
+ }
+
+ if (!SSL_CTX_check_private_key(sslContext)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error checking private key '" << privkey << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SSL_CTX_check_private_key")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+ }
+
+ if (cakey.IsEmpty()) {
+ if (!SSL_CTX_set_default_verify_paths(sslContext)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error loading system's root CAs: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SSL_CTX_set_default_verify_paths")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+ } else {
+ if (!SSL_CTX_load_verify_locations(sslContext, cakey.CStr(), nullptr)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error loading and verifying locations in ca key file '" << cakey << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SSL_CTX_load_verify_locations")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(cakey));
+ }
+
+ STACK_OF(X509_NAME) *cert_names;
+
+ cert_names = SSL_load_client_CA_file(cakey.CStr());
+ if (!cert_names) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error loading client ca key file '" << cakey << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SSL_load_client_CA_file")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(cakey));
+ }
+
+ SSL_CTX_set_client_CA_list(sslContext, cert_names);
+ }
+}
+
+/**
+ * Initializes an SSL context using the specified certificates.
+ *
+ * @param pubkey The public key.
+ * @param privkey The matching private key.
+ * @param cakey CA certificate chain file.
+ * @returns An SSL context.
+ */
+Shared<boost::asio::ssl::context>::Ptr MakeAsioSslContext(const String& pubkey, const String& privkey, const String& cakey)
+{
+ namespace ssl = boost::asio::ssl;
+
+ InitializeOpenSSL();
+
+ auto context (Shared<ssl::context>::Make(ssl::context::tls));
+
+ InitSslContext(context, pubkey, privkey, cakey);
+
+ return context;
+}
+
+/**
+ * Set the cipher list to the specified SSL context.
+ * @param context The ssl context.
+ * @param cipherList The ciper list.
+ **/
+void SetCipherListToSSLContext(const Shared<boost::asio::ssl::context>::Ptr& context, const String& cipherList)
+{
+ char errbuf[256];
+
+ if (SSL_CTX_set_cipher_list(context->native_handle(), cipherList.CStr()) == 0) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Cipher list '"
+ << cipherList
+ << "' does not specify any usable ciphers: "
+ << ERR_peek_error() << ", \""
+ << errbuf << "\"";
+
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SSL_CTX_set_cipher_list")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+ //With OpenSSL 1.1.0, there might not be any returned 0.
+ STACK_OF(SSL_CIPHER) *ciphers;
+ Array::Ptr cipherNames = new Array();
+
+ ciphers = SSL_CTX_get_ciphers(context->native_handle());
+ for (int i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) {
+ const SSL_CIPHER *cipher = sk_SSL_CIPHER_value(ciphers, i);
+ String cipher_name = SSL_CIPHER_get_name(cipher);
+
+ cipherNames->Add(cipher_name);
+ }
+
+ Log(LogNotice, "TlsUtility")
+ << "Available TLS cipher list: " << cipherNames->Join(" ");
+#endif /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
+}
+
+/**
+ * Resolves a string describing a TLS protocol version to the value of a TLS*_VERSION macro of OpenSSL.
+ *
+ * Throws an exception if the version is unknown or not supported.
+ *
+ * @param version String of a TLS version, for example "TLSv1.2".
+ * @return The value of the corresponding TLS*_VERSION macro.
+ */
+int ResolveTlsProtocolVersion(const std::string& version) {
+ if (version == "TLSv1.2") {
+ return TLS1_2_VERSION;
+ } else if (version == "TLSv1.3") {
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+ return TLS1_3_VERSION;
+#else /* OPENSSL_VERSION_NUMBER >= 0x10101000L */
+ throw std::runtime_error("'" + version + "' is only supported with OpenSSL 1.1.1 or newer");
+#endif /* OPENSSL_VERSION_NUMBER >= 0x10101000L */
+ } else {
+ throw std::runtime_error("Unknown TLS protocol version '" + version + "'");
+ }
+}
+
+Shared<boost::asio::ssl::context>::Ptr SetupSslContext(String certPath, String keyPath,
+ String caPath, String crlPath, String cipherList, String protocolmin, DebugInfo di)
+{
+ namespace ssl = boost::asio::ssl;
+
+ Shared<ssl::context>::Ptr context;
+
+ try {
+ context = MakeAsioSslContext(certPath, keyPath, caPath);
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot make SSL context for cert path: '"
+ + certPath + "' key path: '" + keyPath + "' ca path: '" + caPath + "'.", di));
+ }
+
+ if (!crlPath.IsEmpty()) {
+ try {
+ AddCRLToSSLContext(context, crlPath);
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot add certificate revocation list to SSL context for crl path: '"
+ + crlPath + "'.", di));
+ }
+ }
+
+ if (!cipherList.IsEmpty()) {
+ try {
+ SetCipherListToSSLContext(context, cipherList);
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot set cipher list to SSL context for cipher list: '"
+ + cipherList + "'.", di));
+ }
+ }
+
+ if (!protocolmin.IsEmpty()){
+ try {
+ SetTlsProtocolminToSSLContext(context, protocolmin);
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot set minimum TLS protocol version to SSL context with tls_protocolmin: '" + protocolmin + "'.", di));
+ }
+ }
+
+ return context;
+}
+
+/**
+ * Set the minimum TLS protocol version to the specified SSL context.
+ *
+ * @param context The ssl context.
+ * @param tlsProtocolmin The minimum TLS protocol version.
+ */
+void SetTlsProtocolminToSSLContext(const Shared<boost::asio::ssl::context>::Ptr& context, const String& tlsProtocolmin)
+{
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+ int ret = SSL_CTX_set_min_proto_version(context->native_handle(), ResolveTlsProtocolVersion(tlsProtocolmin));
+
+ if (ret != 1) {
+ char errbuf[256];
+
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error setting minimum TLS protocol version: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SSL_CTX_set_min_proto_version")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+#else /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
+ // This should never happen. On this OpenSSL version, ResolveTlsProtocolVersion() should either return TLS 1.2
+ // or throw an exception, as that's the only TLS version supported by both Icinga and ancient OpenSSL.
+ VERIFY(ResolveTlsProtocolVersion(tlsProtocolmin) == TLS1_2_VERSION);
+#endif /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
+}
+
+/**
+ * Loads a CRL and appends its certificates to the specified Boost SSL context.
+ *
+ * @param context The SSL context.
+ * @param crlPath The path to the CRL file.
+ */
+void AddCRLToSSLContext(const Shared<boost::asio::ssl::context>::Ptr& context, const String& crlPath)
+{
+ X509_STORE *x509_store = SSL_CTX_get_cert_store(context->native_handle());
+ AddCRLToSSLContext(x509_store, crlPath);
+}
+
+/**
+ * Loads a CRL and appends its certificates to the specified OpenSSL X509 store.
+ *
+ * @param context The SSL context.
+ * @param crlPath The path to the CRL file.
+ */
+void AddCRLToSSLContext(X509_STORE *x509_store, const String& crlPath)
+{
+ char errbuf[256];
+
+ X509_LOOKUP *lookup;
+ lookup = X509_STORE_add_lookup(x509_store, X509_LOOKUP_file());
+
+ if (!lookup) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error adding X509 store lookup: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("X509_STORE_add_lookup")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (X509_LOOKUP_load_file(lookup, crlPath.CStr(), X509_FILETYPE_PEM) != 1) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error loading crl file '" << crlPath << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("X509_LOOKUP_load_file")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(crlPath));
+ }
+
+ X509_VERIFY_PARAM *param = X509_VERIFY_PARAM_new();
+ X509_VERIFY_PARAM_set_flags(param, X509_V_FLAG_CRL_CHECK);
+ X509_STORE_set1_param(x509_store, param);
+ X509_VERIFY_PARAM_free(param);
+}
+
+static String GetX509NameCN(X509_NAME *name)
+{
+ char errbuf[256];
+ char buffer[256];
+
+ int rc = X509_NAME_get_text_by_NID(name, NID_commonName, buffer, sizeof(buffer));
+
+ if (rc == -1) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error with x509 NAME getting text by NID: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("X509_NAME_get_text_by_NID")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ return buffer;
+}
+
+/**
+ * Retrieves the common name for an X509 certificate.
+ *
+ * @param certificate The X509 certificate.
+ * @returns The common name.
+ */
+String GetCertificateCN(const std::shared_ptr<X509>& certificate)
+{
+ return GetX509NameCN(X509_get_subject_name(certificate.get()));
+}
+
+/**
+ * Retrieves an X509 certificate from the specified file.
+ *
+ * @param pemfile The filename.
+ * @returns An X509 certificate.
+ */
+std::shared_ptr<X509> GetX509Certificate(const String& pemfile)
+{
+ char errbuf[256];
+ X509 *cert;
+ BIO *fpcert = BIO_new(BIO_s_file());
+
+ if (!fpcert) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error creating new BIO: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("BIO_new")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (BIO_read_filename(fpcert, pemfile.CStr()) < 0) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error reading pem file '" << pemfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("BIO_read_filename")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(pemfile));
+ }
+
+ cert = PEM_read_bio_X509_AUX(fpcert, nullptr, nullptr, nullptr);
+ if (!cert) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on bio X509 AUX reading pem file '" << pemfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("PEM_read_bio_X509_AUX")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(pemfile));
+ }
+
+ BIO_free(fpcert);
+
+ return std::shared_ptr<X509>(cert, X509_free);
+}
+
+int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile, const String& certfile, bool ca)
+{
+ char errbuf[256];
+
+ InitializeOpenSSL();
+
+ RSA *rsa = RSA_new();
+ BIGNUM *e = BN_new();
+
+ if (!rsa || !e) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error while creating RSA key: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("RSA_generate_key")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ BN_set_word(e, RSA_F4);
+
+ if (!RSA_generate_key_ex(rsa, 4096, e, nullptr)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error while creating RSA key: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("RSA_generate_key")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ BN_free(e);
+
+ Log(LogInformation, "base")
+ << "Writing private key to '" << keyfile << "'.";
+
+ BIO *bio = BIO_new_file(const_cast<char *>(keyfile.CStr()), "w");
+
+ if (!bio) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error while opening private RSA key file '" << keyfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("BIO_new_file")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(keyfile));
+ }
+
+ if (!PEM_write_bio_RSAPrivateKey(bio, rsa, nullptr, nullptr, 0, nullptr, nullptr)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error while writing private RSA key to file '" << keyfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("PEM_write_bio_RSAPrivateKey")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(keyfile));
+ }
+
+ BIO_free(bio);
+
+#ifndef _WIN32
+ chmod(keyfile.CStr(), 0600);
+#endif /* _WIN32 */
+
+ EVP_PKEY *key = EVP_PKEY_new();
+ EVP_PKEY_assign_RSA(key, rsa);
+
+ if (!certfile.IsEmpty()) {
+ X509_NAME *subject = X509_NAME_new();
+ X509_NAME_add_entry_by_txt(subject, "CN", MBSTRING_ASC, (unsigned char *)cn.CStr(), -1, -1, 0);
+
+ std::shared_ptr<X509> cert = CreateCert(key, subject, subject, key, ca);
+
+ X509_NAME_free(subject);
+
+ Log(LogInformation, "base")
+ << "Writing X509 certificate to '" << certfile << "'.";
+
+ bio = BIO_new_file(const_cast<char *>(certfile.CStr()), "w");
+
+ if (!bio) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error while opening certificate file '" << certfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("BIO_new_file")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(certfile));
+ }
+
+ if (!PEM_write_bio_X509(bio, cert.get())) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error while writing certificate to file '" << certfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("PEM_write_bio_X509")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(certfile));
+ }
+
+ BIO_free(bio);
+ }
+
+ if (!csrfile.IsEmpty()) {
+ X509_REQ *req = X509_REQ_new();
+
+ if (!req)
+ return 0;
+
+ X509_REQ_set_version(req, 0);
+ X509_REQ_set_pubkey(req, key);
+
+ X509_NAME *name = X509_REQ_get_subject_name(req);
+ X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)cn.CStr(), -1, -1, 0);
+
+ if (!ca) {
+ String san = "DNS:" + cn;
+ X509_EXTENSION *subjectAltNameExt = X509V3_EXT_conf_nid(nullptr, nullptr, NID_subject_alt_name, const_cast<char *>(san.CStr()));
+ if (subjectAltNameExt) {
+ /* OpenSSL 0.9.8 requires STACK_OF(X509_EXTENSION), otherwise we would just use stack_st_X509_EXTENSION. */
+ STACK_OF(X509_EXTENSION) *exts = sk_X509_EXTENSION_new_null();
+ sk_X509_EXTENSION_push(exts, subjectAltNameExt);
+ X509_REQ_add_extensions(req, exts);
+ sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
+ }
+ }
+
+ X509_REQ_sign(req, key, EVP_sha256());
+
+ Log(LogInformation, "base")
+ << "Writing certificate signing request to '" << csrfile << "'.";
+
+ bio = BIO_new_file(const_cast<char *>(csrfile.CStr()), "w");
+
+ if (!bio) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error while opening CSR file '" << csrfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("BIO_new_file")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(csrfile));
+ }
+
+ if (!PEM_write_bio_X509_REQ(bio, req)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error while writing CSR to file '" << csrfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("PEM_write_bio_X509")
+ << errinfo_openssl_error(ERR_peek_error())
+ << boost::errinfo_file_name(csrfile));
+ }
+
+ BIO_free(bio);
+
+ X509_REQ_free(req);
+ }
+
+ EVP_PKEY_free(key);
+
+ return 1;
+}
+
+std::shared_ptr<X509> CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca)
+{
+ X509 *cert = X509_new();
+ X509_set_version(cert, 2);
+ X509_gmtime_adj(X509_get_notBefore(cert), 0);
+ X509_gmtime_adj(X509_get_notAfter(cert), ca ? ROOT_VALID_FOR : LEAF_VALID_FOR);
+ X509_set_pubkey(cert, pubkey);
+
+ X509_set_subject_name(cert, subject);
+ X509_set_issuer_name(cert, issuer);
+
+ String id = Utility::NewUniqueID();
+
+ char errbuf[256];
+ SHA_CTX context;
+ unsigned char digest[SHA_DIGEST_LENGTH];
+
+ if (!SHA1_Init(&context)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA1 Init: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA1_Init")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (!SHA1_Update(&context, (unsigned char*)id.CStr(), id.GetLength())) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA1 Update: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA1_Update")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (!SHA1_Final(digest, &context)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA1 Final: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA1_Final")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ BIGNUM *bn = BN_new();
+ BN_bin2bn(digest, sizeof(digest), bn);
+ BN_to_ASN1_INTEGER(bn, X509_get_serialNumber(cert));
+ BN_free(bn);
+
+ X509V3_CTX ctx;
+ X509V3_set_ctx_nodb(&ctx);
+ X509V3_set_ctx(&ctx, cert, cert, nullptr, nullptr, 0);
+
+ const char *attr;
+
+ if (ca)
+ attr = "critical,CA:TRUE";
+ else
+ attr = "critical,CA:FALSE";
+
+ X509_EXTENSION *basicConstraintsExt = X509V3_EXT_conf_nid(nullptr, &ctx, NID_basic_constraints, const_cast<char *>(attr));
+
+ if (basicConstraintsExt) {
+ X509_add_ext(cert, basicConstraintsExt, -1);
+ X509_EXTENSION_free(basicConstraintsExt);
+ }
+
+ String cn = GetX509NameCN(subject);
+
+ if (!ca) {
+ String san = "DNS:" + cn;
+ X509_EXTENSION *subjectAltNameExt = X509V3_EXT_conf_nid(nullptr, &ctx, NID_subject_alt_name, const_cast<char *>(san.CStr()));
+ if (subjectAltNameExt) {
+ X509_add_ext(cert, subjectAltNameExt, -1);
+ X509_EXTENSION_free(subjectAltNameExt);
+ }
+ }
+
+ X509_sign(cert, cakey, EVP_sha256());
+
+ return std::shared_ptr<X509>(cert, X509_free);
+}
+
+String GetIcingaCADir()
+{
+ return Configuration::DataDir + "/ca";
+}
+
+std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject, bool ca)
+{
+ char errbuf[256];
+
+ String cadir = GetIcingaCADir();
+
+ String cakeyfile = cadir + "/ca.key";
+
+ RSA *rsa;
+
+ BIO *cakeybio = BIO_new_file(const_cast<char *>(cakeyfile.CStr()), "r");
+
+ if (!cakeybio) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Could not open CA key file '" << cakeyfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ return std::shared_ptr<X509>();
+ }
+
+ rsa = PEM_read_bio_RSAPrivateKey(cakeybio, nullptr, nullptr, nullptr);
+
+ if (!rsa) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Could not read RSA key from CA key file '" << cakeyfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ return std::shared_ptr<X509>();
+ }
+
+ BIO_free(cakeybio);
+
+ String cacertfile = cadir + "/ca.crt";
+
+ std::shared_ptr<X509> cacert = GetX509Certificate(cacertfile);
+
+ EVP_PKEY *privkey = EVP_PKEY_new();
+ EVP_PKEY_assign_RSA(privkey, rsa);
+
+ return CreateCert(pubkey, subject, X509_get_subject_name(cacert.get()), privkey, ca);
+}
+
+std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert)
+{
+ std::shared_ptr<EVP_PKEY> pkey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
+ return CreateCertIcingaCA(pkey.get(), X509_get_subject_name(cert.get()));
+}
+
+static inline
+bool CertExpiresWithin(X509* cert, int seconds)
+{
+ time_t renewalStart = time(nullptr) + seconds;
+
+ return X509_cmp_time(X509_get_notAfter(cert), &renewalStart) < 0;
+}
+
+bool IsCertUptodate(const std::shared_ptr<X509>& cert)
+{
+ if (CertExpiresWithin(cert.get(), RENEW_THRESHOLD)) {
+ return false;
+ }
+
+ /* auto-renew all certificates which were created before 2017 to force an update of the CA,
+ * because Icinga versions older than 2.4 sometimes create certificates with an invalid
+ * serial number. */
+ time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
+
+ return X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) >= 0;
+}
+
+bool IsCaUptodate(X509* cert)
+{
+ return !CertExpiresWithin(cert, LEAF_VALID_FOR);
+}
+
+String CertificateToString(X509* cert)
+{
+ BIO *mem = BIO_new(BIO_s_mem());
+ PEM_write_bio_X509(mem, cert);
+
+ char *data;
+ long len = BIO_get_mem_data(mem, &data);
+
+ String result = String(data, data + len);
+
+ BIO_free(mem);
+
+ return result;
+}
+
+std::shared_ptr<X509> StringToCertificate(const String& cert)
+{
+ BIO *bio = BIO_new(BIO_s_mem());
+ BIO_write(bio, (const void *)cert.CStr(), cert.GetLength());
+
+ X509 *rawCert = PEM_read_bio_X509_AUX(bio, nullptr, nullptr, nullptr);
+
+ BIO_free(bio);
+
+ if (!rawCert)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The specified X509 certificate is invalid."));
+
+ return std::shared_ptr<X509>(rawCert, X509_free);
+}
+
+String PBKDF2_SHA1(const String& password, const String& salt, int iterations)
+{
+ unsigned char digest[SHA_DIGEST_LENGTH];
+ PKCS5_PBKDF2_HMAC_SHA1(password.CStr(), password.GetLength(), reinterpret_cast<const unsigned char *>(salt.CStr()), salt.GetLength(),
+ iterations, sizeof(digest), digest);
+
+ char output[SHA_DIGEST_LENGTH*2+1];
+ for (int i = 0; i < SHA_DIGEST_LENGTH; i++)
+ sprintf(output + 2 * i, "%02x", digest[i]);
+
+ return output;
+}
+
+String PBKDF2_SHA256(const String& password, const String& salt, int iterations)
+{
+ unsigned char digest[SHA256_DIGEST_LENGTH];
+ PKCS5_PBKDF2_HMAC(password.CStr(), password.GetLength(), reinterpret_cast<const unsigned char *>(salt.CStr()),
+ salt.GetLength(), iterations, EVP_sha256(), SHA256_DIGEST_LENGTH, digest);
+
+ char output[SHA256_DIGEST_LENGTH*2+1];
+ for (int i = 0; i < SHA256_DIGEST_LENGTH; i++)
+ sprintf(output + 2 * i, "%02x", digest[i]);
+
+ return output;
+}
+
+String SHA1(const String& s, bool binary)
+{
+ char errbuf[256];
+ SHA_CTX context;
+ unsigned char digest[SHA_DIGEST_LENGTH];
+
+ if (!SHA1_Init(&context)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA Init: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA1_Init")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (!SHA1_Update(&context, (unsigned char*)s.CStr(), s.GetLength())) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA Update: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA1_Update")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (!SHA1_Final(digest, &context)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA Final: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA1_Final")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (binary)
+ return String(reinterpret_cast<const char*>(digest), reinterpret_cast<const char *>(digest + SHA_DIGEST_LENGTH));
+
+ return BinaryToHex(digest, SHA_DIGEST_LENGTH);
+}
+
+String SHA256(const String& s)
+{
+ char errbuf[256];
+ SHA256_CTX context;
+ unsigned char digest[SHA256_DIGEST_LENGTH];
+
+ if (!SHA256_Init(&context)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA256 Init: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA256_Init")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (!SHA256_Update(&context, (unsigned char*)s.CStr(), s.GetLength())) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA256 Update: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA256_Update")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ if (!SHA256_Final(digest, &context)) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Error on SHA256 Final: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("SHA256_Final")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ char output[SHA256_DIGEST_LENGTH*2+1];
+ for (int i = 0; i < 32; i++)
+ sprintf(output + 2 * i, "%02x", digest[i]);
+
+ return output;
+}
+
+String RandomString(int length)
+{
+ auto *bytes = new unsigned char[length];
+
+ /* Ensure that password generation is atomic. RAND_bytes is not thread-safe
+ * in OpenSSL < 1.1.0.
+ */
+ std::unique_lock<std::mutex> lock(l_RandomMutex);
+
+ if (!RAND_bytes(bytes, length)) {
+ delete [] bytes;
+
+ char errbuf[256];
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+
+ Log(LogCritical, "SSL")
+ << "Error for RAND_bytes: " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("RAND_bytes")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ lock.unlock();
+
+ auto *output = new char[length * 2 + 1];
+ for (int i = 0; i < length; i++)
+ sprintf(output + 2 * i, "%02x", bytes[i]);
+
+ String result = output;
+ delete [] bytes;
+ delete [] output;
+
+ return result;
+}
+
+String BinaryToHex(const unsigned char* data, size_t length) {
+ static const char hexdigits[] = "0123456789abcdef";
+
+ String output(2*length, 0);
+ for (int i = 0; i < SHA_DIGEST_LENGTH; i++) {
+ output[2 * i] = hexdigits[data[i] >> 4];
+ output[2 * i + 1] = hexdigits[data[i] & 0xf];
+ }
+
+ return output;
+}
+
+bool VerifyCertificate(const std::shared_ptr<X509> &caCertificate, const std::shared_ptr<X509> &certificate, const String& crlFile)
+{
+ X509_STORE *store = X509_STORE_new();
+
+ if (!store)
+ return false;
+
+ X509_STORE_add_cert(store, caCertificate.get());
+
+ if (!crlFile.IsEmpty()) {
+ AddCRLToSSLContext(store, crlFile);
+ }
+
+ X509_STORE_CTX *csc = X509_STORE_CTX_new();
+ X509_STORE_CTX_init(csc, store, certificate.get(), nullptr);
+
+ int rc = X509_verify_cert(csc);
+
+ X509_STORE_CTX_free(csc);
+ X509_STORE_free(store);
+
+ if (rc == 0) {
+ int err = X509_STORE_CTX_get_error(csc);
+
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("X509_verify_cert")
+ << errinfo_openssl_error(err));
+ }
+
+ return rc == 1;
+}
+
+bool IsCa(const std::shared_ptr<X509>& cacert)
+{
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+ /* OpenSSL 1.1.x provides https://www.openssl.org/docs/man1.1.0/man3/X509_check_ca.html
+ *
+ * 0 if it is not CA certificate,
+ * 1 if it is proper X509v3 CA certificate with basicConstraints extension CA:TRUE,
+ * 3 if it is self-signed X509 v1 certificate
+ * 4 if it is certificate with keyUsage extension with bit keyCertSign set, but without basicConstraints,
+ * 5 if it has outdated Netscape Certificate Type extension telling that it is CA certificate.
+ */
+ return (X509_check_ca(cacert.get()) == 1);
+#else /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Not supported on this platform, OpenSSL version too old."));
+#endif /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
+}
+
+int GetCertificateVersion(const std::shared_ptr<X509>& cert)
+{
+ return X509_get_version(cert.get()) + 1;
+}
+
+String GetSignatureAlgorithm(const std::shared_ptr<X509>& cert)
+{
+ int alg;
+ int sign_alg;
+ X509_PUBKEY *key;
+ X509_ALGOR *algor;
+
+ key = X509_get_X509_PUBKEY(cert.get());
+
+ X509_PUBKEY_get0_param(nullptr, nullptr, 0, &algor, key); //TODO: Error handling
+
+ alg = OBJ_obj2nid (algor->algorithm);
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+ sign_alg = OBJ_obj2nid((cert.get())->sig_alg->algorithm);
+#else /* OPENSSL_VERSION_NUMBER < 0x10100000L */
+ sign_alg = X509_get_signature_nid(cert.get());
+#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
+
+ return Convert::ToString((sign_alg == NID_undef) ? "Unknown" : OBJ_nid2ln(sign_alg));
+}
+
+Array::Ptr GetSubjectAltNames(const std::shared_ptr<X509>& cert)
+{
+ GENERAL_NAMES* subjectAltNames = (GENERAL_NAMES*)X509_get_ext_d2i(cert.get(), NID_subject_alt_name, nullptr, nullptr);
+
+ Array::Ptr sans = new Array();
+
+ for (int i = 0; i < sk_GENERAL_NAME_num(subjectAltNames); i++) {
+ GENERAL_NAME* gen = sk_GENERAL_NAME_value(subjectAltNames, i);
+ if (gen->type == GEN_URI || gen->type == GEN_DNS || gen->type == GEN_EMAIL) {
+ ASN1_IA5STRING *asn1_str = gen->d.uniformResourceIdentifier;
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+ String san = Convert::ToString(ASN1_STRING_data(asn1_str));
+#else /* OPENSSL_VERSION_NUMBER < 0x10100000L */
+ String san = Convert::ToString(ASN1_STRING_get0_data(asn1_str));
+#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
+
+ sans->Add(san);
+ }
+ }
+
+ GENERAL_NAMES_free(subjectAltNames);
+
+ return sans;
+}
+
+}
diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp
new file mode 100644
index 0000000..b064120
--- /dev/null
+++ b/lib/base/tlsutility.hpp
@@ -0,0 +1,94 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TLSUTILITY_H
+#define TLSUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "base/debuginfo.hpp"
+#include "base/object.hpp"
+#include "base/shared.hpp"
+#include "base/array.hpp"
+#include "base/string.hpp"
+#include <openssl/ssl.h>
+#include <openssl/bio.h>
+#include <openssl/err.h>
+#include <openssl/comp.h>
+#include <openssl/sha.h>
+#include <openssl/pem.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+#include <openssl/evp.h>
+#include <openssl/rand.h>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/exception/info.hpp>
+
+namespace icinga
+{
+
+// Source: https://ssl-config.mozilla.org, i.e.
+// ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
+// Modified so that AES256 is preferred over AES128.
+const char * const DEFAULT_TLS_CIPHERS = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256";
+
+const char * const DEFAULT_TLS_PROTOCOLMIN = "TLSv1.2";
+const unsigned int DEFAULT_CONNECT_TIMEOUT = 15;
+
+const auto ROOT_VALID_FOR = 60 * 60 * 24 * 365 * 15;
+const auto LEAF_VALID_FOR = 60 * 60 * 24 * 397;
+const auto RENEW_THRESHOLD = 60 * 60 * 24 * 30;
+const auto RENEW_INTERVAL = 60 * 60 * 24;
+
+void InitializeOpenSSL();
+
+String GetOpenSSLVersion();
+
+Shared<boost::asio::ssl::context>::Ptr MakeAsioSslContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String());
+void AddCRLToSSLContext(const Shared<boost::asio::ssl::context>::Ptr& context, const String& crlPath);
+void AddCRLToSSLContext(X509_STORE *x509_store, const String& crlPath);
+void SetCipherListToSSLContext(const Shared<boost::asio::ssl::context>::Ptr& context, const String& cipherList);
+void SetTlsProtocolminToSSLContext(const Shared<boost::asio::ssl::context>::Ptr& context, const String& tlsProtocolmin);
+int ResolveTlsProtocolVersion(const std::string& version);
+
+Shared<boost::asio::ssl::context>::Ptr SetupSslContext(String certPath, String keyPath,
+ String caPath, String crlPath, String cipherList, String protocolmin, DebugInfo di);
+
+String GetCertificateCN(const std::shared_ptr<X509>& certificate);
+std::shared_ptr<X509> GetX509Certificate(const String& pemfile);
+int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile = String(), const String& certfile = String(), bool ca = false);
+std::shared_ptr<X509> CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca);
+
+String GetIcingaCADir();
+String CertificateToString(X509* cert);
+
+inline String CertificateToString(const std::shared_ptr<X509>& cert)
+{
+ return CertificateToString(cert.get());
+}
+
+std::shared_ptr<X509> StringToCertificate(const String& cert);
+std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject, bool ca = false);
+std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert);
+bool IsCertUptodate(const std::shared_ptr<X509>& cert);
+bool IsCaUptodate(X509* cert);
+
+String PBKDF2_SHA1(const String& password, const String& salt, int iterations);
+String PBKDF2_SHA256(const String& password, const String& salt, int iterations);
+String SHA1(const String& s, bool binary = false);
+String SHA256(const String& s);
+String RandomString(int length);
+String BinaryToHex(const unsigned char* data, size_t length);
+
+bool VerifyCertificate(const std::shared_ptr<X509>& caCertificate, const std::shared_ptr<X509>& certificate, const String& crlFile);
+bool IsCa(const std::shared_ptr<X509>& cacert);
+int GetCertificateVersion(const std::shared_ptr<X509>& cert);
+String GetSignatureAlgorithm(const std::shared_ptr<X509>& cert);
+Array::Ptr GetSubjectAltNames(const std::shared_ptr<X509>& cert);
+
+class openssl_error : virtual public std::exception, virtual public boost::exception { };
+
+struct errinfo_openssl_error_;
+typedef boost::error_info<struct errinfo_openssl_error_, unsigned long> errinfo_openssl_error;
+
+}
+
+#endif /* TLSUTILITY_H */
diff --git a/lib/base/type.cpp b/lib/base/type.cpp
new file mode 100644
index 0000000..14794cb
--- /dev/null
+++ b/lib/base/type.cpp
@@ -0,0 +1,217 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/type.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/namespace.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+Type::Ptr Type::TypeInstance;
+
+static Namespace::Ptr l_TypesNS = new Namespace(true);
+
+INITIALIZE_ONCE_WITH_PRIORITY([]() {
+ ScriptGlobal::GetGlobals()->Set("Types", l_TypesNS, true);
+}, InitializePriority::CreateNamespaces);
+
+INITIALIZE_ONCE_WITH_PRIORITY([]() {
+ l_TypesNS->Freeze();
+
+ ObjectLock olock (l_TypesNS);
+ for (const auto& t : l_TypesNS) {
+ VERIFY(t.second.Val.IsObjectType<Type>());
+ }
+}, InitializePriority::FreezeNamespaces);
+
+/* Ensure that the priority is lower than the basic namespace initialization in scriptframe.cpp. */
+INITIALIZE_ONCE_WITH_PRIORITY([]() {
+ Type::Ptr type = new TypeType();
+ type->SetPrototype(TypeType::GetPrototype());
+ Type::TypeInstance = type;
+ Type::Register(type);
+}, InitializePriority::RegisterTypeType);
+
+String Type::ToString() const
+{
+ return "type '" + GetName() + "'";
+}
+
+void Type::Register(const Type::Ptr& type)
+{
+ ScriptGlobal::Set("Types." + type->GetName(), type);
+}
+
+Type::Ptr Type::GetByName(const String& name)
+{
+ Value ptype;
+
+ if (!l_TypesNS->Get(name, &ptype))
+ return nullptr;
+
+ return ptype;
+}
+
+std::vector<Type::Ptr> Type::GetAllTypes()
+{
+ std::vector<Type::Ptr> types;
+
+ Namespace::Ptr typesNS = ScriptGlobal::Get("Types", &Empty);
+
+ if (typesNS) {
+ ObjectLock olock(typesNS);
+
+ for (const Namespace::Pair& kv : typesNS) {
+ Value value = kv.second.Val;
+
+ if (value.IsObjectType<Type>())
+ types.push_back(value);
+ }
+ }
+
+ return types;
+}
+
+String Type::GetPluralName() const
+{
+ String name = GetName();
+
+ if (name.GetLength() >= 2 && name[name.GetLength() - 1] == 'y' &&
+ name.SubStr(name.GetLength() - 2, 1).FindFirstOf("aeiou") == String::NPos)
+ return name.SubStr(0, name.GetLength() - 1) + "ies";
+ else
+ return name + "s";
+}
+
+Object::Ptr Type::Instantiate(const std::vector<Value>& args) const
+{
+ ObjectFactory factory = GetFactory();
+
+ if (!factory)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Type does not have a factory function."));
+
+ return factory(args);
+}
+
+bool Type::IsAbstract() const
+{
+ return ((GetAttributes() & TAAbstract) != 0);
+}
+
+bool Type::IsAssignableFrom(const Type::Ptr& other) const
+{
+ for (Type::Ptr t = other; t; t = t->GetBaseType()) {
+ if (t.get() == this)
+ return true;
+ }
+
+ return false;
+}
+
+Object::Ptr Type::GetPrototype() const
+{
+ return m_Prototype;
+}
+
+void Type::SetPrototype(const Object::Ptr& object)
+{
+ m_Prototype = object;
+}
+
+void Type::SetField(int id, const Value& value, bool suppress_events, const Value& cookie)
+{
+ if (id == 1) {
+ SetPrototype(value);
+ return;
+ }
+
+ Object::SetField(id, value, suppress_events, cookie);
+}
+
+Value Type::GetField(int id) const
+{
+ int real_id = id - Object::TypeInstance->GetFieldCount();
+ if (real_id < 0)
+ return Object::GetField(id);
+
+ if (real_id == 0)
+ return GetName();
+ else if (real_id == 1)
+ return GetPrototype();
+ else if (real_id == 2)
+ return GetBaseType();
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid field ID."));
+}
+
+const std::unordered_set<Type*>& Type::GetLoadDependencies() const
+{
+ static const std::unordered_set<Type*> noDeps;
+ return noDeps;
+}
+
+int Type::GetActivationPriority() const
+{
+ return 0;
+}
+
+void Type::RegisterAttributeHandler(int fieldId, const AttributeHandler& callback)
+{
+ throw std::runtime_error("Invalid field ID.");
+}
+
+String TypeType::GetName() const
+{
+ return "Type";
+}
+
+Type::Ptr TypeType::GetBaseType() const
+{
+ return Object::TypeInstance;
+}
+
+int TypeType::GetAttributes() const
+{
+ return 0;
+}
+
+int TypeType::GetFieldId(const String& name) const
+{
+ int base_field_count = GetBaseType()->GetFieldCount();
+
+ if (name == "name")
+ return base_field_count + 0;
+ else if (name == "prototype")
+ return base_field_count + 1;
+ else if (name == "base")
+ return base_field_count + 2;
+
+ return GetBaseType()->GetFieldId(name);
+}
+
+Field TypeType::GetFieldInfo(int id) const
+{
+ int real_id = id - GetBaseType()->GetFieldCount();
+ if (real_id < 0)
+ return GetBaseType()->GetFieldInfo(id);
+
+ if (real_id == 0)
+ return {0, "String", "name", "", nullptr, 0, 0};
+ else if (real_id == 1)
+ return Field(1, "Object", "prototype", "", nullptr, 0, 0);
+ else if (real_id == 2)
+ return Field(2, "Type", "base", "", nullptr, 0, 0);
+
+ throw std::runtime_error("Invalid field ID.");
+}
+
+int TypeType::GetFieldCount() const
+{
+ return GetBaseType()->GetFieldCount() + 3;
+}
+
+ObjectFactory TypeType::GetFactory() const
+{
+ return nullptr;
+}
+
diff --git a/lib/base/type.hpp b/lib/base/type.hpp
new file mode 100644
index 0000000..7b8d1ca
--- /dev/null
+++ b/lib/base/type.hpp
@@ -0,0 +1,148 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TYPE_H
+#define TYPE_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include "base/object.hpp"
+#include "base/initialize.hpp"
+#include <unordered_set>
+#include <vector>
+
+namespace icinga
+{
+
+/* keep this in sync with tools/mkclass/classcompiler.hpp */
+enum FieldAttribute
+{
+ FAEphemeral = 1,
+ FAConfig = 2,
+ FAState = 4,
+ FARequired = 256,
+ FANavigation = 512,
+ FANoUserModify = 1024,
+ FANoUserView = 2048,
+ FADeprecated = 4096,
+};
+
+class Type;
+
+struct Field
+{
+ int ID;
+ const char *TypeName;
+ const char *Name;
+ const char *NavigationName;
+ const char *RefTypeName;
+ int Attributes;
+ int ArrayRank;
+
+ Field(int id, const char *type, const char *name, const char *navigationName, const char *reftype, int attributes, int arrayRank)
+ : ID(id), TypeName(type), Name(name), NavigationName(navigationName), RefTypeName(reftype), Attributes(attributes), ArrayRank(arrayRank)
+ { }
+};
+
+enum TypeAttribute
+{
+ TAAbstract = 1
+};
+
+class ValidationUtils
+{
+public:
+ virtual bool ValidateName(const String& type, const String& name) const = 0;
+};
+
+class Type : public Object
+{
+public:
+ DECLARE_OBJECT(Type);
+
+ String ToString() const override;
+
+ virtual String GetName() const = 0;
+ virtual Type::Ptr GetBaseType() const = 0;
+ virtual int GetAttributes() const = 0;
+ virtual int GetFieldId(const String& name) const = 0;
+ virtual Field GetFieldInfo(int id) const = 0;
+ virtual int GetFieldCount() const = 0;
+
+ String GetPluralName() const;
+
+ Object::Ptr Instantiate(const std::vector<Value>& args) const;
+
+ bool IsAssignableFrom(const Type::Ptr& other) const;
+
+ bool IsAbstract() const;
+
+ Object::Ptr GetPrototype() const;
+ void SetPrototype(const Object::Ptr& object);
+
+ static void Register(const Type::Ptr& type);
+ static Type::Ptr GetByName(const String& name);
+ static std::vector<Type::Ptr> GetAllTypes();
+
+ void SetField(int id, const Value& value, bool suppress_events = false, const Value& cookie = Empty) override;
+ Value GetField(int id) const override;
+
+ virtual const std::unordered_set<Type*>& GetLoadDependencies() const;
+ virtual int GetActivationPriority() const;
+
+ typedef std::function<void (const Object::Ptr&, const Value&)> AttributeHandler;
+ virtual void RegisterAttributeHandler(int fieldId, const AttributeHandler& callback);
+
+protected:
+ virtual ObjectFactory GetFactory() const = 0;
+
+private:
+ Object::Ptr m_Prototype;
+};
+
+class TypeType final : public Type
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Type);
+
+ String GetName() const override;
+ Type::Ptr GetBaseType() const override;
+ int GetAttributes() const override;
+ int GetFieldId(const String& name) const override;
+ Field GetFieldInfo(int id) const override;
+ int GetFieldCount() const override;
+
+ static Object::Ptr GetPrototype();
+
+protected:
+ ObjectFactory GetFactory() const override;
+};
+
+template<typename T>
+class TypeImpl
+{
+};
+
+/* Ensure that the priority is lower than the basic namespace initialization in scriptframe.cpp. */
+#define REGISTER_TYPE(type) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ icinga::Type::Ptr t = new TypeImpl<type>(); \
+ type::TypeInstance = t; \
+ icinga::Type::Register(t); \
+ }, InitializePriority::RegisterTypes); \
+ DEFINE_TYPE_INSTANCE(type)
+
+#define REGISTER_TYPE_WITH_PROTOTYPE(type, prototype) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ icinga::Type::Ptr t = new TypeImpl<type>(); \
+ t->SetPrototype(prototype); \
+ type::TypeInstance = t; \
+ icinga::Type::Register(t); \
+ }, InitializePriority::RegisterTypes); \
+ DEFINE_TYPE_INSTANCE(type)
+
+#define DEFINE_TYPE_INSTANCE(type) \
+ Type::Ptr type::TypeInstance
+
+}
+
+#endif /* TYPE_H */
diff --git a/lib/base/typetype-script.cpp b/lib/base/typetype-script.cpp
new file mode 100644
index 0000000..9077de8
--- /dev/null
+++ b/lib/base/typetype-script.cpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/type.hpp"
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+
+using namespace icinga;
+
+static void TypeRegisterAttributeHandler(const String& fieldName, const Function::Ptr& callback)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Type::Ptr self = static_cast<Type::Ptr>(vframe->Self);
+ REQUIRE_NOT_NULL(self);
+
+ int fid = self->GetFieldId(fieldName);
+ self->RegisterAttributeHandler(fid, [callback](const Object::Ptr& object, const Value& cookie) {
+ callback->Invoke({ object });
+ });
+}
+
+Object::Ptr TypeType::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "register_attribute_handler", new Function("Type#register_attribute_handler", TypeRegisterAttributeHandler, { "field", "callback" }, false) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/base/unix.hpp b/lib/base/unix.hpp
new file mode 100644
index 0000000..7413a5b
--- /dev/null
+++ b/lib/base/unix.hpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef UNIX_H
+#define UNIX_H
+
+#include <limits.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <netdb.h>
+#include <sys/ioctl.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <libgen.h>
+#include <syslog.h>
+#include <sys/file.h>
+#include <sys/wait.h>
+#include <glob.h>
+#include <dlfcn.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <strings.h>
+#include <errno.h>
+
+typedef int SOCKET;
+#define INVALID_SOCKET (-1)
+
+#define closesocket close
+#define ioctlsocket ioctl
+
+#ifndef SUN_LEN
+/* TODO: Ideally this should take into the account how
+ * long the socket path really is.
+ */
+# define SUN_LEN(sun) (sizeof(sockaddr_un))
+#endif /* SUN_LEN */
+
+#ifndef PATH_MAX
+# define PATH_MAX 1024
+#endif /* PATH_MAX */
+
+#ifndef MAXPATHLEN
+# define MAXPATHLEN PATH_MAX
+#endif /* MAXPATHLEN */
+#endif /* UNIX_H */
diff --git a/lib/base/unixsocket.cpp b/lib/base/unixsocket.cpp
new file mode 100644
index 0000000..dcc56ff
--- /dev/null
+++ b/lib/base/unixsocket.cpp
@@ -0,0 +1,53 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/unixsocket.hpp"
+#include "base/exception.hpp"
+
+#ifndef _WIN32
+using namespace icinga;
+
+UnixSocket::UnixSocket()
+{
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+
+ if (fd < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("socket")
+ << boost::errinfo_errno(errno));
+ }
+
+ SetFD(fd);
+}
+
+void UnixSocket::Bind(const String& path)
+{
+ unlink(path.CStr());
+
+ sockaddr_un s_un;
+ memset(&s_un, 0, sizeof(s_un));
+ s_un.sun_family = AF_UNIX;
+ strncpy(s_un.sun_path, path.CStr(), sizeof(s_un.sun_path));
+ s_un.sun_path[sizeof(s_un.sun_path) - 1] = '\0';
+
+ if (bind(GetFD(), (sockaddr *)&s_un, SUN_LEN(&s_un)) < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("bind")
+ << boost::errinfo_errno(errno));
+ }
+}
+
+void UnixSocket::Connect(const String& path)
+{
+ sockaddr_un s_un;
+ memset(&s_un, 0, sizeof(s_un));
+ s_un.sun_family = AF_UNIX;
+ strncpy(s_un.sun_path, path.CStr(), sizeof(s_un.sun_path));
+ s_un.sun_path[sizeof(s_un.sun_path) - 1] = '\0';
+
+ if (connect(GetFD(), (sockaddr *)&s_un, SUN_LEN(&s_un)) < 0 && errno != EINPROGRESS) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("connect")
+ << boost::errinfo_errno(errno));
+ }
+}
+#endif /* _WIN32 */
diff --git a/lib/base/unixsocket.hpp b/lib/base/unixsocket.hpp
new file mode 100644
index 0000000..80a9f25
--- /dev/null
+++ b/lib/base/unixsocket.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef UNIXSOCKET_H
+#define UNIXSOCKET_H
+
+#include "base/socket.hpp"
+
+#ifndef _WIN32
+namespace icinga
+{
+
+/**
+ * A TCP socket. DEPRECATED - Use Boost ASIO instead.
+ *
+ * @ingroup base
+ */
+class UnixSocket final : public Socket
+{
+public:
+ DECLARE_PTR_TYPEDEFS(UnixSocket);
+
+ UnixSocket();
+
+ void Bind(const String& path);
+
+ void Connect(const String& path);
+};
+
+}
+#endif /* _WIN32 */
+
+#endif /* UNIXSOCKET_H */
diff --git a/lib/base/utility.cpp b/lib/base/utility.cpp
new file mode 100644
index 0000000..6ff84ae
--- /dev/null
+++ b/lib/base/utility.cpp
@@ -0,0 +1,1975 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/atomic-file.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/application.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/socket.hpp"
+#include "base/utility.hpp"
+#include "base/json.hpp"
+#include "base/objectlock.hpp"
+#include <algorithm>
+#include <cstdint>
+#include <mmatch.h>
+#include <boost/filesystem.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/thread/tss.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/uuid/uuid_io.hpp>
+#include <boost/uuid/uuid_generators.hpp>
+#include <boost/regex.hpp>
+#include <ios>
+#include <fstream>
+#include <iostream>
+#include <iterator>
+#include <stdlib.h>
+#include <future>
+#include <set>
+#include <utf8.h>
+#include <vector>
+
+#ifdef __FreeBSD__
+# include <pthread_np.h>
+#endif /* __FreeBSD__ */
+
+#ifdef HAVE_CXXABI_H
+# include <cxxabi.h>
+#endif /* HAVE_CXXABI_H */
+
+#ifndef _WIN32
+# include <sys/types.h>
+# include <sys/utsname.h>
+# include <pwd.h>
+# include <grp.h>
+# include <errno.h>
+# include <unistd.h>
+#endif /* _WIN32 */
+
+#ifdef _WIN32
+# include <VersionHelpers.h>
+# include <windows.h>
+# include <io.h>
+# include <msi.h>
+# include <shlobj.h>
+#endif /*_WIN32*/
+
+using namespace icinga;
+
+boost::thread_specific_ptr<String> Utility::m_ThreadName;
+boost::thread_specific_ptr<unsigned int> Utility::m_RandSeed;
+
+#ifdef I2_DEBUG
+double Utility::m_DebugTime = -1;
+#endif /* I2_DEBUG */
+
+/**
+ * Demangles a symbol name.
+ *
+ * @param sym The symbol name.
+ * @returns A human-readable version of the symbol name.
+ */
+String Utility::DemangleSymbolName(const String& sym)
+{
+ String result = sym;
+
+#ifdef HAVE_CXXABI_H
+ int status;
+ char *realname = abi::__cxa_demangle(sym.CStr(), nullptr, nullptr, &status);
+
+ if (realname) {
+ result = String(realname);
+ free(realname);
+ }
+#elif defined(_MSC_VER) /* HAVE_CXXABI_H */
+ CHAR output[256];
+
+ if (UnDecorateSymbolName(sym.CStr(), output, sizeof(output), UNDNAME_COMPLETE) > 0)
+ result = output;
+#else /* _MSC_VER */
+ /* We're pretty much out of options here. */
+#endif /* _MSC_VER */
+
+ return result;
+}
+
+/**
+ * Returns a human-readable type name of a type_info object.
+ *
+ * @param ti A type_info object.
+ * @returns The type name of the object.
+ */
+String Utility::GetTypeName(const std::type_info& ti)
+{
+ return DemangleSymbolName(ti.name());
+}
+
+String Utility::GetSymbolName(const void *addr)
+{
+#ifdef HAVE_DLADDR
+ Dl_info dli;
+
+ if (dladdr(const_cast<void *>(addr), &dli) > 0)
+ return dli.dli_sname;
+#endif /* HAVE_DLADDR */
+
+#ifdef _WIN32
+ char buffer[sizeof(SYMBOL_INFO)+MAX_SYM_NAME * sizeof(TCHAR)];
+ PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;
+ pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
+ pSymbol->MaxNameLen = MAX_SYM_NAME;
+
+ DWORD64 dwAddress = (DWORD64)addr;
+ DWORD64 dwDisplacement;
+
+ IMAGEHLP_LINE64 line;
+ line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
+
+ if (SymFromAddr(GetCurrentProcess(), dwAddress, &dwDisplacement, pSymbol)) {
+ char output[256];
+ if (UnDecorateSymbolName(pSymbol->Name, output, sizeof(output), UNDNAME_COMPLETE))
+ return String(output) + "+" + Convert::ToString(dwDisplacement);
+ else
+ return String(pSymbol->Name) + "+" + Convert::ToString(dwDisplacement);
+ }
+#endif /* _WIN32 */
+
+ return "(unknown function)";
+}
+
+/**
+ * Performs wildcard pattern matching.
+ *
+ * @param pattern The wildcard pattern.
+ * @param text The String that should be checked.
+ * @returns true if the wildcard pattern matches, false otherwise.
+ */
+bool Utility::Match(const String& pattern, const String& text)
+{
+ return (match(pattern.CStr(), text.CStr()) == 0);
+}
+
+static bool ParseIp(const String& ip, char addr[16], int *proto)
+{
+ if (inet_pton(AF_INET, ip.CStr(), addr + 12) == 1) {
+ /* IPv4-mapped IPv6 address (::ffff:<ipv4-bits>) */
+ memset(addr, 0, 10);
+ memset(addr + 10, 0xff, 2);
+ *proto = AF_INET;
+
+ return true;
+ }
+
+ if (inet_pton(AF_INET6, ip.CStr(), addr) == 1) {
+ *proto = AF_INET6;
+
+ return true;
+ }
+
+ return false;
+}
+
+static void ParseIpMask(const String& ip, char mask[16], int *bits)
+{
+ String::SizeType slashp = ip.FindFirstOf("/");
+ String uip;
+
+ if (slashp == String::NPos) {
+ uip = ip;
+ *bits = 0;
+ } else {
+ uip = ip.SubStr(0, slashp);
+ *bits = Convert::ToLong(ip.SubStr(slashp + 1));
+ }
+
+ int proto;
+
+ if (!ParseIp(uip, mask, &proto))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid IP address specified."));
+
+ if (proto == AF_INET) {
+ if (*bits > 32 || *bits < 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Mask must be between 0 and 32 for IPv4 CIDR masks."));
+
+ *bits += 96;
+ }
+
+ if (slashp == String::NPos)
+ *bits = 128;
+
+ if (*bits > 128 || *bits < 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Mask must be between 0 and 128 for IPv6 CIDR masks."));
+
+ for (int i = 0; i < 16; i++) {
+ int lbits = std::max(0, *bits - i * 8);
+
+ if (lbits >= 8)
+ continue;
+
+ if (mask[i] & (0xff >> lbits))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Masked-off bits must all be zero."));
+ }
+}
+
+static bool IpMaskCheck(char addr[16], char mask[16], int bits)
+{
+ for (int i = 0; i < 16; i++) {
+ if (bits < 8)
+ return !((addr[i] ^ mask[i]) >> (8 - bits));
+
+ if (mask[i] != addr[i])
+ return false;
+
+ bits -= 8;
+
+ if (bits == 0)
+ return true;
+ }
+
+ return true;
+}
+
+bool Utility::CidrMatch(const String& pattern, const String& ip)
+{
+ char mask[16];
+ int bits;
+
+ ParseIpMask(pattern, mask, &bits);
+
+ char addr[16];
+ int proto;
+
+ if (!ParseIp(ip, addr, &proto))
+ return false;
+
+ return IpMaskCheck(addr, mask, bits);
+}
+
+/**
+ * Returns the directory component of a path. See dirname(3) for details.
+ *
+ * @param path The full path.
+ * @returns The directory.
+ */
+String Utility::DirName(const String& path)
+{
+ return boost::filesystem::path(path.Begin(), path.End()).parent_path().string();
+}
+
+/**
+ * Returns the file component of a path. See basename(3) for details.
+ *
+ * @param path The full path.
+ * @returns The filename.
+ */
+String Utility::BaseName(const String& path)
+{
+ return boost::filesystem::path(path.Begin(), path.End()).filename().string();
+}
+
+/**
+ * Null deleter. Used as a parameter for the shared_ptr constructor.
+ *
+ * @param - The object that should be deleted.
+ */
+void Utility::NullDeleter(void *)
+{
+ /* Nothing to do here. */
+}
+
+#ifdef I2_DEBUG
+/**
+ * (DEBUG / TESTING ONLY) Sets the current system time to a static value,
+ * that will be be retrieved by any component of Icinga, when using GetTime().
+ *
+ * This should be only used for testing purposes, e.g. unit tests and debugging of certain functionalities.
+ */
+void Utility::SetTime(double time)
+{
+ m_DebugTime = time;
+}
+
+/**
+ * (DEBUG / TESTING ONLY) Increases the set debug system time by X seconds.
+ *
+ * This should be only used for testing purposes, e.g. unit tests and debugging of certain functionalities.
+ */
+void Utility::IncrementTime(double diff)
+{
+ m_DebugTime += diff;
+}
+#endif /* I2_DEBUG */
+
+/**
+ * Returns the current UNIX timestamp including fractions of seconds.
+ *
+ * @returns The current time.
+ */
+double Utility::GetTime()
+{
+#ifdef I2_DEBUG
+ if (m_DebugTime >= 0) {
+ // (DEBUG / TESTING ONLY) this will return a *STATIC* system time, if the value has been set!
+ return m_DebugTime;
+ }
+#endif /* I2_DEBUG */
+#ifdef _WIN32
+ FILETIME cft;
+ GetSystemTimeAsFileTime(&cft);
+
+ ULARGE_INTEGER ucft;
+ ucft.HighPart = cft.dwHighDateTime;
+ ucft.LowPart = cft.dwLowDateTime;
+
+ SYSTEMTIME est = { 1970, 1, 4, 1, 0, 0, 0, 0};
+ FILETIME eft;
+ SystemTimeToFileTime(&est, &eft);
+
+ ULARGE_INTEGER ueft;
+ ueft.HighPart = eft.dwHighDateTime;
+ ueft.LowPart = eft.dwLowDateTime;
+
+ return ((ucft.QuadPart - ueft.QuadPart) / 10000) / 1000.0;
+#else /* _WIN32 */
+ struct timeval tv;
+
+ int rc = gettimeofday(&tv, nullptr);
+ VERIFY(rc >= 0);
+
+ return tv.tv_sec + tv.tv_usec / 1000000.0;
+#endif /* _WIN32 */
+}
+
+/**
+ * Returns the ID of the current process.
+ *
+ * @returns The PID.
+ */
+pid_t Utility::GetPid()
+{
+#ifndef _WIN32
+ return getpid();
+#else /* _WIN32 */
+ return GetCurrentProcessId();
+#endif /* _WIN32 */
+}
+
+/**
+ * Sleeps for the specified amount of time.
+ *
+ * @param timeout The timeout in seconds.
+ */
+void Utility::Sleep(double timeout)
+{
+#ifndef _WIN32
+ unsigned long micros = timeout * 1000000u;
+ if (timeout >= 1.0)
+ sleep((unsigned)timeout);
+
+ usleep(micros % 1000000u);
+#else /* _WIN32 */
+ ::Sleep(timeout * 1000);
+#endif /* _WIN32 */
+}
+
+/**
+ * Generates a new unique ID.
+ *
+ * @returns The new unique ID.
+ */
+String Utility::NewUniqueID()
+{
+ return boost::lexical_cast<std::string>(boost::uuids::random_generator()());
+}
+
+#ifdef _WIN32
+static bool GlobHelper(const String& pathSpec, int type, std::vector<String>& files, std::vector<String>& dirs)
+{
+ HANDLE handle;
+ WIN32_FIND_DATA wfd;
+
+ handle = FindFirstFile(pathSpec.CStr(), &wfd);
+
+ if (handle == INVALID_HANDLE_VALUE) {
+ DWORD errorCode = GetLastError();
+
+ if (errorCode == ERROR_FILE_NOT_FOUND)
+ return false;
+
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("FindFirstFile")
+ << errinfo_win32_error(errorCode)
+ << boost::errinfo_file_name(pathSpec));
+ }
+
+ do {
+ if (strcmp(wfd.cFileName, ".") == 0 || strcmp(wfd.cFileName, "..") == 0)
+ continue;
+
+ String path = Utility::DirName(pathSpec) + "/" + wfd.cFileName;
+
+ if ((wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && (type & GlobDirectory))
+ dirs.push_back(path);
+ else if (!(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && (type & GlobFile))
+ files.push_back(path);
+ } while (FindNextFile(handle, &wfd));
+
+ if (!FindClose(handle)) {
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("FindClose")
+ << errinfo_win32_error(GetLastError()));
+ }
+
+ return true;
+}
+#endif /* _WIN32 */
+
+#ifndef _WIN32
+static int GlobErrorHandler(const char *epath, int eerrno)
+{
+ if (eerrno == ENOTDIR)
+ return 0;
+
+ return eerrno;
+}
+#endif /* _WIN32 */
+
+/**
+ * Calls the specified callback for each file matching the path specification.
+ *
+ * @param pathSpec The path specification.
+ * @param callback The callback which is invoked for each matching file.
+ * @param type The file type (a combination of GlobFile and GlobDirectory)
+ */
+bool Utility::Glob(const String& pathSpec, const std::function<void (const String&)>& callback, int type)
+{
+ std::vector<String> files, dirs;
+
+#ifdef _WIN32
+ std::vector<String> tokens = pathSpec.Split("\\/");
+
+ String part1;
+
+ for (std::vector<String>::size_type i = 0; i < tokens.size() - 1; i++) {
+ const String& token = tokens[i];
+
+ if (!part1.IsEmpty())
+ part1 += "/";
+
+ part1 += token;
+
+ if (token.FindFirstOf("?*") != String::NPos) {
+ String part2;
+
+ for (std::vector<String>::size_type k = i + 1; k < tokens.size(); k++) {
+ if (!part2.IsEmpty())
+ part2 += "/";
+
+ part2 += tokens[k];
+ }
+
+ std::vector<String> files2, dirs2;
+
+ if (!GlobHelper(part1, GlobDirectory, files2, dirs2))
+ return false;
+
+ for (const String& dir : dirs2) {
+ if (!Utility::Glob(dir + "/" + part2, callback, type))
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ if (!GlobHelper(part1 + "/" + tokens[tokens.size() - 1], type, files, dirs))
+ return false;
+#else /* _WIN32 */
+ glob_t gr;
+
+ int rc = glob(pathSpec.CStr(), GLOB_NOSORT, GlobErrorHandler, &gr);
+
+ if (rc) {
+ if (rc == GLOB_NOMATCH)
+ return false;
+
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("glob")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(pathSpec));
+ }
+
+ if (gr.gl_pathc == 0) {
+ globfree(&gr);
+ return false;
+ }
+
+ size_t left;
+ char **gp;
+ for (gp = gr.gl_pathv, left = gr.gl_pathc; left > 0; gp++, left--) {
+ struct stat statbuf;
+
+ if (stat(*gp, &statbuf) < 0)
+ continue;
+
+ if (!S_ISDIR(statbuf.st_mode) && !S_ISREG(statbuf.st_mode))
+ continue;
+
+ if (S_ISDIR(statbuf.st_mode) && (type & GlobDirectory))
+ dirs.emplace_back(*gp);
+ else if (!S_ISDIR(statbuf.st_mode) && (type & GlobFile))
+ files.emplace_back(*gp);
+ }
+
+ globfree(&gr);
+#endif /* _WIN32 */
+
+ std::sort(files.begin(), files.end());
+ for (const String& cpath : files) {
+ callback(cpath);
+ }
+
+ std::sort(dirs.begin(), dirs.end());
+ for (const String& cpath : dirs) {
+ callback(cpath);
+ }
+
+ return true;
+}
+
+/**
+ * Calls the specified callback for each file in the specified directory
+ * or any of its child directories if the file name matches the specified
+ * pattern.
+ *
+ * @param path The path.
+ * @param pattern The pattern.
+ * @param callback The callback which is invoked for each matching file.
+ * @param type The file type (a combination of GlobFile and GlobDirectory)
+ */
+bool Utility::GlobRecursive(const String& path, const String& pattern, const std::function<void (const String&)>& callback, int type)
+{
+ std::vector<String> files, dirs, alldirs;
+
+#ifdef _WIN32
+ HANDLE handle;
+ WIN32_FIND_DATA wfd;
+
+ String pathSpec = path + "/*";
+
+ handle = FindFirstFile(pathSpec.CStr(), &wfd);
+
+ if (handle == INVALID_HANDLE_VALUE) {
+ DWORD errorCode = GetLastError();
+
+ if (errorCode == ERROR_FILE_NOT_FOUND)
+ return false;
+
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("FindFirstFile")
+ << errinfo_win32_error(errorCode)
+ << boost::errinfo_file_name(pathSpec));
+ }
+
+ do {
+ if (strcmp(wfd.cFileName, ".") == 0 || strcmp(wfd.cFileName, "..") == 0)
+ continue;
+
+ String cpath = path + "/" + wfd.cFileName;
+
+ if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
+ alldirs.push_back(cpath);
+
+ if (!Utility::Match(pattern, wfd.cFileName))
+ continue;
+
+ if (!(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && (type & GlobFile))
+ files.push_back(cpath);
+
+ if ((wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && (type & GlobDirectory))
+ dirs.push_back(cpath);
+ } while (FindNextFile(handle, &wfd));
+
+ if (!FindClose(handle)) {
+ BOOST_THROW_EXCEPTION(win32_error()
+ << boost::errinfo_api_function("FindClose")
+ << errinfo_win32_error(GetLastError()));
+ }
+#else /* _WIN32 */
+ DIR *dirp;
+
+ dirp = opendir(path.CStr());
+
+ if (!dirp)
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("opendir")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(path));
+
+ while (dirp) {
+ dirent *pent;
+
+ errno = 0;
+ pent = readdir(dirp);
+ if (!pent && errno != 0) {
+ closedir(dirp);
+
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("readdir")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(path));
+ }
+
+ if (!pent)
+ break;
+
+ if (strcmp(pent->d_name, ".") == 0 || strcmp(pent->d_name, "..") == 0)
+ continue;
+
+ String cpath = path + "/" + pent->d_name;
+
+ struct stat statbuf;
+
+ if (stat(cpath.CStr(), &statbuf) < 0)
+ continue;
+
+ if (S_ISDIR(statbuf.st_mode))
+ alldirs.push_back(cpath);
+
+ if (!Utility::Match(pattern, pent->d_name))
+ continue;
+
+ if (S_ISDIR(statbuf.st_mode) && (type & GlobDirectory))
+ dirs.push_back(cpath);
+
+ if (!S_ISDIR(statbuf.st_mode) && (type & GlobFile))
+ files.push_back(cpath);
+ }
+
+ closedir(dirp);
+
+#endif /* _WIN32 */
+
+ std::sort(files.begin(), files.end());
+ for (const String& cpath : files) {
+ callback(cpath);
+ }
+
+ std::sort(dirs.begin(), dirs.end());
+ for (const String& cpath : dirs) {
+ callback(cpath);
+ }
+
+ std::sort(alldirs.begin(), alldirs.end());
+ for (const String& cpath : alldirs) {
+ GlobRecursive(cpath, pattern, callback, type);
+ }
+
+ return true;
+}
+
+
+void Utility::MkDir(const String& path, int mode)
+{
+
+#ifndef _WIN32
+ if (mkdir(path.CStr(), mode) < 0 && errno != EEXIST) {
+#else /*_ WIN32 */
+ if (mkdir(path.CStr()) < 0 && errno != EEXIST) {
+#endif /* _WIN32 */
+
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("mkdir")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(path));
+ }
+}
+
+void Utility::MkDirP(const String& path, int mode)
+{
+ size_t pos = 0;
+
+ while (pos != String::NPos) {
+#ifndef _WIN32
+ pos = path.Find("/", pos + 1);
+#else /*_ WIN32 */
+ pos = path.FindFirstOf("/\\", pos + 1);
+#endif /* _WIN32 */
+
+ String spath = path.SubStr(0, pos + 1);
+ struct stat statbuf;
+ if (stat(spath.CStr(), &statbuf) < 0 && errno == ENOENT)
+ MkDir(path.SubStr(0, pos), mode);
+ }
+}
+
+void Utility::Remove(const String& path)
+{
+ namespace fs = boost::filesystem;
+
+ (void)fs::remove(fs::path(path.Begin(), path.End()));
+}
+
+void Utility::RemoveDirRecursive(const String& path)
+{
+ namespace fs = boost::filesystem;
+
+ (void)fs::remove_all(fs::path(path.Begin(), path.End()));
+}
+
+/*
+ * Copies a source file to a target location.
+ * Caller must ensure that the target's base directory exists and is writable.
+ */
+void Utility::CopyFile(const String& source, const String& target)
+{
+ namespace fs = boost::filesystem;
+
+#if BOOST_VERSION >= 107400
+ fs::copy_file(fs::path(source.Begin(), source.End()), fs::path(target.Begin(), target.End()), fs::copy_options::overwrite_existing);
+#else /* BOOST_VERSION */
+ fs::copy_file(fs::path(source.Begin(), source.End()), fs::path(target.Begin(), target.End()), fs::copy_option::overwrite_if_exists);
+#endif /* BOOST_VERSION */
+}
+
+/*
+ * Renames a source file to a target location.
+ * Caller must ensure that the target's base directory exists and is writable.
+ */
+void Utility::RenameFile(const String& source, const String& target)
+{
+ namespace fs = boost::filesystem;
+
+ fs::path sourcePath(source.Begin(), source.End()), targetPath(target.Begin(), target.End());
+
+#ifndef _WIN32
+ fs::rename(sourcePath, targetPath);
+#else /* _WIN32 */
+ /*
+ * Renaming files can be tricky on Windows, especially if your application is built around POSIX filesystem
+ * semantics. For example, the quite common pattern of replacing a file by writing a new version to a temporary
+ * location and then moving it to the final location can fail if the destination file already exists and any
+ * process has an open file handle to it.
+ *
+ * We try to handle this situation as best as we can by retrying the rename operation a few times hoping the other
+ * process closes its file handle in the meantime. This is similar to what for example Go does internally in some
+ * situations (https://golang.org/pkg/cmd/go/internal/robustio/#Rename):
+ *
+ * robustio.Rename is like os.Rename, but on Windows retries errors that may occur if the file is concurrently
+ * read or overwritten. (See https://golang.org/issue/31247 and https://golang.org/issue/32188)
+ */
+
+ double sleep = 0.1;
+ int last_error = ERROR_SUCCESS;
+
+ for (int retries = 0, remaining = 15;; retries++, remaining--) {
+ try {
+ fs::rename(sourcePath, targetPath);
+
+ if (retries > 0) {
+ Log(LogWarning, "Utility") << "Renaming '" << source << "' to '" << target
+ << "' succeeded after " << retries << " retries";
+ }
+
+ break;
+ } catch (const fs::filesystem_error& ex) {
+ int error = ex.code().value();
+ bool ephemeral = error == ERROR_ACCESS_DENIED ||
+ error == ERROR_FILE_NOT_FOUND ||
+ error == ERROR_SHARING_VIOLATION;
+
+ if (remaining <= 0 || !ephemeral) {
+ throw; // giving up
+ }
+
+ if (error != last_error) {
+ Log(LogWarning, "Utility") << "Renaming '" << source << "' to '" << target << "' failed: "
+ << ex.code().message() << " (trying up to " << remaining << " more times)";
+ last_error = error;
+ }
+
+ Utility::Sleep(sleep);
+ sleep *= 1.3;
+ }
+ }
+#endif /* _WIN32 */
+}
+
+/*
+ * Set file permissions
+ */
+bool Utility::SetFileOwnership(const String& file, const String& user, const String& group)
+{
+#ifndef _WIN32
+ errno = 0;
+ struct passwd *pw = getpwnam(user.CStr());
+
+ if (!pw) {
+ if (errno == 0) {
+ Log(LogCritical, "cli")
+ << "Invalid user specified: " << user;
+ return false;
+ } else {
+ Log(LogCritical, "cli")
+ << "getpwnam() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ return false;
+ }
+ }
+
+ errno = 0;
+ struct group *gr = getgrnam(group.CStr());
+
+ if (!gr) {
+ if (errno == 0) {
+ Log(LogCritical, "cli")
+ << "Invalid group specified: " << group;
+ return false;
+ } else {
+ Log(LogCritical, "cli")
+ << "getgrnam() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ return false;
+ }
+ }
+
+ if (chown(file.CStr(), pw->pw_uid, gr->gr_gid) < 0) {
+ Log(LogCritical, "cli")
+ << "chown() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ return false;
+ }
+#endif /* _WIN32 */
+
+ return true;
+}
+
+#ifndef _WIN32
+void Utility::SetNonBlocking(int fd, bool nb)
+{
+ int flags = fcntl(fd, F_GETFL, 0);
+
+ if (flags < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("fcntl")
+ << boost::errinfo_errno(errno));
+ }
+
+ if (nb)
+ flags |= O_NONBLOCK;
+ else
+ flags &= ~O_NONBLOCK;
+
+ if (fcntl(fd, F_SETFL, flags) < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("fcntl")
+ << boost::errinfo_errno(errno));
+ }
+}
+
+void Utility::SetCloExec(int fd, bool cloexec)
+{
+ int flags = fcntl(fd, F_GETFD, 0);
+
+ if (flags < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("fcntl")
+ << boost::errinfo_errno(errno));
+ }
+
+ if (cloexec)
+ flags |= FD_CLOEXEC;
+ else
+ flags &= ~FD_CLOEXEC;
+
+ if (fcntl(fd, F_SETFD, flags) < 0) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("fcntl")
+ << boost::errinfo_errno(errno));
+ }
+}
+
+void Utility::CloseAllFDs(const std::vector<int>& except, std::function<void(int)> onClose)
+{
+#if defined(__linux__) || defined(__APPLE__)
+ namespace fs = boost::filesystem;
+
+ std::set<int> fds;
+
+#ifdef __linux__
+ const char *dir = "/proc/self/fd";
+#endif /* __linux__ */
+#ifdef __APPLE__
+ const char *dir = "/dev/fd";
+#endif /* __APPLE__ */
+
+ for (fs::directory_iterator current {fs::path(dir)}, end; current != end; ++current) {
+ auto entry (current->path().filename());
+ int fd;
+
+ try {
+ fd = boost::lexical_cast<int>(entry.c_str());
+ } catch (...) {
+ continue;
+ }
+
+ fds.emplace(fd);
+ }
+
+ for (auto fd : except) {
+ fds.erase(fd);
+ }
+
+ for (auto fd : fds) {
+ if (close(fd) >= 0 && onClose) {
+ onClose(fd);
+ }
+ }
+#else /* __linux__ || __APPLE__ */
+ rlimit rl;
+
+ if (getrlimit(RLIMIT_NOFILE, &rl) >= 0) {
+ rlim_t maxfds = rl.rlim_max;
+
+ if (maxfds == RLIM_INFINITY) {
+ maxfds = 65536;
+ }
+
+ for (int fd = 0; fd < maxfds; ++fd) {
+ if (std::find(except.begin(), except.end(), fd) == except.end() && close(fd) >= 0 && onClose) {
+ onClose(fd);
+ }
+ }
+ }
+#endif /* __linux__ || __APPLE__ */
+}
+#endif /* _WIN32 */
+
+void Utility::SetNonBlockingSocket(SOCKET s, bool nb)
+{
+#ifndef _WIN32
+ SetNonBlocking(s, nb);
+#else /* _WIN32 */
+ unsigned long lflag = nb;
+ ioctlsocket(s, FIONBIO, &lflag);
+#endif /* _WIN32 */
+}
+
+void Utility::QueueAsyncCallback(const std::function<void ()>& callback, SchedulerPolicy policy)
+{
+ Application::GetTP().Post(callback, policy);
+}
+
+String Utility::NaturalJoin(const std::vector<String>& tokens)
+{
+ String result;
+
+ for (std::vector<String>::size_type i = 0; i < tokens.size(); i++) {
+ result += tokens[i];
+
+ if (tokens.size() > i + 1) {
+ if (i < tokens.size() - 2)
+ result += ", ";
+ else if (i == tokens.size() - 2)
+ result += " and ";
+ }
+ }
+
+ return result;
+}
+
+String Utility::Join(const Array::Ptr& tokens, char separator, bool escapeSeparator)
+{
+ String result;
+ bool first = true;
+
+ ObjectLock olock(tokens);
+ for (const Value& vtoken : tokens) {
+ String token = Convert::ToString(vtoken);
+
+ if (escapeSeparator) {
+ boost::algorithm::replace_all(token, "\\", "\\\\");
+
+ char sep_before[2], sep_after[3];
+ sep_before[0] = separator;
+ sep_before[1] = '\0';
+ sep_after[0] = '\\';
+ sep_after[1] = separator;
+ sep_after[2] = '\0';
+ boost::algorithm::replace_all(token, sep_before, sep_after);
+ }
+
+ if (first)
+ first = false;
+ else
+ result += String(1, separator);
+
+ result += token;
+ }
+
+ return result;
+}
+
+String Utility::FormatDuration(double duration)
+{
+ std::vector<String> tokens;
+ String result;
+
+ if (duration >= 86400) {
+ int days = duration / 86400;
+ tokens.emplace_back(Convert::ToString(days) + (days != 1 ? " days" : " day"));
+ duration = static_cast<int>(duration) % 86400;
+ }
+
+ if (duration >= 3600) {
+ int hours = duration / 3600;
+ tokens.emplace_back(Convert::ToString(hours) + (hours != 1 ? " hours" : " hour"));
+ duration = static_cast<int>(duration) % 3600;
+ }
+
+ if (duration >= 60) {
+ int minutes = duration / 60;
+ tokens.emplace_back(Convert::ToString(minutes) + (minutes != 1 ? " minutes" : " minute"));
+ duration = static_cast<int>(duration) % 60;
+ }
+
+ if (duration >= 1) {
+ int seconds = duration;
+ tokens.emplace_back(Convert::ToString(seconds) + (seconds != 1 ? " seconds" : " second"));
+ }
+
+ if (tokens.size() == 0) {
+ int milliseconds = std::floor(duration * 1000);
+ if (milliseconds >= 1)
+ tokens.emplace_back(Convert::ToString(milliseconds) + (milliseconds != 1 ? " milliseconds" : " millisecond"));
+ else
+ tokens.emplace_back("less than 1 millisecond");
+ }
+
+ return NaturalJoin(tokens);
+}
+
+String Utility::FormatDateTime(const char *format, double ts)
+{
+ char timestamp[128];
+ auto tempts = (time_t)ts; /* We don't handle sub-second timestamps here just yet. */
+ tm tmthen;
+
+#ifdef _MSC_VER
+ tm *temp = localtime(&tempts);
+
+ if (!temp) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("localtime")
+ << boost::errinfo_errno(errno));
+ }
+
+ tmthen = *temp;
+#else /* _MSC_VER */
+ if (!localtime_r(&tempts, &tmthen)) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("localtime_r")
+ << boost::errinfo_errno(errno));
+ }
+#endif /* _MSC_VER */
+
+ strftime(timestamp, sizeof(timestamp), format, &tmthen);
+
+ return timestamp;
+}
+
+String Utility::FormatErrorNumber(int code) {
+ std::ostringstream msgbuf;
+
+#ifdef _WIN32
+ char *message;
+ String result = "Unknown error.";
+
+ DWORD rc = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, code, 0, (char *)&message,
+ 0, nullptr);
+
+ if (rc != 0) {
+ result = String(message);
+ LocalFree(message);
+
+ /* remove trailing new-line characters */
+ boost::algorithm::trim_right(result);
+ }
+
+ msgbuf << code << ", \"" << result << "\"";
+#else
+ msgbuf << strerror(code);
+#endif
+ return msgbuf.str();
+}
+
+String Utility::EscapeShellCmd(const String& s)
+{
+ String result;
+ size_t prev_quote = String::NPos;
+ int index = -1;
+
+ for (char ch : s) {
+ bool escape = false;
+
+ index++;
+
+#ifdef _WIN32
+ if (ch == '%' || ch == '"' || ch == '\'')
+ escape = true;
+#else /* _WIN32 */
+ if (ch == '"' || ch == '\'') {
+ /* Find a matching closing quotation character. */
+ if (prev_quote == String::NPos && (prev_quote = s.FindFirstOf(ch, index + 1)) != String::NPos)
+ ; /* Empty statement. */
+ else if (prev_quote != String::NPos && s[prev_quote] == ch)
+ prev_quote = String::NPos;
+ else
+ escape = true;
+ }
+#endif /* _WIN32 */
+
+ if (ch == '#' || ch == '&' || ch == ';' || ch == '`' || ch == '|' ||
+ ch == '*' || ch == '?' || ch == '~' || ch == '<' || ch == '>' ||
+ ch == '^' || ch == '(' || ch == ')' || ch == '[' || ch == ']' ||
+ ch == '{' || ch == '}' || ch == '$' || ch == '\\' || ch == '\x0A' ||
+ ch == '\xFF')
+ escape = true;
+
+ if (escape)
+#ifdef _WIN32
+ result += '^';
+#else /* _WIN32 */
+ result += '\\';
+#endif /* _WIN32 */
+
+ result += ch;
+ }
+
+ return result;
+}
+
+String Utility::EscapeShellArg(const String& s)
+{
+ String result;
+
+#ifdef _WIN32
+ result = "\"";
+#else /* _WIN32 */
+ result = "'";
+#endif /* _WIN32 */
+
+ for (char ch : s) {
+#ifdef _WIN32
+ if (ch == '"' || ch == '%') {
+ result += ' ';
+ }
+#else /* _WIN32 */
+ if (ch == '\'')
+ result += "'\\'";
+#endif
+ result += ch;
+ }
+
+#ifdef _WIN32
+ result += '"';
+#else /* _WIN32 */
+ result += '\'';
+#endif /* _WIN32 */
+
+ return result;
+}
+
+#ifdef _WIN32
+String Utility::EscapeCreateProcessArg(const String& arg)
+{
+ if (arg.FindFirstOf(" \t\n\v\"") == String::NPos)
+ return arg;
+
+ String result = "\"";
+
+ for (String::ConstIterator it = arg.Begin(); ; it++) {
+ int numBackslashes = 0;
+
+ while (it != arg.End() && *it == '\\') {
+ it++;
+ numBackslashes++;
+ }
+
+ if (it == arg.End()) {
+ result.Append(numBackslashes * 2, '\\');
+ break;
+ } else if (*it == '"') {
+ result.Append(numBackslashes * 2 + 1, '\\');
+ result.Append(1, *it);
+ } else {
+ result.Append(numBackslashes, '\\');
+ result.Append(1, *it);
+ }
+ }
+
+ result += "\"";
+
+ return result;
+}
+#endif /* _WIN32 */
+
+#ifdef _WIN32
+static void WindowsSetThreadName(const char *name)
+{
+ THREADNAME_INFO info;
+ info.dwType = 0x1000;
+ info.szName = name;
+ info.dwThreadID = -1;
+ info.dwFlags = 0;
+
+ __try {
+ RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR *)&info);
+ } __except(EXCEPTION_EXECUTE_HANDLER) {
+ /* Nothing to do here. */
+ }
+}
+#endif /* _WIN32 */
+
+void Utility::SetThreadName(const String& name, bool os)
+{
+ m_ThreadName.reset(new String(name));
+
+ if (!os)
+ return;
+
+#ifdef _WIN32
+ WindowsSetThreadName(name.CStr());
+#endif /* _WIN32 */
+
+#ifdef HAVE_PTHREAD_SET_NAME_NP
+ pthread_set_name_np(pthread_self(), name.CStr());
+#endif /* HAVE_PTHREAD_SET_NAME_NP */
+
+#ifdef HAVE_PTHREAD_SETNAME_NP
+# ifdef __APPLE__
+ pthread_setname_np(name.CStr());
+# else /* __APPLE__ */
+ String tname = name.SubStr(0, 15);
+ pthread_setname_np(pthread_self(), tname.CStr());
+# endif /* __APPLE__ */
+#endif /* HAVE_PTHREAD_SETNAME_NP */
+}
+
+String Utility::GetThreadName()
+{
+ String *name = m_ThreadName.get();
+
+ if (!name) {
+ std::ostringstream idbuf;
+ idbuf << std::this_thread::get_id();
+ return idbuf.str();
+ }
+
+ return *name;
+}
+
+unsigned long Utility::SDBM(const String& str, size_t len)
+{
+ unsigned long hash = 0;
+ size_t current = 0;
+
+ for (char c : str) {
+ if (current >= len)
+ break;
+
+ hash = c + (hash << 6) + (hash << 16) - hash;
+
+ current++;
+ }
+
+ return hash;
+}
+
+String Utility::ParseVersion(const String& v)
+{
+ /*
+ * 2.11.0-0.rc1.1
+ * v2.10.5
+ * r2.10.3
+ * v2.11.0-rc1-58-g7c1f716da
+ */
+ boost::regex pattern("^[vr]?(2\\.\\d+\\.\\d+).*$");
+ boost::smatch result;
+
+ if (boost::regex_search(v.GetData(), result, pattern)) {
+ String res(result[1].first, result[1].second);
+ return res;
+ }
+
+ // Couldn't not extract anything, return unparsed version
+ return v;
+}
+
+int Utility::CompareVersion(const String& v1, const String& v2)
+{
+ std::vector<String> tokensv1 = v1.Split(".");
+ std::vector<String> tokensv2 = v2.Split(".");
+
+ for (std::vector<String>::size_type i = 0; i < tokensv2.size() - tokensv1.size(); i++)
+ tokensv1.emplace_back("0");
+
+ for (std::vector<String>::size_type i = 0; i < tokensv1.size() - tokensv2.size(); i++)
+ tokensv2.emplace_back("0");
+
+ for (std::vector<String>::size_type i = 0; i < tokensv1.size(); i++) {
+ if (Convert::ToLong(tokensv2[i]) > Convert::ToLong(tokensv1[i]))
+ return 1;
+ else if (Convert::ToLong(tokensv2[i]) < Convert::ToLong(tokensv1[i]))
+ return -1;
+ }
+
+ return 0;
+}
+
+String Utility::GetHostName()
+{
+ char name[255];
+
+ if (gethostname(name, sizeof(name)) < 0)
+ return "localhost";
+
+ return name;
+}
+
+/**
+ * Returns the fully-qualified domain name for the host
+ * we're running on.
+ *
+ * @returns The FQDN.
+ */
+String Utility::GetFQDN()
+{
+ String hostname = GetHostName();
+
+ addrinfo hints;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+ hints.ai_flags = AI_CANONNAME;
+
+ addrinfo *result;
+ int rc = getaddrinfo(hostname.CStr(), nullptr, &hints, &result);
+
+ if (rc != 0)
+ result = nullptr;
+
+ if (result) {
+ if (strcmp(result->ai_canonname, "localhost") != 0)
+ hostname = result->ai_canonname;
+
+ freeaddrinfo(result);
+ }
+
+ return hostname;
+}
+
+int Utility::Random()
+{
+#ifdef _WIN32
+ return rand();
+#else /* _WIN32 */
+ unsigned int *seed = m_RandSeed.get();
+
+ if (!seed) {
+ seed = new unsigned int(Utility::GetTime());
+ m_RandSeed.reset(seed);
+ }
+
+ return rand_r(seed);
+#endif /* _WIN32 */
+}
+
+tm Utility::LocalTime(time_t ts)
+{
+#ifdef _MSC_VER
+ tm *result = localtime(&ts);
+
+ if (!result) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("localtime")
+ << boost::errinfo_errno(errno));
+ }
+
+ return *result;
+#else /* _MSC_VER */
+ tm result;
+
+ if (!localtime_r(&ts, &result)) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("localtime_r")
+ << boost::errinfo_errno(errno));
+ }
+
+ return result;
+#endif /* _MSC_VER */
+}
+
+bool Utility::PathExists(const String& path)
+{
+ namespace fs = boost::filesystem;
+
+ boost::system::error_code ec;
+
+ return fs::exists(fs::path(path.Begin(), path.End()), ec) && !ec;
+}
+
+time_t Utility::GetFileCreationTime(const String& path)
+{
+ namespace fs = boost::filesystem;
+
+ return fs::last_write_time(boost::lexical_cast<fs::path>(path));
+}
+
+Value Utility::LoadJsonFile(const String& path)
+{
+ std::ifstream fp;
+ fp.open(path.CStr());
+
+ String json((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+
+ fp.close();
+
+ if (fp.fail())
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not read JSON file '" + path + "'."));
+
+ return JsonDecode(json);
+}
+
+void Utility::SaveJsonFile(const String& path, int mode, const Value& value)
+{
+ AtomicFile::Write(path, mode, JsonEncode(value));
+}
+
+static void HexEncode(char ch, std::ostream& os)
+{
+ const char *hex_chars = "0123456789ABCDEF";
+
+ os << hex_chars[ch >> 4 & 0x0f];
+ os << hex_chars[ch & 0x0f];
+}
+
+static int HexDecode(char hc)
+{
+ if (hc >= '0' && hc <= '9')
+ return hc - '0';
+ else if (hc >= 'a' && hc <= 'f')
+ return hc - 'a' + 10;
+ else if (hc >= 'A' && hc <= 'F')
+ return hc - 'A' + 10;
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid hex character."));
+}
+
+String Utility::EscapeString(const String& s, const String& chars, const bool illegal)
+{
+ std::ostringstream result;
+ if (illegal) {
+ for (char ch : s) {
+ if (chars.FindFirstOf(ch) != String::NPos || ch == '%') {
+ result << '%';
+ HexEncode(ch, result);
+ } else
+ result << ch;
+ }
+ } else {
+ for (char ch : s) {
+ if (chars.FindFirstOf(ch) == String::NPos || ch == '%') {
+ result << '%';
+ HexEncode(ch, result);
+ } else
+ result << ch;
+ }
+ }
+
+ return result.str();
+}
+
+String Utility::UnescapeString(const String& s)
+{
+ std::ostringstream result;
+
+ for (String::SizeType i = 0; i < s.GetLength(); i++) {
+ if (s[i] == '%') {
+ if (i + 2 > s.GetLength() - 1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid escape sequence."));
+
+ char ch = HexDecode(s[i + 1]) * 16 + HexDecode(s[i + 2]);
+ result << ch;
+
+ i += 2;
+ } else
+ result << s[i];
+ }
+
+ return result.str();
+}
+
+#ifndef _WIN32
+static String UnameHelper(char type)
+{
+ struct utsname name;
+ uname(&name);
+
+ switch (type) {
+ case 'm':
+ return (char*)name.machine;
+ case 'n':
+ return (char*)name.nodename;
+ case 'r':
+ return (char*)name.release;
+ case 's':
+ return (char*)name.sysname;
+ case 'v':
+ return (char*)name.version;
+ default:
+ VERIFY(!"Invalid uname query.");
+ }
+}
+#endif /* _WIN32 */
+static bool ReleaseHelper(String *platformName, String *platformVersion)
+{
+#ifdef _WIN32
+ if (platformName)
+ *platformName = "Windows";
+
+ if (platformVersion) {
+ *platformVersion = "Vista";
+ if (IsWindowsVistaSP1OrGreater())
+ *platformVersion = "Vista SP1";
+ if (IsWindowsVistaSP2OrGreater())
+ *platformVersion = "Vista SP2";
+ if (IsWindows7OrGreater())
+ *platformVersion = "7";
+ if (IsWindows7SP1OrGreater())
+ *platformVersion = "7 SP1";
+ if (IsWindows8OrGreater())
+ *platformVersion = "8";
+ if (IsWindows8Point1OrGreater())
+ *platformVersion = "8.1 or greater";
+ if (IsWindowsServer())
+ *platformVersion += " (Server)";
+ }
+
+ return true;
+#else /* _WIN32 */
+ if (platformName)
+ *platformName = "Unknown";
+
+ if (platformVersion)
+ *platformVersion = "Unknown";
+
+ /* You have systemd or Ubuntu etc. */
+ std::ifstream release("/etc/os-release");
+ if (release.is_open()) {
+ std::string release_line;
+ while (getline(release, release_line)) {
+ std::string::size_type pos = release_line.find("=");
+
+ if (pos == std::string::npos)
+ continue;
+
+ std::string key = release_line.substr(0, pos);
+ std::string value = release_line.substr(pos + 1);
+
+ std::string::size_type firstQuote = value.find("\"");
+
+ if (firstQuote != std::string::npos)
+ value.erase(0, firstQuote + 1);
+
+ std::string::size_type lastQuote = value.rfind("\"");
+
+ if (lastQuote != std::string::npos)
+ value.erase(lastQuote);
+
+ if (platformName && key == "NAME")
+ *platformName = value;
+
+ if (platformVersion && key == "VERSION")
+ *platformVersion = value;
+ }
+
+ return true;
+ }
+
+ /* You are using a distribution which supports LSB. */
+ FILE *fp = popen("type lsb_release >/dev/null 2>&1 && lsb_release -s -i 2>&1", "r");
+
+ if (fp) {
+ std::ostringstream msgbuf;
+ char line[1024];
+ while (fgets(line, sizeof(line), fp))
+ msgbuf << line;
+ int status = pclose(fp);
+ if (WEXITSTATUS(status) == 0) {
+ if (platformName)
+ *platformName = msgbuf.str();
+ }
+ }
+
+ fp = popen("type lsb_release >/dev/null 2>&1 && lsb_release -s -r 2>&1", "r");
+
+ if (fp) {
+ std::ostringstream msgbuf;
+ char line[1024];
+ while (fgets(line, sizeof(line), fp))
+ msgbuf << line;
+ int status = pclose(fp);
+ if (WEXITSTATUS(status) == 0) {
+ if (platformVersion)
+ *platformVersion = msgbuf.str();
+ }
+ }
+
+ /* OS X */
+ fp = popen("type sw_vers >/dev/null 2>&1 && sw_vers -productName 2>&1", "r");
+
+ if (fp) {
+ std::ostringstream msgbuf;
+ char line[1024];
+ while (fgets(line, sizeof(line), fp))
+ msgbuf << line;
+ int status = pclose(fp);
+ if (WEXITSTATUS(status) == 0) {
+ String info = msgbuf.str();
+ info = info.Trim();
+
+ if (platformName)
+ *platformName = info;
+ }
+ }
+
+ fp = popen("type sw_vers >/dev/null 2>&1 && sw_vers -productVersion 2>&1", "r");
+
+ if (fp) {
+ std::ostringstream msgbuf;
+ char line[1024];
+ while (fgets(line, sizeof(line), fp))
+ msgbuf << line;
+ int status = pclose(fp);
+ if (WEXITSTATUS(status) == 0) {
+ String info = msgbuf.str();
+ info = info.Trim();
+
+ if (platformVersion)
+ *platformVersion = info;
+
+ return true;
+ }
+ }
+
+ /* Centos/RHEL < 7 */
+ release.close();
+ release.open("/etc/redhat-release");
+ if (release.is_open()) {
+ std::string release_line;
+ getline(release, release_line);
+
+ String info = release_line;
+
+ /* example: Red Hat Enterprise Linux Server release 6.7 (Santiago) */
+ if (platformName)
+ *platformName = info.SubStr(0, info.Find("release") - 1);
+
+ if (platformVersion)
+ *platformVersion = info.SubStr(info.Find("release") + 8);
+
+ return true;
+ }
+
+ /* sles 11 sp3, opensuse w/e */
+ release.close();
+ release.open("/etc/SuSE-release");
+ if (release.is_open()) {
+ std::string release_line;
+ getline(release, release_line);
+
+ String info = release_line;
+
+ if (platformName)
+ *platformName = info.SubStr(0, info.FindFirstOf(" "));
+
+ if (platformVersion)
+ *platformVersion = info.SubStr(info.FindFirstOf(" ") + 1);
+
+ return true;
+ }
+
+ /* Just give up */
+ return false;
+#endif /* _WIN32 */
+}
+
+String Utility::GetPlatformKernel()
+{
+#ifdef _WIN32
+ return "Windows";
+#else /* _WIN32 */
+ return UnameHelper('s');
+#endif /* _WIN32 */
+}
+
+String Utility::GetPlatformKernelVersion()
+{
+#ifdef _WIN32
+ OSVERSIONINFO info;
+ info.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
+ GetVersionEx(&info);
+
+ std::ostringstream msgbuf;
+ msgbuf << info.dwMajorVersion << "." << info.dwMinorVersion;
+
+ return msgbuf.str();
+#else /* _WIN32 */
+ return UnameHelper('r');
+#endif /* _WIN32 */
+}
+
+String Utility::GetPlatformName()
+{
+ String platformName;
+ if (!ReleaseHelper(&platformName, nullptr))
+ return "Unknown";
+ return platformName;
+}
+
+String Utility::GetPlatformVersion()
+{
+ String platformVersion;
+ if (!ReleaseHelper(nullptr, &platformVersion))
+ return "Unknown";
+ return platformVersion;
+}
+
+String Utility::GetPlatformArchitecture()
+{
+#ifdef _WIN32
+ SYSTEM_INFO info;
+ GetNativeSystemInfo(&info);
+ switch (info.wProcessorArchitecture) {
+ case PROCESSOR_ARCHITECTURE_AMD64:
+ return "x86_64";
+ case PROCESSOR_ARCHITECTURE_ARM:
+ return "arm";
+ case PROCESSOR_ARCHITECTURE_INTEL:
+ return "x86";
+ default:
+ return "unknown";
+ }
+#else /* _WIN32 */
+ return UnameHelper('m');
+#endif /* _WIN32 */
+}
+
+const char l_Utf8Replacement[] = "\xEF\xBF\xBD";
+
+String Utility::ValidateUTF8(const String& input)
+{
+ std::string output;
+ output.reserve(input.GetLength());
+
+ try {
+ utf8::replace_invalid(input.Begin(), input.End(), std::back_inserter(output));
+ } catch (const utf8::not_enough_room&) {
+ output.insert(output.end(), (const char*)l_Utf8Replacement, (const char*)l_Utf8Replacement + 3);
+ }
+
+ return String(std::move(output));
+}
+
+#ifdef _WIN32
+/* mkstemp extracted from libc/sysdeps/posix/tempname.c. Copyright
+ * (C) 1991-1999, 2000, 2001, 2006 Free Software Foundation, Inc.
+ *
+ * The GNU C Library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ */
+
+#define _O_EXCL 0x0400
+#define _O_CREAT 0x0100
+#define _O_RDWR 0x0002
+#define O_EXCL _O_EXCL
+#define O_CREAT _O_CREAT
+#define O_RDWR _O_RDWR
+
+static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
+/* Generate a temporary file name based on TMPL. TMPL must match the
+ * rules for mk[s]temp (i.e. end in "XXXXXX"). The name constructed
+ * does not exist at the time of the call to mkstemp. TMPL is
+ * overwritten with the result.
+ */
+int Utility::MksTemp(char *tmpl)
+{
+ int len;
+ char *XXXXXX;
+ static unsigned long long value;
+ unsigned long long random_time_bits;
+ unsigned int count;
+ int fd = -1;
+ int save_errno = errno;
+
+ /* A lower bound on the number of temporary files to attempt to
+ * generate. The maximum total number of temporary file names that
+ * can exist for a given template is 62**6. It should never be
+ * necessary to try all these combinations. Instead if a reasonable
+ * number of names is tried (we define reasonable as 62**3) fail to
+ * give the system administrator the chance to remove the problems.
+ */
+#define ATTEMPTS_MIN (62 * 62 * 62)
+
+ /* The number of times to attempt to generate a temporary file
+ * To conform to POSIX, this must be no smaller than TMP_MAX.
+ */
+#if ATTEMPTS_MIN < TMP_MAX
+ unsigned int attempts = TMP_MAX;
+#else
+ unsigned int attempts = ATTEMPTS_MIN;
+#endif
+
+ len = strlen (tmpl);
+ if (len < 6 || strcmp (&tmpl[len - 6], "XXXXXX")) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ /* This is where the Xs start. */
+ XXXXXX = &tmpl[len - 6];
+
+ /* Get some more or less random data. */
+ {
+ SYSTEMTIME stNow;
+ FILETIME ftNow;
+
+ // get system time
+ GetSystemTime(&stNow);
+ stNow.wMilliseconds = 500;
+ if (!SystemTimeToFileTime(&stNow, &ftNow)) {
+ errno = -1;
+ return -1;
+ }
+
+ random_time_bits = (((unsigned long long)ftNow.dwHighDateTime << 32) | (unsigned long long)ftNow.dwLowDateTime);
+ }
+
+ value += random_time_bits ^ (unsigned long long)GetCurrentThreadId();
+
+ for (count = 0; count < attempts; value += 7777, ++count) {
+ unsigned long long v = value;
+
+ /* Fill in the random bits. */
+ XXXXXX[0] = letters[v % 62];
+ v /= 62;
+ XXXXXX[1] = letters[v % 62];
+ v /= 62;
+ XXXXXX[2] = letters[v % 62];
+ v /= 62;
+ XXXXXX[3] = letters[v % 62];
+ v /= 62;
+ XXXXXX[4] = letters[v % 62];
+ v /= 62;
+ XXXXXX[5] = letters[v % 62];
+
+ fd = open(tmpl, O_RDWR | O_CREAT | O_EXCL, _S_IREAD | _S_IWRITE);
+ if (fd >= 0) {
+ errno = save_errno;
+ return fd;
+ } else if (errno != EEXIST)
+ return -1;
+ }
+
+ /* We got out of the loop because we ran out of combinations to try. */
+ errno = EEXIST;
+ return -1;
+}
+
+String Utility::GetIcingaInstallPath()
+{
+ char szProduct[39];
+
+ for (int i = 0; MsiEnumProducts(i, szProduct) == ERROR_SUCCESS; i++) {
+ char szName[128];
+ DWORD cbName = sizeof(szName);
+ if (MsiGetProductInfo(szProduct, INSTALLPROPERTY_INSTALLEDPRODUCTNAME, szName, &cbName) != ERROR_SUCCESS)
+ continue;
+
+ if (strcmp(szName, "Icinga 2") != 0)
+ continue;
+
+ char szLocation[1024];
+ DWORD cbLocation = sizeof(szLocation);
+ if (MsiGetProductInfo(szProduct, INSTALLPROPERTY_INSTALLLOCATION, szLocation, &cbLocation) == ERROR_SUCCESS)
+ return szLocation;
+ }
+
+ return "";
+}
+
+String Utility::GetIcingaDataPath()
+{
+ char path[MAX_PATH];
+ if (!SUCCEEDED(SHGetFolderPath(nullptr, CSIDL_COMMON_APPDATA, nullptr, 0, path)))
+ return "";
+ return String(path) + "\\icinga2";
+}
+
+#endif /* _WIN32 */
+
+/**
+ * Retrieve the environment variable value by given key.
+ *
+ * @param env Environment variable name.
+ */
+
+String Utility::GetFromEnvironment(const String& env)
+{
+ const char *envValue = getenv(env.CStr());
+
+ if (envValue == NULL)
+ return String();
+ else
+ return String(envValue);
+}
+
+/**
+ * Compare the password entered by a client with the actual password.
+ * The comparision is safe against timing attacks.
+ */
+bool Utility::ComparePasswords(const String& enteredPassword, const String& actualPassword)
+{
+ volatile const char * volatile enteredPasswordCStr = enteredPassword.CStr();
+ volatile size_t enteredPasswordLen = enteredPassword.GetLength();
+
+ volatile const char * volatile actualPasswordCStr = actualPassword.CStr();
+ volatile size_t actualPasswordLen = actualPassword.GetLength();
+
+ volatile uint_fast8_t result = enteredPasswordLen == actualPasswordLen;
+
+ if (result) {
+ auto cStr (actualPasswordCStr);
+ auto len (actualPasswordLen);
+
+ actualPasswordCStr = cStr;
+ actualPasswordLen = len;
+ } else {
+ auto cStr (enteredPasswordCStr);
+ auto len (enteredPasswordLen);
+
+ actualPasswordCStr = cStr;
+ actualPasswordLen = len;
+ }
+
+ for (volatile size_t i = 0; i < enteredPasswordLen; ++i) {
+ result &= uint_fast8_t(enteredPasswordCStr[i] == actualPasswordCStr[i]);
+ }
+
+ return result;
+}
diff --git a/lib/base/utility.hpp b/lib/base/utility.hpp
new file mode 100644
index 0000000..47b68d2
--- /dev/null
+++ b/lib/base/utility.hpp
@@ -0,0 +1,200 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef UTILITY_H
+#define UTILITY_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include "base/array.hpp"
+#include "base/threadpool.hpp"
+#include "base/tlsutility.hpp"
+#include <boost/thread/tss.hpp>
+#include <openssl/sha.h>
+#include <functional>
+#include <typeinfo>
+#include <vector>
+
+namespace icinga
+{
+
+#ifdef _WIN32
+#define MS_VC_EXCEPTION 0x406D1388
+
+# pragma pack(push, 8)
+struct THREADNAME_INFO
+{
+ DWORD dwType;
+ LPCSTR szName;
+ DWORD dwThreadID;
+ DWORD dwFlags;
+};
+# pragma pack(pop)
+#endif
+
+enum GlobType
+{
+ GlobFile = 1,
+ GlobDirectory = 2
+};
+
+/**
+ * Helper functions.
+ *
+ * @ingroup base
+ */
+class Utility
+{
+public:
+ static String DemangleSymbolName(const String& sym);
+ static String GetTypeName(const std::type_info& ti);
+ static String GetSymbolName(const void *addr);
+
+ static bool Match(const String& pattern, const String& text);
+ static bool CidrMatch(const String& pattern, const String& ip);
+
+ static String DirName(const String& path);
+ static String BaseName(const String& path);
+
+ static void NullDeleter(void *);
+
+ static double GetTime();
+
+ static pid_t GetPid();
+
+ static void Sleep(double timeout);
+
+ static String NewUniqueID();
+
+ static bool Glob(const String& pathSpec, const std::function<void (const String&)>& callback, int type = GlobFile | GlobDirectory);
+ static bool GlobRecursive(const String& path, const String& pattern, const std::function<void (const String&)>& callback, int type = GlobFile | GlobDirectory);
+ static void MkDir(const String& path, int mode);
+ static void MkDirP(const String& path, int mode);
+ static bool SetFileOwnership(const String& file, const String& user, const String& group);
+
+ static void QueueAsyncCallback(const std::function<void ()>& callback, SchedulerPolicy policy = DefaultScheduler);
+
+ static String NaturalJoin(const std::vector<String>& tokens);
+ static String Join(const Array::Ptr& tokens, char separator, bool escapeSeparator = true);
+
+ static String FormatDuration(double duration);
+ static String FormatDateTime(const char *format, double ts);
+ static String FormatErrorNumber(int code);
+
+#ifndef _WIN32
+ static void SetNonBlocking(int fd, bool nb = true);
+ static void SetCloExec(int fd, bool cloexec = true);
+
+ static void CloseAllFDs(const std::vector<int>& except, std::function<void(int)> onClose = nullptr);
+#endif /* _WIN32 */
+
+ static void SetNonBlockingSocket(SOCKET s, bool nb = true);
+
+ static String EscapeShellCmd(const String& s);
+ static String EscapeShellArg(const String& s);
+#ifdef _WIN32
+ static String EscapeCreateProcessArg(const String& arg);
+#endif /* _WIN32 */
+
+ static String EscapeString(const String& s, const String& chars, const bool illegal);
+ static String UnescapeString(const String& s);
+
+ static void SetThreadName(const String& name, bool os = true);
+ static String GetThreadName();
+
+ static unsigned long SDBM(const String& str, size_t len = String::NPos);
+
+ static String ParseVersion(const String& v);
+ static int CompareVersion(const String& v1, const String& v2);
+
+ static int Random();
+
+ static String GetHostName();
+ static String GetFQDN();
+
+ static tm LocalTime(time_t ts);
+
+ static bool PathExists(const String& path);
+ static time_t GetFileCreationTime(const String& path);
+
+ static void Remove(const String& path);
+ static void RemoveDirRecursive(const String& path);
+ static void CopyFile(const String& source, const String& target);
+ static void RenameFile(const String& source, const String& target);
+
+ static Value LoadJsonFile(const String& path);
+ static void SaveJsonFile(const String& path, int mode, const Value& value);
+
+ static String GetPlatformKernel();
+ static String GetPlatformKernelVersion();
+ static String GetPlatformName();
+ static String GetPlatformVersion();
+ static String GetPlatformArchitecture();
+
+ static String ValidateUTF8(const String& input);
+
+#ifdef _WIN32
+ static int MksTemp(char *tmpl);
+#endif /* _WIN32 */
+
+#ifdef _WIN32
+ static String GetIcingaInstallPath();
+ static String GetIcingaDataPath();
+#endif /* _WIN32 */
+
+ static String GetFromEnvironment(const String& env);
+
+ static bool ComparePasswords(const String& enteredPassword, const String& actualPassword);
+
+#ifdef I2_DEBUG
+ static void SetTime(double);
+ static void IncrementTime(double);
+#endif /* I2_DEBUG */
+
+ /**
+ * TruncateUsingHash truncates a given string to an allowed maximum length while avoiding collisions in the output
+ * using a hash function (SHA1).
+ *
+ * For inputs shorter than the maximum output length, the output will be the same as the input. If the input has at
+ * least the maximum output length, it is hashed used SHA1 and the output has the format "A...B" where A is a prefix
+ * of the input and B is the hex-encoded SHA1 hash of the input. The length of A is chosen so that the result has
+ * the maximum allowed output length.
+ *
+ * @tparam maxLength Maximum length of the output string (must be at least 44)
+ * @param in String to truncate
+ * @return A truncated string derived from in of at most length maxLength
+ */
+ template<size_t maxLength>
+ static String TruncateUsingHash(const String &in) {
+ /*
+ * Note: be careful when changing this function as it is used to derive file names that should not change
+ * between versions or would need special handling if they do (/var/lib/icinga2/api/packages/_api).
+ */
+
+ const size_t sha1HexLength = SHA_DIGEST_LENGTH*2;
+ static_assert(maxLength >= 1 + 3 + sha1HexLength,
+ "maxLength must be at least 44 to hold one character, '...', and a hex-encoded SHA1 hash");
+
+ /* If the input is shorter than the limit, no truncation is needed */
+ if (in.GetLength() < maxLength) {
+ return in;
+ }
+
+ const char *trunc = "...";
+
+ return in.SubStr(0, maxLength - sha1HexLength - strlen(trunc)) + trunc + SHA1(in);
+ }
+
+private:
+ Utility();
+
+#ifdef I2_DEBUG
+ static double m_DebugTime;
+#endif /* I2_DEBUG */
+
+ static boost::thread_specific_ptr<String> m_ThreadName;
+ static boost::thread_specific_ptr<unsigned int> m_RandSeed;
+};
+
+}
+
+#endif /* UTILITY_H */
diff --git a/lib/base/value-operators.cpp b/lib/base/value-operators.cpp
new file mode 100644
index 0000000..d00c3e2
--- /dev/null
+++ b/lib/base/value-operators.cpp
@@ -0,0 +1,719 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/value.hpp"
+#include "base/array.hpp"
+#include "base/dictionary.hpp"
+#include "base/datetime.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/objectlock.hpp"
+#include <boost/lexical_cast.hpp>
+
+using namespace icinga;
+
+Value::operator double() const
+{
+ const double *value = boost::get<double>(&m_Value);
+
+ if (value)
+ return *value;
+
+ const bool *fvalue = boost::get<bool>(&m_Value);
+
+ if (fvalue)
+ return *fvalue;
+
+ if (IsEmpty())
+ return 0;
+
+ try {
+ return boost::lexical_cast<double>(m_Value);
+ } catch (const std::exception&) {
+ std::ostringstream msgbuf;
+ msgbuf << "Can't convert '" << *this << "' to a floating point number.";
+ BOOST_THROW_EXCEPTION(std::invalid_argument(msgbuf.str()));
+ }
+}
+
+Value::operator String() const
+{
+ Object *object;
+
+ switch (GetType()) {
+ case ValueEmpty:
+ return String();
+ case ValueNumber:
+ return Convert::ToString(boost::get<double>(m_Value));
+ case ValueBoolean:
+ if (boost::get<bool>(m_Value))
+ return "true";
+ else
+ return "false";
+ case ValueString:
+ return boost::get<String>(m_Value);
+ case ValueObject:
+ object = boost::get<Object::Ptr>(m_Value).get();
+ return object->ToString();
+ default:
+ BOOST_THROW_EXCEPTION(std::runtime_error("Unknown value type."));
+ }
+}
+
+std::ostream& icinga::operator<<(std::ostream& stream, const Value& value)
+{
+ if (value.IsBoolean())
+ stream << static_cast<int>(value);
+ else
+ stream << static_cast<String>(value);
+
+ return stream;
+}
+
+std::istream& icinga::operator>>(std::istream& stream, Value& value)
+{
+ String tstr;
+ stream >> tstr;
+ value = tstr;
+ return stream;
+}
+
+bool Value::operator==(bool rhs) const
+{
+ return *this == Value(rhs);
+}
+
+bool Value::operator!=(bool rhs) const
+{
+ return !(*this == rhs);
+}
+
+bool Value::operator==(int rhs) const
+{
+ return *this == Value(rhs);
+}
+
+bool Value::operator!=(int rhs) const
+{
+ return !(*this == rhs);
+}
+
+bool Value::operator==(double rhs) const
+{
+ return *this == Value(rhs);
+}
+
+bool Value::operator!=(double rhs) const
+{
+ return !(*this == rhs);
+}
+
+bool Value::operator==(const char *rhs) const
+{
+ return static_cast<String>(*this) == rhs;
+}
+
+bool Value::operator!=(const char *rhs) const
+{
+ return !(*this == rhs);
+}
+
+bool Value::operator==(const String& rhs) const
+{
+ return static_cast<String>(*this) == rhs;
+}
+
+bool Value::operator!=(const String& rhs) const
+{
+ return !(*this == rhs);
+}
+
+bool Value::operator==(const Value& rhs) const
+{
+ if (IsNumber() && rhs.IsNumber())
+ return Get<double>() == rhs.Get<double>();
+ else if ((IsBoolean() || IsNumber()) && (rhs.IsBoolean() || rhs.IsNumber()) && !(IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(*this) == static_cast<double>(rhs);
+
+ if (IsString() && rhs.IsString())
+ return Get<String>() == rhs.Get<String>();
+ else if ((IsString() || IsEmpty()) && (rhs.IsString() || rhs.IsEmpty()) && !(IsEmpty() && rhs.IsEmpty()))
+ return static_cast<String>(*this) == static_cast<String>(rhs);
+
+ if (IsEmpty() != rhs.IsEmpty())
+ return false;
+
+ if (IsEmpty())
+ return true;
+
+ if (IsObject() != rhs.IsObject())
+ return false;
+
+ if (IsObject()) {
+ if (IsObjectType<DateTime>() && rhs.IsObjectType<DateTime>()) {
+ DateTime::Ptr dt1 = *this;
+ DateTime::Ptr dt2 = rhs;
+
+ return dt1->GetValue() == dt2->GetValue();
+ }
+
+ if (IsObjectType<Array>() && rhs.IsObjectType<Array>()) {
+ Array::Ptr arr1 = *this;
+ Array::Ptr arr2 = rhs;
+
+ if (arr1 == arr2)
+ return true;
+
+ if (arr1->GetLength() != arr2->GetLength())
+ return false;
+
+ for (Array::SizeType i = 0; i < arr1->GetLength(); i++) {
+ if (arr1->Get(i) != arr2->Get(i))
+ return false;
+ }
+
+ return true;
+ }
+
+ return Get<Object::Ptr>() == rhs.Get<Object::Ptr>();
+ }
+
+ return false;
+}
+
+bool Value::operator!=(const Value& rhs) const
+{
+ return !(*this == rhs);
+}
+
+Value icinga::operator+(const Value& lhs, const char *rhs)
+{
+ return lhs + Value(rhs);
+}
+
+Value icinga::operator+(const char *lhs, const Value& rhs)
+{
+ return Value(lhs) + rhs;
+}
+
+Value icinga::operator+(const Value& lhs, const String& rhs)
+{
+ return lhs + Value(rhs);
+}
+
+Value icinga::operator+(const String& lhs, const Value& rhs)
+{
+ return Value(lhs) + rhs;
+}
+
+Value icinga::operator+(const Value& lhs, const Value& rhs)
+{
+ if ((lhs.IsEmpty() || lhs.IsNumber()) && !lhs.IsString() && (rhs.IsEmpty() || rhs.IsNumber()) && !rhs.IsString() && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(lhs) + static_cast<double>(rhs);
+ if ((lhs.IsString() || lhs.IsEmpty() || lhs.IsNumber()) && (rhs.IsString() || rhs.IsEmpty() || rhs.IsNumber()) && (!(lhs.IsEmpty() && rhs.IsEmpty()) || lhs.IsString() || rhs.IsString()))
+ return static_cast<String>(lhs) + static_cast<String>(rhs);
+ else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(lhs) + static_cast<double>(rhs);
+ else if (lhs.IsObjectType<DateTime>() && rhs.IsNumber())
+ return new DateTime(Convert::ToDateTimeValue(lhs) + rhs);
+ else if ((lhs.IsObjectType<Array>() || lhs.IsEmpty()) && (rhs.IsObjectType<Array>() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) {
+ Array::Ptr result = new Array();
+ if (!lhs.IsEmpty())
+ static_cast<Array::Ptr>(lhs)->CopyTo(result);
+ if (!rhs.IsEmpty())
+ static_cast<Array::Ptr>(rhs)->CopyTo(result);
+ return result;
+ } else if ((lhs.IsObjectType<Dictionary>() || lhs.IsEmpty()) && (rhs.IsObjectType<Dictionary>() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) {
+ Dictionary::Ptr result = new Dictionary();
+ if (!lhs.IsEmpty())
+ static_cast<Dictionary::Ptr>(lhs)->CopyTo(result);
+ if (!rhs.IsEmpty())
+ static_cast<Dictionary::Ptr>(rhs)->CopyTo(result);
+ return result;
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator + cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+ }
+}
+
+Value icinga::operator+(const Value& lhs, double rhs)
+{
+ return lhs + Value(rhs);
+}
+
+Value icinga::operator+(double lhs, const Value& rhs)
+{
+ return Value(lhs) + rhs;
+}
+
+Value icinga::operator+(const Value& lhs, int rhs)
+{
+ return lhs + Value(rhs);
+}
+
+Value icinga::operator+(int lhs, const Value& rhs)
+{
+ return Value(lhs) + rhs;
+}
+
+Value icinga::operator-(const Value& lhs, const Value& rhs)
+{
+ if ((lhs.IsNumber() || lhs.IsEmpty()) && !lhs.IsString() && (rhs.IsNumber() || rhs.IsEmpty()) && !rhs.IsString() && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(lhs) - static_cast<double>(rhs);
+ else if (lhs.IsObjectType<DateTime>() && rhs.IsNumber())
+ return new DateTime(Convert::ToDateTimeValue(lhs) - rhs);
+ else if (lhs.IsObjectType<DateTime>() && rhs.IsObjectType<DateTime>())
+ return Convert::ToDateTimeValue(lhs) - Convert::ToDateTimeValue(rhs);
+ else if ((lhs.IsObjectType<DateTime>() || lhs.IsEmpty()) && (rhs.IsObjectType<DateTime>() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return new DateTime(Convert::ToDateTimeValue(lhs) - Convert::ToDateTimeValue(rhs));
+ else if ((lhs.IsObjectType<Array>() || lhs.IsEmpty()) && (rhs.IsObjectType<Array>() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) {
+ if (lhs.IsEmpty())
+ return new Array();
+
+ ArrayData result;
+ Array::Ptr left = lhs;
+ Array::Ptr right = rhs;
+
+ ObjectLock olock(left);
+ for (const Value& lv : left) {
+ bool found = false;
+ ObjectLock xlock(right);
+ for (const Value& rv : right) {
+ if (lv == rv) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found)
+ continue;
+
+ result.push_back(lv);
+ }
+
+ return new Array(std::move(result));
+ } else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator - cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator-(const Value& lhs, double rhs)
+{
+ return lhs - Value(rhs);
+}
+
+Value icinga::operator-(double lhs, const Value& rhs)
+{
+ return Value(lhs) - rhs;
+}
+
+Value icinga::operator-(const Value& lhs, int rhs)
+{
+ return lhs - Value(rhs);
+}
+
+Value icinga::operator-(int lhs, const Value& rhs)
+{
+ return Value(lhs) - rhs;
+}
+
+Value icinga::operator*(const Value& lhs, const Value& rhs)
+{
+ if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(lhs) * static_cast<double>(rhs);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator * cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator*(const Value& lhs, double rhs)
+{
+ return lhs * Value(rhs);
+}
+
+Value icinga::operator*(double lhs, const Value& rhs)
+{
+ return Value(lhs) * rhs;
+}
+
+Value icinga::operator*(const Value& lhs, int rhs)
+{
+ return lhs * Value(rhs);
+}
+
+Value icinga::operator*(int lhs, const Value& rhs)
+{
+ return Value(lhs) * rhs;
+}
+
+Value icinga::operator/(const Value& lhs, const Value& rhs)
+{
+ if (rhs.IsEmpty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Right-hand side argument for operator / is Empty."));
+ else if ((lhs.IsEmpty() || lhs.IsNumber()) && rhs.IsNumber()) {
+ if (static_cast<double>(rhs) == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Right-hand side argument for operator / is 0."));
+
+ return static_cast<double>(lhs) / static_cast<double>(rhs);
+ } else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator / cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator/(const Value& lhs, double rhs)
+{
+ return lhs / Value(rhs);
+}
+
+Value icinga::operator/(double lhs, const Value& rhs)
+{
+ return Value(lhs) / rhs;
+}
+
+Value icinga::operator/(const Value& lhs, int rhs)
+{
+ return lhs / Value(rhs);
+}
+
+Value icinga::operator/(int lhs, const Value& rhs)
+{
+ return Value(lhs) / rhs;
+}
+
+Value icinga::operator%(const Value& lhs, const Value& rhs)
+{
+ if (rhs.IsEmpty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Right-hand side argument for operator % is Empty."));
+ else if ((rhs.IsNumber() || lhs.IsNumber()) && rhs.IsNumber()) {
+ if (static_cast<double>(rhs) == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Right-hand side argument for operator % is 0."));
+
+ return static_cast<int>(lhs) % static_cast<int>(rhs);
+ } else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator % cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator%(const Value& lhs, double rhs)
+{
+ return lhs % Value(rhs);
+}
+
+Value icinga::operator%(double lhs, const Value& rhs)
+{
+ return Value(lhs) % rhs;
+}
+
+Value icinga::operator%(const Value& lhs, int rhs)
+{
+ return lhs % Value(rhs);
+}
+
+Value icinga::operator%(int lhs, const Value& rhs)
+{
+ return Value(lhs) % rhs;
+}
+
+Value icinga::operator^(const Value& lhs, const Value& rhs)
+{
+ if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<int>(lhs) ^ static_cast<int>(rhs);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator & cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator^(const Value& lhs, double rhs)
+{
+ return lhs ^ Value(rhs);
+}
+
+Value icinga::operator^(double lhs, const Value& rhs)
+{
+ return Value(lhs) ^ rhs;
+}
+
+Value icinga::operator^(const Value& lhs, int rhs)
+{
+ return lhs ^ Value(rhs);
+}
+
+Value icinga::operator^(int lhs, const Value& rhs)
+{
+ return Value(lhs) ^ rhs;
+}
+
+Value icinga::operator&(const Value& lhs, const Value& rhs)
+{
+ if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<int>(lhs) & static_cast<int>(rhs);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator & cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator&(const Value& lhs, double rhs)
+{
+ return lhs & Value(rhs);
+}
+
+Value icinga::operator&(double lhs, const Value& rhs)
+{
+ return Value(lhs) & rhs;
+}
+
+Value icinga::operator&(const Value& lhs, int rhs)
+{
+ return lhs & Value(rhs);
+}
+
+Value icinga::operator&(int lhs, const Value& rhs)
+{
+ return Value(lhs) & rhs;
+}
+
+Value icinga::operator|(const Value& lhs, const Value& rhs)
+{
+ if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<int>(lhs) | static_cast<int>(rhs);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator | cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator|(const Value& lhs, double rhs)
+{
+ return lhs | Value(rhs);
+}
+
+Value icinga::operator|(double lhs, const Value& rhs)
+{
+ return Value(lhs) | rhs;
+}
+
+Value icinga::operator|(const Value& lhs, int rhs)
+{
+ return lhs | Value(rhs);
+}
+
+Value icinga::operator|(int lhs, const Value& rhs)
+{
+ return Value(lhs) | rhs;
+}
+
+Value icinga::operator<<(const Value& lhs, const Value& rhs)
+{
+ if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<int>(lhs) << static_cast<int>(rhs);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator << cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator<<(const Value& lhs, double rhs)
+{
+ return lhs << Value(rhs);
+}
+
+Value icinga::operator<<(double lhs, const Value& rhs)
+{
+ return Value(lhs) << rhs;
+}
+
+Value icinga::operator<<(const Value& lhs, int rhs)
+{
+ return lhs << Value(rhs);
+}
+
+Value icinga::operator<<(int lhs, const Value& rhs)
+{
+ return Value(lhs) << rhs;
+}
+
+Value icinga::operator>>(const Value& lhs, const Value& rhs)
+{
+ if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<int>(lhs) >> static_cast<int>(rhs);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator >> cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+Value icinga::operator>>(const Value& lhs, double rhs)
+{
+ return lhs >> Value(rhs);
+}
+
+Value icinga::operator>>(double lhs, const Value& rhs)
+{
+ return Value(lhs) >> rhs;
+}
+
+Value icinga::operator>>(const Value& lhs, int rhs)
+{
+ return lhs >> Value(rhs);
+}
+
+Value icinga::operator>>(int lhs, const Value& rhs)
+{
+ return Value(lhs) >> rhs;
+}
+
+bool icinga::operator<(const Value& lhs, const Value& rhs)
+{
+ if (lhs.IsString() && rhs.IsString())
+ return static_cast<String>(lhs) < static_cast<String>(rhs);
+ else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(lhs) < static_cast<double>(rhs);
+ else if ((lhs.IsObjectType<DateTime>() || lhs.IsEmpty()) && (rhs.IsObjectType<DateTime>() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return Convert::ToDateTimeValue(lhs) < Convert::ToDateTimeValue(rhs);
+ else if (lhs.IsObjectType<Array>() && rhs.IsObjectType<Array>()) {
+ Array::Ptr larr = lhs;
+ Array::Ptr rarr = rhs;
+
+ ObjectLock llock(larr);
+ ObjectLock rlock(rarr);
+
+ Array::SizeType llen = larr->GetLength();
+ Array::SizeType rlen = rarr->GetLength();
+
+ for (Array::SizeType i = 0; i < std::max(llen, rlen); i++) {
+ Value lval = (i >= llen) ? Empty : larr->Get(i);
+ Value rval = (i >= rlen) ? Empty : rarr->Get(i);
+
+ if (lval < rval)
+ return true;
+ else if (lval > rval)
+ return false;
+ }
+
+ return false;
+ } else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator < cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+bool icinga::operator<(const Value& lhs, double rhs)
+{
+ return lhs < Value(rhs);
+}
+
+bool icinga::operator<(double lhs, const Value& rhs)
+{
+ return Value(lhs) < rhs;
+}
+
+bool icinga::operator<(const Value& lhs, int rhs)
+{
+ return lhs < Value(rhs);
+}
+
+bool icinga::operator<(int lhs, const Value& rhs)
+{
+ return Value(lhs) < rhs;
+}
+
+bool icinga::operator>(const Value& lhs, const Value& rhs)
+{
+ if (lhs.IsString() && rhs.IsString())
+ return static_cast<String>(lhs) > static_cast<String>(rhs);
+ else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(lhs) > static_cast<double>(rhs);
+ else if ((lhs.IsObjectType<DateTime>() || lhs.IsEmpty()) && (rhs.IsObjectType<DateTime>() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return Convert::ToDateTimeValue(lhs) > Convert::ToDateTimeValue(rhs);
+ else if (lhs.IsObjectType<Array>() && rhs.IsObjectType<Array>()) {
+ Array::Ptr larr = lhs;
+ Array::Ptr rarr = rhs;
+
+ ObjectLock llock(larr);
+ ObjectLock rlock(rarr);
+
+ Array::SizeType llen = larr->GetLength();
+ Array::SizeType rlen = rarr->GetLength();
+
+ for (Array::SizeType i = 0; i < std::max(llen, rlen); i++) {
+ Value lval = (i >= llen) ? Empty : larr->Get(i);
+ Value rval = (i >= rlen) ? Empty : rarr->Get(i);
+
+ if (lval > rval)
+ return true;
+ else if (lval < rval)
+ return false;
+ }
+
+ return false;
+ } else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator > cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+bool icinga::operator>(const Value& lhs, double rhs)
+{
+ return lhs > Value(rhs);
+}
+
+bool icinga::operator>(double lhs, const Value& rhs)
+{
+ return Value(lhs) > rhs;
+}
+
+bool icinga::operator>(const Value& lhs, int rhs)
+{
+ return lhs > Value(rhs);
+}
+
+bool icinga::operator>(int lhs, const Value& rhs)
+{
+ return Value(lhs) > rhs;
+}
+
+bool icinga::operator<=(const Value& lhs, const Value& rhs)
+{
+ if (lhs.IsString() && rhs.IsString())
+ return static_cast<String>(lhs) <= static_cast<String>(rhs);
+ else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(lhs) <= static_cast<double>(rhs);
+ else if ((lhs.IsObjectType<DateTime>() || lhs.IsEmpty()) && (rhs.IsObjectType<DateTime>() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return Convert::ToDateTimeValue(lhs) <= Convert::ToDateTimeValue(rhs);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator <= cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+bool icinga::operator<=(const Value& lhs, double rhs)
+{
+ return lhs <= Value(rhs);
+}
+
+bool icinga::operator<=(double lhs, const Value& rhs)
+{
+ return Value(lhs) <= rhs;
+}
+
+bool icinga::operator<=(const Value& lhs, int rhs)
+{
+ return lhs <= Value(rhs);
+}
+
+bool icinga::operator<=(int lhs, const Value& rhs)
+{
+ return Value(lhs) <= rhs;
+}
+
+bool icinga::operator>=(const Value& lhs, const Value& rhs)
+{
+ if (lhs.IsString() && rhs.IsString())
+ return static_cast<String>(lhs) >= static_cast<String>(rhs);
+ else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return static_cast<double>(lhs) >= static_cast<double>(rhs);
+ else if ((lhs.IsObjectType<DateTime>() || lhs.IsEmpty()) && (rhs.IsObjectType<DateTime>() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()))
+ return Convert::ToDateTimeValue(lhs) >= Convert::ToDateTimeValue(rhs);
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Operator >= cannot be applied to values of type '" + lhs.GetTypeName() + "' and '" + rhs.GetTypeName() + "'"));
+}
+
+bool icinga::operator>=(const Value& lhs, double rhs)
+{
+ return lhs >= Value(rhs);
+}
+
+bool icinga::operator>=(double lhs, const Value& rhs)
+{
+ return Value(lhs) >= rhs;
+}
+
+bool icinga::operator>=(const Value& lhs, int rhs)
+{
+ return lhs >= Value(rhs);
+}
+
+bool icinga::operator>=(int lhs, const Value& rhs)
+{
+ return Value(lhs) >= rhs;
+}
diff --git a/lib/base/value.cpp b/lib/base/value.cpp
new file mode 100644
index 0000000..867c821
--- /dev/null
+++ b/lib/base/value.cpp
@@ -0,0 +1,264 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/value.hpp"
+#include "base/array.hpp"
+#include "base/dictionary.hpp"
+#include "base/type.hpp"
+
+using namespace icinga;
+
+template class boost::variant<boost::blank, double, bool, String, Object::Ptr>;
+template const double& Value::Get<double>() const;
+template const bool& Value::Get<bool>() const;
+template const String& Value::Get<String>() const;
+template const Object::Ptr& Value::Get<Object::Ptr>() const;
+
+Value icinga::Empty;
+
+Value::Value(std::nullptr_t)
+{ }
+
+Value::Value(int value)
+ : m_Value(double(value))
+{ }
+
+Value::Value(unsigned int value)
+ : m_Value(double(value))
+{ }
+
+Value::Value(long value)
+ : m_Value(double(value))
+{ }
+
+Value::Value(unsigned long value)
+ : m_Value(double(value))
+{ }
+
+Value::Value(long long value)
+ : m_Value(double(value))
+{ }
+
+Value::Value(unsigned long long value)
+ : m_Value(double(value))
+{ }
+
+Value::Value(double value)
+ : m_Value(value)
+{ }
+
+Value::Value(bool value)
+ : m_Value(value)
+{ }
+
+Value::Value(const String& value)
+ : m_Value(value)
+{ }
+
+Value::Value(String&& value)
+ : m_Value(value)
+{ }
+
+Value::Value(const char *value)
+ : m_Value(String(value))
+{ }
+
+Value::Value(const Value& other)
+ : m_Value(other.m_Value)
+{ }
+
+Value::Value(Value&& other)
+{
+#if BOOST_VERSION >= 105400
+ m_Value = std::move(other.m_Value);
+#else /* BOOST_VERSION */
+ m_Value.swap(other.m_Value);
+#endif /* BOOST_VERSION */
+}
+
+Value::Value(Object *value)
+ : Value(Object::Ptr(value))
+{ }
+
+Value::Value(const intrusive_ptr<Object>& value)
+{
+ if (value)
+ m_Value = value;
+}
+
+Value& Value::operator=(const Value& other)
+{
+ m_Value = other.m_Value;
+ return *this;
+}
+
+Value& Value::operator=(Value&& other)
+{
+#if BOOST_VERSION >= 105400
+ m_Value = std::move(other.m_Value);
+#else /* BOOST_VERSION */
+ m_Value.swap(other.m_Value);
+#endif /* BOOST_VERSION */
+
+ return *this;
+}
+
+/**
+ * Checks whether the variant is empty.
+ *
+ * @returns true if the variant is empty, false otherwise.
+ */
+bool Value::IsEmpty() const
+{
+ return (GetType() == ValueEmpty || (IsString() && boost::get<String>(m_Value).IsEmpty()));
+}
+
+/**
+ * Checks whether the variant is scalar (i.e. not an object and not empty).
+ *
+ * @returns true if the variant is scalar, false otherwise.
+ */
+bool Value::IsScalar() const
+{
+ return !IsEmpty() && !IsObject();
+}
+
+/**
+* Checks whether the variant is a number.
+*
+* @returns true if the variant is a number.
+*/
+bool Value::IsNumber() const
+{
+ return (GetType() == ValueNumber);
+}
+
+/**
+ * Checks whether the variant is a boolean.
+ *
+ * @returns true if the variant is a boolean.
+ */
+bool Value::IsBoolean() const
+{
+ return (GetType() == ValueBoolean);
+}
+
+/**
+ * Checks whether the variant is a string.
+ *
+ * @returns true if the variant is a string.
+ */
+bool Value::IsString() const
+{
+ return (GetType() == ValueString);
+}
+
+/**
+ * Checks whether the variant is a non-null object.
+ *
+ * @returns true if the variant is a non-null object, false otherwise.
+ */
+bool Value::IsObject() const
+{
+ return (GetType() == ValueObject);
+}
+
+/**
+ * Returns the type of the value.
+ *
+ * @returns The type.
+ */
+ValueType Value::GetType() const
+{
+ return static_cast<ValueType>(m_Value.which());
+}
+
+void Value::Swap(Value& other)
+{
+ m_Value.swap(other.m_Value);
+}
+
+bool Value::ToBool() const
+{
+ switch (GetType()) {
+ case ValueNumber:
+ return static_cast<bool>(boost::get<double>(m_Value));
+
+ case ValueBoolean:
+ return boost::get<bool>(m_Value);
+
+ case ValueString:
+ return !boost::get<String>(m_Value).IsEmpty();
+
+ case ValueObject:
+ if (IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dictionary = *this;
+ return dictionary->GetLength() > 0;
+ } else if (IsObjectType<Array>()) {
+ Array::Ptr array = *this;
+ return array->GetLength() > 0;
+ } else {
+ return true;
+ }
+
+ case ValueEmpty:
+ return false;
+
+ default:
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid variant type."));
+ }
+}
+
+String Value::GetTypeName() const
+{
+ Type::Ptr t;
+
+ switch (GetType()) {
+ case ValueEmpty:
+ return "Empty";
+ case ValueNumber:
+ return "Number";
+ case ValueBoolean:
+ return "Boolean";
+ case ValueString:
+ return "String";
+ case ValueObject:
+ t = boost::get<Object::Ptr>(m_Value)->GetReflectionType();
+ if (!t) {
+ if (IsObjectType<Array>())
+ return "Array";
+ else if (IsObjectType<Dictionary>())
+ return "Dictionary";
+ else
+ return "Object";
+ } else
+ return t->GetName();
+ default:
+ return "Invalid";
+ }
+}
+
+Type::Ptr Value::GetReflectionType() const
+{
+ switch (GetType()) {
+ case ValueEmpty:
+ return Object::TypeInstance;
+ case ValueNumber:
+ return Type::GetByName("Number");
+ case ValueBoolean:
+ return Type::GetByName("Boolean");
+ case ValueString:
+ return Type::GetByName("String");
+ case ValueObject:
+ return boost::get<Object::Ptr>(m_Value)->GetReflectionType();
+ default:
+ return nullptr;
+ }
+}
+
+Value Value::Clone() const
+{
+ if (IsObject())
+ return static_cast<Object::Ptr>(*this)->Clone();
+ else
+ return *this;
+}
diff --git a/lib/base/value.hpp b/lib/base/value.hpp
new file mode 100644
index 0000000..86a3b11
--- /dev/null
+++ b/lib/base/value.hpp
@@ -0,0 +1,251 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VALUE_H
+#define VALUE_H
+
+#include "base/object.hpp"
+#include "base/string.hpp"
+#include <boost/variant/variant.hpp>
+#include <boost/variant/get.hpp>
+#include <boost/throw_exception.hpp>
+
+namespace icinga
+{
+
+typedef double Timestamp;
+
+/**
+ * The type of a Value.
+ *
+ * @ingroup base
+ */
+enum ValueType
+{
+ ValueEmpty = 0,
+ ValueNumber = 1,
+ ValueBoolean = 2,
+ ValueString = 3,
+ ValueObject = 4
+};
+
+/**
+ * A type that can hold an arbitrary value.
+ *
+ * @ingroup base
+ */
+class Value
+{
+public:
+ Value() = default;
+ Value(std::nullptr_t);
+ Value(int value);
+ Value(unsigned int value);
+ Value(long value);
+ Value(unsigned long value);
+ Value(long long value);
+ Value(unsigned long long value);
+ Value(double value);
+ Value(bool value);
+ Value(const String& value);
+ Value(String&& value);
+ Value(const char *value);
+ Value(const Value& other);
+ Value(Value&& other);
+ Value(Object *value);
+ Value(const intrusive_ptr<Object>& value);
+
+ template<typename T>
+ Value(const intrusive_ptr<T>& value)
+ : Value(static_pointer_cast<Object>(value))
+ {
+ static_assert(!std::is_same<T, Object>::value, "T must not be Object");
+ }
+
+ bool ToBool() const;
+
+ operator double() const;
+ operator String() const;
+
+ Value& operator=(const Value& other);
+ Value& operator=(Value&& other);
+
+ bool operator==(bool rhs) const;
+ bool operator!=(bool rhs) const;
+
+ bool operator==(int rhs) const;
+ bool operator!=(int rhs) const;
+
+ bool operator==(double rhs) const;
+ bool operator!=(double rhs) const;
+
+ bool operator==(const char *rhs) const;
+ bool operator!=(const char *rhs) const;
+
+ bool operator==(const String& rhs) const;
+ bool operator!=(const String& rhs) const;
+
+ bool operator==(const Value& rhs) const;
+ bool operator!=(const Value& rhs) const;
+
+ template<typename T>
+ operator intrusive_ptr<T>() const
+ {
+ if (IsEmpty() && !IsString())
+ return intrusive_ptr<T>();
+
+ if (!IsObject())
+ BOOST_THROW_EXCEPTION(std::runtime_error("Cannot convert value of type '" + GetTypeName() + "' to an object."));
+
+ const auto& object = Get<Object::Ptr>();
+
+ ASSERT(object);
+
+ intrusive_ptr<T> tobject = dynamic_pointer_cast<T>(object);
+
+ if (!tobject)
+ BOOST_THROW_EXCEPTION(std::bad_cast());
+
+ return tobject;
+ }
+
+ bool IsEmpty() const;
+ bool IsScalar() const;
+ bool IsNumber() const;
+ bool IsBoolean() const;
+ bool IsString() const;
+ bool IsObject() const;
+
+ template<typename T>
+ bool IsObjectType() const
+ {
+ if (!IsObject())
+ return false;
+
+ return dynamic_cast<T *>(Get<Object::Ptr>().get());
+ }
+
+ ValueType GetType() const;
+
+ void Swap(Value& other);
+
+ String GetTypeName() const;
+
+ Type::Ptr GetReflectionType() const;
+
+ Value Clone() const;
+
+ template<typename T>
+ const T& Get() const
+ {
+ return boost::get<T>(m_Value);
+ }
+
+private:
+ boost::variant<boost::blank, double, bool, String, Object::Ptr> m_Value;
+};
+
+extern template const double& Value::Get<double>() const;
+extern template const bool& Value::Get<bool>() const;
+extern template const String& Value::Get<String>() const;
+extern template const Object::Ptr& Value::Get<Object::Ptr>() const;
+
+extern Value Empty;
+
+Value operator+(const Value& lhs, const char *rhs);
+Value operator+(const char *lhs, const Value& rhs);
+
+Value operator+(const Value& lhs, const String& rhs);
+Value operator+(const String& lhs, const Value& rhs);
+
+Value operator+(const Value& lhs, const Value& rhs);
+Value operator+(const Value& lhs, double rhs);
+Value operator+(double lhs, const Value& rhs);
+Value operator+(const Value& lhs, int rhs);
+Value operator+(int lhs, const Value& rhs);
+
+Value operator-(const Value& lhs, const Value& rhs);
+Value operator-(const Value& lhs, double rhs);
+Value operator-(double lhs, const Value& rhs);
+Value operator-(const Value& lhs, int rhs);
+Value operator-(int lhs, const Value& rhs);
+
+Value operator*(const Value& lhs, const Value& rhs);
+Value operator*(const Value& lhs, double rhs);
+Value operator*(double lhs, const Value& rhs);
+Value operator*(const Value& lhs, int rhs);
+Value operator*(int lhs, const Value& rhs);
+
+Value operator/(const Value& lhs, const Value& rhs);
+Value operator/(const Value& lhs, double rhs);
+Value operator/(double lhs, const Value& rhs);
+Value operator/(const Value& lhs, int rhs);
+Value operator/(int lhs, const Value& rhs);
+
+Value operator%(const Value& lhs, const Value& rhs);
+Value operator%(const Value& lhs, double rhs);
+Value operator%(double lhs, const Value& rhs);
+Value operator%(const Value& lhs, int rhs);
+Value operator%(int lhs, const Value& rhs);
+
+Value operator^(const Value& lhs, const Value& rhs);
+Value operator^(const Value& lhs, double rhs);
+Value operator^(double lhs, const Value& rhs);
+Value operator^(const Value& lhs, int rhs);
+Value operator^(int lhs, const Value& rhs);
+
+Value operator&(const Value& lhs, const Value& rhs);
+Value operator&(const Value& lhs, double rhs);
+Value operator&(double lhs, const Value& rhs);
+Value operator&(const Value& lhs, int rhs);
+Value operator&(int lhs, const Value& rhs);
+
+Value operator|(const Value& lhs, const Value& rhs);
+Value operator|(const Value& lhs, double rhs);
+Value operator|(double lhs, const Value& rhs);
+Value operator|(const Value& lhs, int rhs);
+Value operator|(int lhs, const Value& rhs);
+
+Value operator<<(const Value& lhs, const Value& rhs);
+Value operator<<(const Value& lhs, double rhs);
+Value operator<<(double lhs, const Value& rhs);
+Value operator<<(const Value& lhs, int rhs);
+Value operator<<(int lhs, const Value& rhs);
+
+Value operator>>(const Value& lhs, const Value& rhs);
+Value operator>>(const Value& lhs, double rhs);
+Value operator>>(double lhs, const Value& rhs);
+Value operator>>(const Value& lhs, int rhs);
+Value operator>>(int lhs, const Value& rhs);
+
+bool operator<(const Value& lhs, const Value& rhs);
+bool operator<(const Value& lhs, double rhs);
+bool operator<(double lhs, const Value& rhs);
+bool operator<(const Value& lhs, int rhs);
+bool operator<(int lhs, const Value& rhs);
+
+bool operator>(const Value& lhs, const Value& rhs);
+bool operator>(const Value& lhs, double rhs);
+bool operator>(double lhs, const Value& rhs);
+bool operator>(const Value& lhs, int rhs);
+bool operator>(int lhs, const Value& rhs);
+
+bool operator<=(const Value& lhs, const Value& rhs);
+bool operator<=(const Value& lhs, double rhs);
+bool operator<=(double lhs, const Value& rhs);
+bool operator<=(const Value& lhs, int rhs);
+bool operator<=(int lhs, const Value& rhs);
+
+bool operator>=(const Value& lhs, const Value& rhs);
+bool operator>=(const Value& lhs, double rhs);
+bool operator>=(double lhs, const Value& rhs);
+bool operator>=(const Value& lhs, int rhs);
+bool operator>=(int lhs, const Value& rhs);
+
+std::ostream& operator<<(std::ostream& stream, const Value& value);
+std::istream& operator>>(std::istream& stream, Value& value);
+
+}
+
+extern template class boost::variant<boost::blank, double, bool, icinga::String, icinga::Object::Ptr>;
+
+#endif /* VALUE_H */
diff --git a/lib/base/win32.hpp b/lib/base/win32.hpp
new file mode 100644
index 0000000..064c5d6
--- /dev/null
+++ b/lib/base/win32.hpp
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef WIN32_H
+#define WIN32_H
+
+#define WIN32_LEAN_AND_MEAN
+#ifndef _WIN32_WINNT
+#define _WIN32_WINNT _WIN32_WINNT_VISTA
+#endif /* _WIN32_WINNT */
+#define NOMINMAX
+#include <winsock2.h>
+#include <windows.h>
+#include <ws2tcpip.h>
+#include <imagehlp.h>
+#include <shlwapi.h>
+
+#include <direct.h>
+
+#ifdef __MINGW32__
+# ifndef IPV6_V6ONLY
+# define IPV6_V6ONLY 27
+# endif /* IPV6_V6ONLY */
+#endif /* __MINGW32__ */
+
+typedef int socklen_t;
+typedef SSIZE_T ssize_t;
+
+#define MAXPATHLEN MAX_PATH
+
+#ifdef _MSC_VER
+typedef DWORD pid_t;
+#define strcasecmp stricmp
+#endif /* _MSC_VER */
+
+#endif /* WIN32_H */
diff --git a/lib/base/windowseventloglogger-provider.mc b/lib/base/windowseventloglogger-provider.mc
new file mode 100644
index 0000000..09e65ba
--- /dev/null
+++ b/lib/base/windowseventloglogger-provider.mc
@@ -0,0 +1,5 @@
+MessageId=0x1
+SymbolicName=MSG_PLAIN_LOG_ENTRY
+Language=English
+%1
+.
diff --git a/lib/base/windowseventloglogger.cpp b/lib/base/windowseventloglogger.cpp
new file mode 100644
index 0000000..cc28358
--- /dev/null
+++ b/lib/base/windowseventloglogger.cpp
@@ -0,0 +1,83 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#ifdef _WIN32
+#include "base/windowseventloglogger.hpp"
+#include "base/windowseventloglogger-ti.cpp"
+#include "base/windowseventloglogger-provider.h"
+#include "base/configtype.hpp"
+#include "base/statsfunction.hpp"
+#include <windows.h>
+
+using namespace icinga;
+
+REGISTER_TYPE(WindowsEventLogLogger);
+
+REGISTER_STATSFUNCTION(WindowsEventLogLogger, &WindowsEventLogLogger::StatsFunc);
+
+INITIALIZE_ONCE(&WindowsEventLogLogger::StaticInitialize);
+
+static HANDLE l_EventLog = nullptr;
+
+void WindowsEventLogLogger::StaticInitialize()
+{
+ l_EventLog = RegisterEventSourceA(nullptr, "Icinga 2");
+}
+
+void WindowsEventLogLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const WindowsEventLogLogger::Ptr& logger : ConfigType::GetObjectsByType<WindowsEventLogLogger>()) {
+ nodes.emplace_back(logger->GetName(), 1);
+ }
+
+ status->Set("windowseventloglogger", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * Processes a log entry and outputs it to the Windows Event Log.
+ *
+ * This function implements the interface expected by the Logger base class and passes
+ * the log entry to WindowsEventLogLogger::WriteToWindowsEventLog().
+ *
+ * @param entry The log entry.
+ */
+void WindowsEventLogLogger::ProcessLogEntry(const LogEntry& entry) {
+ WindowsEventLogLogger::WriteToWindowsEventLog(entry);
+}
+
+/**
+ * Writes a LogEntry object to the Windows Event Log.
+ *
+ * @param entry The log entry.
+ */
+void WindowsEventLogLogger::WriteToWindowsEventLog(const LogEntry& entry)
+{
+ if (l_EventLog != nullptr) {
+ std::string message = Logger::SeverityToString(entry.Severity) + "/" + entry.Facility + ": " + entry.Message;
+ std::array<const char *, 1> strings{
+ message.c_str()
+ };
+
+ WORD eventType;
+ switch (entry.Severity) {
+ case LogCritical:
+ eventType = EVENTLOG_ERROR_TYPE;
+ break;
+ case LogWarning:
+ eventType = EVENTLOG_WARNING_TYPE;
+ break;
+ default:
+ eventType = EVENTLOG_INFORMATION_TYPE;
+ }
+
+ ReportEventA(l_EventLog, eventType, 0, MSG_PLAIN_LOG_ENTRY, NULL, strings.size(), 0, strings.data(), NULL);
+ }
+}
+
+void WindowsEventLogLogger::Flush()
+{
+ /* Nothing to do here. */
+}
+
+#endif /* _WIN32 */
diff --git a/lib/base/windowseventloglogger.hpp b/lib/base/windowseventloglogger.hpp
new file mode 100644
index 0000000..cefc245
--- /dev/null
+++ b/lib/base/windowseventloglogger.hpp
@@ -0,0 +1,37 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#ifndef WINDOWSEVENTLOGLOGGER_H
+#define WINDOWSEVENTLOGLOGGER_H
+
+#ifdef _WIN32
+#include "base/i2-base.hpp"
+#include "base/windowseventloglogger-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * A logger that logs to the Windows Event Log.
+ *
+ * @ingroup base
+ */
+class WindowsEventLogLogger final : public ObjectImpl<WindowsEventLogLogger>
+{
+public:
+ DECLARE_OBJECT(WindowsEventLogLogger);
+ DECLARE_OBJECTNAME(WindowsEventLogLogger);
+
+ static void StaticInitialize();
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ static void WriteToWindowsEventLog(const LogEntry& entry);
+
+protected:
+ void ProcessLogEntry(const LogEntry& entry) override;
+ void Flush() override;
+};
+
+}
+#endif /* _WIN32 */
+
+#endif /* WINDOWSEVENTLOGLOGGER_H */
diff --git a/lib/base/windowseventloglogger.ti b/lib/base/windowseventloglogger.ti
new file mode 100644
index 0000000..edf65fc
--- /dev/null
+++ b/lib/base/windowseventloglogger.ti
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#include "base/logger.hpp"
+
+library base;
+
+namespace icinga
+{
+
+class WindowsEventLogLogger : Logger
+{
+ activation_priority -100;
+};
+
+}
diff --git a/lib/base/workqueue.cpp b/lib/base/workqueue.cpp
new file mode 100644
index 0000000..0b1214b
--- /dev/null
+++ b/lib/base/workqueue.cpp
@@ -0,0 +1,318 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/workqueue.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/application.hpp"
+#include "base/exception.hpp"
+#include <boost/thread/tss.hpp>
+#include <math.h>
+
+using namespace icinga;
+
+std::atomic<int> WorkQueue::m_NextID(1);
+boost::thread_specific_ptr<WorkQueue *> l_ThreadWorkQueue;
+
+WorkQueue::WorkQueue(size_t maxItems, int threadCount, LogSeverity statsLogLevel)
+ : m_ID(m_NextID++), m_ThreadCount(threadCount), m_MaxItems(maxItems),
+ m_TaskStats(15 * 60), m_StatsLogLevel(statsLogLevel)
+{
+ /* Initialize logger. */
+ m_StatusTimerTimeout = Utility::GetTime();
+
+ m_StatusTimer = Timer::Create();
+ m_StatusTimer->SetInterval(10);
+ m_StatusTimer->OnTimerExpired.connect([this](const Timer * const&) { StatusTimerHandler(); });
+ m_StatusTimer->Start();
+}
+
+WorkQueue::~WorkQueue()
+{
+ m_StatusTimer->Stop(true);
+
+ Join(true);
+}
+
+void WorkQueue::SetName(const String& name)
+{
+ m_Name = name;
+}
+
+String WorkQueue::GetName() const
+{
+ return m_Name;
+}
+
+std::unique_lock<std::mutex> WorkQueue::AcquireLock()
+{
+ return std::unique_lock<std::mutex>(m_Mutex);
+}
+
+/**
+ * Enqueues a task. Tasks are guaranteed to be executed in the order
+ * they were enqueued in except if there is more than one worker thread.
+ */
+void WorkQueue::EnqueueUnlocked(std::unique_lock<std::mutex>& lock, std::function<void ()>&& function, WorkQueuePriority priority)
+{
+ if (!m_Spawned) {
+ Log(LogNotice, "WorkQueue")
+ << "Spawning WorkQueue threads for '" << m_Name << "'";
+
+ for (int i = 0; i < m_ThreadCount; i++) {
+ m_Threads.create_thread([this]() { WorkerThreadProc(); });
+ }
+
+ m_Spawned = true;
+ }
+
+ bool wq_thread = IsWorkerThread();
+
+ if (!wq_thread) {
+ while (m_Tasks.size() >= m_MaxItems && m_MaxItems != 0)
+ m_CVFull.wait(lock);
+ }
+
+ m_Tasks.emplace(std::move(function), priority, ++m_NextTaskID);
+
+ m_CVEmpty.notify_one();
+}
+
+/**
+ * Enqueues a task. Tasks are guaranteed to be executed in the order
+ * they were enqueued in except if there is more than one worker thread or when
+ * allowInterleaved is true in which case the new task might be run
+ * immediately if it's being enqueued from within the WorkQueue thread.
+ */
+void WorkQueue::Enqueue(std::function<void ()>&& function, WorkQueuePriority priority,
+ bool allowInterleaved)
+{
+ bool wq_thread = IsWorkerThread();
+
+ if (wq_thread && allowInterleaved) {
+ function();
+
+ return;
+ }
+
+ auto lock = AcquireLock();
+ EnqueueUnlocked(lock, std::move(function), priority);
+}
+
+/**
+ * Waits until all currently enqueued tasks have completed. This only works reliably
+ * when no other thread is enqueuing new tasks when this method is called.
+ *
+ * @param stop Whether to stop the worker threads
+ */
+void WorkQueue::Join(bool stop)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ while (m_Processing || !m_Tasks.empty())
+ m_CVStarved.wait(lock);
+
+ if (stop) {
+ m_Stopped = true;
+ m_CVEmpty.notify_all();
+ lock.unlock();
+
+ m_Threads.join_all();
+ m_Spawned = false;
+
+ Log(LogNotice, "WorkQueue")
+ << "Stopped WorkQueue threads for '" << m_Name << "'";
+ }
+}
+
+/**
+ * Checks whether the calling thread is one of the worker threads
+ * for this work queue.
+ *
+ * @returns true if called from one of the worker threads, false otherwise
+ */
+bool WorkQueue::IsWorkerThread() const
+{
+ WorkQueue **pwq = l_ThreadWorkQueue.get();
+
+ if (!pwq)
+ return false;
+
+ return *pwq == this;
+}
+
+void WorkQueue::SetExceptionCallback(const ExceptionCallback& callback)
+{
+ m_ExceptionCallback = callback;
+}
+
+/**
+ * Checks whether any exceptions have occurred while executing tasks for this
+ * work queue. When a custom exception callback is set this method will always
+ * return false.
+ */
+bool WorkQueue::HasExceptions() const
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return !m_Exceptions.empty();
+}
+
+/**
+ * Returns all exceptions which have occurred for tasks in this work queue. When a
+ * custom exception callback is set this method will always return an empty list.
+ */
+std::vector<boost::exception_ptr> WorkQueue::GetExceptions() const
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_Exceptions;
+}
+
+void WorkQueue::ReportExceptions(const String& facility, bool verbose) const
+{
+ std::vector<boost::exception_ptr> exceptions = GetExceptions();
+
+ for (const auto& eptr : exceptions) {
+ Log(LogCritical, facility)
+ << DiagnosticInformation(eptr, verbose);
+ }
+
+ Log(LogCritical, facility)
+ << exceptions.size() << " error" << (exceptions.size() != 1 ? "s" : "");
+}
+
+size_t WorkQueue::GetLength() const
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_Tasks.size();
+}
+
+void WorkQueue::StatusTimerHandler()
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ ASSERT(!m_Name.IsEmpty());
+
+ size_t pending = m_Tasks.size();
+
+ double now = Utility::GetTime();
+ double gradient = (pending - m_PendingTasks) / (now - m_PendingTasksTimestamp);
+ double timeToZero = pending / gradient;
+
+ String timeInfo;
+
+ if (pending > GetTaskCount(5)) {
+ timeInfo = " empty in ";
+ if (timeToZero < 0 || std::isinf(timeToZero))
+ timeInfo += "infinite time, your task handler isn't able to keep up";
+ else
+ timeInfo += Utility::FormatDuration(timeToZero);
+ }
+
+ m_PendingTasks = pending;
+ m_PendingTasksTimestamp = now;
+
+ /* Log if there are pending items, or 5 minute timeout is reached. */
+ if (pending > 0 || m_StatusTimerTimeout < now) {
+ Log(m_StatsLogLevel, "WorkQueue")
+ << "#" << m_ID << " (" << m_Name << ") "
+ << "items: " << pending << ", "
+ << "rate: " << std::setw(2) << GetTaskCount(60) / 60.0 << "/s "
+ << "(" << GetTaskCount(60) << "/min " << GetTaskCount(60 * 5) << "/5min " << GetTaskCount(60 * 15) << "/15min);"
+ << timeInfo;
+ }
+
+ /* Reschedule next log entry in 5 minutes. */
+ if (m_StatusTimerTimeout < now) {
+ m_StatusTimerTimeout = now + 60 * 5;
+ }
+}
+
+void WorkQueue::RunTaskFunction(const TaskFunction& func)
+{
+ try {
+ func();
+ } catch (const std::exception&) {
+ boost::exception_ptr eptr = boost::current_exception();
+
+ {
+ std::unique_lock<std::mutex> mutex(m_Mutex);
+
+ if (!m_ExceptionCallback)
+ m_Exceptions.push_back(eptr);
+ }
+
+ if (m_ExceptionCallback)
+ m_ExceptionCallback(eptr);
+ }
+}
+
+void WorkQueue::WorkerThreadProc()
+{
+ std::ostringstream idbuf;
+ idbuf << "WQ #" << m_ID;
+ Utility::SetThreadName(idbuf.str());
+
+ l_ThreadWorkQueue.reset(new WorkQueue *(this));
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ for (;;) {
+ while (m_Tasks.empty() && !m_Stopped)
+ m_CVEmpty.wait(lock);
+
+ if (m_Stopped)
+ break;
+
+ if (m_Tasks.size() >= m_MaxItems && m_MaxItems != 0)
+ m_CVFull.notify_all();
+
+ Task task = m_Tasks.top();
+ m_Tasks.pop();
+
+ m_Processing++;
+
+ lock.unlock();
+
+ RunTaskFunction(task.Function);
+
+ /* clear the task so whatever other resources it holds are released _before_ we re-acquire the mutex */
+ task = Task();
+
+ IncreaseTaskCount();
+
+ lock.lock();
+
+ m_Processing--;
+
+ if (m_Tasks.empty())
+ m_CVStarved.notify_all();
+ }
+}
+
+void WorkQueue::IncreaseTaskCount()
+{
+ m_TaskStats.InsertValue(Utility::GetTime(), 1);
+}
+
+size_t WorkQueue::GetTaskCount(RingBuffer::SizeType span)
+{
+ return m_TaskStats.UpdateAndGetValues(Utility::GetTime(), span);
+}
+
+bool icinga::operator<(const Task& a, const Task& b)
+{
+ if (a.Priority < b.Priority)
+ return true;
+
+ if (a.Priority == b.Priority) {
+ if (a.ID > b.ID)
+ return true;
+ else
+ return false;
+ }
+
+ return false;
+}
diff --git a/lib/base/workqueue.hpp b/lib/base/workqueue.hpp
new file mode 100644
index 0000000..9c8a6b8
--- /dev/null
+++ b/lib/base/workqueue.hpp
@@ -0,0 +1,154 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef WORKQUEUE_H
+#define WORKQUEUE_H
+
+#include "base/i2-base.hpp"
+#include "base/timer.hpp"
+#include "base/ringbuffer.hpp"
+#include "base/logger.hpp"
+#include <boost/thread/thread.hpp>
+#include <boost/exception_ptr.hpp>
+#include <condition_variable>
+#include <mutex>
+#include <queue>
+#include <deque>
+#include <atomic>
+
+namespace icinga
+{
+
+enum WorkQueuePriority
+{
+ PriorityLow = 0,
+ PriorityNormal = 1,
+ PriorityHigh = 2,
+ PriorityImmediate = 4
+};
+
+using TaskFunction = std::function<void ()>;
+
+struct Task
+{
+ Task() = default;
+
+ Task(TaskFunction function, WorkQueuePriority priority, int id)
+ : Function(std::move(function)), Priority(priority), ID(id)
+ { }
+
+ TaskFunction Function;
+ WorkQueuePriority Priority{PriorityNormal};
+ int ID{-1};
+};
+
+bool operator<(const Task& a, const Task& b);
+
+/**
+ * A workqueue.
+ *
+ * @ingroup base
+ */
+class WorkQueue
+{
+public:
+ typedef std::function<void (boost::exception_ptr)> ExceptionCallback;
+
+ WorkQueue(size_t maxItems = 0, int threadCount = 1, LogSeverity statsLogLevel = LogInformation);
+ ~WorkQueue();
+
+ void SetName(const String& name);
+ String GetName() const;
+
+ std::unique_lock<std::mutex> AcquireLock();
+ void EnqueueUnlocked(std::unique_lock<std::mutex>& lock, TaskFunction&& function, WorkQueuePriority priority = PriorityNormal);
+ void Enqueue(TaskFunction&& function, WorkQueuePriority priority = PriorityNormal,
+ bool allowInterleaved = false);
+ void Join(bool stop = false);
+
+ template<typename VectorType, typename FuncType>
+ void ParallelFor(const VectorType& items, const FuncType& func)
+ {
+ ParallelFor(items, true, func);
+ }
+
+ template<typename VectorType, typename FuncType>
+ void ParallelFor(const VectorType& items, bool preChunk, const FuncType& func)
+ {
+ using SizeType = decltype(items.size());
+
+ SizeType totalCount = items.size();
+ SizeType chunks = preChunk ? m_ThreadCount : totalCount;
+
+ auto lock = AcquireLock();
+
+ SizeType offset = 0;
+
+ for (SizeType i = 0; i < chunks; i++) {
+ SizeType count = totalCount / chunks;
+ if (i < totalCount % chunks)
+ count++;
+
+ EnqueueUnlocked(lock, [&items, func, offset, count, this]() {
+ for (SizeType j = offset; j < offset + count; j++) {
+ RunTaskFunction([&func, &items, j]() {
+ func(items[j]);
+ });
+ }
+ });
+
+ offset += count;
+ }
+
+ ASSERT(offset == items.size());
+ }
+
+ bool IsWorkerThread() const;
+
+ size_t GetLength() const;
+ size_t GetTaskCount(RingBuffer::SizeType span);
+
+ void SetExceptionCallback(const ExceptionCallback& callback);
+
+ bool HasExceptions() const;
+ std::vector<boost::exception_ptr> GetExceptions() const;
+ void ReportExceptions(const String& facility, bool verbose = false) const;
+
+protected:
+ void IncreaseTaskCount();
+
+private:
+ int m_ID;
+ String m_Name;
+ static std::atomic<int> m_NextID;
+ int m_ThreadCount;
+ bool m_Spawned{false};
+
+ mutable std::mutex m_Mutex;
+ std::condition_variable m_CVEmpty;
+ std::condition_variable m_CVFull;
+ std::condition_variable m_CVStarved;
+ boost::thread_group m_Threads;
+ size_t m_MaxItems;
+ bool m_Stopped{false};
+ int m_Processing{0};
+ std::priority_queue<Task, std::deque<Task> > m_Tasks;
+ int m_NextTaskID{0};
+ ExceptionCallback m_ExceptionCallback;
+ std::vector<boost::exception_ptr> m_Exceptions;
+ Timer::Ptr m_StatusTimer;
+ double m_StatusTimerTimeout;
+ LogSeverity m_StatsLogLevel;
+
+ RingBuffer m_TaskStats;
+ size_t m_PendingTasks{0};
+ double m_PendingTasksTimestamp{0};
+
+ void WorkerThreadProc();
+ void StatusTimerHandler();
+
+ void RunTaskFunction(const TaskFunction& func);
+};
+
+}
+
+#endif /* WORKQUEUE_H */
diff --git a/lib/checker/CMakeLists.txt b/lib/checker/CMakeLists.txt
new file mode 100644
index 0000000..5a8334c
--- /dev/null
+++ b/lib/checker/CMakeLists.txt
@@ -0,0 +1,34 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(checkercomponent.ti checkercomponent-ti.cpp checkercomponent-ti.hpp)
+
+set(checker_SOURCES
+ checkercomponent.cpp checkercomponent.hpp checkercomponent-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(checker checker checker_SOURCES)
+endif()
+
+add_library(checker OBJECT ${checker_SOURCES})
+
+add_dependencies(checker base config icinga remote)
+
+set_target_properties (
+ checker PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/checker.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+if(NOT WIN32)
+ install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_CONFIGDIR}/features-enabled\")")
+ install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E create_symlink ../features-available/checker.conf \"\$ENV{DESTDIR}${ICINGA2_FULL_CONFIGDIR}/features-enabled/checker.conf\")")
+else()
+ install_if_not_exists(${PROJECT_SOURCE_DIR}/etc/icinga2/features-enabled/checker.conf ${ICINGA2_CONFIGDIR}/features-enabled)
+endif()
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/checker/checkercomponent.cpp b/lib/checker/checkercomponent.cpp
new file mode 100644
index 0000000..d92101f
--- /dev/null
+++ b/lib/checker/checkercomponent.cpp
@@ -0,0 +1,358 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "checker/checkercomponent.hpp"
+#include "checker/checkercomponent-ti.cpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/cib.hpp"
+#include "remote/apilistener.hpp"
+#include "base/configuration.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include "base/statsfunction.hpp"
+#include <chrono>
+
+using namespace icinga;
+
+REGISTER_TYPE(CheckerComponent);
+
+REGISTER_STATSFUNCTION(CheckerComponent, &CheckerComponent::StatsFunc);
+
+void CheckerComponent::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const CheckerComponent::Ptr& checker : ConfigType::GetObjectsByType<CheckerComponent>()) {
+ unsigned long idle = checker->GetIdleCheckables();
+ unsigned long pending = checker->GetPendingCheckables();
+
+ nodes.emplace_back(checker->GetName(), new Dictionary({
+ { "idle", idle },
+ { "pending", pending }
+ }));
+
+ String perfdata_prefix = "checkercomponent_" + checker->GetName() + "_";
+ perfdata->Add(new PerfdataValue(perfdata_prefix + "idle", Convert::ToDouble(idle)));
+ perfdata->Add(new PerfdataValue(perfdata_prefix + "pending", Convert::ToDouble(pending)));
+ }
+
+ status->Set("checkercomponent", new Dictionary(std::move(nodes)));
+}
+
+void CheckerComponent::OnConfigLoaded()
+{
+ ConfigObject::OnActiveChanged.connect([this](const ConfigObject::Ptr& object, const Value&) {
+ ObjectHandler(object);
+ });
+ ConfigObject::OnPausedChanged.connect([this](const ConfigObject::Ptr& object, const Value&) {
+ ObjectHandler(object);
+ });
+
+ Checkable::OnNextCheckChanged.connect([this](const Checkable::Ptr& checkable, const Value&) {
+ NextCheckChangedHandler(checkable);
+ });
+}
+
+void CheckerComponent::Start(bool runtimeCreated)
+{
+ ObjectImpl<CheckerComponent>::Start(runtimeCreated);
+
+ Log(LogInformation, "CheckerComponent")
+ << "'" << GetName() << "' started.";
+
+
+ m_Thread = std::thread([this]() { CheckThreadProc(); });
+
+ m_ResultTimer = Timer::Create();
+ m_ResultTimer->SetInterval(5);
+ m_ResultTimer->OnTimerExpired.connect([this](const Timer * const&) { ResultTimerHandler(); });
+ m_ResultTimer->Start();
+}
+
+void CheckerComponent::Stop(bool runtimeRemoved)
+{
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Stopped = true;
+ m_CV.notify_all();
+ }
+
+ m_ResultTimer->Stop(true);
+ m_Thread.join();
+
+ Log(LogInformation, "CheckerComponent")
+ << "'" << GetName() << "' stopped.";
+
+ ObjectImpl<CheckerComponent>::Stop(runtimeRemoved);
+}
+
+void CheckerComponent::CheckThreadProc()
+{
+ Utility::SetThreadName("Check Scheduler");
+ IcingaApplication::Ptr icingaApp = IcingaApplication::GetInstance();
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ for (;;) {
+ typedef boost::multi_index::nth_index<CheckableSet, 1>::type CheckTimeView;
+ CheckTimeView& idx = boost::get<1>(m_IdleCheckables);
+
+ while (idx.begin() == idx.end() && !m_Stopped)
+ m_CV.wait(lock);
+
+ if (m_Stopped)
+ break;
+
+ auto it = idx.begin();
+ CheckableScheduleInfo csi = *it;
+
+ double wait = csi.NextCheck - Utility::GetTime();
+
+//#ifdef I2_DEBUG
+// Log(LogDebug, "CheckerComponent")
+// << "Pending checks " << Checkable::GetPendingChecks()
+// << " vs. max concurrent checks " << icingaApp->GetMaxConcurrentChecks() << ".";
+//#endif /* I2_DEBUG */
+
+ if (Checkable::GetPendingChecks() >= icingaApp->GetMaxConcurrentChecks())
+ wait = 0.5;
+
+ if (wait > 0) {
+ /* Wait for the next check. */
+ m_CV.wait_for(lock, std::chrono::duration<double>(wait));
+
+ continue;
+ }
+
+ Checkable::Ptr checkable = csi.Object;
+
+ m_IdleCheckables.erase(checkable);
+
+ bool forced = checkable->GetForceNextCheck();
+ bool check = true;
+ bool notifyNextCheck = false;
+
+ if (!forced) {
+ if (!checkable->IsReachable(DependencyCheckExecution)) {
+ Log(LogNotice, "CheckerComponent")
+ << "Skipping check for object '" << checkable->GetName() << "': Dependency failed.";
+
+ check = false;
+ notifyNextCheck = true;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ if (host && !service && (!checkable->GetEnableActiveChecks() || !icingaApp->GetEnableHostChecks())) {
+ Log(LogNotice, "CheckerComponent")
+ << "Skipping check for host '" << host->GetName() << "': active host checks are disabled";
+ check = false;
+ }
+ if (host && service && (!checkable->GetEnableActiveChecks() || !icingaApp->GetEnableServiceChecks())) {
+ Log(LogNotice, "CheckerComponent")
+ << "Skipping check for service '" << service->GetName() << "': active service checks are disabled";
+ check = false;
+ }
+
+ TimePeriod::Ptr tp = checkable->GetCheckPeriod();
+
+ if (tp && !tp->IsInside(Utility::GetTime())) {
+ Log(LogNotice, "CheckerComponent")
+ << "Skipping check for object '" << checkable->GetName()
+ << "': not in check period '" << tp->GetName() << "'";
+
+ check = false;
+ notifyNextCheck = true;
+ }
+ }
+
+ /* reschedule the checkable if checks are disabled */
+ if (!check) {
+ m_IdleCheckables.insert(GetCheckableScheduleInfo(checkable));
+ lock.unlock();
+
+ Log(LogDebug, "CheckerComponent")
+ << "Checks for checkable '" << checkable->GetName() << "' are disabled. Rescheduling check.";
+
+ checkable->UpdateNextCheck();
+
+ if (notifyNextCheck) {
+ // Trigger update event for Icinga DB
+ Checkable::OnNextCheckUpdated(checkable);
+ }
+
+ lock.lock();
+
+ continue;
+ }
+
+
+ csi = GetCheckableScheduleInfo(checkable);
+
+ Log(LogDebug, "CheckerComponent")
+ << "Scheduling info for checkable '" << checkable->GetName() << "' ("
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", checkable->GetNextCheck()) << "): Object '"
+ << csi.Object->GetName() << "', Next Check: "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", csi.NextCheck) << "(" << csi.NextCheck << ").";
+
+ m_PendingCheckables.insert(csi);
+
+ lock.unlock();
+
+ if (forced) {
+ ObjectLock olock(checkable);
+ checkable->SetForceNextCheck(false);
+ }
+
+ Log(LogDebug, "CheckerComponent")
+ << "Executing check for '" << checkable->GetName() << "'";
+
+ Checkable::IncreasePendingChecks();
+
+ /*
+ * Explicitly use CheckerComponent::Ptr to keep the reference counted while the
+ * callback is active and making it crash safe
+ */
+ CheckerComponent::Ptr checkComponent(this);
+
+ Utility::QueueAsyncCallback([this, checkComponent, checkable]() { ExecuteCheckHelper(checkable); });
+
+ lock.lock();
+ }
+}
+
+void CheckerComponent::ExecuteCheckHelper(const Checkable::Ptr& checkable)
+{
+ try {
+ checkable->ExecuteCheck();
+ } catch (const std::exception& ex) {
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetState(ServiceUnknown);
+
+ String output = "Exception occurred while checking '" + checkable->GetName() + "': " + DiagnosticInformation(ex);
+ cr->SetOutput(output);
+
+ double now = Utility::GetTime();
+ cr->SetScheduleStart(now);
+ cr->SetScheduleEnd(now);
+ cr->SetExecutionStart(now);
+ cr->SetExecutionEnd(now);
+
+ checkable->ProcessCheckResult(cr);
+
+ Log(LogCritical, "checker", output);
+ }
+
+ Checkable::DecreasePendingChecks();
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ /* remove the object from the list of pending objects; if it's not in the
+ * list this was a manual (i.e. forced) check and we must not re-add the
+ * object to the list because it's already there. */
+ auto it = m_PendingCheckables.find(checkable);
+
+ if (it != m_PendingCheckables.end()) {
+ m_PendingCheckables.erase(it);
+
+ if (checkable->IsActive())
+ m_IdleCheckables.insert(GetCheckableScheduleInfo(checkable));
+
+ m_CV.notify_all();
+ }
+ }
+
+ Log(LogDebug, "CheckerComponent")
+ << "Check finished for object '" << checkable->GetName() << "'";
+}
+
+void CheckerComponent::ResultTimerHandler()
+{
+ std::ostringstream msgbuf;
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ msgbuf << "Pending checkables: " << m_PendingCheckables.size() << "; Idle checkables: " << m_IdleCheckables.size() << "; Checks/s: "
+ << (CIB::GetActiveHostChecksStatistics(60) + CIB::GetActiveServiceChecksStatistics(60)) / 60.0;
+ }
+
+ Log(LogNotice, "CheckerComponent", msgbuf.str());
+}
+
+void CheckerComponent::ObjectHandler(const ConfigObject::Ptr& object)
+{
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return;
+
+ Zone::Ptr zone = Zone::GetByName(checkable->GetZoneName());
+ bool same_zone = (!zone || Zone::GetLocalZone() == zone);
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ if (object->IsActive() && !object->IsPaused() && same_zone) {
+ if (m_PendingCheckables.find(checkable) != m_PendingCheckables.end())
+ return;
+
+ m_IdleCheckables.insert(GetCheckableScheduleInfo(checkable));
+ } else {
+ m_IdleCheckables.erase(checkable);
+ m_PendingCheckables.erase(checkable);
+ }
+
+ m_CV.notify_all();
+ }
+}
+
+CheckableScheduleInfo CheckerComponent::GetCheckableScheduleInfo(const Checkable::Ptr& checkable)
+{
+ CheckableScheduleInfo csi;
+ csi.Object = checkable;
+ csi.NextCheck = checkable->GetNextCheck();
+ return csi;
+}
+
+void CheckerComponent::NextCheckChangedHandler(const Checkable::Ptr& checkable)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ /* remove and re-insert the object from the set in order to force an index update */
+ typedef boost::multi_index::nth_index<CheckableSet, 0>::type CheckableView;
+ CheckableView& idx = boost::get<0>(m_IdleCheckables);
+
+ auto it = idx.find(checkable);
+
+ if (it == idx.end())
+ return;
+
+ idx.erase(checkable);
+
+ CheckableScheduleInfo csi = GetCheckableScheduleInfo(checkable);
+ idx.insert(csi);
+
+ m_CV.notify_all();
+}
+
+unsigned long CheckerComponent::GetIdleCheckables()
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_IdleCheckables.size();
+}
+
+unsigned long CheckerComponent::GetPendingCheckables()
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_PendingCheckables.size();
+}
diff --git a/lib/checker/checkercomponent.hpp b/lib/checker/checkercomponent.hpp
new file mode 100644
index 0000000..5ace757
--- /dev/null
+++ b/lib/checker/checkercomponent.hpp
@@ -0,0 +1,99 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CHECKERCOMPONENT_H
+#define CHECKERCOMPONENT_H
+
+#include "checker/checkercomponent-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/timer.hpp"
+#include "base/utility.hpp"
+#include <boost/multi_index_container.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index/key_extractors.hpp>
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+
+namespace icinga
+{
+
+/**
+ * @ingroup checker
+ */
+struct CheckableScheduleInfo
+{
+ Checkable::Ptr Object;
+ double NextCheck;
+};
+
+/**
+ * @ingroup checker
+ */
+struct CheckableNextCheckExtractor
+{
+ typedef double result_type;
+
+ /**
+ * @threadsafety Always.
+ */
+ double operator()(const CheckableScheduleInfo& csi)
+ {
+ return csi.NextCheck;
+ }
+};
+
+/**
+ * @ingroup checker
+ */
+class CheckerComponent final : public ObjectImpl<CheckerComponent>
+{
+public:
+ DECLARE_OBJECT(CheckerComponent);
+ DECLARE_OBJECTNAME(CheckerComponent);
+
+ typedef boost::multi_index_container<
+ CheckableScheduleInfo,
+ boost::multi_index::indexed_by<
+ boost::multi_index::ordered_unique<boost::multi_index::member<CheckableScheduleInfo, Checkable::Ptr, &CheckableScheduleInfo::Object> >,
+ boost::multi_index::ordered_non_unique<CheckableNextCheckExtractor>
+ >
+ > CheckableSet;
+
+ void OnConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+ unsigned long GetIdleCheckables();
+ unsigned long GetPendingCheckables();
+
+private:
+ std::mutex m_Mutex;
+ std::condition_variable m_CV;
+ bool m_Stopped{false};
+ std::thread m_Thread;
+
+ CheckableSet m_IdleCheckables;
+ CheckableSet m_PendingCheckables;
+
+ Timer::Ptr m_ResultTimer;
+
+ void CheckThreadProc();
+ void ResultTimerHandler();
+
+ void ExecuteCheckHelper(const Checkable::Ptr& checkable);
+
+ void AdjustCheckTimer();
+
+ void ObjectHandler(const ConfigObject::Ptr& object);
+ void NextCheckChangedHandler(const Checkable::Ptr& checkable);
+
+ void RescheduleCheckTimer();
+
+ static CheckableScheduleInfo GetCheckableScheduleInfo(const Checkable::Ptr& checkable);
+};
+
+}
+
+#endif /* CHECKERCOMPONENT_H */
diff --git a/lib/checker/checkercomponent.ti b/lib/checker/checkercomponent.ti
new file mode 100644
index 0000000..3959aeb
--- /dev/null
+++ b/lib/checker/checkercomponent.ti
@@ -0,0 +1,18 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library checker;
+
+namespace icinga
+{
+
+class CheckerComponent : ConfigObject
+{
+ activation_priority 300;
+
+ /* Has no effect. Keep this here to avoid breaking config changes. */
+ [deprecated, config] int concurrent_checks;
+};
+
+}
diff --git a/lib/cli/CMakeLists.txt b/lib/cli/CMakeLists.txt
new file mode 100644
index 0000000..bbdf801
--- /dev/null
+++ b/lib/cli/CMakeLists.txt
@@ -0,0 +1,49 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+set(cli_SOURCES
+ i2-cli.hpp
+ apisetupcommand.cpp apisetupcommand.hpp
+ apisetuputility.cpp apisetuputility.hpp
+ calistcommand.cpp calistcommand.hpp
+ caremovecommand.cpp caremovecommand.hpp
+ carestorecommand.cpp carestorecommand.hpp
+ casigncommand.cpp casigncommand.hpp
+ clicommand.cpp clicommand.hpp
+ consolecommand.cpp consolecommand.hpp
+ daemoncommand.cpp daemoncommand.hpp
+ daemonutility.cpp daemonutility.hpp
+ editline.hpp
+ featuredisablecommand.cpp featuredisablecommand.hpp
+ featureenablecommand.cpp featureenablecommand.hpp
+ featurelistcommand.cpp featurelistcommand.hpp
+ featureutility.cpp featureutility.hpp
+ internalsignalcommand.cpp internalsignalcommand.hpp
+ nodesetupcommand.cpp nodesetupcommand.hpp
+ nodeutility.cpp nodeutility.hpp
+ nodewizardcommand.cpp nodewizardcommand.hpp
+ objectlistcommand.cpp objectlistcommand.hpp
+ objectlistutility.cpp objectlistutility.hpp
+ pkinewcacommand.cpp pkinewcacommand.hpp
+ pkinewcertcommand.cpp pkinewcertcommand.hpp
+ pkirequestcommand.cpp pkirequestcommand.hpp
+ pkisavecertcommand.cpp pkisavecertcommand.hpp
+ pkisigncsrcommand.cpp pkisigncsrcommand.hpp
+ pkiticketcommand.cpp pkiticketcommand.hpp
+ pkiverifycommand.cpp pkiverifycommand.hpp
+ variablegetcommand.cpp variablegetcommand.hpp
+ variablelistcommand.cpp variablelistcommand.hpp
+ variableutility.cpp variableutility.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(cli cli cli_SOURCES)
+endif()
+
+add_library(cli OBJECT ${cli_SOURCES})
+
+add_dependencies(cli base config icinga remote)
+
+set_target_properties (
+ cli PROPERTIES
+ FOLDER Lib
+)
diff --git a/lib/cli/apisetupcommand.cpp b/lib/cli/apisetupcommand.cpp
new file mode 100644
index 0000000..81b9d8d
--- /dev/null
+++ b/lib/cli/apisetupcommand.cpp
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/apisetupcommand.hpp"
+#include "cli/apisetuputility.hpp"
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("api/setup", ApiSetupCommand);
+
+String ApiSetupCommand::GetDescription() const
+{
+ return "Setup for Icinga 2 API.";
+}
+
+String ApiSetupCommand::GetShortDescription() const
+{
+ return "setup for API";
+}
+
+ImpersonationLevel ApiSetupCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+void ApiSetupCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("cn", po::value<std::string>(), "The certificate's common name");
+}
+
+/**
+ * The entry point for the "api setup" CLI command.
+ *
+ * @returns An exit status.
+ */
+int ApiSetupCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String cn;
+
+ if (vm.count("cn")) {
+ cn = vm["cn"].as<std::string>();
+ } else {
+ cn = VariableUtility::GetVariable("NodeName");
+
+ if (cn.IsEmpty())
+ cn = Utility::GetFQDN();
+ }
+
+ if (!ApiSetupUtility::SetupMaster(cn, true))
+ return 1;
+
+ return 0;
+}
diff --git a/lib/cli/apisetupcommand.hpp b/lib/cli/apisetupcommand.hpp
new file mode 100644
index 0000000..be2693d
--- /dev/null
+++ b/lib/cli/apisetupcommand.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APISETUPCOMMAND_H
+#define APISETUPCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "api setup" command.
+ *
+ * @ingroup cli
+ */
+class ApiSetupCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ApiSetupCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+};
+
+}
+
+#endif /* APISETUPCOMMAND_H */
diff --git a/lib/cli/apisetuputility.cpp b/lib/cli/apisetuputility.cpp
new file mode 100644
index 0000000..8bdd767
--- /dev/null
+++ b/lib/cli/apisetuputility.cpp
@@ -0,0 +1,205 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/apisetuputility.hpp"
+#include "cli/nodeutility.hpp"
+#include "cli/featureutility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/case_conv.hpp>
+#include <iostream>
+#include <string>
+#include <fstream>
+#include <vector>
+
+using namespace icinga;
+
+String ApiSetupUtility::GetConfdPath()
+{
+ return Configuration::ConfigDir + "/conf.d";
+}
+
+String ApiSetupUtility::GetApiUsersConfPath()
+{
+ return ApiSetupUtility::GetConfdPath() + "/api-users.conf";
+}
+
+bool ApiSetupUtility::SetupMaster(const String& cn, bool prompt_restart)
+{
+ if (!SetupMasterCertificates(cn))
+ return false;
+
+ if (!SetupMasterApiUser())
+ return false;
+
+ if (!SetupMasterEnableApi())
+ return false;
+
+ if (!SetupMasterUpdateConstants(cn))
+ return false;
+
+ if (prompt_restart) {
+ std::cout << "Done.\n\n";
+ std::cout << "Now restart your Icinga 2 daemon to finish the installation!\n\n";
+ }
+
+ return true;
+}
+
+bool ApiSetupUtility::SetupMasterCertificates(const String& cn)
+{
+ Log(LogInformation, "cli", "Generating new CA.");
+
+ if (PkiUtility::NewCa() > 0)
+ Log(LogWarning, "cli", "Found CA, skipping and using the existing one.");
+
+ String pki_path = ApiListener::GetCertsDir();
+ Utility::MkDirP(pki_path, 0700);
+
+ String user = Configuration::RunAsUser;
+ String group = Configuration::RunAsGroup;
+
+ if (!Utility::SetFileOwnership(pki_path, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << pki_path << "'.";
+ }
+
+ String key = pki_path + "/" + cn + ".key";
+ String csr = pki_path + "/" + cn + ".csr";
+
+ if (Utility::PathExists(key)) {
+ Log(LogInformation, "cli")
+ << "Private key file '" << key << "' already exists, not generating new certificate.";
+ return true;
+ }
+
+ Log(LogInformation, "cli")
+ << "Generating new CSR in '" << csr << "'.";
+
+ if (Utility::PathExists(key))
+ NodeUtility::CreateBackupFile(key, true);
+ if (Utility::PathExists(csr))
+ NodeUtility::CreateBackupFile(csr);
+
+ if (PkiUtility::NewCert(cn, key, csr, "") > 0) {
+ Log(LogCritical, "cli", "Failed to create certificate signing request.");
+ return false;
+ }
+
+ /* Sign the CSR with the CA key */
+ String cert = pki_path + "/" + cn + ".crt";
+
+ Log(LogInformation, "cli")
+ << "Signing CSR with CA and writing certificate to '" << cert << "'.";
+
+ if (Utility::PathExists(cert))
+ NodeUtility::CreateBackupFile(cert);
+
+ if (PkiUtility::SignCsr(csr, cert) != 0) {
+ Log(LogCritical, "cli", "Could not sign CSR.");
+ return false;
+ }
+
+ /* Copy CA certificate to /etc/icinga2/pki */
+ String ca_path = ApiListener::GetCaDir();
+ String ca = ca_path + "/ca.crt";
+ String ca_key = ca_path + "/ca.key";
+ String target_ca = pki_path + "/ca.crt";
+
+ Log(LogInformation, "cli")
+ << "Copying CA certificate to '" << target_ca << "'.";
+
+ if (Utility::PathExists(target_ca))
+ NodeUtility::CreateBackupFile(target_ca);
+
+ /* does not overwrite existing files! */
+ Utility::CopyFile(ca, target_ca);
+
+ /* fix permissions: root -> icinga daemon user */
+ for (const String& file : { ca_path, ca, ca_key, target_ca, key, csr, cert }) {
+ if (!Utility::SetFileOwnership(file, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << file << "'.";
+ }
+ }
+
+ return true;
+}
+
+bool ApiSetupUtility::SetupMasterApiUser()
+{
+ if (!Utility::PathExists(GetConfdPath())) {
+ Log(LogWarning, "cli")
+ << "Path '" << GetConfdPath() << "' do not exist.";
+ Log(LogInformation, "cli")
+ << "Creating path '" << GetConfdPath() << "'.";
+
+ Utility::MkDirP(GetConfdPath(), 0755);
+ }
+
+ String api_username = "root"; // TODO make this available as cli parameter?
+ String api_password = RandomString(8);
+ String apiUsersPath = GetConfdPath() + "/api-users.conf";
+
+ if (Utility::PathExists(apiUsersPath)) {
+ Log(LogInformation, "cli")
+ << "API user config file '" << apiUsersPath << "' already exists, not creating config file.";
+ return true;
+ }
+
+ Log(LogInformation, "cli")
+ << "Adding new ApiUser '" << api_username << "' in '" << apiUsersPath << "'.";
+
+ NodeUtility::CreateBackupFile(apiUsersPath);
+
+ AtomicFile fp (apiUsersPath, 0644);
+
+ fp << "/**\n"
+ << " * The ApiUser objects are used for authentication against the API.\n"
+ << " */\n"
+ << "object ApiUser \"" << api_username << "\" {\n"
+ << " password = \"" << api_password << "\"\n"
+ << " // client_cn = \"\"\n"
+ << "\n"
+ << " permissions = [ \"*\" ]\n"
+ << "}\n";
+
+ fp.Commit();
+
+ return true;
+}
+
+bool ApiSetupUtility::SetupMasterEnableApi()
+{
+ /*
+ * Ensure the api-users.conf file is included, when conf.d inclusion is disabled.
+ */
+ if (!NodeUtility::GetConfigurationIncludeState("\"conf.d\"", true))
+ NodeUtility::UpdateConfiguration("\"conf.d/api-users.conf\"", true, false);
+
+ /*
+ * Enable the API feature
+ */
+ Log(LogInformation, "cli", "Enabling the 'api' feature.");
+
+ FeatureUtility::EnableFeatures({ "api" });
+
+ return true;
+}
+
+bool ApiSetupUtility::SetupMasterUpdateConstants(const String& cn)
+{
+ NodeUtility::UpdateConstant("NodeName", cn);
+ NodeUtility::UpdateConstant("ZoneName", cn);
+
+ return true;
+}
diff --git a/lib/cli/apisetuputility.hpp b/lib/cli/apisetuputility.hpp
new file mode 100644
index 0000000..d361446
--- /dev/null
+++ b/lib/cli/apisetuputility.hpp
@@ -0,0 +1,39 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APISETUPUTILITY_H
+#define APISETUPUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "base/value.hpp"
+#include "base/string.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class ApiSetupUtility
+{
+public:
+ static bool SetupMaster(const String& cn, bool prompt_restart = false);
+
+ static bool SetupMasterCertificates(const String& cn);
+ static bool SetupMasterApiUser();
+ static bool SetupMasterEnableApi();
+ static bool SetupMasterUpdateConstants(const String& cn);
+
+ static String GetConfdPath();
+ static String GetApiUsersConfPath();
+
+private:
+ ApiSetupUtility();
+};
+
+}
+
+#endif /* APISETUPUTILITY_H */
diff --git a/lib/cli/calistcommand.cpp b/lib/cli/calistcommand.cpp
new file mode 100644
index 0000000..f693ad7
--- /dev/null
+++ b/lib/cli/calistcommand.cpp
@@ -0,0 +1,89 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/calistcommand.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/json.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("ca/list", CAListCommand);
+
+/**
+ * Provide a long CLI description sentence.
+ *
+ * @return text
+ */
+String CAListCommand::GetDescription() const
+{
+ return "Lists pending certificate signing requests.";
+}
+
+/**
+ * Provide a short CLI description.
+ *
+ * @return text
+ */
+String CAListCommand::GetShortDescription() const
+{
+ return "lists pending certificate signing requests";
+}
+
+/**
+ * Initialize available CLI parameters.
+ *
+ * @param visibleDesc Register visible parameters.
+ * @param hiddenDesc Register hidden parameters.
+ */
+void CAListCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("all", "List all certificate signing requests, including signed. Note: Old requests are automatically cleaned by Icinga after 1 week.")
+ ("removed", "List all removed CSRs (for use with 'ca restore')")
+ ("json", "encode output as JSON");
+}
+
+/**
+ * The entry point for the "ca list" CLI command.
+ *
+ * @return An exit status.
+ */
+int CAListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ Dictionary::Ptr requests = PkiUtility::GetCertificateRequests(vm.count("removed"));
+
+ if (vm.count("json"))
+ std::cout << JsonEncode(requests);
+ else {
+ ObjectLock olock(requests);
+
+ std::cout << "Fingerprint | Timestamp | Signed | Subject\n";
+ std::cout << "-----------------------------------------------------------------|--------------------------|--------|--------\n";
+
+ for (auto& kv : requests) {
+ Dictionary::Ptr request = kv.second;
+
+ /* Skip signed requests by default. */
+ if (!vm.count("all") && request->Contains("cert_response"))
+ continue;
+
+ std::cout << kv.first
+ << " | "
+/* << Utility::FormatDateTime("%Y/%m/%d %H:%M:%S", request->Get("timestamp")) */
+ << request->Get("timestamp")
+ << " | "
+ << (request->Contains("cert_response") ? "*" : " ") << " "
+ << " | "
+ << request->Get("subject")
+ << "\n";
+ }
+ }
+
+ return 0;
+}
diff --git a/lib/cli/calistcommand.hpp b/lib/cli/calistcommand.hpp
new file mode 100644
index 0000000..ddf44d4
--- /dev/null
+++ b/lib/cli/calistcommand.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CALISTCOMMAND_H
+#define CALISTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "ca list" command.
+ *
+ * @ingroup cli
+ */
+class CAListCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CAListCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+private:
+ static void PrintRequest(const String& requestFile);
+};
+
+}
+
+#endif /* CALISTCOMMAND_H */
diff --git a/lib/cli/caremovecommand.cpp b/lib/cli/caremovecommand.cpp
new file mode 100644
index 0000000..d894494
--- /dev/null
+++ b/lib/cli/caremovecommand.cpp
@@ -0,0 +1,93 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/caremovecommand.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+REGISTER_CLICOMMAND("ca/remove", CARemoveCommand);
+
+/**
+ * Provide a long CLI description sentence.
+ *
+ * @return text
+ */
+String CARemoveCommand::GetDescription() const
+{
+ return "Removes an outstanding certificate request.";
+}
+
+/**
+ * Provide a short CLI description.
+ *
+ * @return text
+ */
+String CARemoveCommand::GetShortDescription() const
+{
+ return "removes an outstanding certificate request";
+}
+
+/**
+ * Define minimum arguments without key parameter.
+ *
+ * @return number of arguments
+ */
+int CARemoveCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+/**
+ * Impersonate as Icinga user.
+ *
+ * @return impersonate level
+ */
+ImpersonationLevel CARemoveCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "ca remove" CLI command.
+ *
+ * @returns An exit status.
+ */
+int CARemoveCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String fingerPrint = ap[0];
+ String requestFile = ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".json";
+
+ if (!Utility::PathExists(requestFile)) {
+ Log(LogCritical, "cli")
+ << "No request exists for fingerprint '" << fingerPrint << "'.";
+ return 1;
+ }
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestFile);
+ std::shared_ptr<X509> certRequest = StringToCertificate(request->Get("cert_request"));
+
+ if (!certRequest) {
+ Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute.");
+ return 1;
+ }
+
+ String cn = GetCertificateCN(certRequest);
+
+ if (request->Contains("cert_response")) {
+ Log(LogCritical, "cli")
+ << "Certificate request for CN '" << cn << "' already signed, removal is not possible.";
+ return 1;
+ }
+
+ Utility::SaveJsonFile(ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".removed", 0600, request);
+
+ Utility::Remove(requestFile);
+
+ Log(LogInformation, "cli")
+ << "Certificate request for CN " << cn << " removed.";
+
+ return 0;
+}
diff --git a/lib/cli/caremovecommand.hpp b/lib/cli/caremovecommand.hpp
new file mode 100644
index 0000000..2da92d3
--- /dev/null
+++ b/lib/cli/caremovecommand.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CAREMOVECOMMAND_H
+#define CAREMOVECOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "ca remove" command.
+ *
+ * @ingroup cli
+ */
+class CARemoveCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CARemoveCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* CAREMOVECOMMAND_H */
diff --git a/lib/cli/carestorecommand.cpp b/lib/cli/carestorecommand.cpp
new file mode 100644
index 0000000..5020368
--- /dev/null
+++ b/lib/cli/carestorecommand.cpp
@@ -0,0 +1,88 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/carestorecommand.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+REGISTER_CLICOMMAND("ca/restore", CARestoreCommand);
+
+/**
+ * Provide a long CLI description sentence.
+ *
+ * @return text
+ */
+String CARestoreCommand::GetDescription() const
+{
+ return "Restores a previously removed certificate request.";
+}
+
+/**
+ * Provide a short CLI description.
+ *
+ * @return text
+ */
+String CARestoreCommand::GetShortDescription() const
+{
+ return "restores a removed certificate request";
+}
+
+/**
+ * Define minimum arguments without key parameter.
+ *
+ * @return number of arguments
+ */
+int CARestoreCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+/**
+ * Impersonate as Icinga user.
+ *
+ * @return impersonate level
+ */
+ImpersonationLevel CARestoreCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "ca restore" CLI command.
+ *
+ * @returns An exit status.
+ */
+int CARestoreCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String fingerPrint = ap[0];
+ String removedRequestFile = ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".removed";
+
+ if (!Utility::PathExists(removedRequestFile)) {
+ Log(LogCritical, "cli")
+ << "Cannot find removed fingerprint '" << fingerPrint << "', bailing out.";
+ return 1;
+ }
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(removedRequestFile);
+ std::shared_ptr<X509> certRequest = StringToCertificate(request->Get("cert_request"));
+
+ if (!certRequest) {
+ Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute.");
+ /* Purge the file when we know that it is broken. */
+ Utility::Remove(removedRequestFile);
+ return 1;
+ }
+
+ Utility::SaveJsonFile(ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".json", 0600, request);
+
+ Utility::Remove(removedRequestFile);
+
+ Log(LogInformation, "cli")
+ << "Restored certificate request for CN '" << GetCertificateCN(certRequest) << "', sign it with:\n"
+ << "\"icinga2 ca sign " << fingerPrint << "\"";
+
+ return 0;
+}
diff --git a/lib/cli/carestorecommand.hpp b/lib/cli/carestorecommand.hpp
new file mode 100644
index 0000000..74a27df
--- /dev/null
+++ b/lib/cli/carestorecommand.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CARESTORECOMMAND_H
+#define CARESTORECOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "ca restore" command.
+ *
+ * @ingroup cli
+ */
+class CARestoreCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CARestoreCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* CASTORECOMMAND_H */
diff --git a/lib/cli/casigncommand.cpp b/lib/cli/casigncommand.cpp
new file mode 100644
index 0000000..96d2c2c
--- /dev/null
+++ b/lib/cli/casigncommand.cpp
@@ -0,0 +1,108 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/casigncommand.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+REGISTER_CLICOMMAND("ca/sign", CASignCommand);
+
+/**
+ * Provide a long CLI description sentence.
+ *
+ * @return text
+ */
+String CASignCommand::GetDescription() const
+{
+ return "Signs an outstanding certificate request.";
+}
+
+/**
+ * Provide a short CLI description.
+ *
+ * @return text
+ */
+String CASignCommand::GetShortDescription() const
+{
+ return "signs an outstanding certificate request";
+}
+
+/**
+ * Define minimum arguments without key parameter.
+ *
+ * @return number of arguments
+ */
+int CASignCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+/**
+ * Impersonate as Icinga user.
+ *
+ * @return impersonate level
+ */
+ImpersonationLevel CASignCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "ca sign" CLI command.
+ *
+ * @return An exit status.
+ */
+int CASignCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String requestFile = ApiListener::GetCertificateRequestsDir() + "/" + ap[0] + ".json";
+
+ if (!Utility::PathExists(requestFile)) {
+ Log(LogCritical, "cli")
+ << "No request exists for fingerprint '" << ap[0] << "'.";
+ return 1;
+ }
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestFile);
+
+ if (!request)
+ return 1;
+
+ String certRequestText = request->Get("cert_request");
+
+ std::shared_ptr<X509> certRequest = StringToCertificate(certRequestText);
+
+ if (!certRequest) {
+ Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute.");
+ return 1;
+ }
+
+ std::shared_ptr<X509> certResponse = CreateCertIcingaCA(certRequest);
+
+ BIO *out = BIO_new(BIO_s_mem());
+ X509_NAME_print_ex(out, X509_get_subject_name(certRequest.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
+
+ char *data;
+ long length;
+ length = BIO_get_mem_data(out, &data);
+
+ String subject = String(data, data + length);
+ BIO_free(out);
+
+ if (!certResponse) {
+ Log(LogCritical, "cli")
+ << "Could not sign certificate for '" << subject << "'.";
+ return 1;
+ }
+
+ request->Set("cert_response", CertificateToString(certResponse));
+
+ Utility::SaveJsonFile(requestFile, 0600, request);
+
+ Log(LogInformation, "cli")
+ << "Signed certificate for '" << subject << "'.";
+
+ return 0;
+}
diff --git a/lib/cli/casigncommand.hpp b/lib/cli/casigncommand.hpp
new file mode 100644
index 0000000..0089af7
--- /dev/null
+++ b/lib/cli/casigncommand.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CASIGNCOMMAND_H
+#define CASIGNCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "ca sign" command.
+ *
+ * @ingroup cli
+ */
+class CASignCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CASignCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* CASIGNCOMMAND_H */
diff --git a/lib/cli/clicommand.cpp b/lib/cli/clicommand.cpp
new file mode 100644
index 0000000..cfdce09
--- /dev/null
+++ b/lib/cli/clicommand.cpp
@@ -0,0 +1,373 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/clicommand.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/type.hpp"
+#include "base/serializer.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/program_options.hpp>
+#include <algorithm>
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+std::vector<String> icinga::GetBashCompletionSuggestions(const String& type, const String& word)
+{
+ std::vector<String> result;
+
+#ifndef _WIN32
+ String bashArg = "compgen -A " + Utility::EscapeShellArg(type) + " " + Utility::EscapeShellArg(word);
+ String cmd = "bash -c " + Utility::EscapeShellArg(bashArg);
+
+ FILE *fp = popen(cmd.CStr(), "r");
+
+ char line[4096];
+ while (fgets(line, sizeof(line), fp)) {
+ String wline = line;
+ boost::algorithm::trim_right_if(wline, boost::is_any_of("\r\n"));
+ result.push_back(wline);
+ }
+
+ pclose(fp);
+
+ /* Append a slash if there's only one suggestion and it's a directory */
+ if ((type == "file" || type == "directory") && result.size() == 1) {
+ String path = result[0];
+
+ struct stat statbuf;
+ if (lstat(path.CStr(), &statbuf) >= 0) {
+ if (S_ISDIR(statbuf.st_mode)) {
+ result.clear(),
+ result.push_back(path + "/");
+ }
+ }
+ }
+#endif /* _WIN32 */
+
+ return result;
+}
+
+std::vector<String> icinga::GetFieldCompletionSuggestions(const Type::Ptr& type, const String& word)
+{
+ std::vector<String> result;
+
+ for (int i = 0; i < type->GetFieldCount(); i++) {
+ Field field = type->GetFieldInfo(i);
+
+ if (field.Attributes & FANoUserView)
+ continue;
+
+ if (strcmp(field.TypeName, "int") != 0 && strcmp(field.TypeName, "double") != 0
+ && strcmp(field.TypeName, "bool") != 0 && strcmp(field.TypeName, "String") != 0)
+ continue;
+
+ String fname = field.Name;
+
+ String suggestion = fname + "=";
+
+ if (suggestion.Find(word) == 0)
+ result.push_back(suggestion);
+ }
+
+ return result;
+}
+
+int CLICommand::GetMinArguments() const
+{
+ return 0;
+}
+
+int CLICommand::GetMaxArguments() const
+{
+ return GetMinArguments();
+}
+
+bool CLICommand::IsHidden() const
+{
+ return false;
+}
+
+bool CLICommand::IsDeprecated() const
+{
+ return false;
+}
+
+std::mutex& CLICommand::GetRegistryMutex()
+{
+ static std::mutex mtx;
+ return mtx;
+}
+
+std::map<std::vector<String>, CLICommand::Ptr>& CLICommand::GetRegistry()
+{
+ static std::map<std::vector<String>, CLICommand::Ptr> registry;
+ return registry;
+}
+
+CLICommand::Ptr CLICommand::GetByName(const std::vector<String>& name)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+
+ auto it = GetRegistry().find(name);
+
+ if (it == GetRegistry().end())
+ return nullptr;
+
+ return it->second;
+}
+
+void CLICommand::Register(const std::vector<String>& name, const CLICommand::Ptr& function)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+ GetRegistry()[name] = function;
+}
+
+void CLICommand::Unregister(const std::vector<String>& name)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+ GetRegistry().erase(name);
+}
+
+std::vector<String> CLICommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ return std::vector<String>();
+}
+
+std::vector<String> CLICommand::GetPositionalSuggestions(const String& word) const
+{
+ return std::vector<String>();
+}
+
+void CLICommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{ }
+
+ImpersonationLevel CLICommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+bool CLICommand::ParseCommand(int argc, char **argv, po::options_description& visibleDesc,
+ po::options_description& hiddenDesc,
+ po::positional_options_description& positionalDesc,
+ po::variables_map& vm, String& cmdname, CLICommand::Ptr& command, bool autocomplete)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+
+ typedef std::map<std::vector<String>, CLICommand::Ptr>::value_type CLIKeyValue;
+
+ std::vector<String> best_match;
+ int arg_end = 0;
+ bool tried_command = false;
+
+ for (const CLIKeyValue& kv : GetRegistry()) {
+ const std::vector<String>& vname = kv.first;
+
+ std::vector<String>::size_type i;
+ int k;
+ for (i = 0, k = 1; i < vname.size() && k < argc; i++, k++) {
+ if (strncmp(argv[k], "-", 1) == 0 || strncmp(argv[k], "--", 2) == 0) {
+ i--;
+ continue;
+ }
+
+ tried_command = true;
+
+ if (vname[i] != argv[k])
+ break;
+
+ if (i >= best_match.size())
+ best_match.push_back(vname[i]);
+
+ if (i == vname.size() - 1) {
+ cmdname = boost::algorithm::join(vname, " ");
+ command = kv.second;
+ arg_end = k;
+ goto found_command;
+ }
+ }
+ }
+
+found_command:
+ lock.unlock();
+
+ if (command) {
+ po::options_description vdesc("Command options");
+ command->InitParameters(vdesc, hiddenDesc);
+ visibleDesc.add(vdesc);
+ }
+
+ if (autocomplete || (tried_command && !command))
+ return true;
+
+ po::options_description adesc;
+ adesc.add(visibleDesc);
+ adesc.add(hiddenDesc);
+
+ if (command && command->IsDeprecated()) {
+ std::cerr << ConsoleColorTag(Console_ForegroundRed | Console_Bold)
+ << "Warning: CLI command '" << cmdname << "' is DEPRECATED! Please read the Changelog."
+ << ConsoleColorTag(Console_Normal) << std::endl << std::endl;
+ }
+
+ po::store(po::command_line_parser(argc - arg_end, argv + arg_end).options(adesc).positional(positionalDesc).run(), vm);
+ po::notify(vm);
+
+ return true;
+}
+
+void CLICommand::ShowCommands(int argc, char **argv, po::options_description *visibleDesc,
+ po::options_description *hiddenDesc,
+ ArgumentCompletionCallback globalArgCompletionCallback,
+ bool autocomplete, int autoindex)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+
+ typedef std::map<std::vector<String>, CLICommand::Ptr>::value_type CLIKeyValue;
+
+ std::vector<String> best_match;
+ int arg_begin = 0;
+ CLICommand::Ptr command;
+
+ for (const CLIKeyValue& kv : GetRegistry()) {
+ const std::vector<String>& vname = kv.first;
+
+ arg_begin = 0;
+
+ std::vector<String>::size_type i;
+ int k;
+ for (i = 0, k = 1; i < vname.size() && k < argc; i++, k++) {
+ if (strcmp(argv[k], "--no-stack-rlimit") == 0 || strcmp(argv[k], "--autocomplete") == 0 || strcmp(argv[k], "--scm") == 0) {
+ i--;
+ arg_begin++;
+ continue;
+ }
+
+ if (autocomplete && static_cast<int>(i) >= autoindex - 1)
+ break;
+
+ if (vname[i] != argv[k])
+ break;
+
+ if (i >= best_match.size()) {
+ best_match.push_back(vname[i]);
+ }
+
+ if (i == vname.size() - 1) {
+ command = kv.second;
+ break;
+ }
+ }
+ }
+
+ String aword;
+
+ if (autocomplete) {
+ if (autoindex < argc)
+ aword = argv[autoindex];
+
+ if (autoindex - 1 > static_cast<int>(best_match.size()) && !command)
+ return;
+ } else
+ std::cout << "Supported commands: " << std::endl;
+
+ for (const CLIKeyValue& kv : GetRegistry()) {
+ const std::vector<String>& vname = kv.first;
+
+ if (vname.size() < best_match.size() || kv.second->IsHidden())
+ continue;
+
+ bool match = true;
+
+ for (std::vector<String>::size_type i = 0; i < best_match.size(); i++) {
+ if (vname[i] != best_match[i]) {
+ match = false;
+ break;
+ }
+ }
+
+ if (!match)
+ continue;
+
+ if (autocomplete) {
+ String cname;
+
+ if (autoindex - 1 < static_cast<int>(vname.size())) {
+ cname = vname[autoindex - 1];
+
+ if (cname.Find(aword) == 0)
+ std::cout << cname << "\n";
+ }
+ } else {
+ std::cout << " * " << boost::algorithm::join(vname, " ")
+ << " (" << kv.second->GetShortDescription() << ")"
+ << (kv.second->IsDeprecated() ? " (DEPRECATED)" : "") << std::endl;
+ }
+ }
+
+ if (!autocomplete)
+ std::cout << std::endl;
+
+ if (command && autocomplete) {
+ String aname, prefix, pword;
+ const po::option_description *odesc;
+
+ if (autoindex - 2 >= 0 && strcmp(argv[autoindex - 1], "=") == 0 && strstr(argv[autoindex - 2], "--") == argv[autoindex - 2]) {
+ aname = argv[autoindex - 2] + 2;
+ pword = aword;
+ } else if (autoindex - 1 >= 0 && argv[autoindex - 1][0] == '-' && argv[autoindex - 1][1] == '-') {
+ aname = argv[autoindex - 1] + 2;
+ pword = aword;
+
+ if (pword == "=")
+ pword = "";
+ } else if (autoindex - 1 >= 0 && argv[autoindex - 1][0] == '-' && argv[autoindex - 1][1] != '-') {
+ aname = argv[autoindex - 1];
+ pword = aword;
+
+ if (pword == "=")
+ pword = "";
+ } else if (aword.GetLength() > 1 && aword[0] == '-' && aword[1] != '-') {
+ aname = aword.SubStr(0, 2);
+ prefix = aname;
+ pword = aword.SubStr(2);
+ } else {
+ goto complete_option;
+ }
+
+ odesc = visibleDesc->find_nothrow(aname, false);
+
+ if (!odesc)
+ return;
+
+ if (odesc->semantic()->min_tokens() == 0)
+ goto complete_option;
+
+ for (const String& suggestion : globalArgCompletionCallback(odesc->long_name(), pword)) {
+ std::cout << prefix << suggestion << "\n";
+ }
+
+ for (const String& suggestion : command->GetArgumentSuggestions(odesc->long_name(), pword)) {
+ std::cout << prefix << suggestion << "\n";
+ }
+
+ return;
+
+complete_option:
+ for (const boost::shared_ptr<po::option_description>& odesc : visibleDesc->options()) {
+ String cname = "--" + odesc->long_name();
+
+ if (cname.Find(aword) == 0)
+ std::cout << cname << "\n";
+ }
+
+ for (const String& suggestion : command->GetPositionalSuggestions(aword)) {
+ std::cout << suggestion << "\n";
+ }
+ }
+
+ return;
+}
diff --git a/lib/cli/clicommand.hpp b/lib/cli/clicommand.hpp
new file mode 100644
index 0000000..ce58b54
--- /dev/null
+++ b/lib/cli/clicommand.hpp
@@ -0,0 +1,79 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CLICOMMAND_H
+#define CLICOMMAND_H
+
+#include "cli/i2-cli.hpp"
+#include "base/value.hpp"
+#include "base/utility.hpp"
+#include "base/type.hpp"
+#include <vector>
+#include <boost/program_options.hpp>
+
+namespace icinga
+{
+
+std::vector<String> GetBashCompletionSuggestions(const String& type, const String& word);
+std::vector<String> GetFieldCompletionSuggestions(const Type::Ptr& type, const String& word);
+
+enum ImpersonationLevel
+{
+ ImpersonateNone,
+ ImpersonateRoot,
+ ImpersonateIcinga
+};
+
+/**
+ * A CLI command.
+ *
+ * @ingroup base
+ */
+class CLICommand : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CLICommand);
+
+ typedef std::vector<String>(*ArgumentCompletionCallback)(const String&, const String&);
+
+ virtual String GetDescription() const = 0;
+ virtual String GetShortDescription() const = 0;
+ virtual int GetMinArguments() const;
+ virtual int GetMaxArguments() const;
+ virtual bool IsHidden() const;
+ virtual bool IsDeprecated() const;
+ virtual void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const;
+ virtual ImpersonationLevel GetImpersonationLevel() const;
+ virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const = 0;
+ virtual std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const;
+ virtual std::vector<String> GetPositionalSuggestions(const String& word) const;
+
+ static CLICommand::Ptr GetByName(const std::vector<String>& name);
+ static void Register(const std::vector<String>& name, const CLICommand::Ptr& command);
+ static void Unregister(const std::vector<String>& name);
+
+ static bool ParseCommand(int argc, char **argv, boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc,
+ boost::program_options::positional_options_description& positionalDesc,
+ boost::program_options::variables_map& vm, String& cmdname, CLICommand::Ptr& command, bool autocomplete);
+
+ static void ShowCommands(int argc, char **argv,
+ boost::program_options::options_description *visibleDesc = nullptr,
+ boost::program_options::options_description *hiddenDesc = nullptr,
+ ArgumentCompletionCallback globalArgCompletionCallback = nullptr,
+ bool autocomplete = false, int autoindex = -1);
+
+private:
+ static std::mutex& GetRegistryMutex();
+ static std::map<std::vector<String>, CLICommand::Ptr>& GetRegistry();
+};
+
+#define REGISTER_CLICOMMAND(name, klass) \
+ INITIALIZE_ONCE([]() { \
+ std::vector<String> vname = String(name).Split("/"); \
+ CLICommand::Register(vname, new klass()); \
+ })
+
+}
+
+#endif /* CLICOMMAND_H */
diff --git a/lib/cli/consolecommand.cpp b/lib/cli/consolecommand.cpp
new file mode 100644
index 0000000..78906bb
--- /dev/null
+++ b/lib/cli/consolecommand.cpp
@@ -0,0 +1,723 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/consolecommand.hpp"
+#include "config/configcompiler.hpp"
+#include "remote/consolehandler.hpp"
+#include "remote/url.hpp"
+#include "base/configwriter.hpp"
+#include "base/serializer.hpp"
+#include "base/json.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include "base/unixsocket.hpp"
+#include "base/utility.hpp"
+#include "base/networkstream.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/stream.hpp"
+#include "base/tcpsocket.hpp" /* include global icinga::Connect */
+#include <base/base64.hpp>
+#include "base/exception.hpp"
+#include <boost/asio/ssl/context.hpp>
+#include <boost/beast/core/flat_buffer.hpp>
+#include <boost/beast/http/field.hpp>
+#include <boost/beast/http/message.hpp>
+#include <boost/beast/http/parser.hpp>
+#include <boost/beast/http/read.hpp>
+#include <boost/beast/http/status.hpp>
+#include <boost/beast/http/string_body.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <boost/beast/http/write.hpp>
+#include <iostream>
+#include <fstream>
+
+
+#ifdef HAVE_EDITLINE
+#include "cli/editline.hpp"
+#endif /* HAVE_EDITLINE */
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+static ScriptFrame *l_ScriptFrame;
+static Url::Ptr l_Url;
+static Shared<AsioTlsStream>::Ptr l_TlsStream;
+static String l_Session;
+
+REGISTER_CLICOMMAND("console", ConsoleCommand);
+
+INITIALIZE_ONCE(&ConsoleCommand::StaticInitialize);
+
+extern "C" void dbg_spawn_console()
+{
+ ScriptFrame frame(true);
+ ConsoleCommand::RunScriptConsole(frame);
+}
+
+extern "C" void dbg_inspect_value(const Value& value)
+{
+ ConfigWriter::EmitValue(std::cout, 1, Serialize(value, 0));
+ std::cout << std::endl;
+}
+
+extern "C" void dbg_inspect_object(Object *obj)
+{
+ Object::Ptr objr = obj;
+ dbg_inspect_value(objr);
+}
+
+extern "C" void dbg_eval(const char *text)
+{
+ std::unique_ptr<Expression> expr;
+
+ try {
+ ScriptFrame frame(true);
+ expr = ConfigCompiler::CompileText("<dbg>", text);
+ Value result = Serialize(expr->Evaluate(frame), 0);
+ dbg_inspect_value(result);
+ } catch (const std::exception& ex) {
+ std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
+ }
+}
+
+extern "C" void dbg_eval_with_value(const Value& value, const char *text)
+{
+ std::unique_ptr<Expression> expr;
+
+ try {
+ ScriptFrame frame(true);
+ frame.Locals = new Dictionary({
+ { "arg", value }
+ });
+ expr = ConfigCompiler::CompileText("<dbg>", text);
+ Value result = Serialize(expr->Evaluate(frame), 0);
+ dbg_inspect_value(result);
+ } catch (const std::exception& ex) {
+ std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
+ }
+}
+
+extern "C" void dbg_eval_with_object(Object *object, const char *text)
+{
+ std::unique_ptr<Expression> expr;
+
+ try {
+ ScriptFrame frame(true);
+ frame.Locals = new Dictionary({
+ { "arg", object }
+ });
+ expr = ConfigCompiler::CompileText("<dbg>", text);
+ Value result = Serialize(expr->Evaluate(frame), 0);
+ dbg_inspect_value(result);
+ } catch (const std::exception& ex) {
+ std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
+ }
+}
+
+void ConsoleCommand::BreakpointHandler(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di)
+{
+ static std::mutex mutex;
+ std::unique_lock<std::mutex> lock(mutex);
+
+ if (!Application::GetScriptDebuggerEnabled())
+ return;
+
+ if (ex && ex->IsHandledByDebugger())
+ return;
+
+ std::cout << "Breakpoint encountered.\n";
+
+ if (ex) {
+ std::cout << "Exception: " << DiagnosticInformation(*ex) << "\n";
+ ex->SetHandledByDebugger(true);
+ } else
+ ShowCodeLocation(std::cout, di);
+
+ std::cout << "You can inspect expressions (such as variables) by entering them at the prompt.\n"
+ << "To leave the debugger and continue the program use \"$continue\".\n"
+ << "For further commands see \"$help\".\n";
+
+#ifdef HAVE_EDITLINE
+ rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
+ rl_completion_append_character = '\0';
+#endif /* HAVE_EDITLINE */
+
+ ConsoleCommand::RunScriptConsole(frame);
+}
+
+void ConsoleCommand::StaticInitialize()
+{
+ Expression::OnBreakpoint.connect(&ConsoleCommand::BreakpointHandler);
+}
+
+String ConsoleCommand::GetDescription() const
+{
+ return "Interprets Icinga script expressions.";
+}
+
+String ConsoleCommand::GetShortDescription() const
+{
+ return "Icinga console";
+}
+
+ImpersonationLevel ConsoleCommand::GetImpersonationLevel() const
+{
+ return ImpersonateNone;
+}
+
+void ConsoleCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("connect,c", po::value<std::string>(), "connect to an Icinga 2 instance")
+ ("eval,e", po::value<std::string>(), "evaluate expression and terminate")
+ ("file,r", po::value<std::string>(), "evaluate a file and terminate")
+ ("syntax-only", "only validate syntax (requires --eval or --file)")
+ ("sandbox", "enable sandbox mode")
+ ;
+}
+
+#ifdef HAVE_EDITLINE
+char *ConsoleCommand::ConsoleCompleteHelper(const char *word, int state)
+{
+ static std::vector<String> matches;
+
+ if (state == 0) {
+ if (!l_Url)
+ matches = ConsoleHandler::GetAutocompletionSuggestions(word, *l_ScriptFrame);
+ else {
+ Array::Ptr suggestions;
+
+ /* Remote debug console. */
+ try {
+ suggestions = AutoCompleteScript(l_Session, word, l_ScriptFrame->Sandboxed);
+ } catch (...) {
+ return nullptr; //Errors are just ignored here.
+ }
+
+ matches.clear();
+
+ ObjectLock olock(suggestions);
+ std::copy(suggestions->Begin(), suggestions->End(), std::back_inserter(matches));
+ }
+ }
+
+ if (state >= static_cast<int>(matches.size()))
+ return nullptr;
+
+ return strdup(matches[state].CStr());
+}
+#endif /* HAVE_EDITLINE */
+
+/**
+ * The entry point for the "console" CLI command.
+ *
+ * @returns An exit status.
+ */
+int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::string>& ap) const
+{
+#ifdef HAVE_EDITLINE
+ rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
+ rl_completion_append_character = '\0';
+#endif /* HAVE_EDITLINE */
+
+ String addr, session;
+ ScriptFrame scriptFrame(true);
+
+ session = Utility::NewUniqueID();
+
+ if (vm.count("sandbox"))
+ scriptFrame.Sandboxed = true;
+
+ scriptFrame.Self = scriptFrame.Locals;
+
+ if (!vm.count("eval") && !vm.count("file"))
+ std::cout << "Icinga 2 (version: " << Application::GetAppVersion() << ")\n"
+ << "Type $help to view available commands.\n";
+
+ String addrEnv = Utility::GetFromEnvironment("ICINGA2_API_URL");
+ if (!addrEnv.IsEmpty())
+ addr = addrEnv;
+
+ /* Initialize remote connect parameters. */
+ if (vm.count("connect")) {
+ addr = vm["connect"].as<std::string>();
+
+ try {
+ l_Url = new Url(addr);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ConsoleCommand", ex.what());
+ return EXIT_FAILURE;
+ }
+
+ String usernameEnv = Utility::GetFromEnvironment("ICINGA2_API_USERNAME");
+ String passwordEnv = Utility::GetFromEnvironment("ICINGA2_API_PASSWORD");
+
+ if (!usernameEnv.IsEmpty())
+ l_Url->SetUsername(usernameEnv);
+ if (!passwordEnv.IsEmpty())
+ l_Url->SetPassword(passwordEnv);
+
+ if (l_Url->GetPort().IsEmpty())
+ l_Url->SetPort("5665");
+
+ /* User passed --connect and wants to run the expression via REST API.
+ * Evaluate this now before any user input happens.
+ */
+ try {
+ l_TlsStream = ConsoleCommand::Connect();
+ } catch (const std::exception& ex) {
+ return EXIT_FAILURE;
+ }
+ }
+
+ String command;
+ bool syntaxOnly = false;
+
+ if (vm.count("syntax-only")) {
+ if (vm.count("eval") || vm.count("file"))
+ syntaxOnly = true;
+ else {
+ std::cerr << "The option --syntax-only can only be used in combination with --eval or --file." << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+
+ String commandFileName;
+
+ if (vm.count("eval"))
+ command = vm["eval"].as<std::string>();
+ else if (vm.count("file")) {
+ commandFileName = vm["file"].as<std::string>();
+
+ try {
+ std::ifstream fp(commandFileName.CStr());
+ fp.exceptions(std::ifstream::failbit | std::ifstream::badbit);
+ command = String(std::istreambuf_iterator<char>(fp), std::istreambuf_iterator<char>());
+ } catch (const std::exception&) {
+ std::cerr << "Could not read file '" << commandFileName << "'." << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+
+ return RunScriptConsole(scriptFrame, addr, session, command, commandFileName, syntaxOnly);
+}
+
+int ConsoleCommand::RunScriptConsole(ScriptFrame& scriptFrame, const String& connectAddr, const String& session,
+ const String& commandOnce, const String& commandOnceFileName, bool syntaxOnly)
+{
+ std::map<String, String> lines;
+ int next_line = 1;
+
+#ifdef HAVE_EDITLINE
+ String homeEnv = Utility::GetFromEnvironment("HOME");
+
+ String historyPath;
+ std::fstream historyfp;
+
+ if (!homeEnv.IsEmpty()) {
+ historyPath = String(homeEnv) + "/.icinga2_history";
+
+ historyfp.open(historyPath.CStr(), std::fstream::in);
+
+ String line;
+ while (std::getline(historyfp, line.GetData()))
+ add_history(line.CStr());
+
+ historyfp.close();
+ }
+#endif /* HAVE_EDITLINE */
+
+ l_ScriptFrame = &scriptFrame;
+ l_Session = session;
+
+ while (std::cin.good()) {
+ String fileName;
+
+ if (commandOnceFileName.IsEmpty())
+ fileName = "<" + Convert::ToString(next_line) + ">";
+ else
+ fileName = commandOnceFileName;
+
+ next_line++;
+
+ bool continuation = false;
+ std::string command;
+
+incomplete:
+ std::string line;
+
+ if (commandOnce.IsEmpty()) {
+#ifdef HAVE_EDITLINE
+ std::ostringstream promptbuf;
+ std::ostream& os = promptbuf;
+#else /* HAVE_EDITLINE */
+ std::ostream& os = std::cout;
+#endif /* HAVE_EDITLINE */
+
+ os << fileName;
+
+ if (!continuation)
+ os << " => ";
+ else
+ os << " .. ";
+
+#ifdef HAVE_EDITLINE
+ String prompt = promptbuf.str();
+
+ char *cline;
+ cline = readline(prompt.CStr());
+
+ if (!cline)
+ break;
+
+ if (commandOnce.IsEmpty() && cline[0] != '\0') {
+ add_history(cline);
+
+ if (!historyPath.IsEmpty()) {
+ historyfp.open(historyPath.CStr(), std::fstream::out | std::fstream::app);
+ historyfp << cline << "\n";
+ historyfp.close();
+ }
+ }
+
+ line = cline;
+
+ free(cline);
+#else /* HAVE_EDITLINE */
+ std::getline(std::cin, line);
+#endif /* HAVE_EDITLINE */
+ } else
+ line = commandOnce;
+
+ if (!line.empty() && line[0] == '$') {
+ if (line == "$continue" || line == "$quit" || line == "$exit")
+ break;
+ else if (line == "$help")
+ std::cout << "Welcome to the Icinga 2 debug console.\n"
+ "Usable commands:\n"
+ " $continue Continue running Icinga 2 (script debugger).\n"
+ " $quit, $exit Stop debugging and quit the console.\n"
+ " $help Print this help.\n\n"
+ "For more information on how to use this console, please consult the documentation at https://icinga.com/docs\n";
+ else
+ std::cout << "Unknown debugger command: " << line << "\n";
+
+ continue;
+ }
+
+ if (!command.empty())
+ command += "\n";
+
+ command += line;
+
+ std::unique_ptr<Expression> expr;
+
+ try {
+ lines[fileName] = command;
+
+ Value result;
+
+ /* Local debug console. */
+ if (connectAddr.IsEmpty()) {
+ expr = ConfigCompiler::CompileText(fileName, command);
+
+ /* This relies on the fact that - for syntax errors - CompileText()
+ * returns an AST where the top-level expression is a 'throw'. */
+ if (!syntaxOnly || dynamic_cast<ThrowExpression *>(expr.get())) {
+ if (syntaxOnly)
+ std::cerr << " => " << command << std::endl;
+ result = Serialize(expr->Evaluate(scriptFrame), 0);
+ } else
+ result = true;
+ } else {
+ /* Remote debug console. */
+ try {
+ result = ExecuteScript(l_Session, command, scriptFrame.Sandboxed);
+ } catch (const ScriptError&) {
+ /* Re-throw the exception for the outside try-catch block. */
+ boost::rethrow_exception(boost::current_exception());
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ConsoleCommand")
+ << "HTTP query failed: " << ex.what();
+
+#ifdef HAVE_EDITLINE
+ /* Ensures that the terminal state is reset */
+ rl_deprep_terminal();
+#endif /* HAVE_EDITLINE */
+
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (commandOnce.IsEmpty()) {
+ std::cout << ConsoleColorTag(Console_ForegroundCyan);
+ ConfigWriter::EmitValue(std::cout, 1, result);
+ std::cout << ConsoleColorTag(Console_Normal) << "\n";
+ } else {
+ std::cout << JsonEncode(result) << "\n";
+ break;
+ }
+ } catch (const ScriptError& ex) {
+ if (ex.IsIncompleteExpression() && commandOnce.IsEmpty()) {
+ continuation = true;
+ goto incomplete;
+ }
+
+ DebugInfo di = ex.GetDebugInfo();
+
+ if (commandOnceFileName.IsEmpty() && lines.find(di.Path) != lines.end()) {
+ String text = lines[di.Path];
+
+ std::vector<String> ulines = text.Split("\n");
+
+ for (decltype(ulines.size()) i = 1; i <= ulines.size(); i++) {
+ int start, len;
+
+ if (i == (decltype(i))di.FirstLine)
+ start = di.FirstColumn;
+ else
+ start = 0;
+
+ if (i == (decltype(i))di.LastLine)
+ len = di.LastColumn - di.FirstColumn + 1;
+ else
+ len = ulines[i - 1].GetLength();
+
+ int offset;
+
+ if (di.Path != fileName) {
+ std::cout << di.Path << ": " << ulines[i - 1] << "\n";
+ offset = 2;
+ } else
+ offset = 4;
+
+ if (i >= (decltype(i))di.FirstLine && i <= (decltype(i))di.LastLine) {
+ std::cout << String(di.Path.GetLength() + offset, ' ');
+ std::cout << String(start, ' ') << String(len, '^') << "\n";
+ }
+ }
+ } else {
+ ShowCodeLocation(std::cout, di);
+ }
+
+ std::cout << ex.what() << "\n";
+
+ if (!commandOnce.IsEmpty())
+ return EXIT_FAILURE;
+ } catch (const std::exception& ex) {
+ std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
+
+ if (!commandOnce.IsEmpty())
+ return EXIT_FAILURE;
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
+
+/**
+ * Connects to host:port and performs a TLS shandshake
+ *
+ * @returns AsioTlsStream pointer for future HTTP connections.
+ */
+Shared<AsioTlsStream>::Ptr ConsoleCommand::Connect()
+{
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext(Empty, Empty, Empty); //TODO: Add support for cert, key, ca parameters
+ } catch(const std::exception& ex) {
+ Log(LogCritical, "DebugConsole")
+ << "Cannot make SSL context: " << ex.what();
+ throw;
+ }
+
+ String host = l_Url->GetHost();
+ String port = l_Url->GetPort();
+
+ Shared<AsioTlsStream>::Ptr stream = Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, host);
+
+ try {
+ icinga::Connect(stream->lowest_layer(), host, port);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "DebugConsole")
+ << "Cannot connect to REST API on host '" << host << "' port '" << port << "': " << ex.what();
+ throw;
+ }
+
+ auto& tlsStream (stream->next_layer());
+
+ try {
+ tlsStream.handshake(tlsStream.client);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "DebugConsole")
+ << "TLS handshake with host '" << host << "' failed: " << ex.what();
+ throw;
+ }
+
+ return stream;
+}
+
+/**
+ * Sends the request via REST API and returns the parsed response.
+ *
+ * @param tlsStream Caller must prepare TLS stream/handshake.
+ * @param url Fully prepared Url object.
+ * @return A dictionary decoded from JSON.
+ */
+Dictionary::Ptr ConsoleCommand::SendRequest()
+{
+ namespace beast = boost::beast;
+ namespace http = beast::http;
+
+ l_TlsStream = ConsoleCommand::Connect();
+
+ Defer s ([&]() {
+ l_TlsStream->next_layer().shutdown();
+ });
+
+ http::request<http::string_body> request(http::verb::post, std::string(l_Url->Format(false)), 10);
+
+ request.set(http::field::user_agent, "Icinga/DebugConsole/" + Application::GetAppVersion());
+ request.set(http::field::host, l_Url->GetHost() + ":" + l_Url->GetPort());
+
+ request.set(http::field::accept, "application/json");
+ request.set(http::field::authorization, "Basic " + Base64::Encode(l_Url->GetUsername() + ":" + l_Url->GetPassword()));
+
+ try {
+ http::write(*l_TlsStream, request);
+ l_TlsStream->flush();
+ } catch (const std::exception &ex) {
+ Log(LogWarning, "DebugConsole")
+ << "Cannot write HTTP request to REST API at URL '" << l_Url->Format(true) << "': " << ex.what();
+ throw;
+ }
+
+ http::parser<false, http::string_body> parser;
+ beast::flat_buffer buf;
+
+ try {
+ http::read(*l_TlsStream, buf, parser);
+ } catch (const std::exception &ex) {
+ Log(LogWarning, "DebugConsole")
+ << "Failed to parse HTTP response from REST API at URL '" << l_Url->Format(true) << "': " << ex.what();
+ throw;
+ }
+
+ auto &response(parser.get());
+
+ /* Handle HTTP errors first. */
+ if (response.result() != http::status::ok) {
+ String message = "HTTP request failed; Code: " + Convert::ToString(response.result())
+ + "; Body: " + response.body();
+ BOOST_THROW_EXCEPTION(ScriptError(message));
+ }
+
+ Dictionary::Ptr jsonResponse;
+ auto &body(response.body());
+
+ //Log(LogWarning, "Console")
+ // << "Got response: " << response.body();
+
+ try {
+ jsonResponse = JsonDecode(body);
+ } catch (...) {
+ String message = "Cannot parse JSON response body: " + response.body();
+ BOOST_THROW_EXCEPTION(ScriptError(message));
+ }
+
+ return jsonResponse;
+}
+
+/**
+ * Executes the DSL script via HTTP and returns HTTP and user errors.
+ *
+ * @param session Local session handler.
+ * @param command The DSL string.
+ * @param sandboxed Whether to run this sandboxed.
+ * @return Result value, also contains user errors.
+ */
+Value ConsoleCommand::ExecuteScript(const String& session, const String& command, bool sandboxed)
+{
+ /* Extend the url parameters for the request. */
+ l_Url->SetPath({"v1", "console", "execute-script"});
+
+ l_Url->SetQuery({
+ {"session", session},
+ {"command", command},
+ {"sandboxed", sandboxed ? "1" : "0"}
+ });
+
+ Dictionary::Ptr jsonResponse = SendRequest();
+
+ /* Extract the result, and handle user input errors too. */
+ Array::Ptr results = jsonResponse->Get("results");
+ Value result;
+
+ if (results && results->GetLength() > 0) {
+ Dictionary::Ptr resultInfo = results->Get(0);
+
+ if (resultInfo->Get("code") >= 200 && resultInfo->Get("code") <= 299) {
+ result = resultInfo->Get("result");
+ } else {
+ String errorMessage = resultInfo->Get("status");
+
+ DebugInfo di;
+ Dictionary::Ptr debugInfo = resultInfo->Get("debug_info");
+
+ if (debugInfo) {
+ di.Path = debugInfo->Get("path");
+ di.FirstLine = debugInfo->Get("first_line");
+ di.FirstColumn = debugInfo->Get("first_column");
+ di.LastLine = debugInfo->Get("last_line");
+ di.LastColumn = debugInfo->Get("last_column");
+ }
+
+ bool incompleteExpression = resultInfo->Get("incomplete_expression");
+ BOOST_THROW_EXCEPTION(ScriptError(errorMessage, di, incompleteExpression));
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Executes the auto completion script via HTTP and returns HTTP and user errors.
+ *
+ * @param session Local session handler.
+ * @param command The auto completion string.
+ * @param sandboxed Whether to run this sandboxed.
+ * @return Result value, also contains user errors.
+ */
+Array::Ptr ConsoleCommand::AutoCompleteScript(const String& session, const String& command, bool sandboxed)
+{
+ /* Extend the url parameters for the request. */
+ l_Url->SetPath({ "v1", "console", "auto-complete-script" });
+
+ l_Url->SetQuery({
+ {"session", session},
+ {"command", command},
+ {"sandboxed", sandboxed ? "1" : "0"}
+ });
+
+ Dictionary::Ptr jsonResponse = SendRequest();
+
+ /* Extract the result, and handle user input errors too. */
+ Array::Ptr results = jsonResponse->Get("results");
+ Array::Ptr suggestions;
+
+ if (results && results->GetLength() > 0) {
+ Dictionary::Ptr resultInfo = results->Get(0);
+
+ if (resultInfo->Get("code") >= 200 && resultInfo->Get("code") <= 299) {
+ suggestions = resultInfo->Get("suggestions");
+ } else {
+ String errorMessage = resultInfo->Get("status");
+ BOOST_THROW_EXCEPTION(ScriptError(errorMessage));
+ }
+ }
+
+ return suggestions;
+}
diff --git a/lib/cli/consolecommand.hpp b/lib/cli/consolecommand.hpp
new file mode 100644
index 0000000..631ec21
--- /dev/null
+++ b/lib/cli/consolecommand.hpp
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONSOLECOMMAND_H
+#define CONSOLECOMMAND_H
+
+#include "cli/clicommand.hpp"
+#include "base/exception.hpp"
+#include "base/scriptframe.hpp"
+#include "base/tlsstream.hpp"
+#include "remote/url.hpp"
+#include <condition_variable>
+
+
+namespace icinga
+{
+
+/**
+ * The "console" CLI command.
+ *
+ * @ingroup cli
+ */
+class ConsoleCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConsoleCommand);
+
+ static void StaticInitialize();
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+ static int RunScriptConsole(ScriptFrame& scriptFrame, const String& connectAddr = String(),
+ const String& session = String(), const String& commandOnce = String(), const String& commandOnceFileName = String(),
+ bool syntaxOnly = false);
+
+private:
+ mutable std::mutex m_Mutex;
+ mutable std::condition_variable m_CV;
+
+ static Shared<AsioTlsStream>::Ptr Connect();
+
+ static Value ExecuteScript(const String& session, const String& command, bool sandboxed);
+ static Array::Ptr AutoCompleteScript(const String& session, const String& command, bool sandboxed);
+
+ static Dictionary::Ptr SendRequest();
+
+#ifdef HAVE_EDITLINE
+ static char *ConsoleCompleteHelper(const char *word, int state);
+#endif /* HAVE_EDITLINE */
+
+ static void BreakpointHandler(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di);
+
+};
+
+}
+
+#endif /* CONSOLECOMMAND_H */
diff --git a/lib/cli/daemoncommand.cpp b/lib/cli/daemoncommand.cpp
new file mode 100644
index 0000000..3a9ce8c
--- /dev/null
+++ b/lib/cli/daemoncommand.cpp
@@ -0,0 +1,882 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/daemoncommand.hpp"
+#include "cli/daemonutility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/configobjectutility.hpp"
+#include "config/configcompiler.hpp"
+#include "config/configcompilercontext.hpp"
+#include "config/configitembuilder.hpp"
+#include "base/atomic.hpp"
+#include "base/defer.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/process.hpp"
+#include "base/timer.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/context.hpp"
+#include "config.h"
+#include <cstdint>
+#include <cstring>
+#include <boost/program_options.hpp>
+#include <iostream>
+#include <fstream>
+
+#ifdef _WIN32
+#include <windows.h>
+#else /* _WIN32 */
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#endif /* _WIN32 */
+
+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-daemon.h>
+#endif /* HAVE_SYSTEMD */
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+static po::variables_map g_AppParams;
+
+REGISTER_CLICOMMAND("daemon", DaemonCommand);
+
+static inline
+void NotifyStatus(const char* status)
+{
+#ifdef HAVE_SYSTEMD
+ (void)sd_notifyf(0, "STATUS=%s", status);
+#endif /* HAVE_SYSTEMD */
+}
+
+/*
+ * Daemonize(). On error, this function logs by itself and exits (i.e. does not return).
+ *
+ * Implementation note: We're only supposed to call exit() in one of the forked processes.
+ * The other process calls _exit(). This prevents issues with exit handlers like atexit().
+ */
+static void Daemonize() noexcept
+{
+#ifndef _WIN32
+ try {
+ Application::UninitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to stop thread pool before daemonizing, unexpected error: " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+
+ pid_t pid = fork();
+ if (pid == -1) {
+ Log(LogCritical, "cli")
+ << "fork() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ exit(EXIT_FAILURE);
+ }
+
+ if (pid) {
+ // systemd requires that the pidfile of the daemon is written before the forking
+ // process terminates. So wait till either the forked daemon has written a pidfile or died.
+
+ int status;
+ int ret;
+ pid_t readpid;
+ do {
+ Utility::Sleep(0.1);
+
+ readpid = Application::ReadPidFile(Configuration::PidPath);
+ ret = waitpid(pid, &status, WNOHANG);
+ } while (readpid != pid && ret == 0);
+
+ if (ret == pid) {
+ Log(LogCritical, "cli", "The daemon could not be started. See log output for details.");
+ _exit(EXIT_FAILURE);
+ } else if (ret == -1) {
+ Log(LogCritical, "cli")
+ << "waitpid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ _exit(EXIT_FAILURE);
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ Log(LogDebug, "Daemonize()")
+ << "Child process with PID " << Utility::GetPid() << " continues; re-initializing base.";
+
+ // Detach from controlling terminal
+ pid_t sid = setsid();
+ if (sid == -1) {
+ Log(LogCritical, "cli")
+ << "setsid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ exit(EXIT_FAILURE);
+ }
+
+ try {
+ Application::InitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to re-initialize thread pool after daemonizing: " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+#endif /* _WIN32 */
+}
+
+static void CloseStdIO(const String& stderrFile)
+{
+#ifndef _WIN32
+ int fdnull = open("/dev/null", O_RDWR);
+ if (fdnull >= 0) {
+ if (fdnull != 0)
+ dup2(fdnull, 0);
+
+ if (fdnull != 1)
+ dup2(fdnull, 1);
+
+ if (fdnull > 1)
+ close(fdnull);
+ }
+
+ const char *errPath = "/dev/null";
+
+ if (!stderrFile.IsEmpty())
+ errPath = stderrFile.CStr();
+
+ int fderr = open(errPath, O_WRONLY | O_APPEND);
+
+ if (fderr < 0 && errno == ENOENT)
+ fderr = open(errPath, O_CREAT | O_WRONLY | O_APPEND, 0600);
+
+ if (fderr >= 0) {
+ if (fderr != 2)
+ dup2(fderr, 2);
+
+ if (fderr > 2)
+ close(fderr);
+ }
+#endif
+}
+
+String DaemonCommand::GetDescription() const
+{
+ return "Starts Icinga 2.";
+}
+
+String DaemonCommand::GetShortDescription() const
+{
+ return "starts Icinga 2";
+}
+
+void DaemonCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("config,c", po::value<std::vector<std::string> >(), "parse a configuration file")
+ ("no-config,z", "start without a configuration file")
+ ("validate,C", "exit after validating the configuration")
+ ("dump-objects", "write icinga2.debug cache file for icinga2 object list")
+ ("errorlog,e", po::value<std::string>(), "log fatal errors to the specified log file (only works in combination with --daemonize or --close-stdio)")
+#ifndef _WIN32
+ ("daemonize,d", "detach from the controlling terminal")
+ ("close-stdio", "do not log to stdout (or stderr) after startup")
+#endif /* _WIN32 */
+ ;
+}
+
+std::vector<String> DaemonCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "config" || argument == "errorlog")
+ return GetBashCompletionSuggestions("file", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+#ifndef _WIN32
+// The PID of the Icinga umbrella process
+pid_t l_UmbrellaPid = 0;
+
+// Whether the umbrella process allowed us to continue working beyond config validation
+static Atomic<bool> l_AllowedToWork (false);
+#endif /* _WIN32 */
+
+#ifdef I2_DEBUG
+/**
+ * Determine whether the developer wants to delay the worker process to attach a debugger to it.
+ *
+ * @return Internal.DebugWorkerDelay double
+ */
+static double GetDebugWorkerDelay()
+{
+ Namespace::Ptr internal = ScriptGlobal::Get("Internal", &Empty);
+
+ Value vdebug;
+ if (internal && internal->Get("DebugWorkerDelay", &vdebug))
+ return Convert::ToDouble(vdebug);
+
+ return 0.0;
+}
+#endif /* I2_DEBUG */
+
+static String l_ObjectsPath;
+
+/**
+ * Do the actual work (config loading, ...)
+ *
+ * @param configs Files to read config from
+ * @param closeConsoleLog Whether to close the console log after config loading
+ * @param stderrFile Where to log errors
+ *
+ * @return Exit code
+ */
+static inline
+int RunWorker(const std::vector<std::string>& configs, bool closeConsoleLog = false, const String& stderrFile = String())
+{
+
+#ifdef I2_DEBUG
+ double delay = GetDebugWorkerDelay();
+
+ if (delay > 0.0) {
+ Log(LogInformation, "RunWorker")
+ << "DEBUG: Current PID: " << Utility::GetPid() << ". Sleeping for " << delay << " seconds to allow lldb/gdb -p <PID> attachment.";
+
+ Utility::Sleep(delay);
+ }
+#endif /* I2_DEBUG */
+
+ Log(LogInformation, "cli", "Loading configuration file(s).");
+ NotifyStatus("Loading configuration file(s)...");
+
+ {
+ std::vector<ConfigItem::Ptr> newItems;
+
+ if (!DaemonUtility::LoadConfigFiles(configs, newItems, l_ObjectsPath, Configuration::VarsPath)) {
+ Log(LogCritical, "cli", "Config validation failed. Re-run with 'icinga2 daemon -C' after fixing the config.");
+ NotifyStatus("Config validation failed.");
+ return EXIT_FAILURE;
+ }
+
+#ifndef _WIN32
+ Log(LogNotice, "cli")
+ << "Notifying umbrella process (PID " << l_UmbrellaPid << ") about the config loading success";
+
+ (void)kill(l_UmbrellaPid, SIGUSR2);
+
+ Log(LogNotice, "cli")
+ << "Waiting for the umbrella process to let us doing the actual work";
+
+ NotifyStatus("Waiting for the umbrella process to let us doing the actual work...");
+
+ if (closeConsoleLog) {
+ CloseStdIO(stderrFile);
+ Logger::DisableConsoleLog();
+ }
+
+ while (!l_AllowedToWork.load()) {
+ Utility::Sleep(0.2);
+ }
+
+ Log(LogNotice, "cli")
+ << "The umbrella process let us continuing";
+#endif /* _WIN32 */
+
+ NotifyStatus("Restoring the previous program state...");
+
+ /* restore the previous program state */
+ try {
+ ConfigObject::RestoreObjects(Configuration::StatePath);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to restore state file: " << DiagnosticInformation(ex);
+
+ NotifyStatus("Failed to restore state file.");
+
+ return EXIT_FAILURE;
+ }
+
+ NotifyStatus("Activating config objects...");
+
+ // activate config only after daemonization: it starts threads and that is not compatible with fork()
+ if (!ConfigItem::ActivateItems(newItems, false, true, true)) {
+ Log(LogCritical, "cli", "Error activating configuration.");
+
+ NotifyStatus("Error activating configuration.");
+
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Create the internal API object storage. Do this here too with setups without API. */
+ ConfigObjectUtility::CreateStorage();
+
+ /* Remove ignored Downtime/Comment objects. */
+ try {
+ String configDir = ConfigObjectUtility::GetConfigDir();
+ ConfigItem::RemoveIgnoredItems(configDir);
+ } catch (const std::exception& ex) {
+ Log(LogNotice, "cli")
+ << "Cannot clean ignored downtimes/comments: " << ex.what();
+ }
+
+ ApiListener::UpdateObjectAuthority();
+
+ NotifyStatus("Startup finished.");
+
+ return Application::GetInstance()->Run();
+}
+
+#ifndef _WIN32
+// The signals to block temporarily in StartUnixWorker().
+static const sigset_t l_UnixWorkerSignals = ([]() -> sigset_t {
+ sigset_t s;
+
+ (void)sigemptyset(&s);
+ (void)sigaddset(&s, SIGUSR1);
+ (void)sigaddset(&s, SIGUSR2);
+ (void)sigaddset(&s, SIGINT);
+ (void)sigaddset(&s, SIGTERM);
+ (void)sigaddset(&s, SIGHUP);
+
+ return s;
+})();
+
+// The PID of the seamless worker currently being started by StartUnixWorker()
+static Atomic<pid_t> l_CurrentlyStartingUnixWorkerPid (-1);
+
+// The state of the seamless worker currently being started by StartUnixWorker()
+static Atomic<bool> l_CurrentlyStartingUnixWorkerReady (false);
+
+// The last temination signal we received
+static Atomic<int> l_TermSignal (-1);
+
+// Whether someone requested to re-load config (and we didn't handle that request, yet)
+static Atomic<bool> l_RequestedReload (false);
+
+// Whether someone requested to re-open logs (and we didn't handle that request, yet)
+static Atomic<bool> l_RequestedReopenLogs (false);
+
+/**
+ * Umbrella process' signal handlers
+ */
+static void UmbrellaSignalHandler(int num, siginfo_t *info, void*)
+{
+ switch (num) {
+ case SIGUSR1:
+ // Someone requested to re-open logs
+ l_RequestedReopenLogs.store(true);
+ break;
+ case SIGUSR2:
+ if (!l_CurrentlyStartingUnixWorkerReady.load()
+ && (info->si_pid == 0 || info->si_pid == l_CurrentlyStartingUnixWorkerPid.load()) ) {
+ // The seamless worker currently being started by StartUnixWorker() successfully loaded its config
+ l_CurrentlyStartingUnixWorkerReady.store(true);
+ }
+ break;
+ case SIGINT:
+ case SIGTERM:
+ // Someone requested our termination
+
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_handler = SIG_DFL;
+
+ (void)sigaction(num, &sa, nullptr);
+ }
+
+ l_TermSignal.store(num);
+ break;
+ case SIGHUP:
+ // Someone requested to re-load config
+ l_RequestedReload.store(true);
+ break;
+ default:
+ // Programming error (or someone has broken the userspace)
+ VERIFY(!"Caught unexpected signal");
+ }
+}
+
+/**
+ * Seamless worker's signal handlers
+ */
+static void WorkerSignalHandler(int num, siginfo_t *info, void*)
+{
+ switch (num) {
+ case SIGUSR1:
+ // Catches SIGUSR1 as long as the actual handler (logrotate)
+ // has not been installed not to let SIGUSR1 terminate the process
+ break;
+ case SIGUSR2:
+ if (info->si_pid == 0 || info->si_pid == l_UmbrellaPid) {
+ // The umbrella process allowed us to continue working beyond config validation
+ l_AllowedToWork.store(true);
+ }
+ break;
+ case SIGINT:
+ case SIGTERM:
+ if (info->si_pid == 0 || info->si_pid == l_UmbrellaPid) {
+ // The umbrella process requested our termination
+ Application::RequestShutdown();
+ }
+ break;
+ default:
+ // Programming error (or someone has broken the userspace)
+ VERIFY(!"Caught unexpected signal");
+ }
+}
+
+#ifdef HAVE_SYSTEMD
+// When we last notified the watchdog.
+static Atomic<double> l_LastNotifiedWatchdog (0);
+
+/**
+ * Notify the watchdog if not notified during the last 2.5s.
+ */
+static void NotifyWatchdog()
+{
+ double now = Utility::GetTime();
+
+ if (now - l_LastNotifiedWatchdog.load() >= 2.5) {
+ sd_notify(0, "WATCHDOG=1");
+ l_LastNotifiedWatchdog.store(now);
+ }
+}
+#endif /* HAVE_SYSTEMD */
+
+/**
+ * Starts seamless worker process doing the actual work (config loading, ...)
+ *
+ * @param configs Files to read config from
+ * @param closeConsoleLog Whether to close the console log after config loading
+ * @param stderrFile Where to log errors
+ *
+ * @return The worker's PID on success, -1 on fork(2) failure, -2 if the worker couldn't load its config
+ */
+static pid_t StartUnixWorker(const std::vector<std::string>& configs, bool closeConsoleLog = false, const String& stderrFile = String())
+{
+ Log(LogNotice, "cli")
+ << "Spawning seamless worker process doing the actual work";
+
+ try {
+ Application::UninitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to stop thread pool before forking, unexpected error: " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Block the signal handlers we'd like to change in the child process until we changed them.
+ * Block SIGUSR2 handler until we've set l_CurrentlyStartingUnixWorkerPid.
+ */
+ (void)sigprocmask(SIG_BLOCK, &l_UnixWorkerSignals, nullptr);
+
+ pid_t pid = fork();
+
+ switch (pid) {
+ case -1:
+ Log(LogCritical, "cli")
+ << "fork() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ try {
+ Application::InitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to re-initialize thread pool after forking (parent): " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+
+ (void)sigprocmask(SIG_UNBLOCK, &l_UnixWorkerSignals, nullptr);
+ return -1;
+
+ case 0:
+ try {
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_handler = SIG_DFL;
+
+ (void)sigaction(SIGUSR1, &sa, nullptr);
+ }
+
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_handler = SIG_IGN;
+
+ (void)sigaction(SIGHUP, &sa, nullptr);
+ }
+
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_sigaction = &WorkerSignalHandler;
+ sa.sa_flags = SA_RESTART | SA_SIGINFO;
+
+ (void)sigaction(SIGUSR1, &sa, nullptr);
+ (void)sigaction(SIGUSR2, &sa, nullptr);
+ (void)sigaction(SIGINT, &sa, nullptr);
+ (void)sigaction(SIGTERM, &sa, nullptr);
+ }
+
+ (void)sigprocmask(SIG_UNBLOCK, &l_UnixWorkerSignals, nullptr);
+
+ try {
+ Application::InitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to re-initialize thread pool after forking (child): " << DiagnosticInformation(ex);
+ _exit(EXIT_FAILURE);
+ }
+
+ try {
+ Process::InitializeSpawnHelper();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to initialize process spawn helper after forking (child): " << DiagnosticInformation(ex);
+ _exit(EXIT_FAILURE);
+ }
+
+ _exit(RunWorker(configs, closeConsoleLog, stderrFile));
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli") << "Exception in main process: " << DiagnosticInformation(ex);
+ _exit(EXIT_FAILURE);
+ } catch (...) {
+ _exit(EXIT_FAILURE);
+ }
+
+ default:
+ l_CurrentlyStartingUnixWorkerPid.store(pid);
+ (void)sigprocmask(SIG_UNBLOCK, &l_UnixWorkerSignals, nullptr);
+
+ Log(LogNotice, "cli")
+ << "Spawned worker process (PID " << pid << "), waiting for it to load its config";
+
+ // Wait for the newly spawned process to either load its config or fail.
+ for (;;) {
+#ifdef HAVE_SYSTEMD
+ NotifyWatchdog();
+#endif /* HAVE_SYSTEMD */
+
+ if (waitpid(pid, nullptr, WNOHANG) > 0) {
+ Log(LogNotice, "cli")
+ << "Worker process couldn't load its config";
+
+ pid = -2;
+ break;
+ }
+
+ if (l_CurrentlyStartingUnixWorkerReady.load()) {
+ Log(LogNotice, "cli")
+ << "Worker process successfully loaded its config";
+ break;
+ }
+
+ Utility::Sleep(0.2);
+ }
+
+ // Reset flags for the next time
+ l_CurrentlyStartingUnixWorkerPid.store(-1);
+ l_CurrentlyStartingUnixWorkerReady.store(false);
+
+ try {
+ Application::InitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to re-initialize thread pool after forking (parent): " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ return pid;
+}
+
+/**
+ * Workaround to instantiate Application (which is abstract) in DaemonCommand#Run()
+ */
+class PidFileManagementApp : public Application
+{
+public:
+ inline int Main() override
+ {
+ return EXIT_FAILURE;
+ }
+};
+#endif /* _WIN32 */
+
+/**
+ * The entry point for the "daemon" CLI command.
+ *
+ * @returns An exit status.
+ */
+int DaemonCommand::Run(const po::variables_map& vm, const std::vector<std::string>& ap) const
+{
+#ifdef _WIN32
+ SetConsoleOutputCP(65001);
+#endif /* _WIN32 */
+
+ Logger::EnableTimestamp();
+
+ Log(LogInformation, "cli")
+ << "Icinga application loader (version: " << Application::GetAppVersion()
+#ifdef I2_DEBUG
+ << "; debug"
+#endif /* I2_DEBUG */
+ << ")";
+
+ std::vector<std::string> configs;
+ if (vm.count("config") > 0)
+ configs = vm["config"].as<std::vector<std::string> >();
+ else if (!vm.count("no-config")) {
+ /* The implicit string assignment is needed for Windows builds. */
+ String configDir = Configuration::ConfigDir;
+ configs.push_back(configDir + "/icinga2.conf");
+ }
+
+ if (vm.count("dump-objects")) {
+ if (!vm.count("validate")) {
+ Log(LogCritical, "cli", "--dump-objects is not allowed without -C");
+ return EXIT_FAILURE;
+ }
+
+ l_ObjectsPath = Configuration::ObjectsPath;
+ }
+
+ if (vm.count("validate")) {
+ Log(LogInformation, "cli", "Loading configuration file(s).");
+
+ std::vector<ConfigItem::Ptr> newItems;
+
+ if (!DaemonUtility::LoadConfigFiles(configs, newItems, l_ObjectsPath, Configuration::VarsPath)) {
+ Log(LogCritical, "cli", "Config validation failed. Re-run with 'icinga2 daemon -C' after fixing the config.");
+ return EXIT_FAILURE;
+ }
+
+ Log(LogInformation, "cli", "Finished validating the configuration file(s).");
+ return EXIT_SUCCESS;
+ }
+
+ {
+ pid_t runningpid = Application::ReadPidFile(Configuration::PidPath);
+ if (runningpid > 0) {
+ Log(LogCritical, "cli")
+ << "Another instance of Icinga already running with PID " << runningpid;
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (vm.count("daemonize")) {
+ // this subroutine either succeeds, or logs an error
+ // and terminates the process (does not return).
+ Daemonize();
+ }
+
+#ifndef _WIN32
+ /* The Application manages the PID file,
+ * but on *nix this process doesn't load any config
+ * so there's no central Application instance.
+ */
+ PidFileManagementApp app;
+
+ try {
+ app.UpdatePidFile(Configuration::PidPath);
+ } catch (const std::exception&) {
+ Log(LogCritical, "Application")
+ << "Cannot update PID file '" << Configuration::PidPath << "'. Aborting.";
+ return EXIT_FAILURE;
+ }
+
+ Defer closePidFile ([&app]() {
+ app.ClosePidFile(true);
+ });
+#endif /* _WIN32 */
+
+ if (vm.count("daemonize")) {
+ // After disabling the console log, any further errors will go to the configured log only.
+ // Let's try to make this clear and say good bye.
+ Log(LogInformation, "cli", "Closing console log.");
+
+ String errorLog;
+ if (vm.count("errorlog"))
+ errorLog = vm["errorlog"].as<std::string>();
+
+ CloseStdIO(errorLog);
+ Logger::DisableConsoleLog();
+ }
+
+#ifdef _WIN32
+ try {
+ return RunWorker(configs);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli") << "Exception in main process: " << DiagnosticInformation(ex);
+ return EXIT_FAILURE;
+ } catch (...) {
+ return EXIT_FAILURE;
+ }
+#else /* _WIN32 */
+ l_UmbrellaPid = getpid();
+ Application::SetUmbrellaProcess(l_UmbrellaPid);
+
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_sigaction = &UmbrellaSignalHandler;
+ sa.sa_flags = SA_NOCLDSTOP | SA_RESTART | SA_SIGINFO;
+
+ (void)sigaction(SIGUSR1, &sa, nullptr);
+ (void)sigaction(SIGUSR2, &sa, nullptr);
+ (void)sigaction(SIGINT, &sa, nullptr);
+ (void)sigaction(SIGTERM, &sa, nullptr);
+ (void)sigaction(SIGHUP, &sa, nullptr);
+ }
+
+ bool closeConsoleLog = !vm.count("daemonize") && vm.count("close-stdio");
+
+ String errorLog;
+ if (vm.count("errorlog"))
+ errorLog = vm["errorlog"].as<std::string>();
+
+ // The PID of the current seamless worker
+ pid_t currentWorker = StartUnixWorker(configs, closeConsoleLog, errorLog);
+
+ if (currentWorker < 0) {
+ return EXIT_FAILURE;
+ }
+
+ if (closeConsoleLog) {
+ // After disabling the console log, any further errors will go to the configured log only.
+ // Let's try to make this clear and say good bye.
+ Log(LogInformation, "cli", "Closing console log.");
+
+ CloseStdIO(errorLog);
+ Logger::DisableConsoleLog();
+ }
+
+ // Immediately allow the first (non-reload) worker to continue working beyond config validation
+ (void)kill(currentWorker, SIGUSR2);
+
+#ifdef HAVE_SYSTEMD
+ sd_notify(0, "READY=1");
+#endif /* HAVE_SYSTEMD */
+
+ // Whether we already forwarded a termination signal to the seamless worker
+ bool requestedTermination = false;
+
+ // Whether we already notified systemd about our termination
+ bool notifiedTermination = false;
+
+ for (;;) {
+#ifdef HAVE_SYSTEMD
+ NotifyWatchdog();
+#endif /* HAVE_SYSTEMD */
+
+ if (!requestedTermination) {
+ int termSig = l_TermSignal.load();
+ if (termSig != -1) {
+ Log(LogNotice, "cli")
+ << "Got signal " << termSig << ", forwarding to seamless worker (PID " << currentWorker << ")";
+
+ (void)kill(currentWorker, termSig);
+ requestedTermination = true;
+
+#ifdef HAVE_SYSTEMD
+ if (!notifiedTermination) {
+ notifiedTermination = true;
+ sd_notify(0, "STOPPING=1");
+ }
+#endif /* HAVE_SYSTEMD */
+ }
+ }
+
+ if (l_RequestedReload.exchange(false)) {
+ Log(LogInformation, "Application")
+ << "Got reload command: Starting new instance.";
+
+#ifdef HAVE_SYSTEMD
+ sd_notify(0, "RELOADING=1");
+#endif /* HAVE_SYSTEMD */
+
+ // The old process is still active, yet.
+ // Its config changes would not be visible to the new one after config load.
+ ConfigObjectsExclusiveLock lock;
+
+ pid_t nextWorker = StartUnixWorker(configs);
+
+ switch (nextWorker) {
+ case -1:
+ break;
+ case -2:
+ Log(LogCritical, "Application", "Found error in config: reloading aborted");
+ Application::SetLastReloadFailed(Utility::GetTime());
+ break;
+ default:
+ Log(LogInformation, "Application")
+ << "Reload done, old process shutting down. Child process with PID '" << nextWorker << "' is taking over.";
+
+ NotifyStatus("Shutting down old instance...");
+
+ Application::SetLastReloadFailed(0);
+ (void)kill(currentWorker, SIGTERM);
+
+ {
+ double start = Utility::GetTime();
+
+ while (waitpid(currentWorker, nullptr, 0) == -1 && errno == EINTR) {
+ #ifdef HAVE_SYSTEMD
+ NotifyWatchdog();
+ #endif /* HAVE_SYSTEMD */
+ }
+
+ Log(LogNotice, "cli")
+ << "Waited for " << Utility::FormatDuration(Utility::GetTime() - start) << " on old process to exit.";
+ }
+
+ // Old instance shut down, allow the new one to continue working beyond config validation
+ (void)kill(nextWorker, SIGUSR2);
+
+ NotifyStatus("Shut down old instance.");
+
+ currentWorker = nextWorker;
+ }
+
+#ifdef HAVE_SYSTEMD
+ sd_notify(0, "READY=1");
+#endif /* HAVE_SYSTEMD */
+
+ }
+
+ if (l_RequestedReopenLogs.exchange(false)) {
+ Log(LogNotice, "cli")
+ << "Got signal " << SIGUSR1 << ", forwarding to seamless worker (PID " << currentWorker << ")";
+
+ (void)kill(currentWorker, SIGUSR1);
+ }
+
+ {
+ int status;
+ if (waitpid(currentWorker, &status, WNOHANG) > 0) {
+ Log(LogNotice, "cli")
+ << "Seamless worker (PID " << currentWorker << ") stopped, stopping as well";
+
+#ifdef HAVE_SYSTEMD
+ if (!notifiedTermination) {
+ notifiedTermination = true;
+ sd_notify(0, "STOPPING=1");
+ }
+#endif /* HAVE_SYSTEMD */
+
+ // If killed by signal, forward it via the exit code (to be as seamless as possible)
+ return WIFSIGNALED(status) ? 128 + WTERMSIG(status) : WEXITSTATUS(status);
+ }
+ }
+
+ Utility::Sleep(0.2);
+ }
+#endif /* _WIN32 */
+}
diff --git a/lib/cli/daemoncommand.hpp b/lib/cli/daemoncommand.hpp
new file mode 100644
index 0000000..da8a34b
--- /dev/null
+++ b/lib/cli/daemoncommand.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DAEMONCOMMAND_H
+#define DAEMONCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "daemon" CLI command.
+ *
+ * @ingroup cli
+ */
+class DaemonCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(DaemonCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* DAEMONCOMMAND_H */
diff --git a/lib/cli/daemonutility.cpp b/lib/cli/daemonutility.cpp
new file mode 100644
index 0000000..9e910f3
--- /dev/null
+++ b/lib/cli/daemonutility.cpp
@@ -0,0 +1,285 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/daemonutility.hpp"
+#include "base/configobject.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/scriptglobal.hpp"
+#include "config/configcompiler.hpp"
+#include "config/configcompilercontext.hpp"
+#include "config/configitembuilder.hpp"
+#include "icinga/dependency.hpp"
+#include <set>
+
+using namespace icinga;
+
+static bool ExecuteExpression(Expression *expression)
+{
+ if (!expression)
+ return false;
+
+ try {
+ ScriptFrame frame(true);
+ expression->Evaluate(frame);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "config", DiagnosticInformation(ex));
+ return false;
+ }
+
+ return true;
+}
+
+static bool IncludeZoneDirRecursive(const String& path, const String& package, bool& success)
+{
+ String zoneName = Utility::BaseName(path);
+
+ /* We don't have an activated zone object yet. We may forcefully guess from configitems
+ * to not include this specific synced zones directory.
+ */
+ if(!ConfigItem::GetByTypeAndName(Type::GetByName("Zone"), zoneName)) {
+ return false;
+ }
+
+ /* register this zone path for cluster config sync */
+ ConfigCompiler::RegisterZoneDir("_etc", path, zoneName);
+
+ std::vector<std::unique_ptr<Expression> > expressions;
+ Utility::GlobRecursive(path, "*.conf", [&expressions, zoneName, package](const String& file) {
+ ConfigCompiler::CollectIncludes(expressions, file, zoneName, package);
+ }, GlobFile);
+
+ DictExpression expr(std::move(expressions));
+ if (!ExecuteExpression(&expr))
+ success = false;
+
+ return true;
+}
+
+static bool IncludeNonLocalZone(const String& zonePath, const String& package, bool& success)
+{
+ /* Note: This include function must not call RegisterZoneDir().
+ * We do not need to copy it for cluster config sync. */
+
+ String zoneName = Utility::BaseName(zonePath);
+
+ /* We don't have an activated zone object yet. We may forcefully guess from configitems
+ * to not include this specific synced zones directory.
+ */
+ if(!ConfigItem::GetByTypeAndName(Type::GetByName("Zone"), zoneName)) {
+ return false;
+ }
+
+ /* Check whether this node already has an authoritative config version
+ * from zones.d in etc or api package directory, or a local marker file)
+ */
+ if (ConfigCompiler::HasZoneConfigAuthority(zoneName) || Utility::PathExists(zonePath + "/.authoritative")) {
+ Log(LogNotice, "config")
+ << "Ignoring non local config include for zone '" << zoneName << "': We already have an authoritative copy included.";
+ return true;
+ }
+
+ std::vector<std::unique_ptr<Expression> > expressions;
+ Utility::GlobRecursive(zonePath, "*.conf", [&expressions, zoneName, package](const String& file) {
+ ConfigCompiler::CollectIncludes(expressions, file, zoneName, package);
+ }, GlobFile);
+
+ DictExpression expr(std::move(expressions));
+ if (!ExecuteExpression(&expr))
+ success = false;
+
+ return true;
+}
+
+static void IncludePackage(const String& packagePath, bool& success)
+{
+ /* Note: Package includes will register their zones
+ * for config sync inside their generated config. */
+ String packageName = Utility::BaseName(packagePath);
+
+ if (Utility::PathExists(packagePath + "/include.conf")) {
+ std::unique_ptr<Expression> expr = ConfigCompiler::CompileFile(packagePath + "/include.conf",
+ String(), packageName);
+
+ if (!ExecuteExpression(&*expr))
+ success = false;
+ }
+}
+
+bool DaemonUtility::ValidateConfigFiles(const std::vector<std::string>& configs, const String& objectsFile)
+{
+ bool success;
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+ VERIFY(systemNS);
+
+ Namespace::Ptr internalNS = ScriptGlobal::Get("Internal");
+ VERIFY(internalNS);
+
+ if (!objectsFile.IsEmpty())
+ ConfigCompilerContext::GetInstance()->OpenObjectsFile(objectsFile);
+
+ if (!configs.empty()) {
+ for (const String& configPath : configs) {
+ try {
+ std::unique_ptr<Expression> expression = ConfigCompiler::CompileFile(configPath, String(), "_etc");
+ success = ExecuteExpression(&*expression);
+ if (!success)
+ return false;
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli", "Could not compile config files: " + DiagnosticInformation(ex, false));
+ Application::Exit(1);
+ }
+ }
+ }
+
+ /* Load cluster config files from /etc/icinga2/zones.d.
+ * This should probably be in libremote but
+ * unfortunately moving it there is somewhat non-trivial. */
+ success = true;
+
+ /* Only load zone directory if we're not in staging validation. */
+ if (!internalNS->Contains("ZonesStageVarDir")) {
+ String zonesEtcDir = Configuration::ZonesDir;
+ if (!zonesEtcDir.IsEmpty() && Utility::PathExists(zonesEtcDir)) {
+ std::set<String> zoneEtcDirs;
+ Utility::Glob(zonesEtcDir + "/*", [&zoneEtcDirs](const String& zoneEtcDir) { zoneEtcDirs.emplace(zoneEtcDir); }, GlobDirectory);
+
+ bool hasSuccess = true;
+
+ while (!zoneEtcDirs.empty() && hasSuccess) {
+ hasSuccess = false;
+
+ for (auto& zoneEtcDir : zoneEtcDirs) {
+ if (IncludeZoneDirRecursive(zoneEtcDir, "_etc", success)) {
+ zoneEtcDirs.erase(zoneEtcDir);
+ hasSuccess = true;
+ break;
+ }
+ }
+ }
+
+ for (auto& zoneEtcDir : zoneEtcDirs) {
+ Log(LogWarning, "config")
+ << "Ignoring directory '" << zoneEtcDir << "' for unknown zone '" << Utility::BaseName(zoneEtcDir) << "'.";
+ }
+ }
+
+ if (!success)
+ return false;
+ }
+
+ /* Load package config files - they may contain additional zones which
+ * are authoritative on this node and are checked in HasZoneConfigAuthority(). */
+ String packagesVarDir = Configuration::DataDir + "/api/packages";
+ if (Utility::PathExists(packagesVarDir))
+ Utility::Glob(packagesVarDir + "/*", [&success](const String& packagePath) { IncludePackage(packagePath, success); }, GlobDirectory);
+
+ if (!success)
+ return false;
+
+ /* Load cluster synchronized configuration files. This can be overridden for staged sync validations. */
+ String zonesVarDir = Configuration::DataDir + "/api/zones";
+
+ /* Cluster config sync stage validation needs this. */
+ if (internalNS->Contains("ZonesStageVarDir")) {
+ zonesVarDir = internalNS->Get("ZonesStageVarDir");
+
+ Log(LogNotice, "DaemonUtility")
+ << "Overriding zones var directory with '" << zonesVarDir << "' for cluster config sync staging.";
+ }
+
+
+ if (Utility::PathExists(zonesVarDir)) {
+ std::set<String> zoneVarDirs;
+ Utility::Glob(zonesVarDir + "/*", [&zoneVarDirs](const String& zoneVarDir) { zoneVarDirs.emplace(zoneVarDir); }, GlobDirectory);
+
+ bool hasSuccess = true;
+
+ while (!zoneVarDirs.empty() && hasSuccess) {
+ hasSuccess = false;
+
+ for (auto& zoneVarDir : zoneVarDirs) {
+ if (IncludeNonLocalZone(zoneVarDir, "_cluster", success)) {
+ zoneVarDirs.erase(zoneVarDir);
+ hasSuccess = true;
+ break;
+ }
+ }
+ }
+
+ for (auto& zoneEtcDir : zoneVarDirs) {
+ Log(LogWarning, "config")
+ << "Ignoring directory '" << zoneEtcDir << "' for unknown zone '" << Utility::BaseName(zoneEtcDir) << "'.";
+ }
+ }
+
+ if (!success)
+ return false;
+
+ /* This is initialized inside the IcingaApplication class. */
+ Value vAppType;
+ VERIFY(systemNS->Get("ApplicationType", &vAppType));
+
+ Type::Ptr appType = Type::GetByName(vAppType);
+
+ if (ConfigItem::GetItems(appType).empty()) {
+ ConfigItemBuilder builder;
+ builder.SetType(appType);
+ builder.SetName("app");
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+ ConfigItem::Ptr item = builder.Compile();
+ item->Register();
+ }
+
+ return true;
+}
+
+bool DaemonUtility::LoadConfigFiles(const std::vector<std::string>& configs,
+ std::vector<ConfigItem::Ptr>& newItems,
+ const String& objectsFile, const String& varsfile)
+{
+ ActivationScope ascope;
+
+ if (!DaemonUtility::ValidateConfigFiles(configs, objectsFile)) {
+ ConfigCompilerContext::GetInstance()->CancelObjectsFile();
+ return false;
+ }
+
+ // After evaluating the top-level statements of the config files (happening in ValidateConfigFiles() above),
+ // prevent further modification of the global scope. This allows for a faster execution of the following steps
+ // as Freeze() disables locking as it's not necessary on a read-only data structure anymore.
+ ScriptGlobal::GetGlobals()->Freeze();
+
+ WorkQueue upq(25000, Configuration::Concurrency);
+ upq.SetName("DaemonUtility::LoadConfigFiles");
+ bool result = ConfigItem::CommitItems(ascope.GetContext(), upq, newItems);
+
+ if (result) {
+ try {
+ Dependency::AssertNoCycles();
+ } catch (...) {
+ Log(LogCritical, "config")
+ << DiagnosticInformation(boost::current_exception(), false);
+
+ result = false;
+ }
+ }
+
+ if (!result) {
+ ConfigCompilerContext::GetInstance()->CancelObjectsFile();
+ return false;
+ }
+
+ try {
+ ScriptGlobal::WriteToFile(varsfile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli", "Could not write vars file: " + DiagnosticInformation(ex, false));
+ Application::Exit(1);
+ }
+
+ ConfigCompilerContext::GetInstance()->FinishObjectsFile();
+
+ return true;
+}
diff --git a/lib/cli/daemonutility.hpp b/lib/cli/daemonutility.hpp
new file mode 100644
index 0000000..963bfba
--- /dev/null
+++ b/lib/cli/daemonutility.hpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DAEMONUTILITY_H
+#define DAEMONUTILITY_H
+
+#include "cli/i2-cli.hpp"
+#include "config/configitem.hpp"
+#include "base/string.hpp"
+#include <boost/program_options.hpp>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class DaemonUtility
+{
+public:
+ static bool ValidateConfigFiles(const std::vector<std::string>& configs, const String& objectsFile = String());
+ static bool LoadConfigFiles(const std::vector<std::string>& configs, std::vector<ConfigItem::Ptr>& newItems,
+ const String& objectsFile = String(), const String& varsfile = String());
+};
+
+}
+
+#endif /* DAEMONULITIY_H */
diff --git a/lib/cli/editline.hpp b/lib/cli/editline.hpp
new file mode 100644
index 0000000..f97525e
--- /dev/null
+++ b/lib/cli/editline.hpp
@@ -0,0 +1,19 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EDITLINE_H
+#define EDITLINE_H
+
+extern "C" {
+
+char *readline(const char *prompt);
+int add_history(const char *line);
+void rl_deprep_terminal();
+
+typedef char *ELFunction(const char *, int);
+
+extern char rl_completion_append_character;
+extern ELFunction *rl_completion_entry_function;
+
+}
+
+#endif /* EDITLINE_H */
diff --git a/lib/cli/featuredisablecommand.cpp b/lib/cli/featuredisablecommand.cpp
new file mode 100644
index 0000000..95a4a26
--- /dev/null
+++ b/lib/cli/featuredisablecommand.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/featuredisablecommand.hpp"
+#include "cli/featureutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("feature/disable", FeatureDisableCommand);
+
+String FeatureDisableCommand::GetDescription() const
+{
+ return "Disables specified Icinga 2 feature.";
+}
+
+String FeatureDisableCommand::GetShortDescription() const
+{
+ return "disables specified feature";
+}
+
+std::vector<String> FeatureDisableCommand::GetPositionalSuggestions(const String& word) const
+{
+ return FeatureUtility::GetFieldCompletionSuggestions(word, false);
+}
+
+int FeatureDisableCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+int FeatureDisableCommand::GetMaxArguments() const
+{
+ return -1;
+}
+
+ImpersonationLevel FeatureDisableCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "feature disable" CLI command.
+ *
+ * @returns An exit status.
+ */
+int FeatureDisableCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (ap.empty()) {
+ Log(LogCritical, "cli", "Cannot disable feature(s). Name(s) are missing!");
+ return 0;
+ }
+
+ return FeatureUtility::DisableFeatures(ap);
+}
diff --git a/lib/cli/featuredisablecommand.hpp b/lib/cli/featuredisablecommand.hpp
new file mode 100644
index 0000000..b24655d
--- /dev/null
+++ b/lib/cli/featuredisablecommand.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FEATUREDISABLECOMMAND_H
+#define FEATUREDISABLECOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "feature disable" command.
+ *
+ * @ingroup cli
+ */
+class FeatureDisableCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(FeatureDisableCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ int GetMaxArguments() const override;
+ std::vector<String> GetPositionalSuggestions(const String& word) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* FEATUREDISABLECOMMAND_H */
diff --git a/lib/cli/featureenablecommand.cpp b/lib/cli/featureenablecommand.cpp
new file mode 100644
index 0000000..0cf9066
--- /dev/null
+++ b/lib/cli/featureenablecommand.cpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/featureenablecommand.hpp"
+#include "cli/featureutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("feature/enable", FeatureEnableCommand);
+
+String FeatureEnableCommand::GetDescription() const
+{
+ return "Enables specified Icinga 2 feature.";
+}
+
+String FeatureEnableCommand::GetShortDescription() const
+{
+ return "enables specified feature";
+}
+
+std::vector<String> FeatureEnableCommand::GetPositionalSuggestions(const String& word) const
+{
+ return FeatureUtility::GetFieldCompletionSuggestions(word, true);
+}
+
+int FeatureEnableCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+int FeatureEnableCommand::GetMaxArguments() const
+{
+ return -1;
+}
+
+ImpersonationLevel FeatureEnableCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "feature enable" CLI command.
+ *
+ * @returns An exit status.
+ */
+int FeatureEnableCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ return FeatureUtility::EnableFeatures(ap);
+}
diff --git a/lib/cli/featureenablecommand.hpp b/lib/cli/featureenablecommand.hpp
new file mode 100644
index 0000000..fc91778
--- /dev/null
+++ b/lib/cli/featureenablecommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FEATUREENABLECOMMAND_H
+#define FEATUREENABLECOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "feature enable" command.
+ *
+ * @ingroup cli
+ */
+class FeatureEnableCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(FeatureEnableCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ int GetMaxArguments() const override;
+ std::vector<String> GetPositionalSuggestions(const String& word) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* FEATUREENABLECOMMAND_H */
diff --git a/lib/cli/featurelistcommand.cpp b/lib/cli/featurelistcommand.cpp
new file mode 100644
index 0000000..2aad4a9
--- /dev/null
+++ b/lib/cli/featurelistcommand.cpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/featurelistcommand.hpp"
+#include "cli/featureutility.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/console.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("feature/list", FeatureListCommand);
+
+String FeatureListCommand::GetDescription() const
+{
+ return "Lists all available Icinga 2 features.";
+}
+
+String FeatureListCommand::GetShortDescription() const
+{
+ return "lists all available features";
+}
+
+/**
+ * The entry point for the "feature list" CLI command.
+ *
+ * @returns An exit status.
+ */
+int FeatureListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ return FeatureUtility::ListFeatures();
+}
diff --git a/lib/cli/featurelistcommand.hpp b/lib/cli/featurelistcommand.hpp
new file mode 100644
index 0000000..cae1d74
--- /dev/null
+++ b/lib/cli/featurelistcommand.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FEATURELISTCOMMAND_H
+#define FEATURELISTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "feature list" command.
+ *
+ * @ingroup cli
+ */
+class FeatureListCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(FeatureListCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* FEATURELISTCOMMAND_H */
diff --git a/lib/cli/featureutility.cpp b/lib/cli/featureutility.cpp
new file mode 100644
index 0000000..3523868
--- /dev/null
+++ b/lib/cli/featureutility.cpp
@@ -0,0 +1,243 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/featureutility.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+
+using namespace icinga;
+
+String FeatureUtility::GetFeaturesAvailablePath()
+{
+ return Configuration::ConfigDir + "/features-available";
+}
+
+String FeatureUtility::GetFeaturesEnabledPath()
+{
+ return Configuration::ConfigDir + "/features-enabled";
+}
+
+std::vector<String> FeatureUtility::GetFieldCompletionSuggestions(const String& word, bool enable)
+{
+ std::vector<String> cache;
+ std::vector<String> suggestions;
+
+ GetFeatures(cache, enable);
+
+ std::sort(cache.begin(), cache.end());
+
+ for (const String& suggestion : cache) {
+ if (suggestion.Find(word) == 0)
+ suggestions.push_back(suggestion);
+ }
+
+ return suggestions;
+}
+
+int FeatureUtility::EnableFeatures(const std::vector<std::string>& features)
+{
+ String features_available_dir = GetFeaturesAvailablePath();
+ String features_enabled_dir = GetFeaturesEnabledPath();
+
+ if (!Utility::PathExists(features_available_dir) ) {
+ Log(LogCritical, "cli")
+ << "Cannot parse available features. Path '" << features_available_dir << "' does not exist.";
+ return 1;
+ }
+
+ if (!Utility::PathExists(features_enabled_dir) ) {
+ Log(LogCritical, "cli")
+ << "Cannot enable features. Path '" << features_enabled_dir << "' does not exist.";
+ return 1;
+ }
+
+ std::vector<std::string> errors;
+
+ for (const String& feature : features) {
+ String source = features_available_dir + "/" + feature + ".conf";
+
+ if (!Utility::PathExists(source) ) {
+ Log(LogCritical, "cli")
+ << "Cannot enable feature '" << feature << "'. Source file '" << source + "' does not exist.";
+ errors.push_back(feature);
+ continue;
+ }
+
+ String target = features_enabled_dir + "/" + feature + ".conf";
+
+ if (Utility::PathExists(target) ) {
+ Log(LogWarning, "cli")
+ << "Feature '" << feature << "' already enabled.";
+ continue;
+ }
+
+ std::cout << "Enabling feature " << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << feature
+ << ConsoleColorTag(Console_Normal) << ". Make sure to restart Icinga 2 for these changes to take effect.\n";
+
+#ifndef _WIN32
+ String relativeSource = "../features-available/" + feature + ".conf";
+
+ if (symlink(relativeSource.CStr(), target.CStr()) < 0) {
+ Log(LogCritical, "cli")
+ << "Cannot enable feature '" << feature << "'. Linking source '" << relativeSource << "' to target file '" << target
+ << "' failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\".";
+ errors.push_back(feature);
+ continue;
+ }
+#else /* _WIN32 */
+ std::ofstream fp;
+ fp.open(target.CStr());
+ fp << "include \"../features-available/" << feature << ".conf\"" << std::endl;
+ fp.close();
+
+ if (fp.fail()) {
+ Log(LogCritical, "cli")
+ << "Cannot enable feature '" << feature << "'. Failed to open file '" << target << "'.";
+ errors.push_back(feature);
+ continue;
+ }
+#endif /* _WIN32 */
+ }
+
+ if (!errors.empty()) {
+ Log(LogCritical, "cli")
+ << "Cannot enable feature(s): " << boost::algorithm::join(errors, " ");
+ errors.clear();
+ return 1;
+ }
+
+ return 0;
+}
+
+int FeatureUtility::DisableFeatures(const std::vector<std::string>& features)
+{
+ String features_enabled_dir = GetFeaturesEnabledPath();
+
+ if (!Utility::PathExists(features_enabled_dir) ) {
+ Log(LogCritical, "cli")
+ << "Cannot disable features. Path '" << features_enabled_dir << "' does not exist.";
+ return 0;
+ }
+
+ std::vector<std::string> errors;
+
+ for (const String& feature : features) {
+ String target = features_enabled_dir + "/" + feature + ".conf";
+
+ if (!Utility::PathExists(target) ) {
+ Log(LogWarning, "cli")
+ << "Feature '" << feature << "' already disabled.";
+ continue;
+ }
+
+ if (unlink(target.CStr()) < 0) {
+ Log(LogCritical, "cli")
+ << "Cannot disable feature '" << feature << "'. Unlinking target file '" << target
+ << "' failed with error code " << errno << ", \"" + Utility::FormatErrorNumber(errno) << "\".";
+ errors.push_back(feature);
+ continue;
+ }
+
+ std::cout << "Disabling feature " << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << feature
+ << ConsoleColorTag(Console_Normal) << ". Make sure to restart Icinga 2 for these changes to take effect.\n";
+ }
+
+ if (!errors.empty()) {
+ Log(LogCritical, "cli")
+ << "Cannot disable feature(s): " << boost::algorithm::join(errors, " ");
+ errors.clear();
+ return 1;
+ }
+
+ return 0;
+}
+
+int FeatureUtility::ListFeatures(std::ostream& os)
+{
+ std::vector<String> disabled_features;
+ std::vector<String> enabled_features;
+
+ if (!FeatureUtility::GetFeatures(disabled_features, true))
+ return 1;
+
+ os << ConsoleColorTag(Console_ForegroundRed | Console_Bold) << "Disabled features: " << ConsoleColorTag(Console_Normal)
+ << boost::algorithm::join(disabled_features, " ") << "\n";
+
+ if (!FeatureUtility::GetFeatures(enabled_features, false))
+ return 1;
+
+ os << ConsoleColorTag(Console_ForegroundGreen | Console_Bold) << "Enabled features: " << ConsoleColorTag(Console_Normal)
+ << boost::algorithm::join(enabled_features, " ") << "\n";
+
+ return 0;
+}
+
+bool FeatureUtility::GetFeatures(std::vector<String>& features, bool get_disabled)
+{
+ /* request all disabled features */
+ if (get_disabled) {
+ /* disable = available-enabled */
+ String available_pattern = GetFeaturesAvailablePath() + "/*.conf";
+ std::vector<String> available;
+ Utility::Glob(available_pattern, [&available](const String& featureFile) { CollectFeatures(featureFile, available); }, GlobFile);
+
+ String enabled_pattern = GetFeaturesEnabledPath() + "/*.conf";
+ std::vector<String> enabled;
+ Utility::Glob(enabled_pattern, [&enabled](const String& featureFile) { CollectFeatures(featureFile, enabled); }, GlobFile);
+
+ std::sort(available.begin(), available.end());
+ std::sort(enabled.begin(), enabled.end());
+ std::set_difference(
+ available.begin(), available.end(),
+ enabled.begin(), enabled.end(),
+ std::back_inserter(features)
+ );
+ } else {
+ /* all enabled features */
+ String enabled_pattern = GetFeaturesEnabledPath() + "/*.conf";
+
+ Utility::Glob(enabled_pattern, [&features](const String& featureFile) { CollectFeatures(featureFile, features); }, GlobFile);
+ }
+
+ return true;
+}
+
+bool FeatureUtility::CheckFeatureEnabled(const String& feature)
+{
+ return CheckFeatureInternal(feature, false);
+}
+
+bool FeatureUtility::CheckFeatureDisabled(const String& feature)
+{
+ return CheckFeatureInternal(feature, true);
+}
+
+bool FeatureUtility::CheckFeatureInternal(const String& feature, bool check_disabled)
+{
+ std::vector<String> features;
+
+ if (!FeatureUtility::GetFeatures(features, check_disabled))
+ return false;
+
+ for (const String& check_feature : features) {
+ if (check_feature == feature)
+ return true;
+ }
+
+ return false;
+}
+
+void FeatureUtility::CollectFeatures(const String& feature_file, std::vector<String>& features)
+{
+ String feature = Utility::BaseName(feature_file);
+ boost::algorithm::replace_all(feature, ".conf", "");
+
+ Log(LogDebug, "cli")
+ << "Adding feature: " << feature;
+ features.push_back(feature);
+}
diff --git a/lib/cli/featureutility.hpp b/lib/cli/featureutility.hpp
new file mode 100644
index 0000000..9cb2128
--- /dev/null
+++ b/lib/cli/featureutility.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FEATUREUTILITY_H
+#define FEATUREUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/string.hpp"
+#include <vector>
+#include <iostream>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class FeatureUtility
+{
+public:
+ static String GetFeaturesAvailablePath();
+ static String GetFeaturesEnabledPath();
+
+ static std::vector<String> GetFieldCompletionSuggestions(const String& word, bool enable);
+
+ static int EnableFeatures(const std::vector<std::string>& features);
+ static int DisableFeatures(const std::vector<std::string>& features);
+ static int ListFeatures(std::ostream& os = std::cout);
+
+ static bool GetFeatures(std::vector<String>& features, bool enable);
+ static bool CheckFeatureEnabled(const String& feature);
+ static bool CheckFeatureDisabled(const String& feature);
+
+private:
+ FeatureUtility();
+ static void CollectFeatures(const String& feature_file, std::vector<String>& features);
+ static bool CheckFeatureInternal(const String& feature, bool check_disabled);
+};
+
+}
+
+#endif /* FEATUREUTILITY_H */
diff --git a/lib/cli/i2-cli.hpp b/lib/cli/i2-cli.hpp
new file mode 100644
index 0000000..86e5ddd
--- /dev/null
+++ b/lib/cli/i2-cli.hpp
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2CLI_H
+#define I2CLI_H
+
+/**
+ * @defgroup cli CLI commands
+ *
+ * The CLI library implements Icinga's command-line interface.
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2CLI_H */
diff --git a/lib/cli/internalsignalcommand.cpp b/lib/cli/internalsignalcommand.cpp
new file mode 100644
index 0000000..b097965
--- /dev/null
+++ b/lib/cli/internalsignalcommand.cpp
@@ -0,0 +1,67 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/internalsignalcommand.hpp"
+#include "base/logger.hpp"
+#include <signal.h>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("internal/signal", InternalSignalCommand);
+
+String InternalSignalCommand::GetDescription() const
+{
+ return "Send signal as Icinga user";
+}
+
+String InternalSignalCommand::GetShortDescription() const
+{
+ return "Send signal as Icinga user";
+}
+
+ImpersonationLevel InternalSignalCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+bool InternalSignalCommand::IsHidden() const
+{
+ return true;
+}
+
+void InternalSignalCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("pid,p", po::value<int>(), "Target PID")
+ ("sig,s", po::value<String>(), "Signal (POSIX string) to send")
+ ;
+}
+
+/**
+ * The entry point for the "internal signal" CLI command.
+ *
+ * @returns An exit status.
+ */
+int InternalSignalCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+#ifndef _WIN32
+ String signal = vm["sig"].as<String>();
+
+ /* Thank POSIX */
+ if (signal == "SIGKILL")
+ return kill(vm["pid"].as<int>(), SIGKILL);
+ if (signal == "SIGINT")
+ return kill(vm["pid"].as<int>(), SIGINT);
+ if (signal == "SIGCHLD")
+ return kill(vm["pid"].as<int>(), SIGCHLD);
+ if (signal == "SIGHUP")
+ return kill(vm["pid"].as<int>(), SIGHUP);
+
+ Log(LogCritical, "cli") << "Unsupported signal \"" << signal << "\"";
+#else
+ Log(LogCritical, "cli", "Unsupported action on Windows.");
+#endif /* _Win32 */
+ return 1;
+}
+
diff --git a/lib/cli/internalsignalcommand.hpp b/lib/cli/internalsignalcommand.hpp
new file mode 100644
index 0000000..d599b80
--- /dev/null
+++ b/lib/cli/internalsignalcommand.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef INTERNALSIGNALCOMMAND_H
+#define INTERNALSIGNALCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "internal signal" command.
+ *
+ * @ingroup cli
+ */
+class InternalSignalCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(InternalSignalCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ bool IsHidden() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* INTERNALSIGNALCOMMAND_H */
diff --git a/lib/cli/nodesetupcommand.cpp b/lib/cli/nodesetupcommand.cpp
new file mode 100644
index 0000000..2a685b5
--- /dev/null
+++ b/lib/cli/nodesetupcommand.cpp
@@ -0,0 +1,559 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/nodesetupcommand.hpp"
+#include "cli/nodeutility.hpp"
+#include "cli/featureutility.hpp"
+#include "cli/apisetuputility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+
+#include <iostream>
+#include <fstream>
+#include <vector>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("node/setup", NodeSetupCommand);
+
+String NodeSetupCommand::GetDescription() const
+{
+ return "Sets up an Icinga 2 node.";
+}
+
+String NodeSetupCommand::GetShortDescription() const
+{
+ return "set up node";
+}
+
+void NodeSetupCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("zone", po::value<std::string>(), "The name of the local zone")
+ ("endpoint", po::value<std::vector<std::string> >(), "Connect to remote endpoint; syntax: cn[,host,port]")
+ ("parent_host", po::value<std::string>(), "The name of the parent host for auto-signing the csr; syntax: host[,port]")
+ ("parent_zone", po::value<std::string>(), "The name of the parent zone")
+ ("listen", po::value<std::string>(), "Listen on host,port")
+ ("ticket", po::value<std::string>(), "Generated ticket number for this request (optional)")
+ ("trustedcert", po::value<std::string>(), "Trusted parent certificate file as connection verification (received via 'pki save-cert')")
+ ("cn", po::value<std::string>(), "The certificate's common name")
+ ("accept-config", "Accept config from parent node")
+ ("accept-commands", "Accept commands from parent node")
+ ("master", "Use setup for a master instance")
+ ("global_zones", po::value<std::vector<std::string> >(), "The names of the additional global zones to 'global-templates' and 'director-global'.")
+ ("disable-confd", "Disables the conf.d directory during the setup");
+
+ hiddenDesc.add_options()
+ ("master_zone", po::value<std::string>(), "DEPRECATED: The name of the master zone")
+ ("master_host", po::value<std::string>(), "DEPRECATED: The name of the master host for auto-signing the csr; syntax: host[,port]");
+}
+
+std::vector<String> NodeSetupCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "key" || argument == "cert" || argument == "trustedcert")
+ return GetBashCompletionSuggestions("file", word);
+ else if (argument == "host")
+ return GetBashCompletionSuggestions("hostname", word);
+ else if (argument == "port")
+ return GetBashCompletionSuggestions("service", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+ImpersonationLevel NodeSetupCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "node setup" CLI command.
+ *
+ * @returns An exit status.
+ */
+int NodeSetupCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!ap.empty()) {
+ Log(LogWarning, "cli")
+ << "Ignoring parameters: " << boost::algorithm::join(ap, " ");
+ }
+
+ if (vm.count("master"))
+ return SetupMaster(vm, ap);
+ else
+ return SetupNode(vm, ap);
+}
+
+int NodeSetupCommand::SetupMaster(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap)
+{
+ /* Ignore not required parameters */
+ if (vm.count("ticket"))
+ Log(LogWarning, "cli", "Master for Node setup: Ignoring --ticket");
+
+ if (vm.count("endpoint"))
+ Log(LogWarning, "cli", "Master for Node setup: Ignoring --endpoint");
+
+ if (vm.count("trustedcert"))
+ Log(LogWarning, "cli", "Master for Node setup: Ignoring --trustedcert");
+
+ String cn = Utility::GetFQDN();
+
+ if (vm.count("cn"))
+ cn = vm["cn"].as<std::string>();
+
+ /* Setup command hardcodes this as FQDN */
+ String endpointName = cn;
+
+ /* Allow to specify zone name. */
+ String zoneName = "master";
+
+ if (vm.count("zone"))
+ zoneName = vm["zone"].as<std::string>();
+
+ /* check whether the user wants to generate a new certificate or not */
+ String existingPath = ApiListener::GetCertsDir() + "/" + cn + ".crt";
+
+ Log(LogInformation, "cli")
+ << "Checking in existing certificates for common name '" << cn << "'...";
+
+ if (Utility::PathExists(existingPath)) {
+ Log(LogWarning, "cli")
+ << "Certificate '" << existingPath << "' for CN '" << cn << "' already exists. Not generating new certificate.";
+ } else {
+ Log(LogInformation, "cli")
+ << "Certificates not yet generated. Running 'api setup' now.";
+
+ ApiSetupUtility::SetupMasterCertificates(cn);
+ }
+
+ Log(LogInformation, "cli", "Generating master configuration for Icinga 2.");
+ ApiSetupUtility::SetupMasterApiUser();
+
+ if (!FeatureUtility::CheckFeatureEnabled("api")) {
+ ApiSetupUtility::SetupMasterEnableApi();
+ } else {
+ Log(LogInformation, "cli")
+ << "'api' feature already enabled.\n";
+ }
+
+ /* write zones.conf and update with zone + endpoint information */
+ Log(LogInformation, "cli", "Generating zone and object configuration.");
+
+ std::vector<String> globalZones { "global-templates", "director-global" };
+ std::vector<std::string> setupGlobalZones;
+
+ if (vm.count("global_zones"))
+ setupGlobalZones = vm["global_zones"].as<std::vector<std::string> >();
+
+ for (decltype(setupGlobalZones.size()) i = 0; i < setupGlobalZones.size(); i++) {
+ if (std::find(globalZones.begin(), globalZones.end(), setupGlobalZones[i]) != globalZones.end()) {
+ Log(LogCritical, "cli")
+ << "The global zone '" << setupGlobalZones[i] << "' is already specified.";
+ return 1;
+ }
+ }
+
+ globalZones.insert(globalZones.end(), setupGlobalZones.begin(), setupGlobalZones.end());
+
+ /* Generate master configuration. */
+ NodeUtility::GenerateNodeMasterIcingaConfig(endpointName, zoneName, globalZones);
+
+ /* Update the ApiListener config. */
+ Log(LogInformation, "cli", "Updating the APIListener feature.");
+
+ String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
+ NodeUtility::CreateBackupFile(apipath);
+
+ AtomicFile fp (apipath, 0644);
+
+ fp << "/**\n"
+ << " * The API listener is used for distributed monitoring setups.\n"
+ << " */\n"
+ << "object ApiListener \"api\" {\n";
+
+ if (vm.count("listen")) {
+ std::vector<String> tokens = String(vm["listen"].as<std::string>()).Split(",");
+
+ if (tokens.size() > 0)
+ fp << " bind_host = \"" << tokens[0] << "\"\n";
+ if (tokens.size() > 1)
+ fp << " bind_port = " << tokens[1] << "\n";
+ }
+
+ fp << "\n";
+
+ if (vm.count("accept-config"))
+ fp << " accept_config = true\n";
+ else
+ fp << " accept_config = false\n";
+
+ if (vm.count("accept-commands"))
+ fp << " accept_commands = true\n";
+ else
+ fp << " accept_commands = false\n";
+
+ fp << "\n"
+ << " ticket_salt = TicketSalt\n"
+ << "}\n";
+
+ fp.Commit();
+
+ /* update constants.conf with NodeName = CN + TicketSalt = random value */
+ if (endpointName != Utility::GetFQDN()) {
+ Log(LogWarning, "cli")
+ << "CN/Endpoint name '" << endpointName << "' does not match the default FQDN '" << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!";
+ }
+
+ NodeUtility::UpdateConstant("NodeName", endpointName);
+ NodeUtility::UpdateConstant("ZoneName", zoneName);
+
+ String salt = RandomString(16);
+
+ NodeUtility::UpdateConstant("TicketSalt", salt);
+
+ Log(LogInformation, "cli")
+ << "Edit the api feature config file '" << apipath << "' and set a secure 'ticket_salt' attribute.";
+
+ if (vm.count("disable-confd")) {
+ /* Disable conf.d inclusion */
+ if (NodeUtility::UpdateConfiguration("\"conf.d\"", false, true)) {
+ Log(LogInformation, "cli")
+ << "Disabled conf.d inclusion";
+ } else {
+ Log(LogWarning, "cli")
+ << "Tried to disable conf.d inclusion but failed, possibly it's already disabled.";
+ }
+
+ /* Include api-users.conf */
+ String apiUsersFilePath = ApiSetupUtility::GetApiUsersConfPath();
+
+ if (Utility::PathExists(apiUsersFilePath)) {
+ NodeUtility::UpdateConfiguration("\"conf.d/api-users.conf\"", true, false);
+ } else {
+ Log(LogWarning, "cli")
+ << "Included file doesn't exist " << apiUsersFilePath;
+ }
+ }
+
+ /* tell the user to reload icinga2 */
+ Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
+
+ return 0;
+}
+
+int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap)
+{
+ /* require at least one endpoint. Ticket is optional. */
+ if (!vm.count("endpoint")) {
+ Log(LogCritical, "cli", "You need to specify at least one endpoint (--endpoint).");
+ return 1;
+ }
+
+ if (!vm.count("zone")) {
+ Log(LogCritical, "cli", "You need to specify the local zone (--zone).");
+ return 1;
+ }
+
+ /* Deprecation warnings. TODO: Remove in 2.10.0. */
+ if (vm.count("master_zone"))
+ Log(LogWarning, "cli", "The 'master_zone' parameter has been deprecated. Use 'parent_zone' instead.");
+ if (vm.count("master_host"))
+ Log(LogWarning, "cli", "The 'master_host' parameter has been deprecated. Use 'parent_host' instead.");
+
+ String ticket;
+
+ if (vm.count("ticket"))
+ ticket = vm["ticket"].as<std::string>();
+
+ if (ticket.IsEmpty()) {
+ Log(LogInformation, "cli")
+ << "Requesting certificate without a ticket.";
+ } else {
+ Log(LogInformation, "cli")
+ << "Requesting certificate with ticket '" << ticket << "'.";
+ }
+
+ /* Decide whether to directly connect to the parent node for CSR signing, or leave it to the user. */
+ bool connectToParent = false;
+ String parentHost;
+ String parentPort = "5665";
+ std::shared_ptr<X509> trustedParentCert;
+
+ /* TODO: remove master_host in 2.10.0. */
+ if (!vm.count("master_host") && !vm.count("parent_host")) {
+ connectToParent = false;
+
+ Log(LogWarning, "cli")
+ << "Node to master/satellite connection setup skipped. Please configure your parent node to\n"
+ << "connect to this node by setting the 'host' attribute for the node Endpoint object.\n";
+ } else {
+ connectToParent = true;
+
+ String parentHostInfo;
+
+ if (vm.count("parent_host"))
+ parentHostInfo = vm["parent_host"].as<std::string>();
+ else if (vm.count("master_host")) /* TODO: Remove in 2.10.0. */
+ parentHostInfo = vm["master_host"].as<std::string>();
+
+ std::vector<String> tokens = parentHostInfo.Split(",");
+
+ if (tokens.size() == 1 || tokens.size() == 2)
+ parentHost = tokens[0];
+
+ if (tokens.size() == 2)
+ parentPort = tokens[1];
+
+ Log(LogInformation, "cli")
+ << "Verifying parent host connection information: host '" << parentHost << "', port '" << parentPort << "'.";
+
+ }
+
+ /* retrieve CN and pass it (defaults to FQDN) */
+ String cn = Utility::GetFQDN();
+
+ if (vm.count("cn"))
+ cn = vm["cn"].as<std::string>();
+
+ Log(LogInformation, "cli")
+ << "Using the following CN (defaults to FQDN): '" << cn << "'.";
+
+ /* pki request a signed certificate from the master */
+ String certsDir = ApiListener::GetCertsDir();
+ Utility::MkDirP(certsDir, 0700);
+
+ String user = Configuration::RunAsUser;
+ String group = Configuration::RunAsGroup;
+
+ if (!Utility::SetFileOwnership(certsDir, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << certsDir << "'. Verify it yourself!";
+ }
+
+ String key = certsDir + "/" + cn + ".key";
+ String cert = certsDir + "/" + cn + ".crt";
+ String ca = certsDir + "/ca.crt";
+
+ if (Utility::PathExists(key))
+ NodeUtility::CreateBackupFile(key, true);
+ if (Utility::PathExists(cert))
+ NodeUtility::CreateBackupFile(cert);
+
+ if (PkiUtility::NewCert(cn, key, String(), cert) != 0) {
+ Log(LogCritical, "cli", "Failed to generate new self-signed certificate.");
+ return 1;
+ }
+
+ /* fix permissions: root -> icinga daemon user */
+ if (!Utility::SetFileOwnership(key, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << key << "'. Verify it yourself!";
+ }
+
+ /* Send a signing request to the parent immediately, or leave it to the user. */
+ if (connectToParent) {
+ /* In contrast to `node wizard` the user must manually fetch
+ * the trustedParentCert to prove the trust relationship (fetched with 'pki save-cert').
+ */
+ if (!vm.count("trustedcert")) {
+ Log(LogCritical, "cli")
+ << "Please pass the trusted cert retrieved from the parent node (master or satellite)\n"
+ << "(Hint: 'icinga2 pki save-cert --host <parenthost> --port <5665> --key local.key --cert local.crt --trustedcert trusted-parent.crt').";
+ return 1;
+ }
+
+ String trustedCert = vm["trustedcert"].as<std::string>();
+
+ try{
+ trustedParentCert = GetX509Certificate(trustedCert);
+ } catch (const std::exception&) {
+ Log(LogCritical, "cli")
+ << "Can't read trusted cert at '" << trustedCert << "'.";
+ return 1;
+ }
+
+ try {
+ if (IsCa(trustedParentCert)) {
+ Log(LogCritical, "cli")
+ << "The trusted parent certificate is NOT a client certificate. It seems you passed the 'ca.crt' CA certificate via '--trustedcert' parameter.";
+ return 1;
+ }
+ } catch (const std::exception&) {
+ /* Swallow the error and do not run the check on unsupported OpenSSL platforms. */
+ }
+
+ Log(LogInformation, "cli")
+ << "Verifying trusted certificate file '" << vm["trustedcert"].as<std::string>() << "'.";
+
+ Log(LogInformation, "cli", "Requesting a signed certificate from the parent Icinga node.");
+
+ if (PkiUtility::RequestCertificate(parentHost, parentPort, key, cert, ca, trustedParentCert, ticket) > 0) {
+ Log(LogCritical, "cli")
+ << "Failed to fetch signed certificate from parent Icinga node '"
+ << parentHost << ", "
+ << parentPort << "'. Please try again.";
+ return 1;
+ }
+ } else {
+ /* We cannot retrieve the parent certificate.
+ * Tell the user to manually copy the ca.crt file
+ * into DataDir + "/certs"
+ */
+ Log(LogWarning, "cli")
+ << "\nNo connection to the parent node was specified.\n\n"
+ << "Please copy the public CA certificate from your master/satellite\n"
+ << "into '" << ca << "' before starting Icinga 2.\n";
+
+ if (Utility::PathExists(ca)) {
+ Log(LogInformation, "cli")
+ << "\nFound public CA certificate in '" << ca << "'.\n"
+ << "Please verify that it is the same as on your master/satellite.\n";
+ }
+ }
+
+ if (!Utility::SetFileOwnership(ca, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << ca << "'. Verify it yourself!";
+ }
+
+ /* fix permissions (again) when updating the signed certificate */
+ if (!Utility::SetFileOwnership(cert, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << cert << "'. Verify it yourself!";
+ }
+
+ /* disable the notifications feature */
+ Log(LogInformation, "cli", "Disabling the Notification feature.");
+
+ FeatureUtility::DisableFeatures({ "notification" });
+
+ /* enable the ApiListener config */
+
+ Log(LogInformation, "cli", "Updating the ApiListener feature.");
+
+ FeatureUtility::EnableFeatures({ "api" });
+
+ String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
+ NodeUtility::CreateBackupFile(apipath);
+
+ AtomicFile fp (apipath, 0644);
+
+ fp << "/**\n"
+ << " * The API listener is used for distributed monitoring setups.\n"
+ << " */\n"
+ << "object ApiListener \"api\" {\n";
+
+ if (vm.count("listen")) {
+ std::vector<String> tokens = String(vm["listen"].as<std::string>()).Split(",");
+
+ if (tokens.size() > 0)
+ fp << " bind_host = \"" << tokens[0] << "\"\n";
+ if (tokens.size() > 1)
+ fp << " bind_port = " << tokens[1] << "\n";
+ }
+
+ fp << "\n";
+
+ if (vm.count("accept-config"))
+ fp << " accept_config = true\n";
+ else
+ fp << " accept_config = false\n";
+
+ if (vm.count("accept-commands"))
+ fp << " accept_commands = true\n";
+ else
+ fp << " accept_commands = false\n";
+
+ fp << "\n"
+ << "}\n";
+
+ fp.Commit();
+
+ /* Generate zones configuration. */
+ Log(LogInformation, "cli", "Generating zone and object configuration.");
+
+ /* Setup command hardcodes this as FQDN */
+ String endpointName = cn;
+
+ /* Allow to specify zone name. */
+ String zoneName = vm["zone"].as<std::string>();
+
+ /* Allow to specify the parent zone name. */
+ String parentZoneName = "master";
+
+ if (vm.count("parent_zone"))
+ parentZoneName = vm["parent_zone"].as<std::string>();
+
+ std::vector<String> globalZones { "global-templates", "director-global" };
+ std::vector<std::string> setupGlobalZones;
+
+ if (vm.count("global_zones"))
+ setupGlobalZones = vm["global_zones"].as<std::vector<std::string> >();
+
+ for (decltype(setupGlobalZones.size()) i = 0; i < setupGlobalZones.size(); i++) {
+ if (std::find(globalZones.begin(), globalZones.end(), setupGlobalZones[i]) != globalZones.end()) {
+ Log(LogCritical, "cli")
+ << "The global zone '" << setupGlobalZones[i] << "' is already specified.";
+ return 1;
+ }
+ }
+
+ globalZones.insert(globalZones.end(), setupGlobalZones.begin(), setupGlobalZones.end());
+
+ /* Generate node configuration. */
+ NodeUtility::GenerateNodeIcingaConfig(endpointName, zoneName, parentZoneName, vm["endpoint"].as<std::vector<std::string> >(), globalZones);
+
+ /* update constants.conf with NodeName = CN */
+ if (endpointName != Utility::GetFQDN()) {
+ Log(LogWarning, "cli")
+ << "CN/Endpoint name '" << endpointName << "' does not match the default FQDN '"
+ << Utility::GetFQDN() << "'. Requires an update for the NodeName constant in constants.conf!";
+ }
+
+ NodeUtility::UpdateConstant("NodeName", endpointName);
+ NodeUtility::UpdateConstant("ZoneName", zoneName);
+
+ if (!ticket.IsEmpty()) {
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+ AtomicFile af (ticketPath, 0600);
+
+ if (!Utility::SetFileOwnership(af.GetTempFilename(), user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group
+ << "' on file '" << ticketPath << "'. Verify it yourself!";
+ }
+
+ af << ticket;
+ af.Commit();
+ }
+
+ /* If no parent connection was made, the user must supply the ca.crt before restarting Icinga 2.*/
+ if (!connectToParent) {
+ Log(LogWarning, "cli")
+ << "No connection to the parent node was specified.\n\n"
+ << "Please copy the public CA certificate from your master/satellite\n"
+ << "into '" << ca << "' before starting Icinga 2.\n";
+ } else {
+ Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
+ }
+
+ if (vm.count("disable-confd")) {
+ /* Disable conf.d inclusion */
+ NodeUtility::UpdateConfiguration("\"conf.d\"", false, true);
+ }
+
+ /* tell the user to reload icinga2 */
+ Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
+
+ return 0;
+}
diff --git a/lib/cli/nodesetupcommand.hpp b/lib/cli/nodesetupcommand.hpp
new file mode 100644
index 0000000..d25d21e
--- /dev/null
+++ b/lib/cli/nodesetupcommand.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NODESETUPCOMMAND_H
+#define NODESETUPCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "node setup" command.
+ *
+ * @ingroup cli
+ */
+class NodeSetupCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(NodeSetupCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+private:
+ static int SetupMaster(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap);
+ static int SetupNode(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap);
+};
+
+}
+
+#endif /* NODESETUPCOMMAND_H */
diff --git a/lib/cli/nodeutility.cpp b/lib/cli/nodeutility.cpp
new file mode 100644
index 0000000..523532a
--- /dev/null
+++ b/lib/cli/nodeutility.cpp
@@ -0,0 +1,378 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/nodeutility.hpp"
+#include "cli/clicommand.hpp"
+#include "cli/variableutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/json.hpp"
+#include "base/netstring.hpp"
+#include "base/stdiostream.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include "base/exception.hpp"
+#include "base/configwriter.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+
+using namespace icinga;
+
+String NodeUtility::GetConstantsConfPath()
+{
+ return Configuration::ConfigDir + "/constants.conf";
+}
+
+String NodeUtility::GetZonesConfPath()
+{
+ return Configuration::ConfigDir + "/zones.conf";
+}
+
+/*
+ * Node Setup helpers
+ */
+
+int NodeUtility::GenerateNodeIcingaConfig(const String& endpointName, const String& zoneName,
+ const String& parentZoneName, const std::vector<std::string>& endpoints,
+ const std::vector<String>& globalZones)
+{
+ Array::Ptr config = new Array();
+
+ Array::Ptr myParentZoneMembers = new Array();
+
+ for (const String& endpoint : endpoints) {
+ /* extract all --endpoint arguments and store host,port info */
+ std::vector<String> tokens = endpoint.Split(",");
+
+ Dictionary::Ptr myParentEndpoint = new Dictionary();
+
+ if (tokens.size() > 1) {
+ String host = tokens[1].Trim();
+
+ if (!host.IsEmpty())
+ myParentEndpoint->Set("host", host);
+ }
+
+ if (tokens.size() > 2) {
+ String port = tokens[2].Trim();
+
+ if (!port.IsEmpty())
+ myParentEndpoint->Set("port", port);
+ }
+
+ String myEndpointName = tokens[0].Trim();
+ myParentEndpoint->Set("__name", myEndpointName);
+ myParentEndpoint->Set("__type", "Endpoint");
+
+ /* save endpoint in master zone */
+ myParentZoneMembers->Add(myEndpointName);
+
+ config->Add(myParentEndpoint);
+ }
+
+ /* add the parent zone to the config */
+ config->Add(new Dictionary({
+ { "__name", parentZoneName },
+ { "__type", "Zone" },
+ { "endpoints", myParentZoneMembers }
+ }));
+
+ /* store the local generated node configuration */
+ config->Add(new Dictionary({
+ { "__name", endpointName },
+ { "__type", "Endpoint" }
+ }));
+
+ config->Add(new Dictionary({
+ { "__name", zoneName },
+ { "__type", "Zone" },
+ { "parent", parentZoneName },
+ { "endpoints", new Array({ endpointName }) }
+ }));
+
+ for (const String& globalzone : globalZones) {
+ config->Add(new Dictionary({
+ { "__name", globalzone },
+ { "__type", "Zone" },
+ { "global", true }
+ }));
+ }
+
+ /* Write the newly generated configuration. */
+ NodeUtility::WriteNodeConfigObjects(GetZonesConfPath(), config);
+
+ return 0;
+}
+
+int NodeUtility::GenerateNodeMasterIcingaConfig(const String& endpointName, const String& zoneName,
+ const std::vector<String>& globalZones)
+{
+ Array::Ptr config = new Array();
+
+ /* store the local generated node master configuration */
+ config->Add(new Dictionary({
+ { "__name", endpointName },
+ { "__type", "Endpoint" }
+ }));
+
+ config->Add(new Dictionary({
+ { "__name", zoneName },
+ { "__type", "Zone" },
+ { "endpoints", new Array({ endpointName }) }
+ }));
+
+ for (const String& globalzone : globalZones) {
+ config->Add(new Dictionary({
+ { "__name", globalzone },
+ { "__type", "Zone" },
+ { "global", true }
+ }));
+ }
+
+ /* Write the newly generated configuration. */
+ NodeUtility::WriteNodeConfigObjects(GetZonesConfPath(), config);
+
+ return 0;
+}
+
+bool NodeUtility::WriteNodeConfigObjects(const String& filename, const Array::Ptr& objects)
+{
+ Log(LogInformation, "cli")
+ << "Dumping config items to file '" << filename << "'.";
+
+ /* create a backup first */
+ CreateBackupFile(filename);
+
+ String path = Utility::DirName(filename);
+
+ Utility::MkDirP(path, 0755);
+
+ String user = Configuration::RunAsUser;
+ String group = Configuration::RunAsGroup;
+
+ if (!Utility::SetFileOwnership(path, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on path '" << path << "'. Verify it yourself!";
+ }
+
+ AtomicFile fp (filename, 0644);
+
+ fp << "/*\n";
+ fp << " * Generated by Icinga 2 node setup commands\n";
+ fp << " * on " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime()) << "\n";
+ fp << " */\n\n";
+
+ ObjectLock olock(objects);
+ for (const Dictionary::Ptr& object : objects) {
+ SerializeObject(fp, object);
+ }
+
+ fp << std::endl;
+ fp.Commit();
+
+ return true;
+}
+
+
+/*
+ * We generally don't overwrite files without backup before
+ */
+bool NodeUtility::CreateBackupFile(const String& target, bool isPrivate)
+{
+ if (!Utility::PathExists(target))
+ return false;
+
+ String backup = target + ".orig";
+
+ if (Utility::PathExists(backup)) {
+ Log(LogInformation, "cli")
+ << "Backup file '" << backup << "' already exists. Skipping backup.";
+ return false;
+ }
+
+ Utility::CopyFile(target, backup);
+
+#ifndef _WIN32
+ if (isPrivate)
+ chmod(backup.CStr(), 0600);
+#endif /* _WIN32 */
+
+ Log(LogInformation, "cli")
+ << "Created backup file '" << backup << "'.";
+
+ return true;
+}
+
+void NodeUtility::SerializeObject(std::ostream& fp, const Dictionary::Ptr& object)
+{
+ fp << "object ";
+ ConfigWriter::EmitIdentifier(fp, object->Get("__type"), false);
+ fp << " ";
+ ConfigWriter::EmitValue(fp, 0, object->Get("__name"));
+ fp << " {\n";
+
+ ObjectLock olock(object);
+ for (const Dictionary::Pair& kv : object) {
+ if (kv.first == "__type" || kv.first == "__name")
+ continue;
+
+ fp << "\t";
+ ConfigWriter::EmitIdentifier(fp, kv.first, true);
+ fp << " = ";
+ ConfigWriter::EmitValue(fp, 1, kv.second);
+ fp << "\n";
+ }
+
+ fp << "}\n\n";
+}
+
+/*
+* Returns true if the include is found, otherwise false
+*/
+bool NodeUtility::GetConfigurationIncludeState(const String& value, bool recursive) {
+ String configurationFile = Configuration::ConfigDir + "/icinga2.conf";
+
+ Log(LogInformation, "cli")
+ << "Reading '" << configurationFile << "'.";
+
+ std::ifstream ifp(configurationFile.CStr());
+
+ String affectedInclude = value;
+
+ if (recursive)
+ affectedInclude = "include_recursive " + affectedInclude;
+ else
+ affectedInclude = "include " + affectedInclude;
+
+ bool isIncluded = false;
+
+ std::string line;
+
+ while(std::getline(ifp, line)) {
+ /*
+ * Trying to find if the inclusion is enabled.
+ * First hit breaks out of the loop.
+ */
+
+ if (line.compare(0, affectedInclude.GetLength(), affectedInclude) == 0) {
+ isIncluded = true;
+
+ /*
+ * We can safely break out here, since an enabled include always win.
+ */
+ break;
+ }
+ }
+
+ ifp.close();
+
+ return isIncluded;
+}
+
+/*
+ * include = false, will comment out the include statement
+ * include = true, will add an include statement or uncomment a statement if one is existing
+ * resursive = false, will search for a non-resursive include statement
+ * recursive = true, will search for a resursive include statement
+ * Returns true on success, false if option was not found
+ */
+bool NodeUtility::UpdateConfiguration(const String& value, bool include, bool recursive)
+{
+ String configurationFile = Configuration::ConfigDir + "/icinga2.conf";
+
+ Log(LogInformation, "cli")
+ << "Updating '" << value << "' include in '" << configurationFile << "'.";
+
+ NodeUtility::CreateBackupFile(configurationFile);
+
+ std::ifstream ifp(configurationFile.CStr());
+ AtomicFile ofp (configurationFile, 0644);
+
+ String affectedInclude = value;
+
+ if (recursive)
+ affectedInclude = "include_recursive " + affectedInclude;
+ else
+ affectedInclude = "include " + affectedInclude;
+
+ bool found = false;
+
+ std::string line;
+
+ while (std::getline(ifp, line)) {
+ if (include) {
+ if (line.find("//" + affectedInclude) != std::string::npos || line.find("// " + affectedInclude) != std::string::npos) {
+ found = true;
+ ofp << "// Added by the node setup CLI command on "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime())
+ << "\n" + affectedInclude + "\n";
+ } else if (line.find(affectedInclude) != std::string::npos) {
+ found = true;
+
+ Log(LogInformation, "cli")
+ << "Include statement '" + affectedInclude + "' already set.";
+
+ ofp << line << "\n";
+ } else {
+ ofp << line << "\n";
+ }
+ } else {
+ if (line.find(affectedInclude) != std::string::npos) {
+ found = true;
+ ofp << "// Disabled by the node setup CLI command on "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime())
+ << "\n// " + affectedInclude + "\n";
+ } else {
+ ofp << line << "\n";
+ }
+ }
+ }
+
+ if (include && !found) {
+ ofp << "// Added by the node setup CLI command on "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime())
+ << "\n" + affectedInclude + "\n";
+ }
+
+ ifp.close();
+ ofp.Commit();
+
+ return (found || include);
+}
+
+void NodeUtility::UpdateConstant(const String& name, const String& value)
+{
+ String constantsConfPath = NodeUtility::GetConstantsConfPath();
+
+ Log(LogInformation, "cli")
+ << "Updating '" << name << "' constant in '" << constantsConfPath << "'.";
+
+ NodeUtility::CreateBackupFile(constantsConfPath);
+
+ std::ifstream ifp(constantsConfPath.CStr());
+ AtomicFile ofp (constantsConfPath, 0644);
+
+ bool found = false;
+
+ std::string line;
+ while (std::getline(ifp, line)) {
+ if (line.find("const " + name + " = ") != std::string::npos) {
+ ofp << "const " + name + " = \"" + value + "\"\n";
+ found = true;
+ } else
+ ofp << line << "\n";
+ }
+
+ if (!found)
+ ofp << "const " + name + " = \"" + value + "\"\n";
+
+ ifp.close();
+ ofp.Commit();
+}
diff --git a/lib/cli/nodeutility.hpp b/lib/cli/nodeutility.hpp
new file mode 100644
index 0000000..7016b6b
--- /dev/null
+++ b/lib/cli/nodeutility.hpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NODEUTILITY_H
+#define NODEUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "base/value.hpp"
+#include "base/string.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class NodeUtility
+{
+public:
+ static String GetConstantsConfPath();
+ static String GetZonesConfPath();
+
+ static bool CreateBackupFile(const String& target, bool isPrivate = false);
+
+ static bool WriteNodeConfigObjects(const String& filename, const Array::Ptr& objects);
+
+ static bool GetConfigurationIncludeState(const String& value, bool recursive);
+ static bool UpdateConfiguration(const String& value, bool include, bool recursive);
+ static void UpdateConstant(const String& name, const String& value);
+
+ /* node setup helpers */
+ static int GenerateNodeIcingaConfig(const String& endpointName, const String& zoneName,
+ const String& parentZoneName, const std::vector<std::string>& endpoints,
+ const std::vector<String>& globalZones);
+ static int GenerateNodeMasterIcingaConfig(const String& endpointName, const String& zoneName,
+ const std::vector<String>& globalZones);
+
+private:
+ NodeUtility();
+
+ static void SerializeObject(std::ostream& fp, const Dictionary::Ptr& object);
+};
+
+}
+
+#endif /* NODEUTILITY_H */
diff --git a/lib/cli/nodewizardcommand.cpp b/lib/cli/nodewizardcommand.cpp
new file mode 100644
index 0000000..3a3cd42
--- /dev/null
+++ b/lib/cli/nodewizardcommand.cpp
@@ -0,0 +1,815 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/nodewizardcommand.hpp"
+#include "cli/nodeutility.hpp"
+#include "cli/featureutility.hpp"
+#include "cli/apisetuputility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/case_conv.hpp>
+#include <iostream>
+#include <string>
+#include <fstream>
+#include <vector>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("node/wizard", NodeWizardCommand);
+
+String NodeWizardCommand::GetDescription() const
+{
+ return "Wizard for Icinga 2 node setup.";
+}
+
+String NodeWizardCommand::GetShortDescription() const
+{
+ return "wizard for node setup";
+}
+
+ImpersonationLevel NodeWizardCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+int NodeWizardCommand::GetMaxArguments() const
+{
+ return -1;
+}
+
+void NodeWizardCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("verbose", "increase log level");
+}
+
+/**
+ * The entry point for the "node wizard" CLI command.
+ *
+ * @returns An exit status.
+ */
+int NodeWizardCommand::Run(const boost::program_options::variables_map& vm,
+ const std::vector<std::string>& ap) const
+{
+ if (!vm.count("verbose"))
+ Logger::SetConsoleLogSeverity(LogCritical);
+
+ /*
+ * The wizard will get all information from the user,
+ * and then call all required functions.
+ */
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundBlue)
+ << "Welcome to the Icinga 2 Setup Wizard!\n"
+ << "\n"
+ << "We will guide you through all required configuration details.\n"
+ << "\n"
+ << ConsoleColorTag(Console_Normal);
+
+ /* 0. master or node setup?
+ * 1. Ticket
+ * 2. Master information for autosigning
+ * 3. Trusted cert location
+ * 4. CN to use (defaults to FQDN)
+ * 5. Local CA
+ * 6. New self signed certificate
+ * 7. Request signed certificate from master
+ * 8. copy key information to /var/lib/icinga2/certs
+ * 9. enable ApiListener feature
+ * 10. generate zones.conf with endpoints and zone objects
+ * 11. set NodeName = cn and ZoneName in constants.conf
+ * 12. disable conf.d directory?
+ * 13. reload icinga2, or tell the user to
+ */
+
+ std::string answer;
+ /* master or satellite/agent setup */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify if this is an agent/satellite setup "
+ << "('n' installs a master setup)" << ConsoleColorTag(Console_Normal)
+ << " [Y/n]: ";
+ std::getline (std::cin, answer);
+
+ boost::algorithm::to_lower(answer);
+
+ String choice = answer;
+
+ std::cout << "\n";
+
+ int res = 0;
+
+ if (choice.Contains("n"))
+ res = MasterSetup();
+ else
+ res = AgentSatelliteSetup();
+
+ if (res != 0)
+ return res;
+
+ std::cout << "\n";
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Done.\n\n"
+ << ConsoleColorTag(Console_Normal);
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundRed)
+ << "Now restart your Icinga 2 daemon to finish the installation!\n"
+ << ConsoleColorTag(Console_Normal);
+
+ return 0;
+}
+
+int NodeWizardCommand::AgentSatelliteSetup() const
+{
+ std::string answer;
+ String choice;
+ bool connectToParent = false;
+
+ std::cout << "Starting the Agent/Satellite setup routine...\n\n";
+
+ /* CN */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the common name (CN)"
+ << ConsoleColorTag(Console_Normal)
+ << " [" << Utility::GetFQDN() << "]: ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = Utility::GetFQDN();
+
+ String cn = answer;
+ cn = cn.Trim();
+
+ std::vector<std::string> endpoints;
+
+ String endpointBuffer;
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "\nPlease specify the parent endpoint(s) (master or satellite) where this node should connect to:"
+ << ConsoleColorTag(Console_Normal) << "\n";
+ String parentEndpointName;
+
+wizard_endpoint_loop_start:
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Master/Satellite Common Name" << ConsoleColorTag(Console_Normal)
+ << " (CN from your master/satellite node): ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ Log(LogWarning, "cli", "Master/Satellite CN is required! Please retry.");
+ goto wizard_endpoint_loop_start;
+ }
+
+ endpointBuffer = answer;
+ endpointBuffer = endpointBuffer.Trim();
+
+ std::cout << "\nDo you want to establish a connection to the parent node "
+ << ConsoleColorTag(Console_Bold) << "from this node?"
+ << ConsoleColorTag(Console_Normal) << " [Y/n]: ";
+
+ std::getline (std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ String parentEndpointPort = "5665";
+
+ if (choice.Contains("n")) {
+ connectToParent = false;
+
+ Log(LogWarning, "cli", "Node to master/satellite connection setup skipped");
+ std::cout << "Connection setup skipped. Please configure your parent node to\n"
+ << "connect to this node by setting the 'host' attribute for the node Endpoint object.\n";
+
+ } else {
+ connectToParent = true;
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the master/satellite connection information:"
+ << ConsoleColorTag(Console_Normal) << "\n"
+ << ConsoleColorTag(Console_Bold) << "Master/Satellite endpoint host"
+ << ConsoleColorTag(Console_Normal) << " (IP address or FQDN): ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ Log(LogWarning, "cli", "Please enter the parent endpoint (master/satellite) connection information.");
+ goto wizard_endpoint_loop_start;
+ }
+
+ String tmp = answer;
+ tmp = tmp.Trim();
+
+ endpointBuffer += "," + tmp;
+ parentEndpointName = tmp;
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Master/Satellite endpoint port" << ConsoleColorTag(Console_Normal)
+ << " [" << parentEndpointPort << "]: ";
+
+ std::getline(std::cin, answer);
+
+ if (!answer.empty())
+ parentEndpointPort = answer;
+
+ endpointBuffer += "," + parentEndpointPort.Trim();
+ }
+
+ endpoints.push_back(endpointBuffer);
+
+ std::cout << ConsoleColorTag(Console_Bold) << "\nAdd more master/satellite endpoints?"
+ << ConsoleColorTag(Console_Normal) << " [y/N]: ";
+ std::getline (std::cin, answer);
+
+ boost::algorithm::to_lower(answer);
+
+ choice = answer;
+
+ if (choice.Contains("y"))
+ goto wizard_endpoint_loop_start;
+
+ /* Extract parent node information. */
+ String parentHost, parentPort;
+
+ for (const String& endpoint : endpoints) {
+ std::vector<String> tokens = endpoint.Split(",");
+
+ if (tokens.size() > 1)
+ parentHost = tokens[1];
+
+ if (tokens.size() > 2)
+ parentPort = tokens[2];
+ }
+
+ /* workaround for fetching the master cert */
+ String certsDir = ApiListener::GetCertsDir();
+ Utility::MkDirP(certsDir, 0700);
+
+ String user = Configuration::RunAsUser;
+ String group = Configuration::RunAsGroup;
+
+ if (!Utility::SetFileOwnership(certsDir, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group
+ << "' on file '" << certsDir << "'. Verify it yourself!";
+ }
+
+ String nodeCert = certsDir + "/" + cn + ".crt";
+ String nodeKey = certsDir + "/" + cn + ".key";
+
+ if (Utility::PathExists(nodeKey))
+ NodeUtility::CreateBackupFile(nodeKey, true);
+ if (Utility::PathExists(nodeCert))
+ NodeUtility::CreateBackupFile(nodeCert);
+
+ if (PkiUtility::NewCert(cn, nodeKey, Empty, nodeCert) > 0) {
+ Log(LogCritical, "cli")
+ << "Failed to create new self-signed certificate for CN '"
+ << cn << "'. Please try again.";
+ return 1;
+ }
+
+ /* fix permissions: root -> icinga daemon user */
+ if (!Utility::SetFileOwnership(nodeKey, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group
+ << "' on file '" << nodeKey << "'. Verify it yourself!";
+ }
+
+ std::shared_ptr<X509> trustedParentCert;
+
+ /* Check whether we should connect to the parent node and present its trusted certificate. */
+ if (connectToParent) {
+ //save-cert and store the master certificate somewhere
+ Log(LogInformation, "cli")
+ << "Fetching public certificate from master ("
+ << parentHost << ", " << parentPort << "):\n";
+
+ trustedParentCert = PkiUtility::FetchCert(parentHost, parentPort);
+ if (!trustedParentCert) {
+ Log(LogCritical, "cli", "Peer did not present a valid certificate.");
+ return 1;
+ }
+
+ std::cout << ConsoleColorTag(Console_Bold) << "Parent certificate information:\n"
+ << ConsoleColorTag(Console_Normal) << PkiUtility::GetCertificateInformation(trustedParentCert)
+ << ConsoleColorTag(Console_Bold) << "\nIs this information correct?"
+ << ConsoleColorTag(Console_Normal) << " [y/N]: ";
+
+ std::getline (std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ if (answer != "y") {
+ Log(LogWarning, "cli", "Process aborted.");
+ return 1;
+ }
+
+ Log(LogInformation, "cli", "Received trusted parent certificate.\n");
+ }
+
+wizard_ticket:
+ String nodeCA = certsDir + "/ca.crt";
+ String ticket;
+
+ /* Check whether we can connect to the parent node and fetch the client and CA certificate. */
+ if (connectToParent) {
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "\nPlease specify the request ticket generated on your Icinga 2 master "
+ << ConsoleColorTag(Console_Normal) << "(optional)"
+ << ConsoleColorTag(Console_Bold) << "."
+ << ConsoleColorTag(Console_Normal) << "\n"
+ << " (Hint: # icinga2 pki ticket --cn '" << cn << "'): ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ std::cout << ConsoleColorTag(Console_Bold) << "\n"
+ << "No ticket was specified. Please approve the certificate signing request manually\n"
+ << "on the master (see 'icinga2 ca list' and 'icinga2 ca sign --help' for details)."
+ << ConsoleColorTag(Console_Normal) << "\n";
+ }
+
+ ticket = answer;
+ ticket = ticket.Trim();
+
+ if (ticket.IsEmpty()) {
+ Log(LogInformation, "cli")
+ << "Requesting certificate without a ticket.";
+ } else {
+ Log(LogInformation, "cli")
+ << "Requesting certificate with ticket '" << ticket << "'.";
+ }
+
+ if (Utility::PathExists(nodeCA))
+ NodeUtility::CreateBackupFile(nodeCA);
+ if (Utility::PathExists(nodeCert))
+ NodeUtility::CreateBackupFile(nodeCert);
+
+ if (PkiUtility::RequestCertificate(parentHost, parentPort, nodeKey,
+ nodeCert, nodeCA, trustedParentCert, ticket) > 0) {
+ Log(LogCritical, "cli")
+ << "Failed to fetch signed certificate from master '"
+ << parentHost << ", "
+ << parentPort << "'. Please try again.";
+ goto wizard_ticket;
+ }
+
+ /* fix permissions (again) when updating the signed certificate */
+ if (!Utility::SetFileOwnership(nodeCert, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group << "' on file '"
+ << nodeCert << "'. Verify it yourself!";
+ }
+ } else {
+ /* We cannot retrieve the parent certificate.
+ * Tell the user to manually copy the ca.crt file
+ * into DataDir + "/certs"
+ */
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "\nNo connection to the parent node was specified.\n\n"
+ << "Please copy the public CA certificate from your master/satellite\n"
+ << "into '" << nodeCA << "' before starting Icinga 2.\n"
+ << ConsoleColorTag(Console_Normal);
+
+ if (Utility::PathExists(nodeCA)) {
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "\nFound public CA certificate in '" << nodeCA << "'.\n"
+ << "Please verify that it is the same as on your master/satellite.\n"
+ << ConsoleColorTag(Console_Normal);
+ }
+
+ }
+
+ /* apilistener config */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the API bind host/port "
+ << ConsoleColorTag(Console_Normal) << "(optional)"
+ << ConsoleColorTag(Console_Bold) << ":\n";
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Bind Host" << ConsoleColorTag(Console_Normal) << " []: ";
+
+ std::getline(std::cin, answer);
+
+ String bindHost = answer;
+ bindHost = bindHost.Trim();
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Bind Port" << ConsoleColorTag(Console_Normal) << " []: ";
+
+ std::getline(std::cin, answer);
+
+ String bindPort = answer;
+ bindPort = bindPort.Trim();
+
+ std::cout << ConsoleColorTag(Console_Bold) << "\n"
+ << "Accept config from parent node?" << ConsoleColorTag(Console_Normal)
+ << " [y/N]: ";
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ String acceptConfig = choice.Contains("y") ? "true" : "false";
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Accept commands from parent node?" << ConsoleColorTag(Console_Normal)
+ << " [y/N]: ";
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ String acceptCommands = choice.Contains("y") ? "true" : "false";
+
+ std::cout << "\n";
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Reconfiguring Icinga...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ /* disable the notifications feature on agent/satellite nodes */
+ Log(LogInformation, "cli", "Disabling the Notification feature.");
+
+ FeatureUtility::DisableFeatures({ "notification" });
+
+ Log(LogInformation, "cli", "Enabling the ApiListener feature.");
+
+ FeatureUtility::EnableFeatures({ "api" });
+
+ String apiConfPath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
+ NodeUtility::CreateBackupFile(apiConfPath);
+
+ AtomicFile fp (apiConfPath, 0644);
+
+ fp << "/**\n"
+ << " * The API listener is used for distributed monitoring setups.\n"
+ << " */\n"
+ << "object ApiListener \"api\" {\n"
+ << " accept_config = " << acceptConfig << "\n"
+ << " accept_commands = " << acceptCommands << "\n";
+
+ if (!bindHost.IsEmpty())
+ fp << " bind_host = \"" << bindHost << "\"\n";
+ if (!bindPort.IsEmpty())
+ fp << " bind_port = " << bindPort << "\n";
+
+ fp << "}\n";
+
+ fp.Commit();
+
+ /* Zones configuration. */
+ Log(LogInformation, "cli", "Generating local zones.conf.");
+
+ /* Setup command hardcodes this as FQDN */
+ String endpointName = cn;
+
+ /* Different local zone name. */
+ std::cout << "\nLocal zone name [" + endpointName + "]: ";
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = endpointName;
+
+ String zoneName = answer;
+ zoneName = zoneName.Trim();
+
+ /* Different parent zone name. */
+ std::cout << "Parent zone name [master]: ";
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = "master";
+
+ String parentZoneName = answer;
+ parentZoneName = parentZoneName.Trim();
+
+ /* Global zones. */
+ std::vector<String> globalZones { "global-templates", "director-global" };
+
+ std::cout << "\nDefault global zones: " << boost::algorithm::join(globalZones, " ");
+ std::cout << "\nDo you want to specify additional global zones? [y/N]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+wizard_global_zone_loop_start:
+ if (choice.Contains("y")) {
+ std::cout << "\nPlease specify the name of the global Zone: ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ std::cout << "\nName of the global Zone is required! Please retry.";
+ goto wizard_global_zone_loop_start;
+ }
+
+ String globalZoneName = answer;
+ globalZoneName = globalZoneName.Trim();
+
+ if (std::find(globalZones.begin(), globalZones.end(), globalZoneName) != globalZones.end()) {
+ std::cout << "The global zone '" << globalZoneName << "' is already specified."
+ << " Please retry.";
+ goto wizard_global_zone_loop_start;
+ }
+
+ globalZones.push_back(globalZoneName);
+
+ std::cout << "\nDo you want to specify another global zone? [y/N]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ if (choice.Contains("y"))
+ goto wizard_global_zone_loop_start;
+ } else
+ Log(LogInformation, "cli", "No additional global Zones have been specified");
+
+ /* Generate node configuration. */
+ NodeUtility::GenerateNodeIcingaConfig(endpointName, zoneName, parentZoneName, endpoints, globalZones);
+
+ if (endpointName != Utility::GetFQDN()) {
+ Log(LogWarning, "cli")
+ << "CN/Endpoint name '" << endpointName << "' does not match the default FQDN '"
+ << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!";
+ }
+
+ NodeUtility::UpdateConstant("NodeName", endpointName);
+ NodeUtility::UpdateConstant("ZoneName", zoneName);
+
+ if (!ticket.IsEmpty()) {
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+ AtomicFile af (ticketPath, 0600);
+
+ if (!Utility::SetFileOwnership(af.GetTempFilename(), user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group
+ << "' on file '" << ticketPath << "'. Verify it yourself!";
+ }
+
+ af << ticket;
+ af.Commit();
+ }
+
+ /* If no parent connection was made, the user must supply the ca.crt before restarting Icinga 2.*/
+ if (!connectToParent) {
+ Log(LogWarning, "cli")
+ << "No connection to the parent node was specified.\n\n"
+ << "Please copy the public CA certificate from your master/satellite\n"
+ << "into '" << nodeCA << "' before starting Icinga 2.\n";
+ } else {
+ Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
+ }
+
+ /* Disable conf.d inclusion */
+ std::cout << "\nDo you want to disable the inclusion of the conf.d directory [Y/n]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ if (choice.Contains("n"))
+ Log(LogInformation, "cli")
+ << "conf.d directory has not been disabled.";
+ else {
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Disabling the inclusion of the conf.d directory...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ if (!NodeUtility::UpdateConfiguration("\"conf.d\"", false, true)) {
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundRed)
+ << "Failed to disable the conf.d inclusion, it may already have been disabled.\n"
+ << ConsoleColorTag(Console_Normal);
+ }
+
+ /* Satellite/Agents should not include the api-users.conf file.
+ * The configuration should instead be managed via config sync or automation tools.
+ */
+ }
+
+ return 0;
+}
+
+int NodeWizardCommand::MasterSetup() const
+{
+ std::string answer;
+ String choice;
+
+ std::cout << ConsoleColorTag(Console_Bold) << "Starting the Master setup routine...\n\n";
+
+ /* CN */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the common name" << ConsoleColorTag(Console_Normal)
+ << " (CN) [" << Utility::GetFQDN() << "]: ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = Utility::GetFQDN();
+
+ String cn = answer;
+ cn = cn.Trim();
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Reconfiguring Icinga...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ /* check whether the user wants to generate a new certificate or not */
+ String existing_path = ApiListener::GetCertsDir() + "/" + cn + ".crt";
+
+ std::cout << ConsoleColorTag(Console_Normal)
+ << "Checking for existing certificates for common name '" << cn << "'...\n";
+
+ if (Utility::PathExists(existing_path)) {
+ std::cout << "Certificate '" << existing_path << "' for CN '"
+ << cn << "' already existing. Skipping certificate generation.\n";
+ } else {
+ std::cout << "Certificates not yet generated. Running 'api setup' now.\n";
+ ApiSetupUtility::SetupMasterCertificates(cn);
+ }
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Generating master configuration for Icinga 2.\n"
+ << ConsoleColorTag(Console_Normal);
+
+ ApiSetupUtility::SetupMasterApiUser();
+
+ if (!FeatureUtility::CheckFeatureEnabled("api"))
+ ApiSetupUtility::SetupMasterEnableApi();
+ else
+ std::cout << "'api' feature already enabled.\n";
+
+ /* Setup command hardcodes this as FQDN */
+ String endpointName = cn;
+
+ /* Different zone name. */
+ std::cout << "\nMaster zone name [master]: ";
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = "master";
+
+ String zoneName = answer;
+ zoneName = zoneName.Trim();
+
+ /* Global zones. */
+ std::vector<String> globalZones { "global-templates", "director-global" };
+
+ std::cout << "\nDefault global zones: " << boost::algorithm::join(globalZones, " ");
+ std::cout << "\nDo you want to specify additional global zones? [y/N]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+wizard_global_zone_loop_start:
+ if (choice.Contains("y")) {
+ std::cout << "\nPlease specify the name of the global Zone: ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ std::cout << "\nName of the global Zone is required! Please retry.";
+ goto wizard_global_zone_loop_start;
+ }
+
+ String globalZoneName = answer;
+ globalZoneName = globalZoneName.Trim();
+
+ if (std::find(globalZones.begin(), globalZones.end(), globalZoneName) != globalZones.end()) {
+ std::cout << "The global zone '" << globalZoneName << "' is already specified."
+ << " Please retry.";
+ goto wizard_global_zone_loop_start;
+ }
+
+ globalZones.push_back(globalZoneName);
+
+ std::cout << "\nDo you want to specify another global zone? [y/N]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ if (choice.Contains("y"))
+ goto wizard_global_zone_loop_start;
+ } else
+ Log(LogInformation, "cli", "No additional global Zones have been specified");
+
+ /* Generate master configuration. */
+ NodeUtility::GenerateNodeMasterIcingaConfig(endpointName, zoneName, globalZones);
+
+ /* apilistener config */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the API bind host/port "
+ << ConsoleColorTag(Console_Normal) << "(optional)"
+ << ConsoleColorTag(Console_Bold) << ":\n";
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Bind Host" << ConsoleColorTag(Console_Normal) << " []: ";
+
+ std::getline(std::cin, answer);
+
+ String bindHost = answer;
+ bindHost = bindHost.Trim();
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Bind Port" << ConsoleColorTag(Console_Normal) << " []: ";
+
+ std::getline(std::cin, answer);
+
+ String bindPort = answer;
+ bindPort = bindPort.Trim();
+
+ /* api feature is always enabled, check above */
+ String apiConfPath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
+ NodeUtility::CreateBackupFile(apiConfPath);
+
+ AtomicFile fp (apiConfPath, 0644);
+
+ fp << "/**\n"
+ << " * The API listener is used for distributed monitoring setups.\n"
+ << " */\n"
+ << "object ApiListener \"api\" {\n";
+
+ if (!bindHost.IsEmpty())
+ fp << " bind_host = \"" << bindHost << "\"\n";
+ if (!bindPort.IsEmpty())
+ fp << " bind_port = " << bindPort << "\n";
+
+ fp << "\n"
+ << " ticket_salt = TicketSalt\n"
+ << "}\n";
+
+ fp.Commit();
+
+ /* update constants.conf with NodeName = CN + TicketSalt = random value */
+ if (cn != Utility::GetFQDN()) {
+ Log(LogWarning, "cli")
+ << "CN '" << cn << "' does not match the default FQDN '"
+ << Utility::GetFQDN() << "'. Requires an update for the NodeName constant in constants.conf!";
+ }
+
+ Log(LogInformation, "cli", "Updating constants.conf.");
+
+ NodeUtility::CreateBackupFile(NodeUtility::GetConstantsConfPath());
+
+ NodeUtility::UpdateConstant("NodeName", endpointName);
+ NodeUtility::UpdateConstant("ZoneName", zoneName);
+
+ String salt = RandomString(16);
+
+ NodeUtility::UpdateConstant("TicketSalt", salt);
+
+ /* Disable conf.d inclusion */
+ std::cout << "\nDo you want to disable the inclusion of the conf.d directory [Y/n]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ if (choice.Contains("n"))
+ Log(LogInformation, "cli")
+ << "conf.d directory has not been disabled.";
+ else {
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Disabling the inclusion of the conf.d directory...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ if (!NodeUtility::UpdateConfiguration("\"conf.d\"", false, true)) {
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundRed)
+ << "Failed to disable the conf.d inclusion, it may already have been disabled.\n"
+ << ConsoleColorTag(Console_Normal);
+ }
+
+ /* Include api-users.conf */
+ String apiUsersFilePath = Configuration::ConfigDir + "/conf.d/api-users.conf";
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Checking if the api-users.conf file exists...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ if (Utility::PathExists(apiUsersFilePath)) {
+ NodeUtility::UpdateConfiguration("\"conf.d/api-users.conf\"", true, false);
+ } else {
+ Log(LogWarning, "cli")
+ << "Included file '" << apiUsersFilePath << "' does not exist.";
+ }
+ }
+
+ return 0;
+}
diff --git a/lib/cli/nodewizardcommand.hpp b/lib/cli/nodewizardcommand.hpp
new file mode 100644
index 0000000..dfda70c
--- /dev/null
+++ b/lib/cli/nodewizardcommand.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NODEWIZARDCOMMAND_H
+#define NODEWIZARDCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "node wizard" command.
+ *
+ * @ingroup cli
+ */
+class NodeWizardCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(NodeWizardCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMaxArguments() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+
+private:
+ int AgentSatelliteSetup() const;
+ int MasterSetup() const;
+};
+
+}
+
+#endif /* NODEWIZARDCOMMAND_H */
diff --git a/lib/cli/objectlistcommand.cpp b/lib/cli/objectlistcommand.cpp
new file mode 100644
index 0000000..3bcb315
--- /dev/null
+++ b/lib/cli/objectlistcommand.cpp
@@ -0,0 +1,145 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/objectlistcommand.hpp"
+#include "cli/objectlistutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/convert.hpp"
+#include "base/configobject.hpp"
+#include "base/configtype.hpp"
+#include "base/json.hpp"
+#include "base/netstring.hpp"
+#include "base/stdiostream.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+#include <iomanip>
+#include <sys/stat.h>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("object/list", ObjectListCommand);
+
+String ObjectListCommand::GetDescription() const
+{
+ return "Lists all Icinga 2 objects.";
+}
+
+String ObjectListCommand::GetShortDescription() const
+{
+ return "lists all objects";
+}
+
+void ObjectListCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("count,c", "display object counts by types")
+ ("name,n", po::value<std::string>(), "filter by name matches")
+ ("type,t", po::value<std::string>(), "filter by type matches");
+}
+
+static time_t GetCtime(const String& path)
+{
+#ifdef _WIN32
+ struct _stat statbuf;
+ int rc = _stat(path.CStr(), &statbuf);
+#else /* _WIN32 */
+ struct stat statbuf;
+ int rc = stat(path.CStr(), &statbuf);
+#endif /* _WIN32 */
+
+ return rc ? 0 : statbuf.st_ctime;
+}
+
+/**
+ * The entry point for the "object list" CLI command.
+ *
+ * @returns An exit status.
+ */
+int ObjectListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String objectfile = Configuration::ObjectsPath;
+
+ if (!Utility::PathExists(objectfile)) {
+ Log(LogCritical, "cli")
+ << "Cannot open objects file '" << Configuration::ObjectsPath << "'.";
+ Log(LogCritical, "cli", "Run 'icinga2 daemon -C --dump-objects' to validate config and generate the cache file.");
+ return 1;
+ }
+
+ std::fstream fp;
+ fp.open(objectfile.CStr(), std::ios_base::in);
+
+ StdioStream::Ptr sfp = new StdioStream(&fp, false);
+ unsigned long objects_count = 0;
+ std::map<String, int> type_count;
+
+ String name_filter, type_filter;
+
+ if (vm.count("name"))
+ name_filter = vm["name"].as<std::string>();
+ if (vm.count("type"))
+ type_filter = vm["type"].as<std::string>();
+
+ bool first = true;
+
+ String message;
+ StreamReadContext src;
+ for (;;) {
+ StreamReadStatus srs = NetString::ReadStringFromStream(sfp, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ ObjectListUtility::PrintObject(std::cout, first, message, type_count, name_filter, type_filter);
+ objects_count++;
+ }
+
+ sfp->Close();
+ fp.close();
+
+ if (vm.count("count")) {
+ if (!first)
+ std::cout << "\n";
+
+ PrintTypeCounts(std::cout, type_count);
+ std::cout << "\n";
+ }
+
+ Log(LogNotice, "cli")
+ << "Parsed " << objects_count << " objects.";
+
+ auto objectsPathCtime (GetCtime(Configuration::ObjectsPath));
+ auto varsPathCtime (GetCtime(Configuration::VarsPath));
+
+ if (objectsPathCtime < varsPathCtime) {
+ Log(LogWarning, "cli")
+ << "This data is " << Utility::FormatDuration(varsPathCtime - objectsPathCtime)
+ << " older than the last Icinga config (re)load. It may be outdated. Consider running 'icinga2 daemon -C --dump-objects' first.";
+ }
+
+ return 0;
+}
+
+void ObjectListCommand::PrintTypeCounts(std::ostream& fp, const std::map<String, int>& type_count)
+{
+ typedef std::map<String, int>::value_type TypeCount;
+
+ for (const TypeCount& kv : type_count) {
+ fp << "Found " << kv.second << " " << kv.first << " object";
+
+ if (kv.second != 1)
+ fp << "s";
+
+ fp << ".\n";
+ }
+}
diff --git a/lib/cli/objectlistcommand.hpp b/lib/cli/objectlistcommand.hpp
new file mode 100644
index 0000000..bafe3ec
--- /dev/null
+++ b/lib/cli/objectlistcommand.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTLISTCOMMAND_H
+#define OBJECTLISTCOMMAND_H
+
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "cli/clicommand.hpp"
+#include <ostream>
+
+namespace icinga
+{
+
+/**
+ * The "object list" command.
+ *
+ * @ingroup cli
+ */
+class ObjectListCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ObjectListCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+private:
+ static void PrintTypeCounts(std::ostream& fp, const std::map<String, int>& type_count);
+};
+
+}
+
+#endif /* OBJECTLISTCOMMAND_H */
diff --git a/lib/cli/objectlistutility.cpp b/lib/cli/objectlistutility.cpp
new file mode 100644
index 0000000..a8135d9
--- /dev/null
+++ b/lib/cli/objectlistutility.cpp
@@ -0,0 +1,155 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/objectlistutility.hpp"
+#include "base/json.hpp"
+#include "base/utility.hpp"
+#include "base/console.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include <iostream>
+#include <iomanip>
+
+using namespace icinga;
+
+bool ObjectListUtility::PrintObject(std::ostream& fp, bool& first, const String& message, std::map<String, int>& type_count, const String& name_filter, const String& type_filter)
+{
+ Dictionary::Ptr object = JsonDecode(message);
+
+ Dictionary::Ptr properties = object->Get("properties");
+
+ String internal_name = properties->Get("__name");
+ String name = object->Get("name");
+ String type = object->Get("type");
+
+ if (!name_filter.IsEmpty() && !Utility::Match(name_filter, name) && !Utility::Match(name_filter, internal_name))
+ return false;
+ if (!type_filter.IsEmpty() && !Utility::Match(type_filter, type))
+ return false;
+
+ if (first)
+ first = false;
+ else
+ fp << "\n";
+
+ Dictionary::Ptr debug_hints = object->Get("debug_hints");
+
+ fp << "Object '" << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << internal_name << ConsoleColorTag(Console_Normal) << "'";
+ fp << " of type '" << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << type << ConsoleColorTag(Console_Normal) << "':\n";
+
+ Array::Ptr di = object->Get("debug_info");
+
+ if (di) {
+ fp << ConsoleColorTag(Console_ForegroundCyan) << " % declared in '" << di->Get(0) << "', lines "
+ << di->Get(1) << ":" << di->Get(2) << "-" << di->Get(3) << ":" << di->Get(4) << ConsoleColorTag(Console_Normal) << "\n";
+ }
+
+ PrintProperties(fp, properties, debug_hints, 2);
+
+ type_count[type]++;
+ return true;
+}
+
+void ObjectListUtility::PrintProperties(std::ostream& fp, const Dictionary::Ptr& props, const Dictionary::Ptr& debug_hints, int indent)
+{
+ /* get debug hint props */
+ Dictionary::Ptr debug_hint_props;
+ if (debug_hints)
+ debug_hint_props = debug_hints->Get("properties");
+
+ int offset = 2;
+
+ ObjectLock olock(props);
+ for (const Dictionary::Pair& kv : props)
+ {
+ String key = kv.first;
+ Value val = kv.second;
+
+ /* key & value */
+ fp << std::setw(indent) << " " << "* " << ConsoleColorTag(Console_ForegroundGreen) << key << ConsoleColorTag(Console_Normal);
+
+ /* extract debug hints for key */
+ Dictionary::Ptr debug_hints_fwd;
+ if (debug_hint_props)
+ debug_hints_fwd = debug_hint_props->Get(key);
+
+ /* print dicts recursively */
+ if (val.IsObjectType<Dictionary>()) {
+ fp << "\n";
+ PrintHints(fp, debug_hints_fwd, indent + offset);
+ PrintProperties(fp, val, debug_hints_fwd, indent + offset);
+ } else {
+ fp << " = ";
+ PrintValue(fp, val);
+ fp << "\n";
+ PrintHints(fp, debug_hints_fwd, indent + offset);
+ }
+ }
+}
+
+void ObjectListUtility::PrintHints(std::ostream& fp, const Dictionary::Ptr& debug_hints, int indent)
+{
+ if (!debug_hints)
+ return;
+
+ Array::Ptr messages = debug_hints->Get("messages");
+
+ if (messages) {
+ ObjectLock olock(messages);
+
+ for (const Value& msg : messages)
+ {
+ PrintHint(fp, msg, indent);
+ }
+ }
+}
+
+void ObjectListUtility::PrintHint(std::ostream& fp, const Array::Ptr& msg, int indent)
+{
+ fp << std::setw(indent) << " " << ConsoleColorTag(Console_ForegroundCyan) << "% " << msg->Get(0) << " modified in '" << msg->Get(1) << "', lines "
+ << msg->Get(2) << ":" << msg->Get(3) << "-" << msg->Get(4) << ":" << msg->Get(5) << ConsoleColorTag(Console_Normal) << "\n";
+}
+
+void ObjectListUtility::PrintValue(std::ostream& fp, const Value& val)
+{
+ if (val.IsObjectType<Array>()) {
+ PrintArray(fp, val);
+ return;
+ }
+
+ if (val.IsString()) {
+ fp << "\"" << Convert::ToString(val) << "\"";
+ return;
+ }
+
+ if (val.IsEmpty()) {
+ fp << "null";
+ return;
+ }
+
+ fp << Convert::ToString(val);
+}
+
+void ObjectListUtility::PrintArray(std::ostream& fp, const Array::Ptr& arr)
+{
+ bool first = true;
+
+ fp << "[ ";
+
+ if (arr) {
+ ObjectLock olock(arr);
+ for (const Value& value : arr)
+ {
+ if (first)
+ first = false;
+ else
+ fp << ", ";
+
+ PrintValue(fp, value);
+ }
+ }
+
+ if (!first)
+ fp << " ";
+
+ fp << "]";
+}
diff --git a/lib/cli/objectlistutility.hpp b/lib/cli/objectlistutility.hpp
new file mode 100644
index 0000000..ee1b97c
--- /dev/null
+++ b/lib/cli/objectlistutility.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTLISTUTILITY_H
+#define OBJECTLISTUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "base/value.hpp"
+#include "base/string.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class ObjectListUtility
+{
+public:
+ static bool PrintObject(std::ostream& fp, bool& first, const String& message, std::map<String, int>& type_count, const String& name_filter, const String& type_filter);
+
+private:
+ static void PrintProperties(std::ostream& fp, const Dictionary::Ptr& props, const Dictionary::Ptr& debug_hints, int indent);
+ static void PrintHints(std::ostream& fp, const Dictionary::Ptr& debug_hints, int indent);
+ static void PrintHint(std::ostream& fp, const Array::Ptr& msg, int indent);
+ static void PrintValue(std::ostream& fp, const Value& val);
+ static void PrintArray(std::ostream& fp, const Array::Ptr& arr);
+};
+
+}
+
+#endif /* OBJECTLISTUTILITY_H */
diff --git a/lib/cli/pkinewcacommand.cpp b/lib/cli/pkinewcacommand.cpp
new file mode 100644
index 0000000..eba08c6
--- /dev/null
+++ b/lib/cli/pkinewcacommand.cpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkinewcacommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_CLICOMMAND("pki/new-ca", PKINewCACommand);
+
+String PKINewCACommand::GetDescription() const
+{
+ return "Sets up a new Certificate Authority.";
+}
+
+String PKINewCACommand::GetShortDescription() const
+{
+ return "sets up a new CA";
+}
+
+/**
+ * The entry point for the "pki new-ca" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKINewCACommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ return PkiUtility::NewCa();
+}
diff --git a/lib/cli/pkinewcacommand.hpp b/lib/cli/pkinewcacommand.hpp
new file mode 100644
index 0000000..5b1bff6
--- /dev/null
+++ b/lib/cli/pkinewcacommand.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKINEWCACOMMAND_H
+#define PKINEWCACOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki new-ca" command.
+ *
+ * @ingroup cli
+ */
+class PKINewCACommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKINewCACommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKINEWCACOMMAND_H */
diff --git a/lib/cli/pkinewcertcommand.cpp b/lib/cli/pkinewcertcommand.cpp
new file mode 100644
index 0000000..5201d92
--- /dev/null
+++ b/lib/cli/pkinewcertcommand.cpp
@@ -0,0 +1,66 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkinewcertcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/new-cert", PKINewCertCommand);
+
+String PKINewCertCommand::GetDescription() const
+{
+ return "Creates a new Certificate Signing Request, a self-signed X509 certificate or both.";
+}
+
+String PKINewCertCommand::GetShortDescription() const
+{
+ return "creates a new CSR";
+}
+
+void PKINewCertCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("cn", po::value<std::string>(), "Common Name")
+ ("key", po::value<std::string>(), "Key file path (output)")
+ ("csr", po::value<std::string>(), "CSR file path (optional, output)")
+ ("cert", po::value<std::string>(), "Certificate file path (optional, output)");
+}
+
+std::vector<String> PKINewCertCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "key" || argument == "csr" || argument == "cert")
+ return GetBashCompletionSuggestions("file", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki new-cert" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKINewCertCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("cn")) {
+ Log(LogCritical, "cli", "Common name (--cn) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("key")) {
+ Log(LogCritical, "cli", "Key file path (--key) must be specified.");
+ return 1;
+ }
+
+ String csr, cert;
+
+ if (vm.count("csr"))
+ csr = vm["csr"].as<std::string>();
+
+ if (vm.count("cert"))
+ cert = vm["cert"].as<std::string>();
+
+ return PkiUtility::NewCert(vm["cn"].as<std::string>(), vm["key"].as<std::string>(), csr, cert);
+}
diff --git a/lib/cli/pkinewcertcommand.hpp b/lib/cli/pkinewcertcommand.hpp
new file mode 100644
index 0000000..0c39bb6
--- /dev/null
+++ b/lib/cli/pkinewcertcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKINEWCERTCOMMAND_H
+#define PKINEWCERTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki new-cert" command.
+ *
+ * @ingroup cli
+ */
+class PKINewCertCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKINewCertCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKINEWCERTCOMMAND_H */
diff --git a/lib/cli/pkirequestcommand.cpp b/lib/cli/pkirequestcommand.cpp
new file mode 100644
index 0000000..d2b79f0
--- /dev/null
+++ b/lib/cli/pkirequestcommand.cpp
@@ -0,0 +1,93 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkirequestcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+#include "base/tlsutility.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/request", PKIRequestCommand);
+
+String PKIRequestCommand::GetDescription() const
+{
+ return "Sends a PKI request to Icinga 2.";
+}
+
+String PKIRequestCommand::GetShortDescription() const
+{
+ return "requests a certificate";
+}
+
+void PKIRequestCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("key", po::value<std::string>(), "Key file path (input)")
+ ("cert", po::value<std::string>(), "Certificate file path (input + output)")
+ ("ca", po::value<std::string>(), "CA file path (output)")
+ ("trustedcert", po::value<std::string>(), "Trusted certificate file path (input)")
+ ("host", po::value<std::string>(), "Icinga 2 host")
+ ("port", po::value<std::string>(), "Icinga 2 port")
+ ("ticket", po::value<std::string>(), "Icinga 2 PKI ticket");
+}
+
+std::vector<String> PKIRequestCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "key" || argument == "cert" || argument == "ca" || argument == "trustedcert")
+ return GetBashCompletionSuggestions("file", word);
+ else if (argument == "host")
+ return GetBashCompletionSuggestions("hostname", word);
+ else if (argument == "port")
+ return GetBashCompletionSuggestions("service", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki request" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKIRequestCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("host")) {
+ Log(LogCritical, "cli", "Icinga 2 host (--host) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("key")) {
+ Log(LogCritical, "cli", "Key input file path (--key) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("cert")) {
+ Log(LogCritical, "cli", "Certificate output file path (--cert) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("ca")) {
+ Log(LogCritical, "cli", "CA certificate output file path (--ca) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("trustedcert")) {
+ Log(LogCritical, "cli", "Trusted certificate input file path (--trustedcert) must be specified.");
+ return 1;
+ }
+
+ String port = "5665";
+ String ticket;
+
+ if (vm.count("port"))
+ port = vm["port"].as<std::string>();
+
+ if (vm.count("ticket"))
+ ticket = vm["ticket"].as<std::string>();
+
+ return PkiUtility::RequestCertificate(vm["host"].as<std::string>(), port, vm["key"].as<std::string>(),
+ vm["cert"].as<std::string>(), vm["ca"].as<std::string>(), GetX509Certificate(vm["trustedcert"].as<std::string>()),
+ ticket);
+}
diff --git a/lib/cli/pkirequestcommand.hpp b/lib/cli/pkirequestcommand.hpp
new file mode 100644
index 0000000..6e2a393
--- /dev/null
+++ b/lib/cli/pkirequestcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKIREQUESTCOMMAND_H
+#define PKIREQUESTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki request" command.
+ *
+ * @ingroup cli
+ */
+class PKIRequestCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKIRequestCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKIREQUESTCOMMAND_H */
diff --git a/lib/cli/pkisavecertcommand.cpp b/lib/cli/pkisavecertcommand.cpp
new file mode 100644
index 0000000..befd0ee
--- /dev/null
+++ b/lib/cli/pkisavecertcommand.cpp
@@ -0,0 +1,89 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkisavecertcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+#include "base/tlsutility.hpp"
+#include "base/console.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/save-cert", PKISaveCertCommand);
+
+String PKISaveCertCommand::GetDescription() const
+{
+ return "Saves another Icinga 2 instance's certificate.";
+}
+
+String PKISaveCertCommand::GetShortDescription() const
+{
+ return "saves another Icinga 2 instance's certificate";
+}
+
+void PKISaveCertCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("trustedcert", po::value<std::string>(), "Trusted certificate file path (output)")
+ ("host", po::value<std::string>(), "Parent Icinga instance to fetch the public TLS certificate from")
+ ("port", po::value<std::string>()->default_value("5665"), "Icinga 2 port");
+
+ hiddenDesc.add_options()
+ ("key", po::value<std::string>())
+ ("cert", po::value<std::string>());
+}
+
+std::vector<String> PKISaveCertCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "trustedcert")
+ return GetBashCompletionSuggestions("file", word);
+ else if (argument == "host")
+ return GetBashCompletionSuggestions("hostname", word);
+ else if (argument == "port")
+ return GetBashCompletionSuggestions("service", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki save-cert" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKISaveCertCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("host")) {
+ Log(LogCritical, "cli", "Icinga 2 host (--host) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("trustedcert")) {
+ Log(LogCritical, "cli", "Trusted certificate output file path (--trustedcert) must be specified.");
+ return 1;
+ }
+
+ String host = vm["host"].as<std::string>();
+ String port = vm["port"].as<std::string>();
+
+ Log(LogInformation, "cli")
+ << "Retrieving TLS certificate for '" << host << ":" << port << "'.";
+
+ std::shared_ptr<X509> cert = PkiUtility::FetchCert(host, port);
+
+ if (!cert) {
+ Log(LogCritical, "cli", "Failed to fetch certificate from host.");
+ return 1;
+ }
+
+ std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
+ std::cout << ConsoleColorTag(Console_ForegroundRed)
+ << "***\n"
+ << "*** You have to ensure that this certificate actually matches the parent\n"
+ << "*** instance's certificate in order to avoid man-in-the-middle attacks.\n"
+ << "***\n\n"
+ << ConsoleColorTag(Console_Normal);
+
+ return PkiUtility::WriteCert(cert, vm["trustedcert"].as<std::string>());
+}
diff --git a/lib/cli/pkisavecertcommand.hpp b/lib/cli/pkisavecertcommand.hpp
new file mode 100644
index 0000000..c552eef
--- /dev/null
+++ b/lib/cli/pkisavecertcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKISAVECERTCOMMAND_H
+#define PKISAVECERTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki save-cert" command.
+ *
+ * @ingroup cli
+ */
+class PKISaveCertCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKISaveCertCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKISAVECERTCOMMAND_H */
diff --git a/lib/cli/pkisigncsrcommand.cpp b/lib/cli/pkisigncsrcommand.cpp
new file mode 100644
index 0000000..ce1427b
--- /dev/null
+++ b/lib/cli/pkisigncsrcommand.cpp
@@ -0,0 +1,56 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkisigncsrcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/sign-csr", PKISignCSRCommand);
+
+String PKISignCSRCommand::GetDescription() const
+{
+ return "Reads a Certificate Signing Request from stdin and prints a signed certificate on stdout.";
+}
+
+String PKISignCSRCommand::GetShortDescription() const
+{
+ return "signs a CSR";
+}
+
+void PKISignCSRCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("csr", po::value<std::string>(), "CSR file path (input)")
+ ("cert", po::value<std::string>(), "Certificate file path (output)");
+}
+
+std::vector<String> PKISignCSRCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "csr" || argument == "cert")
+ return GetBashCompletionSuggestions("file", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki sign-csr" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKISignCSRCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("csr")) {
+ Log(LogCritical, "cli", "Certificate signing request file path (--csr) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("cert")) {
+ Log(LogCritical, "cli", "Certificate file path (--cert) must be specified.");
+ return 1;
+ }
+
+ return PkiUtility::SignCsr(vm["csr"].as<std::string>(), vm["cert"].as<std::string>());
+}
diff --git a/lib/cli/pkisigncsrcommand.hpp b/lib/cli/pkisigncsrcommand.hpp
new file mode 100644
index 0000000..a66fd39
--- /dev/null
+++ b/lib/cli/pkisigncsrcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKISIGNCSRCOMMAND_H
+#define PKISIGNCSRCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki sign-csr" command.
+ *
+ * @ingroup cli
+ */
+class PKISignCSRCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKISignCSRCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKISIGNCSRCOMMAND_H */
diff --git a/lib/cli/pkiticketcommand.cpp b/lib/cli/pkiticketcommand.cpp
new file mode 100644
index 0000000..82f3586
--- /dev/null
+++ b/lib/cli/pkiticketcommand.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkiticketcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/ticket", PKITicketCommand);
+
+String PKITicketCommand::GetDescription() const
+{
+ return "Generates an Icinga 2 ticket";
+}
+
+String PKITicketCommand::GetShortDescription() const
+{
+ return "generates a ticket";
+}
+
+void PKITicketCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("cn", po::value<std::string>(), "Certificate common name")
+ ("salt", po::value<std::string>(), "Ticket salt");
+}
+
+/**
+ * The entry point for the "pki ticket" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKITicketCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("cn")) {
+ Log(LogCritical, "cli", "Common name (--cn) must be specified.");
+ return 1;
+ }
+
+ String salt = VariableUtility::GetVariable("TicketSalt");
+
+ if (vm.count("salt"))
+ salt = vm["salt"].as<std::string>();
+
+ if (salt.IsEmpty()) {
+ Log(LogCritical, "cli", "Ticket salt (--salt) must be specified.");
+ return 1;
+ }
+
+ return PkiUtility::GenTicket(vm["cn"].as<std::string>(), salt, std::cout);
+}
diff --git a/lib/cli/pkiticketcommand.hpp b/lib/cli/pkiticketcommand.hpp
new file mode 100644
index 0000000..500ce86
--- /dev/null
+++ b/lib/cli/pkiticketcommand.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKITICKETCOMMAND_H
+#define PKITICKETCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki ticket" command.
+ *
+ * @ingroup cli
+ */
+class PKITicketCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKITicketCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKITICKETCOMMAND_H */
diff --git a/lib/cli/pkiverifycommand.cpp b/lib/cli/pkiverifycommand.cpp
new file mode 100644
index 0000000..963903a
--- /dev/null
+++ b/lib/cli/pkiverifycommand.cpp
@@ -0,0 +1,226 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkiverifycommand.hpp"
+#include "icinga/service.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/tlsutility.hpp"
+#include "base/logger.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/verify", PKIVerifyCommand);
+
+String PKIVerifyCommand::GetDescription() const
+{
+ return "Verify TLS certificates: CN, signed by CA, is CA; Print certificate";
+}
+
+String PKIVerifyCommand::GetShortDescription() const
+{
+ return "verify TLS certificates: CN, signed by CA, is CA; Print certificate";
+}
+
+void PKIVerifyCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("cn", po::value<std::string>(), "Common Name (optional). Use with '--cert' to check the CN in the certificate.")
+ ("cert", po::value<std::string>(), "Certificate file path (optional). Standalone: print certificate. With '--cacert': Verify against CA.")
+ ("cacert", po::value<std::string>(), "CA certificate file path (optional). If passed standalone, verifies whether this is a CA certificate")
+ ("crl", po::value<std::string>(), "CRL file path (optional). Check the certificate against this revocation list when verifying against CA.");
+}
+
+std::vector<String> PKIVerifyCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "cert" || argument == "cacert" || argument == "crl")
+ return GetBashCompletionSuggestions("file", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki verify" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKIVerifyCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String cn, certFile, caCertFile, crlFile;
+
+ if (vm.count("cn"))
+ cn = vm["cn"].as<std::string>();
+
+ if (vm.count("cert"))
+ certFile = vm["cert"].as<std::string>();
+
+ if (vm.count("cacert"))
+ caCertFile = vm["cacert"].as<std::string>();
+
+ if (vm.count("crl"))
+ crlFile = vm["crl"].as<std::string>();
+
+ /* Verify CN in certificate. */
+ if (!cn.IsEmpty() && !certFile.IsEmpty()) {
+ std::shared_ptr<X509> cert;
+ try {
+ cert = GetX509Certificate(certFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ Log(LogInformation, "cli")
+ << "Verifying common name (CN) '" << cn << " in certificate '" << certFile << "'.";
+
+ std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
+
+ String certCN = GetCertificateCN(cert);
+
+ if (cn == certCN) {
+ Log(LogInformation, "cli")
+ << "OK: CN '" << cn << "' matches certificate CN '" << certCN << "'.";
+
+ return ServiceOK;
+ } else {
+ Log(LogCritical, "cli")
+ << "CRITICAL: CN '" << cn << "' does NOT match certificate CN '" << certCN << "'.";
+
+ return ServiceCritical;
+ }
+ }
+
+ /* Verify certificate. */
+ if (!certFile.IsEmpty() && !caCertFile.IsEmpty()) {
+ std::shared_ptr<X509> cert;
+ try {
+ cert = GetX509Certificate(certFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ std::shared_ptr<X509> cacert;
+ try {
+ cacert = GetX509Certificate(caCertFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read CA certificate file '" << caCertFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ Log(LogInformation, "cli")
+ << "Verifying certificate '" << certFile << "'";
+
+ std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
+
+ Log(LogInformation, "cli")
+ << " with CA certificate '" << caCertFile << "'.";
+
+ std::cout << PkiUtility::GetCertificateInformation(cacert) << "\n";
+
+ String certCN = GetCertificateCN(cert);
+
+ bool signedByCA;
+
+ try {
+ signedByCA = VerifyCertificate(cacert, cert, crlFile);
+ } catch (const std::exception& ex) {
+ Log logmsg (LogCritical, "cli");
+ logmsg << "CRITICAL: Certificate with CN '" << certCN << "' is NOT signed by CA: ";
+ if (const unsigned long *openssl_code = boost::get_error_info<errinfo_openssl_error>(ex)) {
+ logmsg << X509_verify_cert_error_string(*openssl_code) << " (code " << *openssl_code << ")";
+ } else {
+ logmsg << DiagnosticInformation(ex, false);
+ }
+
+ return ServiceCritical;
+ }
+
+ if (signedByCA) {
+ Log(LogInformation, "cli")
+ << "OK: Certificate with CN '" << certCN << "' is signed by CA.";
+
+ return ServiceOK;
+ } else {
+ Log(LogCritical, "cli")
+ << "CRITICAL: Certificate with CN '" << certCN << "' is NOT signed by CA.";
+
+ return ServiceCritical;
+ }
+ }
+
+
+ /* Standalone CA checks. */
+ if (certFile.IsEmpty() && !caCertFile.IsEmpty()) {
+ std::shared_ptr<X509> cacert;
+ try {
+ cacert = GetX509Certificate(caCertFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read CA certificate file '" << caCertFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ Log(LogInformation, "cli")
+ << "Checking whether certificate '" << caCertFile << "' is a valid CA certificate.";
+
+ std::cout << PkiUtility::GetCertificateInformation(cacert) << "\n";
+
+ if (IsCa(cacert)) {
+ Log(LogInformation, "cli")
+ << "OK: CA certificate file '" << caCertFile << "' was verified successfully.\n";
+
+ return ServiceOK;
+ } else {
+ Log(LogCritical, "cli")
+ << "CRITICAL: The file '" << caCertFile << "' does not seem to be a CA certificate file.\n";
+
+ return ServiceCritical;
+ }
+ }
+
+ /* Print certificate */
+ if (!certFile.IsEmpty()) {
+ std::shared_ptr<X509> cert;
+ try {
+ cert = GetX509Certificate(certFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ Log(LogInformation, "cli")
+ << "Printing certificate '" << certFile << "'";
+
+ std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
+
+ return ServiceOK;
+ }
+
+ /* Error handling. */
+ if (!cn.IsEmpty() && certFile.IsEmpty()) {
+ Log(LogCritical, "cli")
+ << "The '--cn' parameter requires the '--cert' parameter.";
+
+ return ServiceCritical;
+ }
+
+ if (cn.IsEmpty() && certFile.IsEmpty() && caCertFile.IsEmpty()) {
+ Log(LogInformation, "cli")
+ << "Please add the '--help' parameter to see all available options.";
+
+ return ServiceOK;
+ }
+
+ return ServiceOK;
+}
diff --git a/lib/cli/pkiverifycommand.hpp b/lib/cli/pkiverifycommand.hpp
new file mode 100644
index 0000000..8e4b9db
--- /dev/null
+++ b/lib/cli/pkiverifycommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#ifndef PKIVERIFYCOMMAND_H
+#define PKIVERIFYCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki verify" command.
+ *
+ * @ingroup cli
+ */
+class PKIVerifyCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKIVerifyCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKIVERIFYCOMMAND_H */
diff --git a/lib/cli/variablegetcommand.cpp b/lib/cli/variablegetcommand.cpp
new file mode 100644
index 0000000..c05ac96
--- /dev/null
+++ b/lib/cli/variablegetcommand.cpp
@@ -0,0 +1,75 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/variablegetcommand.hpp"
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/convert.hpp"
+#include "base/configobject.hpp"
+#include "base/configtype.hpp"
+#include "base/json.hpp"
+#include "base/netstring.hpp"
+#include "base/stdiostream.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include "base/scriptglobal.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("variable/get", VariableGetCommand);
+
+String VariableGetCommand::GetDescription() const
+{
+ return "Prints the value of an Icinga 2 variable.";
+}
+
+String VariableGetCommand::GetShortDescription() const
+{
+ return "gets a variable";
+}
+
+void VariableGetCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("current", "Uses the current value (i.e. from the running process, rather than from the vars file)");
+}
+
+int VariableGetCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+/**
+ * The entry point for the "variable get" CLI command.
+ *
+ * @returns An exit status.
+ */
+int VariableGetCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (vm.count("current")) {
+ std::cout << ScriptGlobal::Get(ap[0], &Empty) << "\n";
+ return 0;
+ }
+
+ String varsfile = Configuration::VarsPath;
+
+ if (!Utility::PathExists(varsfile)) {
+ Log(LogCritical, "cli")
+ << "Cannot open variables file '" << varsfile << "'.";
+ Log(LogCritical, "cli", "Run 'icinga2 daemon -C' to validate config and generate the cache file.");
+ return 1;
+ }
+
+ Value value = VariableUtility::GetVariable(ap[0]);
+
+ std::cout << value << "\n";
+
+ return 0;
+}
diff --git a/lib/cli/variablegetcommand.hpp b/lib/cli/variablegetcommand.hpp
new file mode 100644
index 0000000..9479b3a
--- /dev/null
+++ b/lib/cli/variablegetcommand.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VARIABLEGETCOMMAND_H
+#define VARIABLEGETCOMMAND_H
+
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "cli/clicommand.hpp"
+#include <ostream>
+
+namespace icinga
+{
+
+/**
+ * The "variable get" command.
+ *
+ * @ingroup cli
+ */
+class VariableGetCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(VariableGetCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* VARIABLEGETCOMMAND_H */
diff --git a/lib/cli/variablelistcommand.cpp b/lib/cli/variablelistcommand.cpp
new file mode 100644
index 0000000..b7ba1be
--- /dev/null
+++ b/lib/cli/variablelistcommand.cpp
@@ -0,0 +1,52 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/variablelistcommand.hpp"
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/convert.hpp"
+#include "base/configobject.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("variable/list", VariableListCommand);
+
+String VariableListCommand::GetDescription() const
+{
+ return "Lists all Icinga 2 variables.";
+}
+
+String VariableListCommand::GetShortDescription() const
+{
+ return "lists all variables";
+}
+
+/**
+ * The entry point for the "variable list" CLI command.
+ *
+ * @returns An exit status.
+ */
+int VariableListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String varsfile = Configuration::VarsPath;
+
+ if (!Utility::PathExists(varsfile)) {
+ Log(LogCritical, "cli")
+ << "Cannot open variables file '" << varsfile << "'.";
+ Log(LogCritical, "cli", "Run 'icinga2 daemon -C' to validate config and generate the cache file.");
+ return 1;
+ }
+
+ VariableUtility::PrintVariables(std::cout);
+
+ return 0;
+}
+
diff --git a/lib/cli/variablelistcommand.hpp b/lib/cli/variablelistcommand.hpp
new file mode 100644
index 0000000..909d9eb
--- /dev/null
+++ b/lib/cli/variablelistcommand.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VARIABLELISTCOMMAND_H
+#define VARIABLELISTCOMMAND_H
+
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "cli/clicommand.hpp"
+#include <ostream>
+
+namespace icinga
+{
+
+/**
+ * The "variable list" command.
+ *
+ * @ingroup cli
+ */
+class VariableListCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(VariableListCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+private:
+ static void PrintVariable(std::ostream& fp, const String& message);
+};
+
+}
+
+#endif /* VARIABLELISTCOMMAND_H */
diff --git a/lib/cli/variableutility.cpp b/lib/cli/variableutility.cpp
new file mode 100644
index 0000000..398c9a0
--- /dev/null
+++ b/lib/cli/variableutility.cpp
@@ -0,0 +1,76 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+#include "base/stdiostream.hpp"
+#include "base/netstring.hpp"
+#include "base/json.hpp"
+#include "remote/jsonrpc.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+Value VariableUtility::GetVariable(const String& name)
+{
+ String varsfile = Configuration::VarsPath;
+
+ std::fstream fp;
+ fp.open(varsfile.CStr(), std::ios_base::in);
+
+ StdioStream::Ptr sfp = new StdioStream(&fp, false);
+
+ String message;
+ StreamReadContext src;
+ for (;;) {
+ StreamReadStatus srs = NetString::ReadStringFromStream(sfp, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ Dictionary::Ptr variable = JsonDecode(message);
+
+ if (variable->Get("name") == name) {
+ return variable->Get("value");
+ }
+ }
+
+ return Empty;
+}
+
+void VariableUtility::PrintVariables(std::ostream& outfp)
+{
+ String varsfile = Configuration::VarsPath;
+
+ std::fstream fp;
+ fp.open(varsfile.CStr(), std::ios_base::in);
+
+ StdioStream::Ptr sfp = new StdioStream(&fp, false);
+ unsigned long variables_count = 0;
+
+ String message;
+ StreamReadContext src;
+ for (;;) {
+ StreamReadStatus srs = NetString::ReadStringFromStream(sfp, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ Dictionary::Ptr variable = JsonDecode(message);
+ outfp << variable->Get("name") << " = " << variable->Get("value") << "\n";
+ variables_count++;
+ }
+
+ sfp->Close();
+ fp.close();
+
+ Log(LogNotice, "cli")
+ << "Parsed " << variables_count << " variables.";
+}
diff --git a/lib/cli/variableutility.hpp b/lib/cli/variableutility.hpp
new file mode 100644
index 0000000..69869b2
--- /dev/null
+++ b/lib/cli/variableutility.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VARIABLEUTILITY_H
+#define VARIABLEUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/dictionary.hpp"
+#include "base/string.hpp"
+#include <ostream>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class VariableUtility
+{
+public:
+ static Value GetVariable(const String& name);
+ static void PrintVariables(std::ostream& outfp);
+
+private:
+ VariableUtility();
+
+};
+
+}
+
+#endif /* VARIABLEUTILITY_H */
diff --git a/lib/compat/CMakeLists.txt b/lib/compat/CMakeLists.txt
new file mode 100644
index 0000000..f7d032f
--- /dev/null
+++ b/lib/compat/CMakeLists.txt
@@ -0,0 +1,38 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(compatlogger.ti compatlogger-ti.cpp compatlogger-ti.hpp)
+mkclass_target(externalcommandlistener.ti externalcommandlistener-ti.cpp externalcommandlistener-ti.hpp)
+
+set(compat_SOURCES
+ compatlogger.cpp compatlogger.hpp compatlogger-ti.hpp
+ externalcommandlistener.cpp externalcommandlistener.hpp externalcommandlistener-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(compat compat compat_SOURCES)
+endif()
+
+add_library(compat OBJECT ${compat_SOURCES})
+
+add_dependencies(compat base config icinga)
+
+set_target_properties (
+ compat PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/command.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/compatlog.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_LOGDIR}/compat/archives\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_SPOOLDIR}\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_INITRUNDIR}/cmd\")")
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/compat/compatlogger.cpp b/lib/compat/compatlogger.cpp
new file mode 100644
index 0000000..95ca830
--- /dev/null
+++ b/lib/compat/compatlogger.cpp
@@ -0,0 +1,614 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "compat/compatlogger.hpp"
+#include "compat/compatlogger-ti.cpp"
+#include "icinga/service.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/externalcommandprocessor.hpp"
+#include "icinga/compatutility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+#include "base/statsfunction.hpp"
+#include <boost/algorithm/string.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE(CompatLogger);
+
+REGISTER_STATSFUNCTION(CompatLogger, &CompatLogger::StatsFunc);
+
+void CompatLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const CompatLogger::Ptr& compat_logger : ConfigType::GetObjectsByType<CompatLogger>()) {
+ nodes.emplace_back(compat_logger->GetName(), 1); // add more stats
+ }
+
+ status->Set("compatlogger", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::Start(bool runtimeCreated)
+{
+ ObjectImpl<CompatLogger>::Start(runtimeCreated);
+
+ Log(LogInformation, "CompatLogger")
+ << "'" << GetName() << "' started.";
+
+ Log(LogWarning, "CompatLogger")
+ << "This feature is DEPRECATED and may be removed in future releases. Check the roadmap at https://github.com/Icinga/icinga2/milestones";
+
+ Checkable::OnNewCheckResult.connect([this](const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ CheckResultHandler(checkable, cr);
+ });
+ Checkable::OnNotificationSentToUser.connect([this](const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const User::Ptr& user, const NotificationType& type, const CheckResult::Ptr& cr, const String& author,
+ const String& commentText, const String& commandName, const MessageOrigin::Ptr&) {
+ NotificationSentHandler(notification, checkable, user, type, cr, author, commentText, commandName);
+ });
+
+ Downtime::OnDowntimeTriggered.connect([this](const Downtime::Ptr& downtime) { TriggerDowntimeHandler(downtime); });
+ Downtime::OnDowntimeRemoved.connect([this](const Downtime::Ptr& downtime) { RemoveDowntimeHandler(downtime); });
+ Checkable::OnEventCommandExecuted.connect([this](const Checkable::Ptr& checkable) { EventCommandHandler(checkable); });
+
+ Checkable::OnFlappingChanged.connect([this](const Checkable::Ptr& checkable, const Value&) { FlappingChangedHandler(checkable); });
+ Checkable::OnEnableFlappingChanged.connect([this](const Checkable::Ptr& checkable, const Value&) { EnableFlappingChangedHandler(checkable); });
+
+ ExternalCommandProcessor::OnNewExternalCommand.connect([this](double, const String& command, const std::vector<String>& arguments) {
+ ExternalCommandHandler(command, arguments);
+ });
+
+ m_RotationTimer = Timer::Create();
+ m_RotationTimer->OnTimerExpired.connect([this](const Timer * const&) { RotationTimerHandler(); });
+ m_RotationTimer->Start();
+
+ ReopenFile(false);
+ ScheduleNextRotation();
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::Stop(bool runtimeRemoved)
+{
+ m_RotationTimer->Stop(true);
+
+ Log(LogInformation, "CompatLogger")
+ << "'" << GetName() << "' stopped.";
+
+ ObjectImpl<CompatLogger>::Stop(runtimeRemoved);
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr &cr)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr vars_after = cr->GetVarsAfter();
+
+ long state_after = vars_after->Get("state");
+ long stateType_after = vars_after->Get("state_type");
+ long attempt_after = vars_after->Get("attempt");
+ bool reachable_after = vars_after->Get("reachable");
+
+ Dictionary::Ptr vars_before = cr->GetVarsBefore();
+
+ if (vars_before) {
+ long state_before = vars_before->Get("state");
+ long stateType_before = vars_before->Get("state_type");
+ long attempt_before = vars_before->Get("attempt");
+ bool reachable_before = vars_before->Get("reachable");
+
+ if (state_before == state_after && stateType_before == stateType_after &&
+ attempt_before == attempt_after && reachable_before == reachable_after)
+ return; /* Nothing changed, ignore this checkresult. */
+ }
+
+ String output;
+ if (cr)
+ output = CompatUtility::GetCheckResultOutput(cr);
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << Service::StateToString(service->GetState()) << ";"
+ << Service::StateTypeToString(service->GetStateType()) << ";"
+ << attempt_after << ";"
+ << output << ""
+ << "";
+ } else {
+ String state = Host::StateToString(Host::CalculateState(static_cast<ServiceState>(state_after)));
+
+ msgbuf << "HOST ALERT: "
+ << host->GetName() << ";"
+ << GetHostStateString(host) << ";"
+ << Host::StateTypeToString(host->GetStateType()) << ";"
+ << attempt_after << ";"
+ << output << ""
+ << "";
+
+ }
+
+ {
+ ObjectLock olock(this);
+ WriteLine(msgbuf.str());
+ Flush();
+ }
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::TriggerDowntimeHandler(const Downtime::Ptr& downtime)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(downtime->GetCheckable());
+
+ if (!downtime)
+ return;
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE DOWNTIME ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << "STARTED" << "; "
+ << "Checkable has entered a period of scheduled downtime."
+ << "";
+ } else {
+ msgbuf << "HOST DOWNTIME ALERT: "
+ << host->GetName() << ";"
+ << "STARTED" << "; "
+ << "Checkable has entered a period of scheduled downtime."
+ << "";
+ }
+
+ {
+ ObjectLock oLock(this);
+ WriteLine(msgbuf.str());
+ Flush();
+ }
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::RemoveDowntimeHandler(const Downtime::Ptr& downtime)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(downtime->GetCheckable());
+
+ if (!downtime)
+ return;
+
+ String downtime_output;
+ String downtime_state_str;
+
+ if (downtime->GetWasCancelled()) {
+ downtime_output = "Scheduled downtime for service has been cancelled.";
+ downtime_state_str = "CANCELLED";
+ } else {
+ downtime_output = "Checkable has exited from a period of scheduled downtime.";
+ downtime_state_str = "STOPPED";
+ }
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE DOWNTIME ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << downtime_state_str << "; "
+ << downtime_output
+ << "";
+ } else {
+ msgbuf << "HOST DOWNTIME ALERT: "
+ << host->GetName() << ";"
+ << downtime_state_str << "; "
+ << downtime_output
+ << "";
+ }
+
+ {
+ ObjectLock oLock(this);
+ WriteLine(msgbuf.str());
+ Flush();
+ }
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::NotificationSentHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const User::Ptr& user, NotificationType notification_type, CheckResult::Ptr const& cr,
+ const String& author, const String& comment_text, const String& command_name)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ String notification_type_str = Notification::NotificationTypeToStringCompat(notification_type);
+
+ /* override problem notifications with their current state string */
+ if (notification_type == NotificationProblem) {
+ if (service)
+ notification_type_str = Service::StateToString(service->GetState());
+ else
+ notification_type_str = GetHostStateString(host);
+ }
+
+ String author_comment = "";
+ if (notification_type == NotificationCustom || notification_type == NotificationAcknowledgement) {
+ author_comment = author + ";" + comment_text;
+ }
+
+ if (!cr)
+ return;
+
+ String output;
+ if (cr)
+ output = CompatUtility::GetCheckResultOutput(cr);
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE NOTIFICATION: "
+ << user->GetName() << ";"
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << notification_type_str << ";"
+ << command_name << ";"
+ << output << ";"
+ << author_comment
+ << "";
+ } else {
+ msgbuf << "HOST NOTIFICATION: "
+ << user->GetName() << ";"
+ << host->GetName() << ";"
+ << notification_type_str << " "
+ << "(" << GetHostStateString(host) << ");"
+ << command_name << ";"
+ << output << ";"
+ << author_comment
+ << "";
+ }
+
+ {
+ ObjectLock oLock(this);
+ WriteLine(msgbuf.str());
+ Flush();
+ }
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::FlappingChangedHandler(const Checkable::Ptr& checkable)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ String flapping_state_str;
+ String flapping_output;
+
+ if (checkable->IsFlapping()) {
+ flapping_output = "Checkable appears to have started flapping (" + Convert::ToString(checkable->GetFlappingCurrent()) + "% change >= " + Convert::ToString(checkable->GetFlappingThresholdHigh()) + "% threshold)";
+ flapping_state_str = "STARTED";
+ } else {
+ flapping_output = "Checkable appears to have stopped flapping (" + Convert::ToString(checkable->GetFlappingCurrent()) + "% change < " + Convert::ToString(checkable->GetFlappingThresholdLow()) + "% threshold)";
+ flapping_state_str = "STOPPED";
+ }
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE FLAPPING ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << flapping_state_str << "; "
+ << flapping_output
+ << "";
+ } else {
+ msgbuf << "HOST FLAPPING ALERT: "
+ << host->GetName() << ";"
+ << flapping_state_str << "; "
+ << flapping_output
+ << "";
+ }
+
+ {
+ ObjectLock oLock(this);
+ WriteLine(msgbuf.str());
+ Flush();
+ }
+}
+
+void CompatLogger::EnableFlappingChangedHandler(const Checkable::Ptr& checkable)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ if (checkable->GetEnableFlapping())
+ return;
+
+ String flapping_output = "Flap detection has been disabled";
+ String flapping_state_str = "DISABLED";
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE FLAPPING ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << flapping_state_str << "; "
+ << flapping_output
+ << "";
+ } else {
+ msgbuf << "HOST FLAPPING ALERT: "
+ << host->GetName() << ";"
+ << flapping_state_str << "; "
+ << flapping_output
+ << "";
+ }
+
+ {
+ ObjectLock oLock(this);
+ WriteLine(msgbuf.str());
+ Flush();
+ }
+}
+
+void CompatLogger::ExternalCommandHandler(const String& command, const std::vector<String>& arguments)
+{
+ std::ostringstream msgbuf;
+ msgbuf << "EXTERNAL COMMAND: "
+ << command << ";"
+ << boost::algorithm::join(arguments, ";")
+ << "";
+
+ {
+ ObjectLock oLock(this);
+ WriteLine(msgbuf.str());
+ Flush();
+ }
+}
+
+void CompatLogger::EventCommandHandler(const Checkable::Ptr& checkable)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ EventCommand::Ptr event_command = checkable->GetEventCommand();
+ String event_command_name = event_command->GetName();
+ long current_attempt = checkable->GetCheckAttempt();
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE EVENT HANDLER: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << Service::StateToString(service->GetState()) << ";"
+ << Service::StateTypeToString(service->GetStateType()) << ";"
+ << current_attempt << ";"
+ << event_command_name;
+ } else {
+ msgbuf << "HOST EVENT HANDLER: "
+ << host->GetName() << ";"
+ << GetHostStateString(host) << ";"
+ << Host::StateTypeToString(host->GetStateType()) << ";"
+ << current_attempt << ";"
+ << event_command_name;
+ }
+
+ {
+ ObjectLock oLock(this);
+ WriteLine(msgbuf.str());
+ Flush();
+ }
+}
+
+String CompatLogger::GetHostStateString(const Host::Ptr& host)
+{
+ if (host->GetState() != HostUp && !host->IsReachable())
+ return "UNREACHABLE"; /* hardcoded compat state */
+
+ return Host::StateToString(host->GetState());
+}
+
+void CompatLogger::WriteLine(const String& line)
+{
+ ASSERT(OwnsLock());
+
+ if (!m_OutputFile.good())
+ return;
+
+ m_OutputFile << "[" << (long)Utility::GetTime() << "] " << line << "\n";
+}
+
+void CompatLogger::Flush()
+{
+ ASSERT(OwnsLock());
+
+ if (!m_OutputFile.good())
+ return;
+
+ m_OutputFile << std::flush;
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::ReopenFile(bool rotate)
+{
+ ObjectLock olock(this);
+
+ String tempFile = GetLogDir() + "/icinga.log";
+
+ if (m_OutputFile) {
+ m_OutputFile.close();
+
+ if (rotate) {
+ String archiveFile = GetLogDir() + "/archives/icinga-" + Utility::FormatDateTime("%m-%d-%Y-%H", Utility::GetTime()) + ".log";
+
+ Log(LogNotice, "CompatLogger")
+ << "Rotating compat log file '" << tempFile << "' -> '" << archiveFile << "'";
+
+ (void) rename(tempFile.CStr(), archiveFile.CStr());
+ }
+ }
+
+ m_OutputFile.open(tempFile.CStr(), std::ofstream::app);
+
+ if (!m_OutputFile) {
+ Log(LogWarning, "CompatLogger")
+ << "Could not open compat log file '" << tempFile << "' for writing. Log output will be lost.";
+
+ return;
+ }
+
+ WriteLine("LOG ROTATION: " + GetRotationMethod());
+ WriteLine("LOG VERSION: 2.0");
+
+ for (const Host::Ptr& host : ConfigType::GetObjectsByType<Host>()) {
+ String output;
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (cr)
+ output = CompatUtility::GetCheckResultOutput(cr);
+
+ std::ostringstream msgbuf;
+ msgbuf << "CURRENT HOST STATE: "
+ << host->GetName() << ";"
+ << GetHostStateString(host) << ";"
+ << Host::StateTypeToString(host->GetStateType()) << ";"
+ << host->GetCheckAttempt() << ";"
+ << output << "";
+
+ WriteLine(msgbuf.str());
+ }
+
+ for (const Service::Ptr& service : ConfigType::GetObjectsByType<Service>()) {
+ Host::Ptr host = service->GetHost();
+
+ String output;
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (cr)
+ output = CompatUtility::GetCheckResultOutput(cr);
+
+ std::ostringstream msgbuf;
+ msgbuf << "CURRENT SERVICE STATE: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << Service::StateToString(service->GetState()) << ";"
+ << Service::StateTypeToString(service->GetStateType()) << ";"
+ << service->GetCheckAttempt() << ";"
+ << output << "";
+
+ WriteLine(msgbuf.str());
+ }
+
+ Flush();
+}
+
+void CompatLogger::ScheduleNextRotation()
+{
+ auto now = (time_t)Utility::GetTime();
+ String method = GetRotationMethod();
+
+ tm tmthen;
+
+#ifdef _MSC_VER
+ tm *temp = localtime(&now);
+
+ if (!temp) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("localtime")
+ << boost::errinfo_errno(errno));
+ }
+
+ tmthen = *temp;
+#else /* _MSC_VER */
+ if (!localtime_r(&now, &tmthen)) {
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("localtime_r")
+ << boost::errinfo_errno(errno));
+ }
+#endif /* _MSC_VER */
+
+ tmthen.tm_min = 0;
+ tmthen.tm_sec = 0;
+
+ if (method == "HOURLY") {
+ tmthen.tm_hour++;
+ } else if (method == "DAILY") {
+ tmthen.tm_mday++;
+ tmthen.tm_hour = 0;
+ } else if (method == "WEEKLY") {
+ tmthen.tm_mday += 7 - tmthen.tm_wday;
+ tmthen.tm_hour = 0;
+ } else if (method == "MONTHLY") {
+ tmthen.tm_mon++;
+ tmthen.tm_mday = 1;
+ tmthen.tm_hour = 0;
+ }
+
+ time_t ts = mktime(&tmthen);
+
+ Log(LogNotice, "CompatLogger")
+ << "Rescheduling rotation timer for compat log '"
+ << GetName() << "' to '" << Utility::FormatDateTime("%Y/%m/%d %H:%M:%S %z", ts) << "'";
+
+ m_RotationTimer->Reschedule(ts);
+}
+
+/**
+ * @threadsafety Always.
+ */
+void CompatLogger::RotationTimerHandler()
+{
+ try {
+ ReopenFile(true);
+ } catch (...) {
+ ScheduleNextRotation();
+
+ throw;
+ }
+
+ ScheduleNextRotation();
+}
+
+void CompatLogger::ValidateRotationMethod(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<CompatLogger>::ValidateRotationMethod(lvalue, utils);
+
+ if (lvalue() != "HOURLY" && lvalue() != "DAILY" &&
+ lvalue() != "WEEKLY" && lvalue() != "MONTHLY" && lvalue() != "NONE") {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "rotation_method" }, "Rotation method '" + lvalue() + "' is invalid."));
+ }
+}
diff --git a/lib/compat/compatlogger.hpp b/lib/compat/compatlogger.hpp
new file mode 100644
index 0000000..9fb0b29
--- /dev/null
+++ b/lib/compat/compatlogger.hpp
@@ -0,0 +1,60 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMPATLOGGER_H
+#define COMPATLOGGER_H
+
+#include "compat/compatlogger-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/timer.hpp"
+#include <fstream>
+
+namespace icinga
+{
+
+/**
+ * An Icinga compat log writer.
+ *
+ * @ingroup compat
+ */
+class CompatLogger final : public ObjectImpl<CompatLogger>
+{
+public:
+ DECLARE_OBJECT(CompatLogger);
+ DECLARE_OBJECTNAME(CompatLogger);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void ValidateRotationMethod(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ void WriteLine(const String& line);
+ void Flush();
+
+ void CheckResultHandler(const Checkable::Ptr& service, const CheckResult::Ptr& cr);
+ void NotificationSentHandler(const Notification::Ptr& notification, const Checkable::Ptr& service,
+ const User::Ptr& user, NotificationType notification_type, CheckResult::Ptr const& cr,
+ const String& author, const String& comment_text, const String& command_name);
+ void FlappingChangedHandler(const Checkable::Ptr& checkable);
+ void EnableFlappingChangedHandler(const Checkable::Ptr& checkable);
+ void TriggerDowntimeHandler(const Downtime::Ptr& downtime);
+ void RemoveDowntimeHandler(const Downtime::Ptr& downtime);
+ void ExternalCommandHandler(const String& command, const std::vector<String>& arguments);
+ void EventCommandHandler(const Checkable::Ptr& service);
+
+ static String GetHostStateString(const Host::Ptr& host);
+
+ Timer::Ptr m_RotationTimer;
+ void RotationTimerHandler();
+ void ScheduleNextRotation();
+
+ std::ofstream m_OutputFile;
+ void ReopenFile(bool rotate);
+};
+
+}
+
+#endif /* COMPATLOGGER_H */
diff --git a/lib/compat/compatlogger.ti b/lib/compat/compatlogger.ti
new file mode 100644
index 0000000..56431ec
--- /dev/null
+++ b/lib/compat/compatlogger.ti
@@ -0,0 +1,23 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/application.hpp"
+
+library compat;
+
+namespace icinga
+{
+
+class CompatLogger : ConfigObject
+{
+ activation_priority 100;
+
+ [config] String log_dir {
+ default {{{ return Configuration::LogDir + "/compat"; }}}
+ };
+ [config] String rotation_method {
+ default {{{ return "HOURLY"; }}}
+ };
+};
+
+}
diff --git a/lib/compat/externalcommandlistener.cpp b/lib/compat/externalcommandlistener.cpp
new file mode 100644
index 0000000..b61813b
--- /dev/null
+++ b/lib/compat/externalcommandlistener.cpp
@@ -0,0 +1,150 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "compat/externalcommandlistener.hpp"
+#include "compat/externalcommandlistener-ti.cpp"
+#include "icinga/externalcommandprocessor.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/application.hpp"
+#include "base/statsfunction.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(ExternalCommandListener);
+
+REGISTER_STATSFUNCTION(ExternalCommandListener, &ExternalCommandListener::StatsFunc);
+
+void ExternalCommandListener::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const ExternalCommandListener::Ptr& externalcommandlistener : ConfigType::GetObjectsByType<ExternalCommandListener>()) {
+ nodes.emplace_back(externalcommandlistener->GetName(), 1); //add more stats
+ }
+
+ status->Set("externalcommandlistener", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * Starts the component.
+ */
+void ExternalCommandListener::Start(bool runtimeCreated)
+{
+ ObjectImpl<ExternalCommandListener>::Start(runtimeCreated);
+
+ Log(LogInformation, "ExternalCommandListener")
+ << "'" << GetName() << "' started.";
+
+ Log(LogWarning, "ExternalCommandListener")
+ << "This feature is DEPRECATED and may be removed in future releases. Check the roadmap at https://github.com/Icinga/icinga2/milestones";
+#ifndef _WIN32
+ String path = GetCommandPath();
+ m_CommandThread = std::thread([this, path]() { CommandPipeThread(path); });
+ m_CommandThread.detach();
+#endif /* _WIN32 */
+}
+
+/**
+ * Stops the component.
+ */
+void ExternalCommandListener::Stop(bool runtimeRemoved)
+{
+ Log(LogInformation, "ExternalCommandListener")
+ << "'" << GetName() << "' stopped.";
+
+ ObjectImpl<ExternalCommandListener>::Stop(runtimeRemoved);
+}
+
+#ifndef _WIN32
+void ExternalCommandListener::CommandPipeThread(const String& commandPath)
+{
+ Utility::SetThreadName("Command Pipe");
+
+ struct stat statbuf;
+ bool fifo_ok = false;
+
+ if (lstat(commandPath.CStr(), &statbuf) >= 0) {
+ if (S_ISFIFO(statbuf.st_mode) && access(commandPath.CStr(), R_OK) >= 0) {
+ fifo_ok = true;
+ } else {
+ Utility::Remove(commandPath);
+ }
+ }
+
+ mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
+
+ if (!fifo_ok && mkfifo(commandPath.CStr(), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) < 0) {
+ Log(LogCritical, "ExternalCommandListener")
+ << "mkfifo() for fifo path '" << commandPath << "' failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ return;
+ }
+
+ /* mkfifo() uses umask to mask off some bits, which means we need to chmod() the
+ * fifo to get the right mask. */
+ if (chmod(commandPath.CStr(), mode) < 0) {
+ Log(LogCritical, "ExternalCommandListener")
+ << "chmod() on fifo '" << commandPath << "' failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ return;
+ }
+
+ for (;;) {
+ int fd = open(commandPath.CStr(), O_RDWR | O_NONBLOCK);
+
+ if (fd < 0) {
+ Log(LogCritical, "ExternalCommandListener")
+ << "open() for fifo path '" << commandPath << "' failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ return;
+ }
+
+ FIFO::Ptr fifo = new FIFO();
+ Socket::Ptr sock = new Socket(fd);
+ StreamReadContext src;
+
+ for (;;) {
+ sock->Poll(true, false);
+
+ char buffer[8192];
+ size_t rc;
+
+ try {
+ rc = sock->Read(buffer, sizeof(buffer));
+ } catch (const std::exception& ex) {
+ /* We have read all data. */
+ if (errno == EAGAIN)
+ continue;
+
+ Log(LogWarning, "ExternalCommandListener")
+ << "Cannot read from command pipe." << DiagnosticInformation(ex);
+ break;
+ }
+
+ /* Empty pipe (EOF) */
+ if (rc == 0)
+ continue;
+
+ fifo->Write(buffer, rc);
+
+ for (;;) {
+ String command;
+ StreamReadStatus srs = fifo->ReadLine(&command, src);
+
+ if (srs != StatusNewItem)
+ break;
+
+ try {
+ Log(LogInformation, "ExternalCommandListener")
+ << "Executing external command: " << command;
+
+ ExternalCommandProcessor::Execute(command);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ExternalCommandListener")
+ << "External command failed: " << DiagnosticInformation(ex, false);
+ Log(LogNotice, "ExternalCommandListener")
+ << "External command failed: " << DiagnosticInformation(ex, true);
+ }
+ }
+ }
+ }
+}
+#endif /* _WIN32 */
diff --git a/lib/compat/externalcommandlistener.hpp b/lib/compat/externalcommandlistener.hpp
new file mode 100644
index 0000000..895531f
--- /dev/null
+++ b/lib/compat/externalcommandlistener.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EXTERNALCOMMANDLISTENER_H
+#define EXTERNALCOMMANDLISTENER_H
+
+#include "compat/externalcommandlistener-ti.hpp"
+#include "base/objectlock.hpp"
+#include "base/timer.hpp"
+#include "base/utility.hpp"
+#include <thread>
+#include <iostream>
+
+namespace icinga
+{
+
+/**
+ * @ingroup compat
+ */
+class ExternalCommandListener final : public ObjectImpl<ExternalCommandListener>
+{
+public:
+ DECLARE_OBJECT(ExternalCommandListener);
+ DECLARE_OBJECTNAME(ExternalCommandListener);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+protected:
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+#ifndef _WIN32
+ std::thread m_CommandThread;
+
+ void CommandPipeThread(const String& commandPath);
+#endif /* _WIN32 */
+};
+
+}
+
+#endif /* EXTERNALCOMMANDLISTENER_H */
diff --git a/lib/compat/externalcommandlistener.ti b/lib/compat/externalcommandlistener.ti
new file mode 100644
index 0000000..5b52944
--- /dev/null
+++ b/lib/compat/externalcommandlistener.ti
@@ -0,0 +1,20 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/application.hpp"
+
+library compat;
+
+namespace icinga
+{
+
+class ExternalCommandListener : ConfigObject
+{
+ activation_priority 100;
+
+ [config] String command_path {
+ default {{{ return Configuration::InitRunDir + "/cmd/icinga2.cmd"; }}}
+ };
+};
+
+}
diff --git a/lib/config/CMakeLists.txt b/lib/config/CMakeLists.txt
new file mode 100644
index 0000000..80b8c2c
--- /dev/null
+++ b/lib/config/CMakeLists.txt
@@ -0,0 +1,47 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+find_package(BISON 2.3.0 REQUIRED)
+find_package(FLEX 2.5.31 REQUIRED)
+
+bison_target(config_parser config_parser.yy ${CMAKE_CURRENT_BINARY_DIR}/config_parser.cc)
+set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/config_parser.cc PROPERTY EXCLUDE_UNITY_BUILD TRUE)
+
+flex_target(config_lexer config_lexer.ll ${CMAKE_CURRENT_BINARY_DIR}/config_lexer.cc)
+set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/config_lexer.cc PROPERTY EXCLUDE_UNITY_BUILD TRUE)
+
+if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/config_parser.cc PROPERTY COMPILE_FLAGS "-Wno-deprecated-register -Wno-parentheses-equality -Wno-unused-function")
+ set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/config_lexer.cc PROPERTY COMPILE_FLAGS "-Wno-deprecated-register -Wno-null-conversion")
+endif()
+
+add_flex_bison_dependency(config_lexer config_parser)
+
+include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
+
+set(config_SOURCES
+ i2-config.hpp
+ activationcontext.cpp activationcontext.hpp
+ applyrule.cpp applyrule-targeted.cpp applyrule.hpp
+ configcompiler.cpp configcompiler.hpp
+ configcompilercontext.cpp configcompilercontext.hpp
+ configfragment.hpp
+ configitem.cpp configitem.hpp
+ configitembuilder.cpp configitembuilder.hpp
+ expression.cpp expression.hpp
+ objectrule.cpp objectrule.hpp
+ vmops.hpp
+ ${FLEX_config_lexer_OUTPUTS} ${BISON_config_parser_OUTPUTS}
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(config config config_SOURCES)
+endif()
+
+add_library(config OBJECT ${config_SOURCES})
+
+add_dependencies(config base)
+
+set_target_properties (
+ config PROPERTIES
+ FOLDER Lib
+)
diff --git a/lib/config/activationcontext.cpp b/lib/config/activationcontext.cpp
new file mode 100644
index 0000000..d050875
--- /dev/null
+++ b/lib/config/activationcontext.cpp
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/activationcontext.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+boost::thread_specific_ptr<std::stack<ActivationContext::Ptr> > ActivationContext::m_ActivationStack;
+
+std::stack<ActivationContext::Ptr>& ActivationContext::GetActivationStack()
+{
+ std::stack<ActivationContext::Ptr> *actx = m_ActivationStack.get();
+
+ if (!actx) {
+ actx = new std::stack<ActivationContext::Ptr>();
+ m_ActivationStack.reset(actx);
+ }
+
+ return *actx;
+}
+
+void ActivationContext::PushContext(const ActivationContext::Ptr& context)
+{
+ GetActivationStack().push(context);
+}
+
+void ActivationContext::PopContext()
+{
+ ASSERT(!GetActivationStack().empty());
+ GetActivationStack().pop();
+}
+
+ActivationContext::Ptr ActivationContext::GetCurrentContext()
+{
+ std::stack<ActivationContext::Ptr>& astack = GetActivationStack();
+
+ if (astack.empty())
+ BOOST_THROW_EXCEPTION(std::runtime_error("Objects may not be created outside of an activation context."));
+
+ return astack.top();
+}
+
+ActivationScope::ActivationScope(ActivationContext::Ptr context)
+ : m_Context(std::move(context))
+{
+ if (!m_Context)
+ m_Context = new ActivationContext();
+
+ ActivationContext::PushContext(m_Context);
+}
+
+ActivationScope::~ActivationScope()
+{
+ ActivationContext::PopContext();
+}
+
+ActivationContext::Ptr ActivationScope::GetContext() const
+{
+ return m_Context;
+}
+
diff --git a/lib/config/activationcontext.hpp b/lib/config/activationcontext.hpp
new file mode 100644
index 0000000..3fe5d09
--- /dev/null
+++ b/lib/config/activationcontext.hpp
@@ -0,0 +1,46 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ACTIVATIONCONTEXT_H
+#define ACTIVATIONCONTEXT_H
+
+#include "config/i2-config.hpp"
+#include "base/object.hpp"
+#include <boost/thread/tss.hpp>
+#include <stack>
+
+namespace icinga
+{
+
+class ActivationContext final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ActivationContext);
+
+ static ActivationContext::Ptr GetCurrentContext();
+
+private:
+ static void PushContext(const ActivationContext::Ptr& context);
+ static void PopContext();
+
+ static std::stack<ActivationContext::Ptr>& GetActivationStack();
+
+ static boost::thread_specific_ptr<std::stack<ActivationContext::Ptr> > m_ActivationStack;
+
+ friend class ActivationScope;
+};
+
+class ActivationScope
+{
+public:
+ ActivationScope(ActivationContext::Ptr context = nullptr);
+ ~ActivationScope();
+
+ ActivationContext::Ptr GetContext() const;
+
+private:
+ ActivationContext::Ptr m_Context;
+};
+
+}
+
+#endif /* ACTIVATIONCONTEXT_H */
diff --git a/lib/config/applyrule-targeted.cpp b/lib/config/applyrule-targeted.cpp
new file mode 100644
index 0000000..c5bfe20
--- /dev/null
+++ b/lib/config/applyrule-targeted.cpp
@@ -0,0 +1,266 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#include "base/string.hpp"
+#include "config/applyrule.hpp"
+#include "config/expression.hpp"
+#include <utility>
+#include <vector>
+
+using namespace icinga;
+
+/**
+ * @returns All ApplyRules targeting only specific parent objects including the given host. (See AddTargetedRule().)
+ */
+const std::set<ApplyRule::Ptr>& ApplyRule::GetTargetedHostRules(const Type::Ptr& sourceType, const String& host)
+{
+ auto perSourceType (m_Rules.find(sourceType.get()));
+
+ if (perSourceType != m_Rules.end()) {
+ auto perHost (perSourceType->second.Targeted.find(host));
+
+ if (perHost != perSourceType->second.Targeted.end()) {
+ return perHost->second.ForHost;
+ }
+ }
+
+ static const std::set<ApplyRule::Ptr> noRules;
+ return noRules;
+}
+
+/**
+ * @returns All ApplyRules targeting only specific parent objects including the given service. (See AddTargetedRule().)
+ */
+const std::set<ApplyRule::Ptr>& ApplyRule::GetTargetedServiceRules(const Type::Ptr& sourceType, const String& host, const String& service)
+{
+ auto perSourceType (m_Rules.find(sourceType.get()));
+
+ if (perSourceType != m_Rules.end()) {
+ auto perHost (perSourceType->second.Targeted.find(host));
+
+ if (perHost != perSourceType->second.Targeted.end()) {
+ auto perService (perHost->second.ForServices.find(service));
+
+ if (perService != perHost->second.ForServices.end()) {
+ return perService->second;
+ }
+ }
+ }
+
+ static const std::set<ApplyRule::Ptr> noRules;
+ return noRules;
+}
+
+/**
+ * If the given ApplyRule targets only specific parent objects, add it to the respective "index".
+ *
+ * - The above means for apply T "N" to Host: assign where host.name == "H" [ || host.name == "h" ... ]
+ * - For apply T "N" to Service it means: assign where host.name == "H" && service.name == "S" [ || host.name == "h" && service.name == "s" ... ]
+ *
+ * The order of operands of || && == doesn't matter.
+ *
+ * @returns Whether the rule has been added to the "index".
+ */
+bool ApplyRule::AddTargetedRule(const ApplyRule::Ptr& rule, const String& targetType, ApplyRule::PerSourceType& rules)
+{
+ if (targetType == "Host") {
+ std::vector<const String *> hosts;
+
+ if (GetTargetHosts(rule->m_Filter.get(), hosts)) {
+ for (auto host : hosts) {
+ rules.Targeted[*host].ForHost.emplace(rule);
+ }
+
+ return true;
+ }
+ } else if (targetType == "Service") {
+ std::vector<std::pair<const String *, const String *>> services;
+
+ if (GetTargetServices(rule->m_Filter.get(), services)) {
+ for (auto service : services) {
+ rules.Targeted[*service.first].ForServices[*service.second].emplace(rule);
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * If the given assign filter is like the following, extract the host names ("H", "h", ...) into the vector:
+ *
+ * host.name == "H" [ || host.name == "h" ... ]
+ *
+ * The order of operands of || == doesn't matter.
+ *
+ * @returns Whether the given assign filter is like above.
+ */
+bool ApplyRule::GetTargetHosts(Expression* assignFilter, std::vector<const String *>& hosts, const Dictionary::Ptr& constants)
+{
+ auto lor (dynamic_cast<LogicalOrExpression*>(assignFilter));
+
+ if (lor) {
+ return GetTargetHosts(lor->GetOperand1().get(), hosts, constants)
+ && GetTargetHosts(lor->GetOperand2().get(), hosts, constants);
+ }
+
+ auto name (GetComparedName(assignFilter, "host", constants));
+
+ if (name) {
+ hosts.emplace_back(name);
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * If the given assign filter is like the following, extract the host+service names ("H"+"S", "h"+"s", ...) into the vector:
+ *
+ * host.name == "H" && service.name == "S" [ || host.name == "h" && service.name == "s" ... ]
+ *
+ * The order of operands of || && == doesn't matter.
+ *
+ * @returns Whether the given assign filter is like above.
+ */
+bool ApplyRule::GetTargetServices(Expression* assignFilter, std::vector<std::pair<const String *, const String *>>& services, const Dictionary::Ptr& constants)
+{
+ auto lor (dynamic_cast<LogicalOrExpression*>(assignFilter));
+
+ if (lor) {
+ return GetTargetServices(lor->GetOperand1().get(), services, constants)
+ && GetTargetServices(lor->GetOperand2().get(), services, constants);
+ }
+
+ auto service (GetTargetService(assignFilter, constants));
+
+ if (service.first) {
+ services.emplace_back(service);
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * If the given filter is like the following, extract the host+service names ("H"+"S"):
+ *
+ * host.name == "H" && service.name == "S"
+ *
+ * The order of operands of && == doesn't matter.
+ *
+ * @returns {host, service} on success and {nullptr, nullptr} on failure.
+ */
+std::pair<const String *, const String *> ApplyRule::GetTargetService(Expression* assignFilter, const Dictionary::Ptr& constants)
+{
+ auto land (dynamic_cast<LogicalAndExpression*>(assignFilter));
+
+ if (!land) {
+ return {nullptr, nullptr};
+ }
+
+ auto op1 (land->GetOperand1().get());
+ auto op2 (land->GetOperand2().get());
+ auto host (GetComparedName(op1, "host", constants));
+
+ if (!host) {
+ std::swap(op1, op2);
+ host = GetComparedName(op1, "host", constants);
+ }
+
+ if (host) {
+ auto service (GetComparedName(op2, "service", constants));
+
+ if (service) {
+ return {host, service};
+ }
+ }
+
+ return {nullptr, nullptr};
+}
+
+/**
+ * If the given filter is like the following, extract the object name ("N"):
+ *
+ * $lcType$.name == "N"
+ *
+ * The order of operands of == doesn't matter.
+ *
+ * @returns The object name on success and nullptr on failure.
+ */
+const String * ApplyRule::GetComparedName(Expression* assignFilter, const char * lcType, const Dictionary::Ptr& constants)
+{
+ auto eq (dynamic_cast<EqualExpression*>(assignFilter));
+
+ if (!eq) {
+ return nullptr;
+ }
+
+ auto op1 (eq->GetOperand1().get());
+ auto op2 (eq->GetOperand2().get());
+
+ if (IsNameIndexer(op1, lcType, constants)) {
+ return GetConstString(op2, constants);
+ }
+
+ if (IsNameIndexer(op2, lcType, constants)) {
+ return GetConstString(op1, constants);
+ }
+
+ return nullptr;
+}
+
+/**
+ * @returns Whether the given expression is like $lcType$.name.
+ */
+bool ApplyRule::IsNameIndexer(Expression* exp, const char * lcType, const Dictionary::Ptr& constants)
+{
+ auto ixr (dynamic_cast<IndexerExpression*>(exp));
+
+ if (!ixr) {
+ return false;
+ }
+
+ auto var (dynamic_cast<VariableExpression*>(ixr->GetOperand1().get()));
+
+ if (!var || var->GetVariable() != lcType) {
+ return false;
+ }
+
+ auto val (GetConstString(ixr->GetOperand2().get(), constants));
+
+ return val && *val == "name";
+}
+
+/**
+ * @returns If the given expression is a constant string, its address. nullptr on failure.
+ */
+const String * ApplyRule::GetConstString(Expression* exp, const Dictionary::Ptr& constants)
+{
+ auto cnst (GetConst(exp, constants));
+
+ return cnst && cnst->IsString() ? &cnst->Get<String>() : nullptr;
+}
+
+/**
+ * @returns If the given expression is a constant, its address. nullptr on failure.
+ */
+const Value * ApplyRule::GetConst(Expression* exp, const Dictionary::Ptr& constants)
+{
+ auto lit (dynamic_cast<LiteralExpression*>(exp));
+
+ if (lit) {
+ return &lit->GetValue();
+ }
+
+ if (constants) {
+ auto var (dynamic_cast<VariableExpression*>(exp));
+
+ if (var) {
+ return constants->GetRef(var->GetVariable());
+ }
+ }
+
+ return nullptr;
+}
diff --git a/lib/config/applyrule.cpp b/lib/config/applyrule.cpp
new file mode 100644
index 0000000..8739971
--- /dev/null
+++ b/lib/config/applyrule.cpp
@@ -0,0 +1,189 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/applyrule.hpp"
+#include "base/logger.hpp"
+#include <set>
+#include <unordered_set>
+
+using namespace icinga;
+
+ApplyRule::RuleMap ApplyRule::m_Rules;
+ApplyRule::TypeMap ApplyRule::m_Types;
+
+ApplyRule::ApplyRule(String name, Expression::Ptr expression,
+ Expression::Ptr filter, String package, String fkvar, String fvvar, Expression::Ptr fterm,
+ bool ignoreOnError, DebugInfo di, Dictionary::Ptr scope)
+ : m_Name(std::move(name)), m_Expression(std::move(expression)), m_Filter(std::move(filter)), m_Package(std::move(package)), m_FKVar(std::move(fkvar)),
+ m_FVVar(std::move(fvvar)), m_FTerm(std::move(fterm)), m_IgnoreOnError(ignoreOnError), m_DebugInfo(std::move(di)), m_Scope(std::move(scope)), m_HasMatches(false)
+{ }
+
+String ApplyRule::GetName() const
+{
+ return m_Name;
+}
+
+Expression::Ptr ApplyRule::GetExpression() const
+{
+ return m_Expression;
+}
+
+Expression::Ptr ApplyRule::GetFilter() const
+{
+ return m_Filter;
+}
+
+String ApplyRule::GetPackage() const
+{
+ return m_Package;
+}
+
+Expression::Ptr ApplyRule::GetFTerm() const
+{
+ return m_FTerm;
+}
+
+bool ApplyRule::GetIgnoreOnError() const
+{
+ return m_IgnoreOnError;
+}
+
+const DebugInfo& ApplyRule::GetDebugInfo() const
+{
+ return m_DebugInfo;
+}
+
+Dictionary::Ptr ApplyRule::GetScope() const
+{
+ return m_Scope;
+}
+
+void ApplyRule::AddRule(const String& sourceType, const String& targetType, const String& name,
+ const Expression::Ptr& expression, const Expression::Ptr& filter, const String& package, const String& fkvar,
+ const String& fvvar, const Expression::Ptr& fterm, bool ignoreOnError, const DebugInfo& di, const Dictionary::Ptr& scope)
+{
+ auto actualTargetType (&targetType);
+
+ if (*actualTargetType == "") {
+ auto& targetTypes (GetTargetTypes(sourceType));
+
+ if (targetTypes.size() == 1u) {
+ actualTargetType = &targetTypes[0];
+ }
+ }
+
+ ApplyRule::Ptr rule = new ApplyRule(name, expression, filter, package, fkvar, fvvar, fterm, ignoreOnError, di, scope);
+ auto& rules (m_Rules[Type::GetByName(sourceType).get()]);
+
+ if (!AddTargetedRule(rule, *actualTargetType, rules)) {
+ rules.Regular[Type::GetByName(*actualTargetType).get()].emplace_back(std::move(rule));
+ }
+}
+
+bool ApplyRule::EvaluateFilter(ScriptFrame& frame) const
+{
+ return Convert::ToBool(m_Filter->Evaluate(frame));
+}
+
+void ApplyRule::RegisterType(const String& sourceType, const std::vector<String>& targetTypes)
+{
+ m_Types[sourceType] = targetTypes;
+}
+
+bool ApplyRule::IsValidSourceType(const String& sourceType)
+{
+ return m_Types.find(sourceType) != m_Types.end();
+}
+
+bool ApplyRule::IsValidTargetType(const String& sourceType, const String& targetType)
+{
+ auto it = m_Types.find(sourceType);
+
+ if (it == m_Types.end())
+ return false;
+
+ if (it->second.size() == 1 && targetType == "")
+ return true;
+
+ for (const String& type : it->second) {
+ if (type == targetType)
+ return true;
+ }
+
+ return false;
+}
+
+const std::vector<String>& ApplyRule::GetTargetTypes(const String& sourceType)
+{
+ auto it = m_Types.find(sourceType);
+
+ if (it == m_Types.end()) {
+ static const std::vector<String> noTypes;
+ return noTypes;
+ }
+
+ return it->second;
+}
+
+void ApplyRule::AddMatch()
+{
+ m_HasMatches.store(true, std::memory_order_relaxed);
+}
+
+bool ApplyRule::HasMatches() const
+{
+ return m_HasMatches.load(std::memory_order_relaxed);
+}
+
+const std::vector<ApplyRule::Ptr>& ApplyRule::GetRules(const Type::Ptr& sourceType, const Type::Ptr& targetType)
+{
+ auto perSourceType (m_Rules.find(sourceType.get()));
+
+ if (perSourceType != m_Rules.end()) {
+ auto perTargetType (perSourceType->second.Regular.find(targetType.get()));
+
+ if (perTargetType != perSourceType->second.Regular.end()) {
+ return perTargetType->second;
+ }
+ }
+
+ static const std::vector<ApplyRule::Ptr> noRules;
+ return noRules;
+}
+
+void ApplyRule::CheckMatches(bool silent)
+{
+ for (auto& perSourceType : m_Rules) {
+ for (auto& perTargetType : perSourceType.second.Regular) {
+ for (auto& rule : perTargetType.second) {
+ CheckMatches(rule, perSourceType.first, silent);
+ }
+ }
+
+ std::unordered_set<ApplyRule*> targeted;
+
+ for (auto& perHost : perSourceType.second.Targeted) {
+ for (auto& rule : perHost.second.ForHost) {
+ targeted.emplace(rule.get());
+ }
+
+ for (auto& perService : perHost.second.ForServices) {
+ for (auto& rule : perService.second) {
+ targeted.emplace(rule.get());
+ }
+ }
+ }
+
+ for (auto rule : targeted) {
+ CheckMatches(rule, perSourceType.first, silent);
+ }
+ }
+}
+
+void ApplyRule::CheckMatches(const ApplyRule::Ptr& rule, Type* sourceType, bool silent)
+{
+ if (!rule->HasMatches() && !silent) {
+ Log(LogWarning, "ApplyRule")
+ << "Apply rule '" << rule->GetName() << "' (" << rule->GetDebugInfo() << ") for type '"
+ << sourceType->GetName() << "' does not match anywhere!";
+ }
+}
diff --git a/lib/config/applyrule.hpp b/lib/config/applyrule.hpp
new file mode 100644
index 0000000..cf9b6e5
--- /dev/null
+++ b/lib/config/applyrule.hpp
@@ -0,0 +1,126 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APPLYRULE_H
+#define APPLYRULE_H
+
+#include "config/i2-config.hpp"
+#include "config/expression.hpp"
+#include "base/debuginfo.hpp"
+#include "base/shared-object.hpp"
+#include "base/type.hpp"
+#include <unordered_map>
+#include <atomic>
+
+namespace icinga
+{
+
+/**
+ * @ingroup config
+ */
+class ApplyRule : public SharedObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ApplyRule);
+
+ struct PerHost
+ {
+ std::set<ApplyRule::Ptr> ForHost;
+ std::unordered_map<String /* service */, std::set<ApplyRule::Ptr>> ForServices;
+ };
+
+ struct PerSourceType
+ {
+ std::unordered_map<Type* /* target type */, std::vector<ApplyRule::Ptr>> Regular;
+ std::unordered_map<String /* host */, PerHost> Targeted;
+ };
+
+ /*
+ * m_Rules[T::TypeInstance.get()].Targeted["H"].ForHost
+ * contains all apply rules like apply T "x" to Host { ... }
+ * which target only specific hosts incl. "H", e.g. via
+ * assign where host.name == "H" || host.name == "h".
+ *
+ * m_Rules[T::TypeInstance.get()].Targeted["H"].ForServices["S"]
+ * contains all apply rules like apply T "x" to Service { ... }
+ * which target only specific services on specific hosts,
+ * e.g. via assign where host.name == "H" && service.name == "S".
+ *
+ * m_Rules[T::TypeInstance.get()].Regular[C::TypeInstance.get()]
+ * contains all other apply rules like apply T "x" to C { ... }.
+ */
+ typedef std::unordered_map<Type* /* source type */, PerSourceType> RuleMap;
+
+ typedef std::map<String, std::vector<String> > TypeMap;
+
+ String GetName() const;
+ Expression::Ptr GetExpression() const;
+ Expression::Ptr GetFilter() const;
+ String GetPackage() const;
+
+ inline const String& GetFKVar() const noexcept
+ {
+ return m_FKVar;
+ }
+
+ inline const String& GetFVVar() const noexcept
+ {
+ return m_FVVar;
+ }
+
+ Expression::Ptr GetFTerm() const;
+ bool GetIgnoreOnError() const;
+ const DebugInfo& GetDebugInfo() const;
+ Dictionary::Ptr GetScope() const;
+ void AddMatch();
+ bool HasMatches() const;
+
+ bool EvaluateFilter(ScriptFrame& frame) const;
+
+ static void AddRule(const String& sourceType, const String& targetType, const String& name, const Expression::Ptr& expression,
+ const Expression::Ptr& filter, const String& package, const String& fkvar, const String& fvvar, const Expression::Ptr& fterm,
+ bool ignoreOnError, const DebugInfo& di, const Dictionary::Ptr& scope);
+ static const std::vector<ApplyRule::Ptr>& GetRules(const Type::Ptr& sourceType, const Type::Ptr& targetType);
+ static const std::set<ApplyRule::Ptr>& GetTargetedHostRules(const Type::Ptr& sourceType, const String& host);
+ static const std::set<ApplyRule::Ptr>& GetTargetedServiceRules(const Type::Ptr& sourceType, const String& host, const String& service);
+ static bool GetTargetHosts(Expression* assignFilter, std::vector<const String *>& hosts, const Dictionary::Ptr& constants = nullptr);
+ static bool GetTargetServices(Expression* assignFilter, std::vector<std::pair<const String *, const String *>>& services, const Dictionary::Ptr& constants = nullptr);
+
+ static void RegisterType(const String& sourceType, const std::vector<String>& targetTypes);
+ static bool IsValidSourceType(const String& sourceType);
+ static bool IsValidTargetType(const String& sourceType, const String& targetType);
+ static const std::vector<String>& GetTargetTypes(const String& sourceType);
+
+ static void CheckMatches(bool silent);
+ static void CheckMatches(const ApplyRule::Ptr& rule, Type* sourceType, bool silent);
+
+private:
+ String m_Name;
+ Expression::Ptr m_Expression;
+ Expression::Ptr m_Filter;
+ String m_Package;
+ String m_FKVar;
+ String m_FVVar;
+ Expression::Ptr m_FTerm;
+ bool m_IgnoreOnError;
+ DebugInfo m_DebugInfo;
+ Dictionary::Ptr m_Scope;
+ std::atomic<bool> m_HasMatches;
+
+ static TypeMap m_Types;
+ static RuleMap m_Rules;
+
+ static bool AddTargetedRule(const ApplyRule::Ptr& rule, const String& targetType, PerSourceType& rules);
+ static std::pair<const String *, const String *> GetTargetService(Expression* assignFilter, const Dictionary::Ptr& constants);
+ static const String * GetComparedName(Expression* assignFilter, const char * lcType, const Dictionary::Ptr& constants);
+ static bool IsNameIndexer(Expression* exp, const char * lcType, const Dictionary::Ptr& constants);
+ static const String * GetConstString(Expression* exp, const Dictionary::Ptr& constants);
+ static const Value * GetConst(Expression* exp, const Dictionary::Ptr& constants);
+
+ ApplyRule(String name, Expression::Ptr expression,
+ Expression::Ptr filter, String package, String fkvar, String fvvar, Expression::Ptr fterm,
+ bool ignoreOnError, DebugInfo di, Dictionary::Ptr scope);
+};
+
+}
+
+#endif /* APPLYRULE_H */
diff --git a/lib/config/config_lexer.ll b/lib/config/config_lexer.ll
new file mode 100644
index 0000000..abfdaff
--- /dev/null
+++ b/lib/config/config_lexer.ll
@@ -0,0 +1,253 @@
+%{
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/configcompiler.hpp"
+#include "config/expression.hpp"
+#include "base/exception.hpp"
+#include <utility>
+
+using namespace icinga;
+
+#include "config/config_parser.hh"
+#include <sstream>
+
+#define YYLTYPE icinga::CompilerDebugInfo
+
+#define YY_EXTRA_TYPE ConfigCompiler *
+#define YY_USER_ACTION \
+do { \
+ yylloc->Path = yyextra->GetPath(); \
+ yylloc->FirstLine = yylineno; \
+ yylloc->FirstColumn = yycolumn; \
+ yylloc->LastLine = yylineno; \
+ yylloc->LastColumn = yycolumn + yyleng - 1; \
+ yycolumn += yyleng; \
+} while (0);
+
+#define YY_INPUT(buf, result, max_size) \
+do { \
+ result = yyextra->ReadInput(buf, max_size); \
+} while (0)
+%}
+
+%option reentrant noyywrap yylineno
+%option bison-bridge bison-locations
+%option never-interactive nounistd
+%option noinput nounput
+
+%x C_COMMENT
+%x STRING
+%x HEREDOC
+
+%%
+\" {
+ yyextra->m_LexBuffer.Clear();
+
+ yyextra->m_LocationBegin = *yylloc;
+
+ BEGIN(STRING);
+ }
+
+<STRING>\" {
+ BEGIN(INITIAL);
+
+ yylloc->FirstLine = yyextra->m_LocationBegin.FirstLine;
+ yylloc->FirstColumn = yyextra->m_LocationBegin.FirstColumn;
+
+ yylval->text = new String(std::move(yyextra->m_LexBuffer));
+
+ return T_STRING;
+ }
+
+<STRING>\n {
+ BOOST_THROW_EXCEPTION(ScriptError("Unterminated string literal", DebugInfoRange(yyextra->m_LocationBegin, *yylloc)));
+ }
+
+<STRING>\\[0-7]{1,3} {
+ /* octal escape sequence */
+ int result;
+
+ (void) sscanf(yytext + 1, "%o", &result);
+
+ if (result > 0xff) {
+ /* error, constant is out-of-bounds */
+ BOOST_THROW_EXCEPTION(ScriptError("Constant is out of bounds: " + String(yytext), *yylloc));
+ }
+
+ yyextra->m_LexBuffer += static_cast<char>(result);
+ }
+
+<STRING>\\[0-9]+ {
+ /* generate error - bad escape sequence; something
+ * like '\48' or '\0777777'
+ */
+ BOOST_THROW_EXCEPTION(ScriptError("Bad escape sequence found: " + String(yytext), *yylloc));
+ }
+<STRING>\\n { yyextra->m_LexBuffer += '\n'; }
+<STRING>\\\\ { yyextra->m_LexBuffer += '\\'; }
+<STRING>\\\" { yyextra->m_LexBuffer += '"'; }
+<STRING>\\t { yyextra->m_LexBuffer += '\t'; }
+<STRING>\\r { yyextra->m_LexBuffer += '\r'; }
+<STRING>\\b { yyextra->m_LexBuffer += '\b'; }
+<STRING>\\f { yyextra->m_LexBuffer += '\f'; }
+<STRING>\\\n { yyextra->m_LexBuffer += yytext[1]; }
+<STRING>\\. {
+ BOOST_THROW_EXCEPTION(ScriptError("Bad escape sequence found: " + String(yytext), *yylloc));
+ }
+
+<STRING>[^\\\n\"]+ {
+ char *yptr = yytext;
+
+ while (*yptr)
+ yyextra->m_LexBuffer += *yptr++;
+ }
+
+<STRING><<EOF>> {
+ BOOST_THROW_EXCEPTION(ScriptError("End-of-file while in string literal", DebugInfoRange(yyextra->m_LocationBegin, *yylloc)));
+ }
+
+\{\{\{ {
+ yyextra->m_LexBuffer.Clear();
+
+ yyextra->m_LocationBegin = *yylloc;
+
+ BEGIN(HEREDOC);
+ }
+
+<HEREDOC><<EOF>> {
+ BOOST_THROW_EXCEPTION(ScriptError("End-of-file while in string literal", DebugInfoRange(yyextra->m_LocationBegin, *yylloc)));
+ }
+
+<HEREDOC>\}\}\} {
+ BEGIN(INITIAL);
+
+ yylloc->FirstLine = yyextra->m_LocationBegin.FirstLine;
+ yylloc->FirstColumn = yyextra->m_LocationBegin.FirstColumn;
+
+ yylval->text = new String(std::move(yyextra->m_LexBuffer));
+
+ return T_STRING;
+ }
+
+<HEREDOC>(.|\n) { yyextra->m_LexBuffer += yytext[0]; }
+
+<INITIAL>{
+"/*" BEGIN(C_COMMENT);
+}
+
+<C_COMMENT>{
+"*/" BEGIN(INITIAL);
+[^*] /* ignore comment */
+"*" /* ignore star */
+}
+
+<C_COMMENT><<EOF>> {
+ BOOST_THROW_EXCEPTION(ScriptError("End-of-file while in comment", *yylloc));
+ }
+
+
+\/\/[^\n]* /* ignore C++-style comments */
+#[^\n]* /* ignore shell-style comments */
+[ \t] /* ignore whitespace */
+
+<INITIAL>{
+object return T_OBJECT;
+template return T_TEMPLATE;
+include return T_INCLUDE;
+include_recursive return T_INCLUDE_RECURSIVE;
+include_zones return T_INCLUDE_ZONES;
+library return T_LIBRARY;
+null return T_NULL;
+true { yylval->boolean = 1; return T_BOOLEAN; }
+false { yylval->boolean = 0; return T_BOOLEAN; }
+const return T_CONST;
+var return T_VAR;
+this return T_THIS;
+globals return T_GLOBALS;
+locals return T_LOCALS;
+use return T_USE;
+using return T_USING;
+apply return T_APPLY;
+default return T_DEFAULT;
+to return T_TO;
+where return T_WHERE;
+import return T_IMPORT;
+assign return T_ASSIGN;
+ignore return T_IGNORE;
+function return T_FUNCTION;
+return return T_RETURN;
+break return T_BREAK;
+continue return T_CONTINUE;
+for return T_FOR;
+if return T_IF;
+else return T_ELSE;
+while return T_WHILE;
+throw return T_THROW;
+try return T_TRY;
+except return T_EXCEPT;
+ignore_on_error return T_IGNORE_ON_ERROR;
+current_filename return T_CURRENT_FILENAME;
+current_line return T_CURRENT_LINE;
+debugger return T_DEBUGGER;
+namespace return T_NAMESPACE;
+=\> return T_FOLLOWS;
+\<\< return T_SHIFT_LEFT;
+\>\> return T_SHIFT_RIGHT;
+\<= return T_LESS_THAN_OR_EQUAL;
+\>= return T_GREATER_THAN_OR_EQUAL;
+== return T_EQUAL;
+!= return T_NOT_EQUAL;
+!in return T_NOT_IN;
+in return T_IN;
+&& return T_LOGICAL_AND;
+\|\| return T_LOGICAL_OR;
+\{\{ return T_NULLARY_LAMBDA_BEGIN;
+\}\} return T_NULLARY_LAMBDA_END;
+[a-zA-Z_][a-zA-Z0-9\_]* { yylval->text = new String(yytext); return T_IDENTIFIER; }
+@[a-zA-Z_][a-zA-Z0-9\_]* { yylval->text = new String(yytext + 1); return T_IDENTIFIER; }
+\<[^ \>]*\> { yytext[yyleng-1] = '\0'; yylval->text = new String(yytext + 1); return T_STRING_ANGLE; }
+[0-9]+(\.[0-9]+)?ms { yylval->num = strtod(yytext, NULL) / 1000; return T_NUMBER; }
+[0-9]+(\.[0-9]+)?d { yylval->num = strtod(yytext, NULL) * 60 * 60 * 24; return T_NUMBER; }
+[0-9]+(\.[0-9]+)?h { yylval->num = strtod(yytext, NULL) * 60 * 60; return T_NUMBER; }
+[0-9]+(\.[0-9]+)?m { yylval->num = strtod(yytext, NULL) * 60; return T_NUMBER; }
+[0-9]+(\.[0-9]+)?s { yylval->num = strtod(yytext, NULL); return T_NUMBER; }
+[0-9]+(\.[0-9]+)? { yylval->num = strtod(yytext, NULL); return T_NUMBER; }
+= { yylval->csop = OpSetLiteral; return T_SET; }
+\+= { yylval->csop = OpSetAdd; return T_SET_ADD; }
+-= { yylval->csop = OpSetSubtract; return T_SET_SUBTRACT; }
+\*= { yylval->csop = OpSetMultiply; return T_SET_MULTIPLY; }
+\/= { yylval->csop = OpSetDivide; return T_SET_DIVIDE; }
+\%= { yylval->csop = OpSetModulo; return T_SET_MODULO; }
+\^= { yylval->csop = OpSetXor; return T_SET_XOR; }
+\&= { yylval->csop = OpSetBinaryAnd; return T_SET_BINARY_AND; }
+\|= { yylval->csop = OpSetBinaryOr; return T_SET_BINARY_OR; }
+\+ return T_PLUS;
+\- return T_MINUS;
+\* return T_MULTIPLY;
+\/ return T_DIVIDE_OP;
+\% return T_MODULO;
+\^ return T_XOR;
+\& return T_BINARY_AND;
+\| return T_BINARY_OR;
+\< return T_LESS_THAN;
+\> return T_GREATER_THAN;
+}
+
+\( { yyextra->m_IgnoreNewlines.push(true); return '('; }
+\) { yyextra->m_IgnoreNewlines.pop(); return ')'; }
+[\r\n]+ { yycolumn -= strlen(yytext) - 1; if (!yyextra->m_IgnoreNewlines.top()) { return T_NEWLINE; } }
+<<EOF>> { if (!yyextra->m_Eof) { yyextra->m_Eof = true; return T_NEWLINE; } else { yyterminate(); } }
+. return yytext[0];
+
+%%
+
+void ConfigCompiler::InitializeScanner()
+{
+ yylex_init(&m_Scanner);
+ yyset_extra(this, m_Scanner);
+}
+
+void ConfigCompiler::DestroyScanner()
+{
+ yylex_destroy(m_Scanner);
+}
diff --git a/lib/config/config_parser.yy b/lib/config/config_parser.yy
new file mode 100644
index 0000000..939681e
--- /dev/null
+++ b/lib/config/config_parser.yy
@@ -0,0 +1,1243 @@
+%{
+#define YYDEBUG 1
+
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/i2-config.hpp"
+#include "config/configcompiler.hpp"
+#include "config/expression.hpp"
+#include "config/applyrule.hpp"
+#include "config/objectrule.hpp"
+#include "base/value.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/configtype.hpp"
+#include "base/exception.hpp"
+#include <sstream>
+#include <stack>
+
+#define YYLTYPE icinga::CompilerDebugInfo
+#define YYERROR_VERBOSE
+
+#define YYLLOC_DEFAULT(Current, Rhs, N) \
+do { \
+ if (N) { \
+ (Current).Path = YYRHSLOC(Rhs, 1).Path; \
+ (Current).FirstLine = YYRHSLOC(Rhs, 1).FirstLine; \
+ (Current).FirstColumn = YYRHSLOC(Rhs, 1).FirstColumn; \
+ (Current).LastLine = YYRHSLOC(Rhs, N).LastLine; \
+ (Current).LastColumn = YYRHSLOC(Rhs, N).LastColumn; \
+ } else { \
+ (Current).Path = YYRHSLOC(Rhs, 0).Path; \
+ (Current).FirstLine = (Current).LastLine = \
+ YYRHSLOC(Rhs, 0).LastLine; \
+ (Current).FirstColumn = (Current).LastColumn = \
+ YYRHSLOC(Rhs, 0).LastColumn; \
+ } \
+} while (0)
+
+#define YY_LOCATION_PRINT(file, loc) \
+do { \
+ std::ostringstream msgbuf; \
+ msgbuf << loc; \
+ std::string str = msgbuf.str(); \
+ fputs(str.c_str(), file); \
+} while (0)
+
+#define YYINITDEPTH 10000
+
+using namespace icinga;
+
+template<typename T>
+static void MakeRBinaryOp(Expression** result, Expression *left, Expression *right, const DebugInfo& diLeft, const DebugInfo& diRight)
+{
+ *result = new T(std::unique_ptr<Expression>(left), std::unique_ptr<Expression>(right), DebugInfoRange(diLeft, diRight));
+}
+
+%}
+
+%pure-parser
+
+%locations
+%defines
+%error-verbose
+%glr-parser
+
+%parse-param { std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> > *llist }
+%parse-param { ConfigCompiler *context }
+%lex-param { void *scanner }
+
+%union {
+ String *text;
+ double num;
+ bool boolean;
+ icinga::Expression *expr;
+ icinga::DictExpression *dexpr;
+ CombinedSetOp csop;
+ std::vector<String> *slist;
+ std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> > *llist;
+ std::vector<std::unique_ptr<Expression> > *elist;
+ std::vector<std::pair<std::unique_ptr<Expression>, std::unique_ptr<Expression> > > *ebranchlist;
+ std::pair<std::unique_ptr<Expression>, std::unique_ptr<Expression> > *ebranch;
+ std::pair<String, std::unique_ptr<Expression> > *cvitem;
+ std::map<String, std::unique_ptr<Expression> > *cvlist;
+ icinga::ScopeSpecifier scope;
+}
+
+%token T_NEWLINE "new-line"
+%token <text> T_STRING
+%token <text> T_STRING_ANGLE
+%token <num> T_NUMBER
+%token <boolean> T_BOOLEAN
+%token T_NULL
+%token <text> T_IDENTIFIER
+
+%token <csop> T_SET "= (T_SET)"
+%token <csop> T_SET_ADD "+= (T_SET_ADD)"
+%token <csop> T_SET_SUBTRACT "-= (T_SET_SUBTRACT)"
+%token <csop> T_SET_MULTIPLY "*= (T_SET_MULTIPLY)"
+%token <csop> T_SET_DIVIDE "/= (T_SET_DIVIDE)"
+%token <csop> T_SET_MODULO "%= (T_SET_MODULO)"
+%token <csop> T_SET_XOR "^= (T_SET_XOR)"
+%token <csop> T_SET_BINARY_AND "&= (T_SET_BINARY_AND)"
+%token <csop> T_SET_BINARY_OR "|= (T_SET_BINARY_OR)"
+
+%token T_SHIFT_LEFT "<< (T_SHIFT_LEFT)"
+%token T_SHIFT_RIGHT ">> (T_SHIFT_RIGHT)"
+%token T_EQUAL "== (T_EQUAL)"
+%token T_NOT_EQUAL "!= (T_NOT_EQUAL)"
+%token T_IN "in (T_IN)"
+%token T_NOT_IN "!in (T_NOT_IN)"
+%token T_LOGICAL_AND "&& (T_LOGICAL_AND)"
+%token T_LOGICAL_OR "|| (T_LOGICAL_OR)"
+%token T_LESS_THAN_OR_EQUAL "<= (T_LESS_THAN_OR_EQUAL)"
+%token T_GREATER_THAN_OR_EQUAL ">= (T_GREATER_THAN_OR_EQUAL)"
+%token T_PLUS "+ (T_PLUS)"
+%token T_MINUS "- (T_MINUS)"
+%token T_MULTIPLY "* (T_MULTIPLY)"
+%token T_DIVIDE_OP "/ (T_DIVIDE_OP)"
+%token T_MODULO "% (T_MODULO)"
+%token T_XOR "^ (T_XOR)"
+%token T_BINARY_AND "& (T_BINARY_AND)"
+%token T_BINARY_OR "| (T_BINARY_OR)"
+%token T_LESS_THAN "< (T_LESS_THAN)"
+%token T_GREATER_THAN "> (T_GREATER_THAN)"
+
+%token T_VAR "var (T_VAR)"
+%token T_GLOBALS "globals (T_GLOBALS)"
+%token T_LOCALS "locals (T_LOCALS)"
+%token T_CONST "const (T_CONST)"
+%token T_DEFAULT "default (T_DEFAULT)"
+%token T_IGNORE_ON_ERROR "ignore_on_error (T_IGNORE_ON_ERROR)"
+%token T_CURRENT_FILENAME "current_filename (T_CURRENT_FILENAME)"
+%token T_CURRENT_LINE "current_line (T_CURRENT_LINE)"
+%token T_DEBUGGER "debugger (T_DEBUGGER)"
+%token T_NAMESPACE "namespace (T_NAMESPACE)"
+%token T_USE "use (T_USE)"
+%token T_USING "using (T_USING)"
+%token T_OBJECT "object (T_OBJECT)"
+%token T_TEMPLATE "template (T_TEMPLATE)"
+%token T_INCLUDE "include (T_INCLUDE)"
+%token T_INCLUDE_RECURSIVE "include_recursive (T_INCLUDE_RECURSIVE)"
+%token T_INCLUDE_ZONES "include_zones (T_INCLUDE_ZONES)"
+%token T_LIBRARY "library (T_LIBRARY)"
+%token T_APPLY "apply (T_APPLY)"
+%token T_TO "to (T_TO)"
+%token T_WHERE "where (T_WHERE)"
+%token T_IMPORT "import (T_IMPORT)"
+%token T_ASSIGN "assign (T_ASSIGN)"
+%token T_IGNORE "ignore (T_IGNORE)"
+%token T_FUNCTION "function (T_FUNCTION)"
+%token T_RETURN "return (T_RETURN)"
+%token T_BREAK "break (T_BREAK)"
+%token T_CONTINUE "continue (T_CONTINUE)"
+%token T_FOR "for (T_FOR)"
+%token T_IF "if (T_IF)"
+%token T_ELSE "else (T_ELSE)"
+%token T_WHILE "while (T_WHILE)"
+%token T_THROW "throw (T_THROW)"
+%token T_TRY "try (T_TRY)"
+%token T_EXCEPT "except (T_EXCEPT)"
+%token T_FOLLOWS "=> (T_FOLLOWS)"
+%token T_NULLARY_LAMBDA_BEGIN "{{ (T_NULLARY_LAMBDA_BEGIN)"
+%token T_NULLARY_LAMBDA_END "}} (T_NULLARY_LAMBDA_END)"
+
+%type <text> identifier
+%type <elist> rterm_items
+%type <elist> rterm_items_inner
+%type <slist> identifier_items
+%type <slist> identifier_items_inner
+%type <csop> combined_set_op
+%type <llist> statements
+%type <llist> lterm_items
+%type <llist> lterm_items_inner
+%type <expr> rterm
+%type <expr> rterm_array
+%type <dexpr> rterm_dict
+%type <dexpr> rterm_scope_require_side_effect
+%type <dexpr> rterm_scope
+%type <ebranchlist> else_if_branches
+%type <ebranch> else_if_branch
+%type <expr> rterm_side_effect
+%type <expr> rterm_no_side_effect
+%type <expr> rterm_no_side_effect_no_dict
+%type <expr> lterm
+%type <expr> object
+%type <expr> apply
+%type <expr> optional_rterm
+%type <text> target_type_specifier
+%type <boolean> default_specifier
+%type <boolean> ignore_specifier
+%type <cvlist> use_specifier
+%type <cvlist> use_specifier_items
+%type <cvitem> use_specifier_item
+%type <num> object_declaration
+
+%right T_FOLLOWS
+%right T_INCLUDE T_INCLUDE_RECURSIVE T_INCLUDE_ZONES T_OBJECT T_TEMPLATE T_APPLY T_IMPORT T_ASSIGN T_IGNORE T_WHERE
+%right T_FUNCTION T_FOR
+%left T_SET T_SET_ADD T_SET_SUBTRACT T_SET_MULTIPLY T_SET_DIVIDE T_SET_MODULO T_SET_XOR T_SET_BINARY_AND T_SET_BINARY_OR
+%right '?' ':'
+%left T_LOGICAL_OR
+%left T_LOGICAL_AND
+%left T_RETURN T_BREAK T_CONTINUE
+%left T_IDENTIFIER
+%left T_BINARY_OR
+%left T_XOR
+%left T_BINARY_AND
+%nonassoc T_EQUAL T_NOT_EQUAL
+%left T_IN T_NOT_IN
+%nonassoc T_LESS_THAN T_LESS_THAN_OR_EQUAL T_GREATER_THAN T_GREATER_THAN_OR_EQUAL
+%left T_SHIFT_LEFT T_SHIFT_RIGHT
+%left T_PLUS T_MINUS
+%left T_MULTIPLY T_DIVIDE_OP T_MODULO
+%left UNARY_MINUS UNARY_PLUS
+%right REF_OP DEREF_OP
+%right '!' '~'
+%left '.' '(' '['
+%left T_VAR T_THIS T_GLOBALS T_LOCALS
+%right ';' ','
+%right T_NEWLINE
+%{
+
+int yylex(YYSTYPE *lvalp, YYLTYPE *llocp, void *scanner);
+
+extern int yydebug;
+
+void yyerror(const YYLTYPE *locp, std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> > *, ConfigCompiler *context, const char *err)
+{
+ bool incomplete = context && context->m_Eof && (context->m_OpenBraces > 0);
+ BOOST_THROW_EXCEPTION(ScriptError(err, *locp, incomplete));
+}
+
+int yyparse(std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> > *llist, ConfigCompiler *context);
+
+static void BeginFlowControlBlock(ConfigCompiler *compiler, int allowedTypes, bool inherit)
+{
+ if (inherit)
+ allowedTypes |= compiler->m_FlowControlInfo.top();
+
+ compiler->m_FlowControlInfo.push(allowedTypes);
+}
+
+static void EndFlowControlBlock(ConfigCompiler *compiler)
+{
+ compiler->m_FlowControlInfo.pop();
+}
+
+static void UseFlowControl(ConfigCompiler *compiler, FlowControlType type, const CompilerDebugInfo& location)
+{
+ int fci = compiler->m_FlowControlInfo.top();
+
+ if ((type & fci) != type)
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid flow control statement.", location));
+}
+
+std::unique_ptr<Expression> ConfigCompiler::Compile()
+{
+ std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> > llist;
+
+ //yydebug = 1;
+
+ m_IgnoreNewlines.push(false);
+ BeginFlowControlBlock(this, 0, false);
+
+ if (yyparse(&llist, this) != 0)
+ return NULL;
+
+ EndFlowControlBlock(this);
+ m_IgnoreNewlines.pop();
+
+ std::vector<std::unique_ptr<Expression> > dlist;
+ decltype(llist.size()) num = 0;
+ for (auto& litem : llist) {
+ if (!litem.second.SideEffect && num != llist.size() - 1) {
+ yyerror(&litem.second.DebugInfo, NULL, NULL, "Value computed is not used.");
+ }
+ dlist.emplace_back(std::move(litem.first));
+ num++;
+ }
+
+ std::unique_ptr<DictExpression> expr{new DictExpression(std::move(dlist))};
+ expr->MakeInline();
+ return std::move(expr);
+}
+
+#define scanner (context->GetScanner())
+
+%}
+
+%%
+script: statements
+ {
+ llist->swap(*$1);
+ delete $1;
+ }
+ ;
+
+statements: optional_newlines lterm_items
+ {
+ $$ = $2;
+ }
+ ;
+
+lterm_items: /* empty */
+ {
+ $$ = new std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> >();
+ }
+ | lterm_items_inner
+ | lterm_items_inner sep
+ ;
+
+lterm_items_inner: lterm %dprec 2
+ {
+ $$ = new std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> >();
+ $$->emplace_back(std::unique_ptr<Expression>($1), EItemInfo{true, @1});
+ }
+ | rterm_no_side_effect
+ {
+ $$ = new std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> >();
+ $$->emplace_back(std::unique_ptr<Expression>($1), EItemInfo{false, @1});
+ }
+ | lterm_items_inner sep lterm %dprec 1
+ {
+ if ($1)
+ $$ = $1;
+ else
+ $$ = new std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> >();
+
+ if ($3) {
+ $$->emplace_back(std::unique_ptr<Expression>($3), EItemInfo{true, @3});
+ }
+ }
+ | lterm_items_inner sep rterm_no_side_effect %dprec 1
+ {
+ if ($1)
+ $$ = $1;
+ else
+ $$ = new std::vector<std::pair<std::unique_ptr<Expression>, EItemInfo> >();
+
+ if ($3) {
+ $$->emplace_back(std::unique_ptr<Expression>($3), EItemInfo{false, @3});
+ }
+ }
+ ;
+
+identifier: T_IDENTIFIER
+ | T_STRING
+ ;
+
+object:
+ {
+ context->m_ObjectAssign.push(true);
+ context->m_SeenAssign.push(false);
+ context->m_SeenIgnore.push(false);
+ context->m_Assign.push(0);
+ context->m_Ignore.push(0);
+ }
+ object_declaration rterm optional_rterm use_specifier default_specifier ignore_specifier
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope_require_side_effect
+ {
+ EndFlowControlBlock(context);
+
+ context->m_ObjectAssign.pop();
+
+ bool abstract = $2;
+ bool defaultTmpl = $6;
+
+ if (!abstract && defaultTmpl)
+ BOOST_THROW_EXCEPTION(ScriptError("'default' keyword is invalid for object definitions", @6));
+
+ bool seen_assign = context->m_SeenAssign.top();
+ context->m_SeenAssign.pop();
+
+ bool seen_ignore = context->m_SeenIgnore.top();
+ context->m_SeenIgnore.pop();
+
+ std::unique_ptr<Expression> ignore{std::move(context->m_Ignore.top())};
+ context->m_Ignore.pop();
+
+ std::unique_ptr<Expression> assign{std::move(context->m_Assign.top())};
+ context->m_Assign.pop();
+
+ std::unique_ptr<Expression> filter;
+
+ if (seen_assign) {
+ if (ignore) {
+ std::unique_ptr<Expression> rex{new LogicalNegateExpression(std::move(ignore), DebugInfoRange(@2, @5))};
+
+ filter.reset(new LogicalAndExpression(std::move(assign), std::move(rex), DebugInfoRange(@2, @5)));
+ } else
+ filter.swap(assign);
+ } else if (seen_ignore) {
+ BOOST_THROW_EXCEPTION(ScriptError("object rule 'ignore where' cannot be used without 'assign where'", DebugInfoRange(@2, @4)));
+ }
+
+ $$ = new ObjectExpression(abstract, std::unique_ptr<Expression>($3), std::unique_ptr<Expression>($4),
+ std::move(filter), context->GetZone(), context->GetPackage(), std::move(*$5), $6, $7,
+ std::unique_ptr<Expression>($9), DebugInfoRange(@2, @7));
+ delete $5;
+ }
+ ;
+
+object_declaration: T_OBJECT
+ {
+ $$ = false;
+ }
+ | T_TEMPLATE
+ {
+ $$ = true;
+ }
+ ;
+
+identifier_items: /* empty */
+ {
+ $$ = new std::vector<String>();
+ }
+ | identifier_items_inner
+ | identifier_items_inner ','
+ ;
+
+identifier_items_inner: identifier
+ {
+ $$ = new std::vector<String>();
+ $$->emplace_back(std::move(*$1));
+ delete $1;
+ }
+ | identifier_items_inner ',' identifier
+ {
+ if ($1)
+ $$ = $1;
+ else
+ $$ = new std::vector<String>();
+
+ $$->emplace_back(std::move(*$3));
+ delete $3;
+ }
+ ;
+
+combined_set_op: T_SET
+ | T_SET_ADD
+ | T_SET_SUBTRACT
+ | T_SET_MULTIPLY
+ | T_SET_DIVIDE
+ | T_SET_MODULO
+ | T_SET_XOR
+ | T_SET_BINARY_AND
+ | T_SET_BINARY_OR
+ ;
+
+optional_var: /* empty */
+ | T_VAR
+ ;
+
+lterm: T_LIBRARY rterm
+ {
+ $$ = new LibraryExpression(std::unique_ptr<Expression>($2), @$);
+ }
+ | rterm combined_set_op rterm
+ {
+ $$ = new SetExpression(std::unique_ptr<Expression>($1), $2, std::unique_ptr<Expression>($3), @$);
+ }
+ | T_INCLUDE rterm
+ {
+ $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr<Expression>($2), NULL, NULL, IncludeRegular, false, context->GetZone(), context->GetPackage(), @$);
+ }
+ | T_INCLUDE T_STRING_ANGLE
+ {
+ $$ = new IncludeExpression(Utility::DirName(context->GetPath()), MakeLiteral(std::move(*$2)), NULL, NULL, IncludeRegular, true, context->GetZone(), context->GetPackage(), @$);
+ delete $2;
+ }
+ | T_INCLUDE_RECURSIVE rterm
+ {
+ $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr<Expression>($2), MakeLiteral("*.conf"), NULL, IncludeRecursive, false, context->GetZone(), context->GetPackage(), @$);
+ }
+ | T_INCLUDE_RECURSIVE rterm ',' rterm
+ {
+ $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr<Expression>($2), std::unique_ptr<Expression>($4), NULL, IncludeRecursive, false, context->GetZone(), context->GetPackage(), @$);
+ }
+ | T_INCLUDE_ZONES rterm ',' rterm
+ {
+ $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr<Expression>($4), MakeLiteral("*.conf"), std::unique_ptr<Expression>($2), IncludeZones, false, context->GetZone(), context->GetPackage(), @$);
+ }
+ | T_INCLUDE_ZONES rterm ',' rterm ',' rterm
+ {
+ $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr<Expression>($4), std::unique_ptr<Expression>($6), std::unique_ptr<Expression>($2), IncludeZones, false, context->GetZone(), context->GetPackage(), @$);
+ }
+ | T_IMPORT rterm
+ {
+ $$ = new ImportExpression(std::unique_ptr<Expression>($2), @$);
+ }
+ | T_ASSIGN T_WHERE
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope %dprec 2
+ {
+ EndFlowControlBlock(context);
+
+ if ((context->m_Apply.empty() || !context->m_Apply.top()) && (context->m_ObjectAssign.empty() || !context->m_ObjectAssign.top()))
+ BOOST_THROW_EXCEPTION(ScriptError("'assign' keyword not valid in this context.", @$));
+
+ context->m_SeenAssign.top() = true;
+
+ if (context->m_Assign.top())
+ context->m_Assign.top() = new LogicalOrExpression(std::unique_ptr<Expression>(context->m_Assign.top()), std::unique_ptr<Expression>($4), @$);
+ else
+ context->m_Assign.top() = $4;
+
+ $$ = MakeLiteralRaw();
+ }
+ | T_ASSIGN T_WHERE rterm %dprec 1
+ {
+ ASSERT(!dynamic_cast<DictExpression *>($3));
+
+ if ((context->m_Apply.empty() || !context->m_Apply.top()) && (context->m_ObjectAssign.empty() || !context->m_ObjectAssign.top()))
+ BOOST_THROW_EXCEPTION(ScriptError("'assign' keyword not valid in this context.", @$));
+
+ context->m_SeenAssign.top() = true;
+
+ if (context->m_Assign.top())
+ context->m_Assign.top() = new LogicalOrExpression(std::unique_ptr<Expression>(context->m_Assign.top()), std::unique_ptr<Expression>($3), @$);
+ else
+ context->m_Assign.top() = $3;
+
+ $$ = MakeLiteralRaw();
+ }
+ | T_IGNORE T_WHERE
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope %dprec 2
+ {
+ EndFlowControlBlock(context);
+
+ if ((context->m_Apply.empty() || !context->m_Apply.top()) && (context->m_ObjectAssign.empty() || !context->m_ObjectAssign.top()))
+ BOOST_THROW_EXCEPTION(ScriptError("'ignore' keyword not valid in this context.", @$));
+
+ context->m_SeenIgnore.top() = true;
+
+ if (context->m_Ignore.top())
+ context->m_Ignore.top() = new LogicalOrExpression(std::unique_ptr<Expression>(context->m_Ignore.top()), std::unique_ptr<Expression>($4), @$);
+ else
+ context->m_Ignore.top() = $4;
+
+ $$ = MakeLiteralRaw();
+ }
+ | T_IGNORE T_WHERE rterm %dprec 1
+ {
+ ASSERT(!dynamic_cast<DictExpression *>($3));
+
+ if ((context->m_Apply.empty() || !context->m_Apply.top()) && (context->m_ObjectAssign.empty() || !context->m_ObjectAssign.top()))
+ BOOST_THROW_EXCEPTION(ScriptError("'ignore' keyword not valid in this context.", @$));
+
+ context->m_SeenIgnore.top() = true;
+
+ if (context->m_Ignore.top())
+ context->m_Ignore.top() = new LogicalOrExpression(std::unique_ptr<Expression>(context->m_Ignore.top()), std::unique_ptr<Expression>($3), @$);
+ else
+ context->m_Ignore.top() = $3;
+
+ $$ = MakeLiteralRaw();
+ }
+ | T_RETURN optional_rterm
+ {
+ UseFlowControl(context, FlowControlReturn, @$);
+ $$ = new ReturnExpression(std::unique_ptr<Expression>($2), @$);
+ }
+ | T_BREAK
+ {
+ UseFlowControl(context, FlowControlBreak, @$);
+ $$ = new BreakExpression(@$);
+ }
+ | T_CONTINUE
+ {
+ UseFlowControl(context, FlowControlContinue, @$);
+ $$ = new ContinueExpression(@$);
+ }
+ | T_DEBUGGER
+ {
+ $$ = new BreakpointExpression(@$);
+ }
+ | T_NAMESPACE rterm
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope_require_side_effect
+ {
+ EndFlowControlBlock(context);
+
+ std::unique_ptr<Expression> expr{$2};
+ BindToScope(expr, ScopeGlobal);
+ $$ = new SetExpression(std::move(expr), OpSetLiteral, std::unique_ptr<Expression>(new NamespaceExpression(std::unique_ptr<Expression>($4), @$)), @$);
+ }
+ | T_USING rterm
+ {
+ Expression::Ptr expr{$2};
+ context->AddImport(std::move(expr));
+ $$ = MakeLiteralRaw();
+ }
+ | apply
+ | object
+ | T_FOR '(' optional_var identifier T_FOLLOWS optional_var identifier T_IN rterm ')'
+ {
+ BeginFlowControlBlock(context, FlowControlContinue | FlowControlBreak, true);
+ }
+ rterm_scope_require_side_effect
+ {
+ EndFlowControlBlock(context);
+
+ $$ = new ForExpression(std::move(*$4), std::move(*$7), std::unique_ptr<Expression>($9), std::unique_ptr<Expression>($12), @$);
+ delete $4;
+ delete $7;
+ }
+ | T_FOR '(' optional_var identifier T_IN rterm ')'
+ {
+ BeginFlowControlBlock(context, FlowControlContinue | FlowControlBreak, true);
+ }
+ rterm_scope_require_side_effect
+ {
+ EndFlowControlBlock(context);
+
+ $$ = new ForExpression(std::move(*$4), "", std::unique_ptr<Expression>($6), std::unique_ptr<Expression>($9), @$);
+ delete $4;
+ }
+ | T_FUNCTION identifier '(' identifier_items ')' use_specifier
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope
+ {
+ EndFlowControlBlock(context);
+
+ std::unique_ptr<FunctionExpression> fexpr{new FunctionExpression(*$2, std::move(*$4), std::move(*$6), std::unique_ptr<Expression>($8), @$)};
+ delete $4;
+ delete $6;
+
+ $$ = new SetExpression(MakeIndexer(ScopeThis, std::move(*$2)), OpSetLiteral, std::move(fexpr), @$);
+ delete $2;
+ }
+ | T_CONST T_IDENTIFIER T_SET rterm
+ {
+ $$ = new SetConstExpression(std::move(*$2), std::unique_ptr<Expression>($4), @$);
+ delete $2;
+ }
+ | T_VAR rterm
+ {
+ std::unique_ptr<Expression> expr{$2};
+ BindToScope(expr, ScopeLocal);
+ $$ = new SetExpression(std::move(expr), OpSetLiteral, MakeLiteral(), @$);
+ }
+ | T_VAR rterm combined_set_op rterm
+ {
+ std::unique_ptr<Expression> expr{$2};
+ BindToScope(expr, ScopeLocal);
+ $$ = new SetExpression(std::move(expr), $3, std::unique_ptr<Expression>($4), @$);
+ }
+ | T_WHILE '(' rterm ')'
+ {
+ BeginFlowControlBlock(context, FlowControlContinue | FlowControlBreak, true);
+ }
+ rterm_scope
+ {
+ EndFlowControlBlock(context);
+
+ $$ = new WhileExpression(std::unique_ptr<Expression>($3), std::unique_ptr<Expression>($6), @$);
+ }
+ | T_THROW rterm
+ {
+ $$ = new ThrowExpression(std::unique_ptr<Expression>($2), false, @$);
+ }
+ | T_TRY rterm_scope T_EXCEPT rterm_scope
+ {
+ $$ = new TryExceptExpression(std::unique_ptr<Expression>($2), std::unique_ptr<Expression>($4), @$);
+ }
+ | rterm_side_effect
+ ;
+
+rterm_items: /* empty */
+ {
+ $$ = new std::vector<std::unique_ptr<Expression> >();
+ }
+ | rterm_items_inner
+ | rterm_items_inner ',' optional_newlines
+ | rterm_items_inner newlines
+ ;
+
+rterm_items_inner: rterm
+ {
+ $$ = new std::vector<std::unique_ptr<Expression> >();
+ $$->emplace_back($1);
+ }
+ | rterm_items_inner ',' optional_newlines rterm
+ {
+ $$ = $1;
+ $$->emplace_back($4);
+ }
+ ;
+
+rterm_array: '['
+ {
+ context->m_OpenBraces++;
+ }
+ optional_newlines rterm_items ']'
+ {
+ context->m_OpenBraces--;
+ $$ = new ArrayExpression(std::move(*$4), @$);
+ delete $4;
+ }
+ ;
+
+rterm_dict: '{'
+ {
+ BeginFlowControlBlock(context, 0, false);
+ context->m_IgnoreNewlines.push(false);
+ context->m_OpenBraces++;
+ }
+ statements '}'
+ {
+ EndFlowControlBlock(context);
+ context->m_OpenBraces--;
+ context->m_IgnoreNewlines.pop();
+ std::vector<std::unique_ptr<Expression> > dlist;
+ for (auto& litem : *$3) {
+ if (!litem.second.SideEffect)
+ yyerror(&litem.second.DebugInfo, NULL, NULL, "Value computed is not used.");
+ dlist.emplace_back(std::move(litem.first));
+ }
+ delete $3;
+ $$ = new DictExpression(std::move(dlist), @$);
+ }
+ ;
+
+rterm_scope_require_side_effect: '{'
+ {
+ context->m_IgnoreNewlines.push(false);
+ context->m_OpenBraces++;
+ }
+ statements '}'
+ {
+ context->m_OpenBraces--;
+ context->m_IgnoreNewlines.pop();
+ std::vector<std::unique_ptr<Expression> > dlist;
+ for (auto& litem : *$3) {
+ if (!litem.second.SideEffect)
+ yyerror(&litem.second.DebugInfo, NULL, NULL, "Value computed is not used.");
+ dlist.emplace_back(std::move(litem.first));
+ }
+ delete $3;
+ $$ = new DictExpression(std::move(dlist), @$);
+ $$->MakeInline();
+ }
+ ;
+
+rterm_scope: '{'
+ {
+ context->m_IgnoreNewlines.push(false);
+ context->m_OpenBraces++;
+ }
+ statements '}'
+ {
+ context->m_OpenBraces--;
+ context->m_IgnoreNewlines.pop();
+ std::vector<std::unique_ptr<Expression> > dlist;
+ decltype($3->size()) num = 0;
+ for (auto& litem : *$3) {
+ if (!litem.second.SideEffect && num != $3->size() - 1)
+ yyerror(&litem.second.DebugInfo, NULL, NULL, "Value computed is not used.");
+ dlist.emplace_back(std::move(litem.first));
+ num++;
+ }
+ delete $3;
+ $$ = new DictExpression(std::move(dlist), @$);
+ $$->MakeInline();
+ }
+ ;
+
+else_if_branch: T_ELSE T_IF '(' rterm ')' rterm_scope
+ {
+ $$ = new std::pair<std::unique_ptr<Expression>, std::unique_ptr<Expression> >(std::unique_ptr<Expression>($4), std::unique_ptr<Expression>($6));
+ }
+ ;
+
+else_if_branches: /* empty */
+ {
+ $$ = new std::vector<std::pair<std::unique_ptr<Expression>, std::unique_ptr<Expression> > >();
+ }
+ | else_if_branches else_if_branch
+ {
+ $$ = $1;
+ $$->emplace_back(std::move(*$2));
+ delete $2;
+ }
+ ;
+
+rterm_side_effect: rterm '(' rterm_items ')'
+ {
+ $$ = new FunctionCallExpression(std::unique_ptr<Expression>($1), std::move(*$3), @$);
+ delete $3;
+ }
+ | T_IF '(' rterm ')' rterm_scope else_if_branches
+ {
+ std::vector<std::pair<std::unique_ptr<Expression>, std::unique_ptr<Expression> > > ebranches;
+ $6->swap(ebranches);
+ delete $6;
+
+ std::unique_ptr<Expression> afalse;
+
+ for (int i = ebranches.size() - 1; i >= 0; i--) {
+ auto& ebranch = ebranches[i];
+ afalse.reset(new ConditionalExpression(std::move(ebranch.first), std::move(ebranch.second), std::move(afalse), @6));
+ }
+
+ $$ = new ConditionalExpression(std::unique_ptr<Expression>($3), std::unique_ptr<Expression>($5), std::move(afalse), @$);
+ }
+ | T_IF '(' rterm ')' rterm_scope else_if_branches T_ELSE rterm_scope
+ {
+ std::vector<std::pair<std::unique_ptr<Expression>, std::unique_ptr<Expression> > > ebranches;
+ $6->swap(ebranches);
+ delete $6;
+
+ $8->MakeInline();
+
+ std::unique_ptr<Expression> afalse{$8};
+
+ for (int i = ebranches.size() - 1; i >= 0; i--) {
+ auto& ebranch = ebranches[i];
+ afalse.reset(new ConditionalExpression(std::move(ebranch.first), std::move(ebranch.second), std::move(afalse), @6));
+ }
+
+ $$ = new ConditionalExpression(std::unique_ptr<Expression>($3), std::unique_ptr<Expression>($5), std::move(afalse), @$);
+ }
+ | rterm '?' rterm ':' rterm
+ {
+ $$ = new ConditionalExpression(std::unique_ptr<Expression>($1), std::unique_ptr<Expression>($3), std::unique_ptr<Expression>($5), @$);
+ }
+ ;
+
+rterm_no_side_effect_no_dict: T_STRING
+ {
+ $$ = MakeLiteralRaw(std::move(*$1));
+ delete $1;
+ }
+ | T_NUMBER
+ {
+ $$ = MakeLiteralRaw($1);
+ }
+ | T_BOOLEAN
+ {
+ $$ = MakeLiteralRaw($1);
+ }
+ | T_NULL
+ {
+ $$ = MakeLiteralRaw();
+ }
+ | rterm '.' T_IDENTIFIER %dprec 2
+ {
+ $$ = new IndexerExpression(std::unique_ptr<Expression>($1), MakeLiteral(std::move(*$3)), @$);
+ delete $3;
+ }
+ | rterm '[' rterm ']'
+ {
+ $$ = new IndexerExpression(std::unique_ptr<Expression>($1), std::unique_ptr<Expression>($3), @$);
+ }
+ | T_IDENTIFIER
+ {
+ $$ = new VariableExpression(std::move(*$1), context->GetImports(), @1);
+ delete $1;
+ }
+ | T_MULTIPLY rterm %prec DEREF_OP
+ {
+ $$ = new DerefExpression(std::unique_ptr<Expression>($2), @$);
+ }
+ | T_BINARY_AND rterm %prec REF_OP
+ {
+ $$ = new RefExpression(std::unique_ptr<Expression>($2), @$);
+ }
+ | '!' rterm
+ {
+ $$ = new LogicalNegateExpression(std::unique_ptr<Expression>($2), @$);
+ }
+ | '~' rterm
+ {
+ $$ = new NegateExpression(std::unique_ptr<Expression>($2), @$);
+ }
+ | T_PLUS rterm %prec UNARY_PLUS
+ {
+ $$ = $2;
+ }
+ | T_MINUS rterm %prec UNARY_MINUS
+ {
+ $$ = new SubtractExpression(MakeLiteral(0), std::unique_ptr<Expression>($2), @$);
+ }
+ | T_THIS
+ {
+ $$ = new GetScopeExpression(ScopeThis);
+ }
+ | T_GLOBALS
+ {
+ $$ = new GetScopeExpression(ScopeGlobal);
+ }
+ | T_LOCALS
+ {
+ $$ = new GetScopeExpression(ScopeLocal);
+ }
+ | T_CURRENT_FILENAME
+ {
+ $$ = MakeLiteralRaw(@$.Path);
+ }
+ | T_CURRENT_LINE
+ {
+ $$ = MakeLiteralRaw(@$.FirstLine);
+ }
+ | identifier T_FOLLOWS
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope %dprec 2
+ {
+ EndFlowControlBlock(context);
+
+ std::vector<String> args;
+ args.emplace_back(std::move(*$1));
+ delete $1;
+
+ $$ = new FunctionExpression("<anonymous>", std::move(args), {}, std::unique_ptr<Expression>($4), @$);
+ }
+ | identifier T_FOLLOWS rterm %dprec 1
+ {
+ ASSERT(!dynamic_cast<DictExpression *>($3));
+
+ std::vector<String> args;
+ args.emplace_back(std::move(*$1));
+ delete $1;
+
+ $$ = new FunctionExpression("<anonymous>", std::move(args), {}, std::unique_ptr<Expression>($3), @$);
+ }
+ | '(' identifier_items ')' use_specifier T_FOLLOWS
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope %dprec 2
+ {
+ EndFlowControlBlock(context);
+
+ $$ = new FunctionExpression("<anonymous>", std::move(*$2), std::move(*$4), std::unique_ptr<Expression>($7), @$);
+ delete $2;
+ delete $4;
+ }
+ | '(' identifier_items ')' use_specifier T_FOLLOWS rterm %dprec 1
+ {
+ ASSERT(!dynamic_cast<DictExpression *>($6));
+
+ $$ = new FunctionExpression("<anonymous>", std::move(*$2), std::move(*$4), std::unique_ptr<Expression>($6), @$);
+ delete $2;
+ delete $4;
+ }
+ | rterm_array
+ | '('
+ {
+ context->m_OpenBraces++;
+ }
+ rterm ')'
+ {
+ context->m_OpenBraces--;
+ $$ = $3;
+ }
+ | rterm T_LOGICAL_OR rterm { MakeRBinaryOp<LogicalOrExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_LOGICAL_AND rterm { MakeRBinaryOp<LogicalAndExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_BINARY_OR rterm { MakeRBinaryOp<BinaryOrExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_BINARY_AND rterm { MakeRBinaryOp<BinaryAndExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_IN rterm { MakeRBinaryOp<InExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_NOT_IN rterm { MakeRBinaryOp<NotInExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_EQUAL rterm { MakeRBinaryOp<EqualExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_NOT_EQUAL rterm { MakeRBinaryOp<NotEqualExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_LESS_THAN rterm { MakeRBinaryOp<LessThanExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_LESS_THAN_OR_EQUAL rterm { MakeRBinaryOp<LessThanOrEqualExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_GREATER_THAN rterm { MakeRBinaryOp<GreaterThanExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_GREATER_THAN_OR_EQUAL rterm { MakeRBinaryOp<GreaterThanOrEqualExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_SHIFT_LEFT rterm { MakeRBinaryOp<ShiftLeftExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_SHIFT_RIGHT rterm { MakeRBinaryOp<ShiftRightExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_PLUS rterm { MakeRBinaryOp<AddExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_MINUS rterm { MakeRBinaryOp<SubtractExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_MULTIPLY rterm { MakeRBinaryOp<MultiplyExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_DIVIDE_OP rterm { MakeRBinaryOp<DivideExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_MODULO rterm { MakeRBinaryOp<ModuloExpression>(&$$, $1, $3, @1, @3); }
+ | rterm T_XOR rterm { MakeRBinaryOp<XorExpression>(&$$, $1, $3, @1, @3); }
+ | T_FUNCTION '(' identifier_items ')' use_specifier
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope
+ {
+ EndFlowControlBlock(context);
+
+ $$ = new FunctionExpression("<anonymous>", std::move(*$3), std::move(*$5), std::unique_ptr<Expression>($7), @$);
+ delete $3;
+ delete $5;
+ }
+ | T_NULLARY_LAMBDA_BEGIN
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ statements T_NULLARY_LAMBDA_END
+ {
+ EndFlowControlBlock(context);
+
+ std::vector<std::unique_ptr<Expression> > dlist;
+ decltype(dlist.size()) num = 0;
+ for (auto& litem : *$3) {
+ if (!litem.second.SideEffect && num != $3->size() - 1)
+ yyerror(&litem.second.DebugInfo, NULL, NULL, "Value computed is not used.");
+ dlist.emplace_back(std::move(litem.first));
+ num++;
+ }
+ delete $3;
+ std::unique_ptr<DictExpression> aexpr{new DictExpression(std::move(dlist), @$)};
+ aexpr->MakeInline();
+
+ $$ = new FunctionExpression("<anonymous>", {}, {}, std::move(aexpr), @$);
+ }
+ ;
+
+rterm_no_side_effect:
+ rterm_no_side_effect_no_dict %dprec 1
+ | rterm_dict %dprec 2
+ {
+ std::unique_ptr<Expression> expr{$1};
+ BindToScope(expr, ScopeThis);
+ $$ = expr.release();
+ }
+ ;
+
+rterm:
+ rterm_side_effect %dprec 2
+ | rterm_no_side_effect %dprec 1
+ ;
+
+target_type_specifier: /* empty */
+ {
+ $$ = new String();
+ }
+ | T_TO identifier
+ {
+ $$ = $2;
+ }
+ ;
+
+default_specifier: /* empty */
+ {
+ $$ = false;
+ }
+ | T_DEFAULT
+ {
+ $$ = true;
+ }
+ ;
+
+ignore_specifier: /* empty */
+ {
+ $$ = false;
+ }
+ | T_IGNORE_ON_ERROR
+ {
+ $$ = true;
+ }
+ ;
+
+use_specifier: /* empty */
+ {
+ $$ = new std::map<String, std::unique_ptr<Expression> >();
+ }
+ | T_USE '(' use_specifier_items ')'
+ {
+ $$ = $3;
+ }
+ ;
+
+use_specifier_items: use_specifier_item
+ {
+ $$ = new std::map<String, std::unique_ptr<Expression> >();
+ $$->emplace(std::move(*$1));
+ delete $1;
+ }
+ | use_specifier_items ',' use_specifier_item
+ {
+ $$ = $1;
+ $$->emplace(std::move(*$3));
+ delete $3;
+ }
+ ;
+
+use_specifier_item: identifier
+ {
+ std::unique_ptr<Expression> var (new VariableExpression(*$1, context->GetImports(), @1));
+ $$ = new std::pair<String, std::unique_ptr<Expression> >(std::move(*$1), std::move(var));
+ delete $1;
+ }
+ | identifier T_SET rterm
+ {
+ $$ = new std::pair<String, std::unique_ptr<Expression> >(std::move(*$1), std::unique_ptr<Expression>($3));
+ delete $1;
+ }
+ ;
+
+apply_for_specifier: /* empty */
+ | T_FOR '(' optional_var identifier T_FOLLOWS optional_var identifier T_IN rterm ')'
+ {
+ context->m_FKVar.top() = std::move(*$4);
+ delete $4;
+
+ context->m_FVVar.top() = std::move(*$7);
+ delete $7;
+
+ context->m_FTerm.top() = $9;
+ }
+ | T_FOR '(' optional_var identifier T_IN rterm ')'
+ {
+ context->m_FKVar.top() = std::move(*$4);
+ delete $4;
+
+ context->m_FVVar.top() = "";
+
+ context->m_FTerm.top() = $6;
+ }
+ ;
+
+optional_rterm: /* empty */
+ {
+ $$ = MakeLiteralRaw();
+ }
+ | rterm
+ ;
+
+apply:
+ {
+ context->m_Apply.push(true);
+ context->m_SeenAssign.push(false);
+ context->m_SeenIgnore.push(false);
+ context->m_Assign.push(NULL);
+ context->m_Ignore.push(NULL);
+ context->m_FKVar.push("");
+ context->m_FVVar.push("");
+ context->m_FTerm.push(NULL);
+ }
+ T_APPLY identifier optional_rterm apply_for_specifier target_type_specifier use_specifier ignore_specifier
+ {
+ BeginFlowControlBlock(context, FlowControlReturn, false);
+ }
+ rterm_scope_require_side_effect
+ {
+ EndFlowControlBlock(context);
+
+ context->m_Apply.pop();
+
+ String type = std::move(*$3);
+ delete $3;
+ String target = std::move(*$6);
+ delete $6;
+
+ if (!ApplyRule::IsValidSourceType(type))
+ BOOST_THROW_EXCEPTION(ScriptError("'apply' cannot be used with type '" + type + "'", @3));
+
+ if (!ApplyRule::IsValidTargetType(type, target)) {
+ if (target == "") {
+ auto& types (ApplyRule::GetTargetTypes(type));
+ String typeNames;
+
+ for (std::vector<String>::size_type i = 0; i < types.size(); i++) {
+ if (typeNames != "") {
+ if (i == types.size() - 1)
+ typeNames += " or ";
+ else
+ typeNames += ", ";
+ }
+
+ typeNames += "'" + types[i] + "'";
+ }
+
+ BOOST_THROW_EXCEPTION(ScriptError("'apply' target type is ambiguous (can be one of " + typeNames + "): use 'to' to specify a type", DebugInfoRange(@2, @3)));
+ } else
+ BOOST_THROW_EXCEPTION(ScriptError("'apply' target type '" + target + "' is invalid", @6));
+ }
+
+ bool seen_assign = context->m_SeenAssign.top();
+ context->m_SeenAssign.pop();
+
+ // assign && !ignore
+ if (!seen_assign && !context->m_FTerm.top())
+ BOOST_THROW_EXCEPTION(ScriptError("'apply' is missing 'assign'/'for'", DebugInfoRange(@2, @3)));
+
+ std::unique_ptr<Expression> ignore{context->m_Ignore.top()};
+ context->m_Ignore.pop();
+
+ std::unique_ptr<Expression> assign;
+
+ if (!seen_assign)
+ assign = MakeLiteral(true);
+ else
+ assign.reset(context->m_Assign.top());
+
+ context->m_Assign.pop();
+
+ std::unique_ptr<Expression> filter;
+
+ if (ignore) {
+ std::unique_ptr<Expression>rex{new LogicalNegateExpression(std::move(ignore), DebugInfoRange(@2, @5))};
+
+ filter.reset(new LogicalAndExpression(std::move(assign), std::move(rex), DebugInfoRange(@2, @5)));
+ } else
+ filter.swap(assign);
+
+ String fkvar = std::move(context->m_FKVar.top());
+ context->m_FKVar.pop();
+
+ String fvvar = std::move(context->m_FVVar.top());
+ context->m_FVVar.pop();
+
+ std::unique_ptr<Expression> fterm{context->m_FTerm.top()};
+ context->m_FTerm.pop();
+
+ $$ = new ApplyExpression(std::move(type), std::move(target), std::unique_ptr<Expression>($4), std::move(filter), context->GetPackage(), std::move(fkvar), std::move(fvvar), std::move(fterm), std::move(*$7), $8, std::unique_ptr<Expression>($10), DebugInfoRange(@2, @8));
+ delete $7;
+ }
+ ;
+
+newlines: T_NEWLINE
+ | T_NEWLINE newlines
+ ;
+
+optional_newlines: /* empty */
+ | newlines
+ ;
+
+/* required separator */
+sep: ',' optional_newlines
+ | ';' optional_newlines
+ | newlines
+ ;
+
+%%
diff --git a/lib/config/configcompiler.cpp b/lib/config/configcompiler.cpp
new file mode 100644
index 0000000..62f02ba
--- /dev/null
+++ b/lib/config/configcompiler.cpp
@@ -0,0 +1,364 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/configcompiler.hpp"
+#include "config/configitem.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/loader.hpp"
+#include "base/context.hpp"
+#include "base/exception.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+std::vector<String> ConfigCompiler::m_IncludeSearchDirs;
+std::mutex ConfigCompiler::m_ZoneDirsMutex;
+std::map<String, std::vector<ZoneFragment> > ConfigCompiler::m_ZoneDirs;
+
+/**
+ * Constructor for the ConfigCompiler class.
+ *
+ * @param path The path of the configuration file (or another name that
+ * identifies the source of the configuration text).
+ * @param input Input stream for the configuration file.
+ * @param zone The zone.
+ */
+ConfigCompiler::ConfigCompiler(String path, std::istream *input,
+ String zone, String package)
+ : m_Path(std::move(path)), m_Input(input), m_Zone(std::move(zone)),
+ m_Package(std::move(package)), m_Eof(false), m_OpenBraces(0)
+{
+ InitializeScanner();
+}
+
+/**
+ * Destructor for the ConfigCompiler class.
+ */
+ConfigCompiler::~ConfigCompiler()
+{
+ DestroyScanner();
+}
+
+/**
+ * Reads data from the input stream. Used internally by the lexer.
+ *
+ * @param buffer Where to store data.
+ * @param max_size The maximum number of bytes to read from the stream.
+ * @returns The actual number of bytes read.
+ */
+size_t ConfigCompiler::ReadInput(char *buffer, size_t max_size)
+{
+ m_Input->read(buffer, max_size);
+ return static_cast<size_t>(m_Input->gcount());
+}
+
+/**
+ * Retrieves the scanner object.
+ *
+ * @returns The scanner object.
+ */
+void *ConfigCompiler::GetScanner() const
+{
+ return m_Scanner;
+}
+
+/**
+ * Retrieves the path for the input file.
+ *
+ * @returns The path.
+ */
+const char *ConfigCompiler::GetPath() const
+{
+ return m_Path.CStr();
+}
+
+void ConfigCompiler::SetZone(const String& zone)
+{
+ m_Zone = zone;
+}
+
+String ConfigCompiler::GetZone() const
+{
+ return m_Zone;
+}
+
+void ConfigCompiler::SetPackage(const String& package)
+{
+ m_Package = package;
+}
+
+String ConfigCompiler::GetPackage() const
+{
+ return m_Package;
+}
+
+void ConfigCompiler::CollectIncludes(std::vector<std::unique_ptr<Expression> >& expressions,
+ const String& file, const String& zone, const String& package)
+{
+ try {
+ expressions.emplace_back(CompileFile(file, zone, package));
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ConfigCompiler")
+ << "Cannot compile file '"
+ << file << "': " << DiagnosticInformation(ex);
+ }
+}
+
+/**
+ * Handles an include directive.
+ *
+ * @param relativeBath The path this include is relative to.
+ * @param path The path from the include directive.
+ * @param search Whether to search global include dirs.
+ * @param debuginfo Debug information.
+ */
+std::unique_ptr<Expression> ConfigCompiler::HandleInclude(const String& relativeBase, const String& path,
+ bool search, const String& zone, const String& package, const DebugInfo& debuginfo)
+{
+ String upath;
+
+ if (search || (IsAbsolutePath(path)))
+ upath = path;
+ else
+ upath = relativeBase + "/" + path;
+
+ String includePath = upath;
+
+ if (search) {
+ for (const String& dir : m_IncludeSearchDirs) {
+ String spath = dir + "/" + path;
+
+ if (Utility::PathExists(spath)) {
+ includePath = spath;
+ break;
+ }
+ }
+ }
+
+ std::vector<std::unique_ptr<Expression> > expressions;
+ auto funcCallback = [&expressions, zone, package](const String& file) { CollectIncludes(expressions, file, zone, package); };
+
+ if (!Utility::Glob(includePath, funcCallback, GlobFile) && includePath.FindFirstOf("*?") == String::NPos) {
+ std::ostringstream msgbuf;
+ msgbuf << "Include file '" + path + "' does not exist";
+ BOOST_THROW_EXCEPTION(ScriptError(msgbuf.str(), debuginfo));
+ }
+
+ std::unique_ptr<DictExpression> expr{new DictExpression(std::move(expressions))};
+ expr->MakeInline();
+ return std::move(expr);
+}
+
+/**
+ * Handles recursive includes.
+ *
+ * @param relativeBase The path this include is relative to.
+ * @param path The directory path.
+ * @param pattern The file pattern.
+ * @param debuginfo Debug information.
+ */
+std::unique_ptr<Expression> ConfigCompiler::HandleIncludeRecursive(const String& relativeBase, const String& path,
+ const String& pattern, const String& zone, const String& package, const DebugInfo&)
+{
+ String ppath;
+
+ if (IsAbsolutePath(path))
+ ppath = path;
+ else
+ ppath = relativeBase + "/" + path;
+
+ std::vector<std::unique_ptr<Expression> > expressions;
+ Utility::GlobRecursive(ppath, pattern, [&expressions, zone, package](const String& file) {
+ CollectIncludes(expressions, file, zone, package);
+ }, GlobFile);
+
+ std::unique_ptr<DictExpression> dict{new DictExpression(std::move(expressions))};
+ dict->MakeInline();
+ return std::move(dict);
+}
+
+void ConfigCompiler::HandleIncludeZone(const String& relativeBase, const String& tag, const String& path, const String& pattern, const String& package, std::vector<std::unique_ptr<Expression> >& expressions)
+{
+ String zoneName = Utility::BaseName(path);
+
+ String ppath;
+
+ if (IsAbsolutePath(path))
+ ppath = path;
+ else
+ ppath = relativeBase + "/" + path;
+
+ RegisterZoneDir(tag, ppath, zoneName);
+
+ Utility::GlobRecursive(ppath, pattern, [&expressions, zoneName, package](const String& file) {
+ CollectIncludes(expressions, file, zoneName, package);
+ }, GlobFile);
+}
+
+/**
+ * Handles zone includes.
+ *
+ * @param relativeBase The path this include is relative to.
+ * @param tag The tag name.
+ * @param path The directory path.
+ * @param pattern The file pattern.
+ * @param debuginfo Debug information.
+ */
+std::unique_ptr<Expression> ConfigCompiler::HandleIncludeZones(const String& relativeBase, const String& tag,
+ const String& path, const String& pattern, const String& package, const DebugInfo&)
+{
+ String ppath;
+ String newRelativeBase = relativeBase;
+
+ if (IsAbsolutePath(path))
+ ppath = path;
+ else {
+ ppath = relativeBase + "/" + path;
+ newRelativeBase = ".";
+ }
+
+ std::vector<std::unique_ptr<Expression> > expressions;
+ Utility::Glob(ppath + "/*", [newRelativeBase, tag, pattern, package, &expressions](const String& path) {
+ HandleIncludeZone(newRelativeBase, tag, path, pattern, package, expressions);
+ }, GlobDirectory);
+
+ return std::unique_ptr<Expression>(new DictExpression(std::move(expressions)));
+}
+
+/**
+ * Compiles a stream.
+ *
+ * @param path A name identifying the stream.
+ * @param stream The input stream.
+ * @returns Configuration items.
+ */
+std::unique_ptr<Expression> ConfigCompiler::CompileStream(const String& path,
+ std::istream *stream, const String& zone, const String& package)
+{
+ CONTEXT("Compiling configuration stream with name '" << path << "'");
+
+ stream->exceptions(std::istream::badbit);
+
+ ConfigCompiler ctx(path, stream, zone, package);
+
+ try {
+ return ctx.Compile();
+ } catch (const ScriptError& ex) {
+ return std::unique_ptr<Expression>(new ThrowExpression(MakeLiteral(ex.what()), ex.IsIncompleteExpression(), ex.GetDebugInfo()));
+ } catch (const std::exception& ex) {
+ return std::unique_ptr<Expression>(new ThrowExpression(MakeLiteral(DiagnosticInformation(ex)), false));
+ }
+}
+
+/**
+ * Compiles a file.
+ *
+ * @param path The path.
+ * @returns Configuration items.
+ */
+std::unique_ptr<Expression> ConfigCompiler::CompileFile(const String& path, const String& zone,
+ const String& package)
+{
+ CONTEXT("Compiling configuration file '" << path << "'");
+
+ std::ifstream stream(path.CStr(), std::ifstream::in);
+
+ if (!stream)
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("std::ifstream::open")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(path));
+
+ Log(LogNotice, "ConfigCompiler")
+ << "Compiling config file: " << path;
+
+ return CompileStream(path, &stream, zone, package);
+}
+
+/**
+ * Compiles a snippet of text.
+ *
+ * @param path A name identifying the text.
+ * @param text The text.
+ * @returns Configuration items.
+ */
+std::unique_ptr<Expression> ConfigCompiler::CompileText(const String& path, const String& text,
+ const String& zone, const String& package)
+{
+ std::stringstream stream(text);
+ return CompileStream(path, &stream, zone, package);
+}
+
+/**
+ * Adds a directory to the list of include search dirs.
+ *
+ * @param dir The new dir.
+ */
+void ConfigCompiler::AddIncludeSearchDir(const String& dir)
+{
+ Log(LogInformation, "ConfigCompiler")
+ << "Adding include search dir: " << dir;
+
+ m_IncludeSearchDirs.push_back(dir);
+}
+
+std::vector<ZoneFragment> ConfigCompiler::GetZoneDirs(const String& zone)
+{
+ std::unique_lock<std::mutex> lock(m_ZoneDirsMutex);
+ auto it = m_ZoneDirs.find(zone);
+ if (it == m_ZoneDirs.end())
+ return std::vector<ZoneFragment>();
+ else
+ return it->second;
+}
+
+void ConfigCompiler::RegisterZoneDir(const String& tag, const String& ppath, const String& zoneName)
+{
+ ZoneFragment zf;
+ zf.Tag = tag;
+ zf.Path = ppath;
+
+ std::unique_lock<std::mutex> lock(m_ZoneDirsMutex);
+ m_ZoneDirs[zoneName].push_back(zf);
+}
+
+bool ConfigCompiler::HasZoneConfigAuthority(const String& zoneName)
+{
+ std::vector<ZoneFragment> zoneDirs = m_ZoneDirs[zoneName];
+
+ bool empty = zoneDirs.empty();
+
+ if (!empty) {
+ std::vector<String> paths;
+ paths.reserve(zoneDirs.size());
+
+ for (const ZoneFragment& zf : zoneDirs) {
+ paths.push_back(zf.Path);
+ }
+
+ Log(LogNotice, "ConfigCompiler")
+ << "Registered authoritative config directories for zone '" << zoneName << "': " << Utility::NaturalJoin(paths);
+ }
+
+ return !empty;
+}
+
+
+bool ConfigCompiler::IsAbsolutePath(const String& path)
+{
+#ifndef _WIN32
+ return (path.GetLength() > 0 && path[0] == '/');
+#else /* _WIN32 */
+ return !PathIsRelative(path.CStr());
+#endif /* _WIN32 */
+}
+
+void ConfigCompiler::AddImport(const Expression::Ptr& import)
+{
+ m_Imports.push_back(import);
+}
+
+std::vector<Expression::Ptr> ConfigCompiler::GetImports() const
+{
+ return m_Imports;
+}
diff --git a/lib/config/configcompiler.hpp b/lib/config/configcompiler.hpp
new file mode 100644
index 0000000..fe00bed
--- /dev/null
+++ b/lib/config/configcompiler.hpp
@@ -0,0 +1,161 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGCOMPILER_H
+#define CONFIGCOMPILER_H
+
+#include "config/i2-config.hpp"
+#include "config/expression.hpp"
+#include "base/debuginfo.hpp"
+#include "base/registry.hpp"
+#include "base/initialize.hpp"
+#include "base/singleton.hpp"
+#include "base/string.hpp"
+#include <future>
+#include <iostream>
+#include <stack>
+
+typedef union YYSTYPE YYSTYPE;
+typedef void *yyscan_t;
+
+namespace icinga
+{
+
+struct CompilerDebugInfo
+{
+ const char *Path;
+
+ int FirstLine;
+ int FirstColumn;
+
+ int LastLine;
+ int LastColumn;
+
+ operator DebugInfo() const
+ {
+ DebugInfo di;
+ di.Path = Path;
+ di.FirstLine = FirstLine;
+ di.FirstColumn = FirstColumn;
+ di.LastLine = LastLine;
+ di.LastColumn = LastColumn;
+ return di;
+ }
+};
+
+struct EItemInfo
+{
+ bool SideEffect;
+ CompilerDebugInfo DebugInfo;
+};
+
+enum FlowControlType
+{
+ FlowControlReturn = 1,
+ FlowControlContinue = 2,
+ FlowControlBreak = 4
+};
+
+struct ZoneFragment
+{
+ String Tag;
+ String Path;
+};
+
+/**
+ * The configuration compiler can be used to compile a configuration file
+ * into a number of configuration items.
+ *
+ * @ingroup config
+ */
+class ConfigCompiler
+{
+public:
+ explicit ConfigCompiler(String path, std::istream *input,
+ String zone = String(), String package = String());
+ virtual ~ConfigCompiler();
+
+ std::unique_ptr<Expression> Compile();
+
+ static std::unique_ptr<Expression>CompileStream(const String& path, std::istream *stream,
+ const String& zone = String(), const String& package = String());
+ static std::unique_ptr<Expression>CompileFile(const String& path, const String& zone = String(),
+ const String& package = String());
+ static std::unique_ptr<Expression>CompileText(const String& path, const String& text,
+ const String& zone = String(), const String& package = String());
+
+ static void AddIncludeSearchDir(const String& dir);
+
+ const char *GetPath() const;
+
+ void SetZone(const String& zone);
+ String GetZone() const;
+
+ void SetPackage(const String& package);
+ String GetPackage() const;
+
+ void AddImport(const Expression::Ptr& import);
+ std::vector<Expression::Ptr> GetImports() const;
+
+ static void CollectIncludes(std::vector<std::unique_ptr<Expression> >& expressions,
+ const String& file, const String& zone, const String& package);
+
+ static std::unique_ptr<Expression> HandleInclude(const String& relativeBase, const String& path, bool search,
+ const String& zone, const String& package, const DebugInfo& debuginfo = DebugInfo());
+ static std::unique_ptr<Expression> HandleIncludeRecursive(const String& relativeBase, const String& path,
+ const String& pattern, const String& zone, const String& package, const DebugInfo& debuginfo = DebugInfo());
+ static std::unique_ptr<Expression> HandleIncludeZones(const String& relativeBase, const String& tag,
+ const String& path, const String& pattern, const String& package, const DebugInfo& debuginfo = DebugInfo());
+
+ size_t ReadInput(char *buffer, size_t max_bytes);
+ void *GetScanner() const;
+
+ static std::vector<ZoneFragment> GetZoneDirs(const String& zone);
+ static void RegisterZoneDir(const String& tag, const String& ppath, const String& zoneName);
+
+ static bool HasZoneConfigAuthority(const String& zoneName);
+
+private:
+ std::promise<Expression::Ptr> m_Promise;
+
+ String m_Path;
+ std::istream *m_Input;
+ String m_Zone;
+ String m_Package;
+ std::vector<Expression::Ptr> m_Imports;
+
+ void *m_Scanner;
+
+ static std::vector<String> m_IncludeSearchDirs;
+ static std::mutex m_ZoneDirsMutex;
+ static std::map<String, std::vector<ZoneFragment> > m_ZoneDirs;
+
+ void InitializeScanner();
+ void DestroyScanner();
+
+ static void HandleIncludeZone(const String& relativeBase, const String& tag, const String& path, const String& pattern, const String& package, std::vector<std::unique_ptr<Expression> >& expressions);
+
+ static bool IsAbsolutePath(const String& path);
+
+public:
+ bool m_Eof;
+ int m_OpenBraces;
+
+ String m_LexBuffer;
+ CompilerDebugInfo m_LocationBegin;
+
+ std::stack<bool> m_IgnoreNewlines;
+ std::stack<bool> m_Apply;
+ std::stack<bool> m_ObjectAssign;
+ std::stack<bool> m_SeenAssign;
+ std::stack<bool> m_SeenIgnore;
+ std::stack<Expression *> m_Assign;
+ std::stack<Expression *> m_Ignore;
+ std::stack<String> m_FKVar;
+ std::stack<String> m_FVVar;
+ std::stack<Expression *> m_FTerm;
+ std::stack<int> m_FlowControlInfo;
+};
+
+}
+
+#endif /* CONFIGCOMPILER_H */
diff --git a/lib/config/configcompilercontext.cpp b/lib/config/configcompilercontext.cpp
new file mode 100644
index 0000000..0161181
--- /dev/null
+++ b/lib/config/configcompilercontext.cpp
@@ -0,0 +1,57 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/configcompilercontext.hpp"
+#include "base/singleton.hpp"
+#include "base/json.hpp"
+#include "base/netstring.hpp"
+#include "base/exception.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+ConfigCompilerContext *ConfigCompilerContext::GetInstance()
+{
+ return Singleton<ConfigCompilerContext>::GetInstance();
+}
+
+void ConfigCompilerContext::OpenObjectsFile(const String& filename)
+{
+ try {
+ m_ObjectsFP = std::make_unique<AtomicFile>(filename, 0600);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli", "Could not create temporary objects file: " + DiagnosticInformation(ex, false));
+ Application::Exit(1);
+ }
+}
+
+void ConfigCompilerContext::WriteObject(const Dictionary::Ptr& object)
+{
+ if (!m_ObjectsFP)
+ return;
+
+ String json = JsonEncode(object);
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ NetString::WriteStringToStream(*m_ObjectsFP, json);
+ }
+}
+
+void ConfigCompilerContext::CancelObjectsFile()
+{
+ if (!m_ObjectsFP)
+ return;
+
+ m_ObjectsFP.reset(nullptr);
+}
+
+void ConfigCompilerContext::FinishObjectsFile()
+{
+ if (!m_ObjectsFP)
+ return;
+
+ m_ObjectsFP->Commit();
+ m_ObjectsFP.reset(nullptr);
+}
+
diff --git a/lib/config/configcompilercontext.hpp b/lib/config/configcompilercontext.hpp
new file mode 100644
index 0000000..c3d5317
--- /dev/null
+++ b/lib/config/configcompilercontext.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGCOMPILERCONTEXT_H
+#define CONFIGCOMPILERCONTEXT_H
+
+#include "config/i2-config.hpp"
+#include "base/atomic-file.hpp"
+#include "base/dictionary.hpp"
+#include <fstream>
+#include <memory>
+#include <mutex>
+
+namespace icinga
+{
+
+/*
+ * @ingroup config
+ */
+class ConfigCompilerContext
+{
+public:
+ void OpenObjectsFile(const String& filename);
+ void WriteObject(const Dictionary::Ptr& object);
+ void CancelObjectsFile();
+ void FinishObjectsFile();
+
+ inline bool IsOpen() const noexcept
+ {
+ return (bool)m_ObjectsFP;
+ }
+
+ static ConfigCompilerContext *GetInstance();
+
+private:
+ std::unique_ptr<AtomicFile> m_ObjectsFP;
+
+ mutable std::mutex m_Mutex;
+};
+
+}
+
+#endif /* CONFIGCOMPILERCONTEXT_H */
diff --git a/lib/config/configfragment.hpp b/lib/config/configfragment.hpp
new file mode 100644
index 0000000..883aef8
--- /dev/null
+++ b/lib/config/configfragment.hpp
@@ -0,0 +1,26 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGFRAGMENT_H
+#define CONFIGFRAGMENT_H
+
+#include "config/configcompiler.hpp"
+#include "base/initialize.hpp"
+#include "base/debug.hpp"
+#include "base/exception.hpp"
+#include "base/application.hpp"
+
+/* Ensure that the priority is lower than the basic namespace initialization in scriptframe.cpp. */
+#define REGISTER_CONFIG_FRAGMENT(name, fragment) \
+ INITIALIZE_ONCE_WITH_PRIORITY([]() { \
+ std::unique_ptr<icinga::Expression> expression = icinga::ConfigCompiler::CompileText(name, fragment); \
+ VERIFY(expression); \
+ try { \
+ icinga::ScriptFrame frame(true); \
+ expression->Evaluate(frame); \
+ } catch (const std::exception& ex) { \
+ std::cerr << icinga::DiagnosticInformation(ex) << std::endl; \
+ icinga::Application::Exit(1); \
+ } \
+ }, icinga::InitializePriority::EvaluateConfigFragments)
+
+#endif /* CONFIGFRAGMENT_H */
diff --git a/lib/config/configitem.cpp b/lib/config/configitem.cpp
new file mode 100644
index 0000000..9dc0f1a
--- /dev/null
+++ b/lib/config/configitem.cpp
@@ -0,0 +1,849 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/configitem.hpp"
+#include "config/configcompilercontext.hpp"
+#include "config/applyrule.hpp"
+#include "config/objectrule.hpp"
+#include "config/configcompiler.hpp"
+#include "base/application.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/logger.hpp"
+#include "base/debug.hpp"
+#include "base/workqueue.hpp"
+#include "base/exception.hpp"
+#include "base/stdiostream.hpp"
+#include "base/netstring.hpp"
+#include "base/serializer.hpp"
+#include "base/json.hpp"
+#include "base/exception.hpp"
+#include "base/function.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <atomic>
+#include <sstream>
+#include <fstream>
+#include <algorithm>
+#include <random>
+#include <unordered_map>
+
+using namespace icinga;
+
+std::mutex ConfigItem::m_Mutex;
+ConfigItem::TypeMap ConfigItem::m_Items;
+ConfigItem::TypeMap ConfigItem::m_DefaultTemplates;
+ConfigItem::ItemList ConfigItem::m_UnnamedItems;
+ConfigItem::IgnoredItemList ConfigItem::m_IgnoredItems;
+
+REGISTER_FUNCTION(Internal, run_with_activation_context, &ConfigItem::RunWithActivationContext, "func");
+
+/**
+ * Constructor for the ConfigItem class.
+ *
+ * @param type The object type.
+ * @param name The name of the item.
+ * @param unit The unit of the item.
+ * @param abstract Whether the item is a template.
+ * @param exprl Expression list for the item.
+ * @param debuginfo Debug information.
+ */
+ConfigItem::ConfigItem(Type::Ptr type, String name,
+ bool abstract, Expression::Ptr exprl,
+ Expression::Ptr filter, bool defaultTmpl, bool ignoreOnError,
+ DebugInfo debuginfo, Dictionary::Ptr scope,
+ String zone, String package)
+ : m_Type(std::move(type)), m_Name(std::move(name)), m_Abstract(abstract),
+ m_Expression(std::move(exprl)), m_Filter(std::move(filter)),
+ m_DefaultTmpl(defaultTmpl), m_IgnoreOnError(ignoreOnError),
+ m_DebugInfo(std::move(debuginfo)), m_Scope(std::move(scope)), m_Zone(std::move(zone)),
+ m_Package(std::move(package))
+{
+}
+
+/**
+ * Retrieves the type of the configuration item.
+ *
+ * @returns The type.
+ */
+Type::Ptr ConfigItem::GetType() const
+{
+ return m_Type;
+}
+
+/**
+ * Retrieves the name of the configuration item.
+ *
+ * @returns The name.
+ */
+String ConfigItem::GetName() const
+{
+ return m_Name;
+}
+
+/**
+ * Checks whether the item is abstract.
+ *
+ * @returns true if the item is abstract, false otherwise.
+ */
+bool ConfigItem::IsAbstract() const
+{
+ return m_Abstract;
+}
+
+bool ConfigItem::IsDefaultTemplate() const
+{
+ return m_DefaultTmpl;
+}
+
+bool ConfigItem::IsIgnoreOnError() const
+{
+ return m_IgnoreOnError;
+}
+
+/**
+ * Retrieves the debug information for the configuration item.
+ *
+ * @returns The debug information.
+ */
+DebugInfo ConfigItem::GetDebugInfo() const
+{
+ return m_DebugInfo;
+}
+
+Dictionary::Ptr ConfigItem::GetScope() const
+{
+ return m_Scope;
+}
+
+ConfigObject::Ptr ConfigItem::GetObject() const
+{
+ return m_Object;
+}
+
+/**
+ * Retrieves the expression list for the configuration item.
+ *
+ * @returns The expression list.
+ */
+Expression::Ptr ConfigItem::GetExpression() const
+{
+ return m_Expression;
+}
+
+/**
+* Retrieves the object filter for the configuration item.
+*
+* @returns The filter expression.
+*/
+Expression::Ptr ConfigItem::GetFilter() const
+{
+ return m_Filter;
+}
+
+class DefaultValidationUtils final : public ValidationUtils
+{
+public:
+ bool ValidateName(const String& type, const String& name) const override
+ {
+ ConfigItem::Ptr item = ConfigItem::GetByTypeAndName(Type::GetByName(type), name);
+
+ if (!item || item->IsAbstract())
+ return false;
+
+ return true;
+ }
+};
+
+/**
+ * Commits the configuration item by creating a ConfigObject
+ * object.
+ *
+ * @returns The ConfigObject that was created/updated.
+ */
+ConfigObject::Ptr ConfigItem::Commit(bool discard)
+{
+ Type::Ptr type = GetType();
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "ConfigItem")
+ << "Commit called for ConfigItem Type=" << type->GetName() << ", Name=" << GetName();
+#endif /* I2_DEBUG */
+
+ /* Make sure the type is valid. */
+ if (!type || !ConfigObject::TypeInstance->IsAssignableFrom(type))
+ BOOST_THROW_EXCEPTION(ScriptError("Type '" + type->GetName() + "' does not exist.", m_DebugInfo));
+
+ if (IsAbstract())
+ return nullptr;
+
+ ConfigObject::Ptr dobj = static_pointer_cast<ConfigObject>(type->Instantiate(std::vector<Value>()));
+
+ dobj->SetDebugInfo(m_DebugInfo);
+ dobj->SetZoneName(m_Zone);
+ dobj->SetPackage(m_Package);
+ dobj->SetName(m_Name);
+
+ DebugHint debugHints;
+
+ ScriptFrame frame(true, dobj);
+ if (m_Scope)
+ m_Scope->CopyTo(frame.Locals);
+ try {
+ m_Expression->Evaluate(frame, &debugHints);
+ } catch (const std::exception& ex) {
+ if (m_IgnoreOnError) {
+ Log(LogNotice, "ConfigObject")
+ << "Ignoring config object '" << m_Name << "' of type '" << type->GetName() << "' due to errors: " << DiagnosticInformation(ex);
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_IgnoredItems.push_back(m_DebugInfo.Path);
+ }
+
+ return nullptr;
+ }
+
+ throw;
+ }
+
+ if (discard)
+ m_Expression.reset();
+
+ String item_name;
+ String short_name = dobj->GetShortName();
+
+ if (!short_name.IsEmpty()) {
+ item_name = short_name;
+ dobj->SetName(short_name);
+ } else
+ item_name = m_Name;
+
+ String name = item_name;
+
+ auto *nc = dynamic_cast<NameComposer *>(type.get());
+
+ if (nc) {
+ if (name.IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Object name must not be empty.", m_DebugInfo));
+
+ name = nc->MakeName(name, dobj);
+
+ if (name.IsEmpty())
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not determine name for object"));
+ }
+
+ if (name != item_name)
+ dobj->SetShortName(item_name);
+
+ dobj->SetName(name);
+
+ Dictionary::Ptr dhint = debugHints.ToDictionary();
+
+ try {
+ DefaultValidationUtils utils;
+ dobj->Validate(FAConfig, utils);
+ } catch (ValidationError& ex) {
+ if (m_IgnoreOnError) {
+ Log(LogNotice, "ConfigObject")
+ << "Ignoring config object '" << m_Name << "' of type '" << type->GetName() << "' due to errors: " << DiagnosticInformation(ex);
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_IgnoredItems.push_back(m_DebugInfo.Path);
+ }
+
+ return nullptr;
+ }
+
+ ex.SetDebugHint(dhint);
+ throw;
+ }
+
+ try {
+ dobj->OnConfigLoaded();
+ } catch (const std::exception& ex) {
+ if (m_IgnoreOnError) {
+ Log(LogNotice, "ConfigObject")
+ << "Ignoring config object '" << m_Name << "' of type '" << m_Type->GetName() << "' due to errors: " << DiagnosticInformation(ex);
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_IgnoredItems.push_back(m_DebugInfo.Path);
+ }
+
+ return nullptr;
+ }
+
+ throw;
+ }
+
+ Value serializedObject;
+
+ try {
+ if (ConfigCompilerContext::GetInstance()->IsOpen()) {
+ serializedObject = Serialize(dobj, FAConfig);
+ } else {
+ AssertNoCircularReferences(dobj);
+ }
+ } catch (const CircularReferenceError& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(dobj, ex.GetPath(), "Circular references are not allowed"));
+ }
+
+ if (ConfigCompilerContext::GetInstance()->IsOpen()) {
+ Dictionary::Ptr persistentItem = new Dictionary({
+ { "type", type->GetName() },
+ { "name", GetName() },
+ { "properties", serializedObject },
+ { "debug_hints", dhint },
+ { "debug_info", new Array({
+ m_DebugInfo.Path,
+ m_DebugInfo.FirstLine,
+ m_DebugInfo.FirstColumn,
+ m_DebugInfo.LastLine,
+ m_DebugInfo.LastColumn,
+ }) }
+ });
+
+ ConfigCompilerContext::GetInstance()->WriteObject(persistentItem);
+ }
+
+ dhint.reset();
+
+ dobj->Register();
+
+ m_Object = dobj;
+
+ return dobj;
+}
+
+/**
+ * Registers the configuration item.
+ */
+void ConfigItem::Register()
+{
+ m_ActivationContext = ActivationContext::GetCurrentContext();
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ /* If this is a non-abstract object with a composite name
+ * we register it in m_UnnamedItems instead of m_Items. */
+ if (!m_Abstract && dynamic_cast<NameComposer *>(m_Type.get()))
+ m_UnnamedItems.emplace_back(this);
+ else {
+ auto& items = m_Items[m_Type];
+
+ auto it = items.find(m_Name);
+
+ if (it != items.end()) {
+ std::ostringstream msgbuf;
+ msgbuf << "A configuration item of type '" << m_Type->GetName()
+ << "' and name '" << GetName() << "' already exists ("
+ << it->second->GetDebugInfo() << "), new declaration: " << GetDebugInfo();
+ BOOST_THROW_EXCEPTION(ScriptError(msgbuf.str()));
+ }
+
+ m_Items[m_Type][m_Name] = this;
+
+ if (m_DefaultTmpl)
+ m_DefaultTemplates[m_Type][m_Name] = this;
+ }
+}
+
+/**
+ * Unregisters the configuration item.
+ */
+void ConfigItem::Unregister()
+{
+ if (m_Object) {
+ m_Object->Unregister();
+ m_Object.reset();
+ }
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_UnnamedItems.erase(std::remove(m_UnnamedItems.begin(), m_UnnamedItems.end(), this), m_UnnamedItems.end());
+ m_Items[m_Type].erase(m_Name);
+ m_DefaultTemplates[m_Type].erase(m_Name);
+}
+
+/**
+ * Retrieves a configuration item by type and name.
+ *
+ * @param type The type of the ConfigItem that is to be looked up.
+ * @param name The name of the ConfigItem that is to be looked up.
+ * @returns The configuration item.
+ */
+ConfigItem::Ptr ConfigItem::GetByTypeAndName(const Type::Ptr& type, const String& name)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ auto it = m_Items.find(type);
+
+ if (it == m_Items.end())
+ return nullptr;
+
+ auto it2 = it->second.find(name);
+
+ if (it2 == it->second.end())
+ return nullptr;
+
+ return it2->second;
+}
+
+bool ConfigItem::CommitNewItems(const ActivationContext::Ptr& context, WorkQueue& upq, std::vector<ConfigItem::Ptr>& newItems)
+{
+ typedef std::pair<ConfigItem::Ptr, bool> ItemPair;
+ std::unordered_map<Type*, std::vector<ItemPair>> itemsByType;
+ std::vector<ItemPair>::size_type total = 0;
+
+ {
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ for (const TypeMap::value_type& kv : m_Items) {
+ std::vector<ItemPair> items;
+
+ for (const ItemMap::value_type& kv2 : kv.second) {
+ if (kv2.second->m_Abstract || kv2.second->m_Object)
+ continue;
+
+ if (kv2.second->m_ActivationContext != context)
+ continue;
+
+ items.emplace_back(kv2.second, false);
+ }
+
+ if (!items.empty()) {
+ total += items.size();
+ itemsByType.emplace(kv.first.get(), std::move(items));
+ }
+ }
+
+ ItemList newUnnamedItems;
+
+ for (const ConfigItem::Ptr& item : m_UnnamedItems) {
+ if (item->m_ActivationContext != context) {
+ newUnnamedItems.push_back(item);
+ continue;
+ }
+
+ if (item->m_Abstract || item->m_Object)
+ continue;
+
+ itemsByType[item->m_Type.get()].emplace_back(item, true);
+ ++total;
+ }
+
+ m_UnnamedItems.swap(newUnnamedItems);
+ }
+
+ if (!total)
+ return true;
+
+ // Shuffle all items to evenly distribute them over the threads of the workqueue. This increases perfomance
+ // noticably in environments with lots of objects and available threads.
+ for (auto& kv : itemsByType) {
+ std::shuffle(std::begin(kv.second), std::end(kv.second), std::default_random_engine{});
+ }
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "configitem")
+ << "Committing " << total << " new items.";
+#endif /* I2_DEBUG */
+
+ std::set<Type::Ptr> types;
+ std::set<Type::Ptr> completed_types;
+ int itemsCount {0};
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ if (ConfigObject::TypeInstance->IsAssignableFrom(type))
+ types.insert(type);
+ }
+
+ while (types.size() != completed_types.size()) {
+ for (const Type::Ptr& type : types) {
+ if (completed_types.find(type) != completed_types.end())
+ continue;
+
+ bool unresolved_dep = false;
+
+ /* skip this type (for now) if there are unresolved load dependencies */
+ for (auto pLoadDep : type->GetLoadDependencies()) {
+ if (types.find(pLoadDep) != types.end() && completed_types.find(pLoadDep) == completed_types.end()) {
+ unresolved_dep = true;
+ break;
+ }
+ }
+
+ if (unresolved_dep)
+ continue;
+
+ std::atomic<int> committed_items(0);
+ std::mutex newItemsMutex;
+
+ {
+ auto items (itemsByType.find(type.get()));
+
+ if (items != itemsByType.end()) {
+ upq.ParallelFor(items->second, [&committed_items, &newItems, &newItemsMutex](const ItemPair& ip) {
+ const ConfigItem::Ptr& item = ip.first;
+
+ if (!item->Commit(ip.second)) {
+ if (item->IsIgnoreOnError()) {
+ item->Unregister();
+ }
+
+ return;
+ }
+
+ committed_items++;
+
+ std::unique_lock<std::mutex> lock(newItemsMutex);
+ newItems.emplace_back(item);
+ });
+
+ upq.Join();
+ }
+ }
+
+ itemsCount += committed_items;
+
+ completed_types.insert(type);
+
+#ifdef I2_DEBUG
+ if (committed_items > 0)
+ Log(LogDebug, "configitem")
+ << "Committed " << committed_items << " items of type '" << type->GetName() << "'.";
+#endif /* I2_DEBUG */
+
+ if (upq.HasExceptions())
+ return false;
+ }
+ }
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "configitem")
+ << "Committed " << itemsCount << " items.";
+#endif /* I2_DEBUG */
+
+ completed_types.clear();
+
+ while (types.size() != completed_types.size()) {
+ for (const Type::Ptr& type : types) {
+ if (completed_types.find(type) != completed_types.end())
+ continue;
+
+ bool unresolved_dep = false;
+
+ /* skip this type (for now) if there are unresolved load dependencies */
+ for (auto pLoadDep : type->GetLoadDependencies()) {
+ if (types.find(pLoadDep) != types.end() && completed_types.find(pLoadDep) == completed_types.end()) {
+ unresolved_dep = true;
+ break;
+ }
+ }
+
+ if (unresolved_dep)
+ continue;
+
+ std::atomic<int> notified_items(0);
+
+ {
+ auto items (itemsByType.find(type.get()));
+
+ if (items != itemsByType.end()) {
+ upq.ParallelFor(items->second, [&notified_items](const ItemPair& ip) {
+ const ConfigItem::Ptr& item = ip.first;
+
+ if (!item->m_Object)
+ return;
+
+ try {
+ item->m_Object->OnAllConfigLoaded();
+ notified_items++;
+ } catch (const std::exception& ex) {
+ if (!item->m_IgnoreOnError)
+ throw;
+
+ Log(LogNotice, "ConfigObject")
+ << "Ignoring config object '" << item->m_Name << "' of type '" << item->m_Type->GetName() << "' due to errors: " << DiagnosticInformation(ex);
+
+ item->Unregister();
+
+ {
+ std::unique_lock<std::mutex> lock(item->m_Mutex);
+ item->m_IgnoredItems.push_back(item->m_DebugInfo.Path);
+ }
+ }
+ });
+
+ upq.Join();
+ }
+ }
+
+ completed_types.insert(type);
+
+#ifdef I2_DEBUG
+ if (notified_items > 0)
+ Log(LogDebug, "configitem")
+ << "Sent OnAllConfigLoaded to " << notified_items << " items of type '" << type->GetName() << "'.";
+#endif /* I2_DEBUG */
+
+ if (upq.HasExceptions())
+ return false;
+
+ notified_items = 0;
+ for (auto loadDep : type->GetLoadDependencies()) {
+ auto items (itemsByType.find(loadDep));
+
+ if (items != itemsByType.end()) {
+ upq.ParallelFor(items->second, [&type, &notified_items](const ItemPair& ip) {
+ const ConfigItem::Ptr& item = ip.first;
+
+ if (!item->m_Object)
+ return;
+
+ ActivationScope ascope(item->m_ActivationContext);
+ item->m_Object->CreateChildObjects(type);
+ notified_items++;
+ });
+ }
+ }
+
+ upq.Join();
+
+#ifdef I2_DEBUG
+ if (notified_items > 0)
+ Log(LogDebug, "configitem")
+ << "Sent CreateChildObjects to " << notified_items << " items of type '" << type->GetName() << "'.";
+#endif /* I2_DEBUG */
+
+ if (upq.HasExceptions())
+ return false;
+
+ // Make sure to activate any additionally generated items
+ if (!CommitNewItems(context, upq, newItems))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool ConfigItem::CommitItems(const ActivationContext::Ptr& context, WorkQueue& upq, std::vector<ConfigItem::Ptr>& newItems, bool silent)
+{
+ if (!silent)
+ Log(LogInformation, "ConfigItem", "Committing config item(s).");
+
+ if (!CommitNewItems(context, upq, newItems)) {
+ upq.ReportExceptions("config");
+
+ for (const ConfigItem::Ptr& item : newItems) {
+ item->Unregister();
+ }
+
+ return false;
+ }
+
+ ApplyRule::CheckMatches(silent);
+
+ if (!silent) {
+ /* log stats for external parsers */
+ typedef std::map<Type::Ptr, int> ItemCountMap;
+ ItemCountMap itemCounts;
+ for (const ConfigItem::Ptr& item : newItems) {
+ if (!item->m_Object)
+ continue;
+
+ itemCounts[item->m_Object->GetReflectionType()]++;
+ }
+
+ for (const ItemCountMap::value_type& kv : itemCounts) {
+ Log(LogInformation, "ConfigItem")
+ << "Instantiated " << kv.second << " " << (kv.second != 1 ? kv.first->GetPluralName() : kv.first->GetName()) << ".";
+ }
+ }
+
+ return true;
+}
+
+/**
+ * ActivateItems activates new config items.
+ *
+ * @param newItems Vector of items to be activated
+ * @param runtimeCreated Whether the objects were created by a runtime object
+ * @param mainConfigActivation Whether this is the call for activating the main configuration during startup
+ * @param withModAttrs Whether this call shall read the modified attributes file
+ * @param cookie Cookie for preventing message loops
+ * @return Whether the config activation was successful (in case of errors, exceptions are thrown)
+ */
+bool ConfigItem::ActivateItems(const std::vector<ConfigItem::Ptr>& newItems, bool runtimeCreated,
+ bool mainConfigActivation, bool withModAttrs, const Value& cookie)
+{
+ static std::mutex mtx;
+ std::unique_lock<std::mutex> lock(mtx);
+
+ if (withModAttrs) {
+ /* restore modified attributes */
+ if (Utility::PathExists(Configuration::ModAttrPath)) {
+ std::unique_ptr<Expression> expression = ConfigCompiler::CompileFile(Configuration::ModAttrPath);
+
+ if (expression) {
+ try {
+ ScriptFrame frame(true);
+ expression->Evaluate(frame);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "config", DiagnosticInformation(ex));
+ }
+ }
+ }
+ }
+
+ for (const ConfigItem::Ptr& item : newItems) {
+ if (!item->m_Object)
+ continue;
+
+ ConfigObject::Ptr object = item->m_Object;
+
+ if (object->IsActive())
+ continue;
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "ConfigItem")
+ << "Setting 'active' to true for object '" << object->GetName() << "' of type '" << object->GetReflectionType()->GetName() << "'";
+#endif /* I2_DEBUG */
+
+ object->PreActivate();
+ }
+
+ if (mainConfigActivation)
+ Log(LogInformation, "ConfigItem", "Triggering Start signal for config items");
+
+ /* Activate objects in priority order. */
+ std::vector<Type::Ptr> types = Type::GetAllTypes();
+
+ std::sort(types.begin(), types.end(), [](const Type::Ptr& a, const Type::Ptr& b) {
+ if (a->GetActivationPriority() < b->GetActivationPriority())
+ return true;
+ return false;
+ });
+
+ /* Find the last logger type to be activated. */
+ Type::Ptr lastLoggerType = nullptr;
+ for (const Type::Ptr& type : types) {
+ if (Logger::TypeInstance->IsAssignableFrom(type)) {
+ lastLoggerType = type;
+ }
+ }
+
+ for (const Type::Ptr& type : types) {
+ for (const ConfigItem::Ptr& item : newItems) {
+ if (!item->m_Object)
+ continue;
+
+ ConfigObject::Ptr object = item->m_Object;
+ Type::Ptr objectType = object->GetReflectionType();
+
+ if (objectType != type)
+ continue;
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "ConfigItem")
+ << "Activating object '" << object->GetName() << "' of type '"
+ << objectType->GetName() << "' with priority "
+ << objectType->GetActivationPriority();
+#endif /* I2_DEBUG */
+
+ object->Activate(runtimeCreated, cookie);
+ }
+
+ if (mainConfigActivation && type == lastLoggerType) {
+ /* Disable early logging configuration once the last logger type was activated. */
+ Logger::DisableEarlyLogging();
+ }
+ }
+
+ if (mainConfigActivation)
+ Log(LogInformation, "ConfigItem", "Activated all objects.");
+
+ return true;
+}
+
+bool ConfigItem::RunWithActivationContext(const Function::Ptr& function)
+{
+ ActivationScope scope;
+
+ if (!function)
+ BOOST_THROW_EXCEPTION(ScriptError("'function' argument must not be null."));
+
+ function->Invoke();
+
+ WorkQueue upq(25000, Configuration::Concurrency);
+ upq.SetName("ConfigItem::RunWithActivationContext");
+
+ std::vector<ConfigItem::Ptr> newItems;
+
+ if (!CommitItems(scope.GetContext(), upq, newItems, true))
+ return false;
+
+ if (!ActivateItems(newItems, false, false))
+ return false;
+
+ return true;
+}
+
+std::vector<ConfigItem::Ptr> ConfigItem::GetItems(const Type::Ptr& type)
+{
+ std::vector<ConfigItem::Ptr> items;
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ auto it = m_Items.find(type);
+
+ if (it == m_Items.end())
+ return items;
+
+ items.reserve(it->second.size());
+
+ for (const ItemMap::value_type& kv : it->second) {
+ items.push_back(kv.second);
+ }
+
+ return items;
+}
+
+std::vector<ConfigItem::Ptr> ConfigItem::GetDefaultTemplates(const Type::Ptr& type)
+{
+ std::vector<ConfigItem::Ptr> items;
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ auto it = m_DefaultTemplates.find(type);
+
+ if (it == m_DefaultTemplates.end())
+ return items;
+
+ items.reserve(it->second.size());
+
+ for (const ItemMap::value_type& kv : it->second) {
+ items.push_back(kv.second);
+ }
+
+ return items;
+}
+
+void ConfigItem::RemoveIgnoredItems(const String& allowedConfigPath)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ for (const String& path : m_IgnoredItems) {
+ if (path.Find(allowedConfigPath) == String::NPos)
+ continue;
+
+ Log(LogNotice, "ConfigItem")
+ << "Removing ignored item path '" << path << "'.";
+
+ (void) unlink(path.CStr());
+ }
+
+ m_IgnoredItems.clear();
+}
diff --git a/lib/config/configitem.hpp b/lib/config/configitem.hpp
new file mode 100644
index 0000000..b99cd08
--- /dev/null
+++ b/lib/config/configitem.hpp
@@ -0,0 +1,106 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGITEM_H
+#define CONFIGITEM_H
+
+#include "config/i2-config.hpp"
+#include "config/expression.hpp"
+#include "config/activationcontext.hpp"
+#include "base/configobject.hpp"
+#include "base/workqueue.hpp"
+
+namespace icinga
+{
+
+
+/**
+ * A configuration item. Non-abstract configuration items can be used to
+ * create configuration objects at runtime.
+ *
+ * @ingroup config
+ */
+class ConfigItem final : public Object {
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigItem);
+
+ ConfigItem(Type::Ptr type, String name, bool abstract,
+ Expression::Ptr exprl,
+ Expression::Ptr filter,
+ bool defaultTmpl, bool ignoreOnError, DebugInfo debuginfo,
+ Dictionary::Ptr scope, String zone,
+ String package);
+
+ Type::Ptr GetType() const;
+ String GetName() const;
+ bool IsAbstract() const;
+ bool IsDefaultTemplate() const;
+ bool IsIgnoreOnError() const;
+
+ std::vector<ConfigItem::Ptr> GetParents() const;
+
+ Expression::Ptr GetExpression() const;
+ Expression::Ptr GetFilter() const;
+
+ void Register();
+ void Unregister();
+
+ DebugInfo GetDebugInfo() const;
+ Dictionary::Ptr GetScope() const;
+
+ ConfigObject::Ptr GetObject() const;
+
+ static ConfigItem::Ptr GetByTypeAndName(const Type::Ptr& type,
+ const String& name);
+
+ static bool CommitItems(const ActivationContext::Ptr& context, WorkQueue& upq, std::vector<ConfigItem::Ptr>& newItems, bool silent = false);
+ static bool ActivateItems(const std::vector<ConfigItem::Ptr>& newItems, bool runtimeCreated = false,
+ bool mainConfigActivation = false, bool withModAttrs = false, const Value& cookie = Empty);
+
+ static bool RunWithActivationContext(const Function::Ptr& function);
+
+ static std::vector<ConfigItem::Ptr> GetItems(const Type::Ptr& type);
+ static std::vector<ConfigItem::Ptr> GetDefaultTemplates(const Type::Ptr& type);
+
+ static void RemoveIgnoredItems(const String& allowedConfigPath);
+
+private:
+ Type::Ptr m_Type; /**< The object type. */
+ String m_Name; /**< The name. */
+ bool m_Abstract; /**< Whether this is a template. */
+
+ Expression::Ptr m_Expression;
+ Expression::Ptr m_Filter;
+ bool m_DefaultTmpl;
+ bool m_IgnoreOnError;
+ DebugInfo m_DebugInfo; /**< Debug information. */
+ Dictionary::Ptr m_Scope; /**< variable scope. */
+ String m_Zone; /**< The zone. */
+ String m_Package;
+ ActivationContext::Ptr m_ActivationContext;
+
+ ConfigObject::Ptr m_Object;
+
+ static std::mutex m_Mutex;
+
+ typedef std::map<String, ConfigItem::Ptr> ItemMap;
+ typedef std::map<Type::Ptr, ItemMap> TypeMap;
+ static TypeMap m_Items; /**< All registered configuration items. */
+ static TypeMap m_DefaultTemplates;
+
+ typedef std::vector<ConfigItem::Ptr> ItemList;
+ static ItemList m_UnnamedItems;
+
+ typedef std::vector<String> IgnoredItemList;
+ static IgnoredItemList m_IgnoredItems;
+
+ static ConfigItem::Ptr GetObjectUnlocked(const String& type,
+ const String& name);
+
+ ConfigObject::Ptr Commit(bool discard = true);
+
+ static bool CommitNewItems(const ActivationContext::Ptr& context, WorkQueue& upq, std::vector<ConfigItem::Ptr>& newItems);
+};
+
+}
+
+#endif /* CONFIGITEM_H */
diff --git a/lib/config/configitembuilder.cpp b/lib/config/configitembuilder.cpp
new file mode 100644
index 0000000..f7a3ead
--- /dev/null
+++ b/lib/config/configitembuilder.cpp
@@ -0,0 +1,120 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/configitembuilder.hpp"
+#include "base/configtype.hpp"
+#include <sstream>
+
+using namespace icinga;
+
+ConfigItemBuilder::ConfigItemBuilder(const DebugInfo& debugInfo)
+ : m_Abstract(false), m_DefaultTmpl(false), m_IgnoreOnError(false)
+{
+ m_DebugInfo = debugInfo;
+}
+
+void ConfigItemBuilder::SetType(const Type::Ptr& type)
+{
+ ASSERT(type);
+ m_Type = type;
+}
+
+void ConfigItemBuilder::SetName(const String& name)
+{
+ m_Name = name;
+}
+
+void ConfigItemBuilder::SetAbstract(bool abstract)
+{
+ m_Abstract = abstract;
+}
+
+void ConfigItemBuilder::SetScope(const Dictionary::Ptr& scope)
+{
+ m_Scope = scope;
+}
+
+void ConfigItemBuilder::SetZone(const String& zone)
+{
+ m_Zone = zone;
+}
+
+void ConfigItemBuilder::SetPackage(const String& package)
+{
+ m_Package = package;
+}
+
+void ConfigItemBuilder::AddExpression(Expression *expr)
+{
+ m_Expressions.emplace_back(expr);
+}
+
+void ConfigItemBuilder::SetFilter(const Expression::Ptr& filter)
+{
+ m_Filter = filter;
+}
+
+void ConfigItemBuilder::SetDefaultTemplate(bool defaultTmpl)
+{
+ m_DefaultTmpl = defaultTmpl;
+}
+
+void ConfigItemBuilder::SetIgnoreOnError(bool ignoreOnError)
+{
+ m_IgnoreOnError = ignoreOnError;
+}
+
+ConfigItem::Ptr ConfigItemBuilder::Compile()
+{
+ if (!m_Type) {
+ std::ostringstream msgbuf;
+ msgbuf << "The type of an object must be specified";
+ BOOST_THROW_EXCEPTION(ScriptError(msgbuf.str(), m_DebugInfo));
+ }
+
+ auto *ctype = dynamic_cast<ConfigType *>(m_Type.get());
+
+ if (!ctype) {
+ std::ostringstream msgbuf;
+ msgbuf << "The type '" + m_Type->GetName() + "' cannot be used for config objects";
+ BOOST_THROW_EXCEPTION(ScriptError(msgbuf.str(), m_DebugInfo));
+ }
+
+ if (m_Name.FindFirstOf("!") != String::NPos) {
+ std::ostringstream msgbuf;
+ msgbuf << "Name for object '" << m_Name << "' of type '" << m_Type->GetName() << "' is invalid: Object names may not contain '!'";
+ BOOST_THROW_EXCEPTION(ScriptError(msgbuf.str(), m_DebugInfo));
+ }
+
+ std::vector<std::unique_ptr<Expression> > exprs;
+
+ Array::Ptr templateArray = new Array({ m_Name });
+
+ exprs.emplace_back(new SetExpression(MakeIndexer(ScopeThis, "templates"), OpSetAdd,
+ std::unique_ptr<LiteralExpression>(new LiteralExpression(templateArray)), m_DebugInfo));
+
+#ifdef I2_DEBUG
+ if (!m_Abstract) {
+ bool foundDefaultImport = false;
+
+ for (const std::unique_ptr<Expression>& expr : m_Expressions) {
+ if (dynamic_cast<ImportDefaultTemplatesExpression *>(expr.get())) {
+ foundDefaultImport = true;
+ break;
+ }
+ }
+
+ ASSERT(foundDefaultImport);
+ }
+#endif /* I2_DEBUG */
+
+ auto *dexpr = new DictExpression(std::move(m_Expressions), m_DebugInfo);
+ dexpr->MakeInline();
+ exprs.emplace_back(dexpr);
+
+ auto exprl = new DictExpression(std::move(exprs), m_DebugInfo);
+ exprl->MakeInline();
+
+ return new ConfigItem(m_Type, m_Name, m_Abstract, exprl, m_Filter,
+ m_DefaultTmpl, m_IgnoreOnError, m_DebugInfo, m_Scope, m_Zone, m_Package);
+}
+
diff --git a/lib/config/configitembuilder.hpp b/lib/config/configitembuilder.hpp
new file mode 100644
index 0000000..9d2e339
--- /dev/null
+++ b/lib/config/configitembuilder.hpp
@@ -0,0 +1,58 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGITEMBUILDER_H
+#define CONFIGITEMBUILDER_H
+
+#include "config/expression.hpp"
+#include "config/configitem.hpp"
+#include "base/debuginfo.hpp"
+#include "base/object.hpp"
+
+namespace icinga
+{
+
+/**
+ * Config item builder. Used to dynamically build configuration objects
+ * at runtime.
+ *
+ * @ingroup config
+ */
+class ConfigItemBuilder final
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigItemBuilder);
+
+ ConfigItemBuilder() = default;
+ explicit ConfigItemBuilder(const DebugInfo& debugInfo);
+
+ void SetType(const Type::Ptr& type);
+ void SetName(const String& name);
+ void SetAbstract(bool abstract);
+ void SetScope(const Dictionary::Ptr& scope);
+ void SetZone(const String& zone);
+ void SetPackage(const String& package);
+ void SetDefaultTemplate(bool defaultTmpl);
+ void SetIgnoreOnError(bool ignoreOnError);
+
+ void AddExpression(Expression *expr);
+ void SetFilter(const Expression::Ptr& filter);
+
+ ConfigItem::Ptr Compile();
+
+private:
+ Type::Ptr m_Type; /**< The object type. */
+ String m_Name; /**< The name. */
+ bool m_Abstract{false}; /**< Whether the item is abstract. */
+ std::vector<std::unique_ptr<Expression> > m_Expressions; /**< Expressions for this item. */
+ Expression::Ptr m_Filter; /**< Filter expression. */
+ DebugInfo m_DebugInfo; /**< Debug information. */
+ Dictionary::Ptr m_Scope; /**< variable scope. */
+ String m_Zone; /**< The zone. */
+ String m_Package; /**< The package name. */
+ bool m_DefaultTmpl{false};
+ bool m_IgnoreOnError{false}; /**< Whether the object should be ignored when an error occurs in one of the expressions. */
+};
+
+}
+
+#endif /* CONFIGITEMBUILDER */
diff --git a/lib/config/expression.cpp b/lib/config/expression.cpp
new file mode 100644
index 0000000..a8e9986
--- /dev/null
+++ b/lib/config/expression.cpp
@@ -0,0 +1,1068 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/expression.hpp"
+#include "config/configitem.hpp"
+#include "config/configcompiler.hpp"
+#include "config/vmops.hpp"
+#include "base/array.hpp"
+#include "base/json.hpp"
+#include "base/object.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/loader.hpp"
+#include "base/reference.hpp"
+#include "base/namespace.hpp"
+#include "base/defer.hpp"
+#include <boost/exception_ptr.hpp>
+#include <boost/exception/errinfo_nested_exception.hpp>
+
+using namespace icinga;
+
+boost::signals2::signal<void (ScriptFrame&, ScriptError *ex, const DebugInfo&)> Expression::OnBreakpoint;
+boost::thread_specific_ptr<bool> l_InBreakpointHandler;
+
+Expression::~Expression()
+{ }
+
+void Expression::ScriptBreakpoint(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di)
+{
+ bool *inHandler = l_InBreakpointHandler.get();
+ if (!inHandler || !*inHandler) {
+ inHandler = new bool(true);
+ l_InBreakpointHandler.reset(inHandler);
+ OnBreakpoint(frame, ex, di);
+ *inHandler = false;
+ }
+}
+
+ExpressionResult Expression::Evaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ try {
+#ifdef I2_DEBUG
+/* std::ostringstream msgbuf;
+ ShowCodeLocation(msgbuf, GetDebugInfo(), false);
+ Log(LogDebug, "Expression")
+ << "Executing:\n" << msgbuf.str();*/
+#endif /* I2_DEBUG */
+
+ frame.IncreaseStackDepth();
+
+ Defer decreaseStackDepth([&frame]{
+ frame.DecreaseStackDepth();
+ });
+
+ ExpressionResult result = DoEvaluate(frame, dhint);
+ return result;
+ } catch (ScriptError& ex) {
+ ScriptBreakpoint(frame, &ex, GetDebugInfo());
+ throw;
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ScriptError("Error while evaluating expression: " + String(ex.what()), GetDebugInfo())
+ << boost::errinfo_nested_exception(boost::current_exception()));
+ }
+}
+
+bool Expression::GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint) const
+{
+ return false;
+}
+
+const DebugInfo& Expression::GetDebugInfo() const
+{
+ static DebugInfo debugInfo;
+ return debugInfo;
+}
+
+std::unique_ptr<Expression> icinga::MakeIndexer(ScopeSpecifier scopeSpec, const String& index)
+{
+ std::unique_ptr<Expression> scope{new GetScopeExpression(scopeSpec)};
+ return std::unique_ptr<Expression>(new IndexerExpression(std::move(scope), MakeLiteral(index)));
+}
+
+void DictExpression::MakeInline()
+{
+ m_Inline = true;
+}
+
+LiteralExpression::LiteralExpression(Value value)
+ : m_Value(std::move(value))
+{ }
+
+ExpressionResult LiteralExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ return m_Value;
+}
+
+const DebugInfo& DebuggableExpression::GetDebugInfo() const
+{
+ return m_DebugInfo;
+}
+
+VariableExpression::VariableExpression(String variable, std::vector<Expression::Ptr> imports, const DebugInfo& debugInfo)
+ : DebuggableExpression(debugInfo), m_Variable(std::move(variable)), m_Imports(std::move(imports))
+{
+ m_Imports.push_back(MakeIndexer(ScopeGlobal, "System").release());
+ m_Imports.push_back(new IndexerExpression(MakeIndexer(ScopeGlobal, "System"), MakeLiteral("Configuration")));
+ m_Imports.push_back(MakeIndexer(ScopeGlobal, "Types").release());
+ m_Imports.push_back(MakeIndexer(ScopeGlobal, "Icinga").release());
+}
+
+ExpressionResult VariableExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ Value value;
+
+ if (frame.Locals && frame.Locals->Get(m_Variable, &value))
+ return value;
+ else if (frame.Self.IsObject() && frame.Locals != frame.Self.Get<Object::Ptr>() && frame.Self.Get<Object::Ptr>()->GetOwnField(m_Variable, &value))
+ return value;
+ else if (VMOps::FindVarImport(frame, m_Imports, m_Variable, &value, m_DebugInfo))
+ return value;
+ else
+ return ScriptGlobal::Get(m_Variable);
+}
+
+bool VariableExpression::GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint) const
+{
+ *index = m_Variable;
+
+ if (frame.Locals && frame.Locals->Contains(m_Variable)) {
+ *parent = frame.Locals;
+
+ if (dhint)
+ *dhint = nullptr;
+ } else if (frame.Self.IsObject() && frame.Locals != frame.Self.Get<Object::Ptr>() && frame.Self.Get<Object::Ptr>()->HasOwnField(m_Variable)) {
+ *parent = frame.Self;
+
+ if (dhint && *dhint)
+ *dhint = new DebugHint((*dhint)->GetChild(m_Variable));
+ } else if (VMOps::FindVarImportRef(frame, m_Imports, m_Variable, parent, m_DebugInfo)) {
+ return true;
+ } else if (ScriptGlobal::Exists(m_Variable)) {
+ *parent = ScriptGlobal::GetGlobals();
+
+ if (dhint)
+ *dhint = nullptr;
+ } else
+ *parent = frame.Self;
+
+ return true;
+}
+
+ExpressionResult RefExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ Value parent;
+ String index;
+
+ if (!m_Operand->GetReference(frame, false, &parent, &index, &dhint))
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot obtain reference for expression.", m_DebugInfo));
+
+ if (!parent.IsObject())
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot obtain reference for expression because parent is not an object.", m_DebugInfo));
+
+ return new Reference(parent, index);
+}
+
+ExpressionResult DerefExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand = m_Operand->Evaluate(frame);
+ CHECK_RESULT(operand);
+
+ Object::Ptr obj = operand.GetValue();
+ Reference::Ptr ref = dynamic_pointer_cast<Reference>(obj);
+
+ if (!ref)
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid reference specified.", GetDebugInfo()));
+
+ return ref->Get();
+}
+
+bool DerefExpression::GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint) const
+{
+ ExpressionResult operand = m_Operand->Evaluate(frame);
+ if (operand.GetCode() != ResultOK)
+ return false;
+
+ Reference::Ptr ref = operand.GetValue();
+
+ *parent = ref->GetParent();
+ *index = ref->GetIndex();
+ return true;
+}
+
+ExpressionResult NegateExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand = m_Operand->Evaluate(frame);
+ CHECK_RESULT(operand);
+
+ return ~(long)operand.GetValue();
+}
+
+ExpressionResult LogicalNegateExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand = m_Operand->Evaluate(frame);
+ CHECK_RESULT(operand);
+
+ return !operand.GetValue().ToBool();
+}
+
+ExpressionResult AddExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() + operand2.GetValue();
+}
+
+ExpressionResult SubtractExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() - operand2.GetValue();
+}
+
+ExpressionResult MultiplyExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() * operand2.GetValue();
+}
+
+ExpressionResult DivideExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() / operand2.GetValue();
+}
+
+ExpressionResult ModuloExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() % operand2.GetValue();
+}
+
+ExpressionResult XorExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() ^ operand2.GetValue();
+}
+
+ExpressionResult BinaryAndExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() & operand2.GetValue();
+}
+
+ExpressionResult BinaryOrExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() | operand2.GetValue();
+}
+
+ExpressionResult ShiftLeftExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() << operand2.GetValue();
+}
+
+ExpressionResult ShiftRightExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() >> operand2.GetValue();
+}
+
+ExpressionResult EqualExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() == operand2.GetValue();
+}
+
+ExpressionResult NotEqualExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() != operand2.GetValue();
+}
+
+ExpressionResult LessThanExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() < operand2.GetValue();
+}
+
+ExpressionResult GreaterThanExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() > operand2.GetValue();
+}
+
+ExpressionResult LessThanOrEqualExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() <= operand2.GetValue();
+}
+
+ExpressionResult GreaterThanOrEqualExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand1.GetValue() >= operand2.GetValue();
+}
+
+ExpressionResult InExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ if (operand2.GetValue().IsEmpty())
+ return false;
+ else if (!operand2.GetValue().IsObjectType<Array>())
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid right side argument for 'in' operator: " + JsonEncode(operand2.GetValue()), m_DebugInfo));
+
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1)
+
+ Array::Ptr arr = operand2.GetValue();
+ return arr->Contains(operand1.GetValue());
+}
+
+ExpressionResult NotInExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ if (operand2.GetValue().IsEmpty())
+ return true;
+ else if (!operand2.GetValue().IsObjectType<Array>())
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid right side argument for 'in' operator: " + JsonEncode(operand2.GetValue()), m_DebugInfo));
+
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ Array::Ptr arr = operand2.GetValue();
+ return !arr->Contains(operand1.GetValue());
+}
+
+ExpressionResult LogicalAndExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ if (!operand1.GetValue().ToBool())
+ return operand1;
+ else {
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand2.GetValue();
+ }
+}
+
+ExpressionResult LogicalOrExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ CHECK_RESULT(operand1);
+
+ if (operand1.GetValue().ToBool())
+ return operand1;
+ else {
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ CHECK_RESULT(operand2);
+
+ return operand2.GetValue();
+ }
+}
+
+ExpressionResult FunctionCallExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ Value self, vfunc;
+ String index;
+
+ if (m_FName->GetReference(frame, false, &self, &index))
+ vfunc = VMOps::GetField(self, index, frame.Sandboxed, m_DebugInfo);
+ else {
+ ExpressionResult vfuncres = m_FName->Evaluate(frame);
+ CHECK_RESULT(vfuncres);
+
+ vfunc = vfuncres.GetValue();
+ }
+
+ if (vfunc.IsObjectType<Type>()) {
+ std::vector<Value> arguments;
+ arguments.reserve(m_Args.size());
+ for (const auto& arg : m_Args) {
+ ExpressionResult argres = arg->Evaluate(frame);
+ CHECK_RESULT(argres);
+
+ arguments.push_back(argres.GetValue());
+ }
+
+ return VMOps::ConstructorCall(vfunc, arguments, m_DebugInfo);
+ }
+
+ if (!vfunc.IsObjectType<Function>())
+ BOOST_THROW_EXCEPTION(ScriptError("Argument is not a callable object.", m_DebugInfo));
+
+ Function::Ptr func = vfunc;
+
+ if (!func->IsSideEffectFree() && frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("Function is not marked as safe for sandbox mode.", m_DebugInfo));
+
+ std::vector<Value> arguments;
+ arguments.reserve(m_Args.size());
+ for (const auto& arg : m_Args) {
+ ExpressionResult argres = arg->Evaluate(frame);
+ CHECK_RESULT(argres);
+
+ arguments.push_back(argres.GetValue());
+ }
+
+ return VMOps::FunctionCall(frame, self, func, arguments);
+}
+
+ExpressionResult ArrayExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ArrayData result;
+ result.reserve(m_Expressions.size());
+
+ for (const auto& aexpr : m_Expressions) {
+ ExpressionResult element = aexpr->Evaluate(frame);
+ CHECK_RESULT(element);
+
+ result.push_back(element.GetValue());
+ }
+
+ return new Array(std::move(result));
+}
+
+ExpressionResult DictExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ Value self;
+
+ if (!m_Inline) {
+ self = frame.Self;
+ frame.Self = new Dictionary();
+ }
+
+ Value result;
+
+ try {
+ for (const auto& aexpr : m_Expressions) {
+ ExpressionResult element = aexpr->Evaluate(frame, m_Inline ? dhint : nullptr);
+ CHECK_RESULT(element);
+ result = element.GetValue();
+ }
+ } catch (...) {
+ if (!m_Inline)
+ std::swap(self, frame.Self);
+ throw;
+ }
+
+ if (m_Inline)
+ return result;
+ else {
+ std::swap(self, frame.Self);
+ return self;
+ }
+}
+
+ExpressionResult GetScopeExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (m_ScopeSpec == ScopeLocal)
+ return frame.Locals;
+ else if (m_ScopeSpec == ScopeThis)
+ return frame.Self;
+ else if (m_ScopeSpec == ScopeGlobal)
+ return ScriptGlobal::GetGlobals();
+ else
+ VERIFY(!"Invalid scope.");
+}
+
+static inline
+void WarnOnImplicitlySetGlobalVar(const std::unique_ptr<Expression>& setLhs, const Value& setLhsParent, CombinedSetOp setOp, const DebugInfo& debug)
+{
+ auto var (dynamic_cast<VariableExpression*>(setLhs.get()));
+
+ if (var && setLhsParent.IsObject()) {
+ auto ns (dynamic_pointer_cast<Namespace>(setLhsParent.Get<Object::Ptr>()));
+
+ if (ns && ns == ScriptGlobal::GetGlobals() && debug.Path.GetLength()) {
+ const char *opStr = nullptr;
+
+ switch (setOp) {
+ case OpSetLiteral:
+ opStr = "=";
+ break;
+ case OpSetAdd:
+ opStr = "+=";
+ break;
+ case OpSetSubtract:
+ opStr = "-=";
+ break;
+ case OpSetMultiply:
+ opStr = "*=";
+ break;
+ case OpSetDivide:
+ opStr = "/=";
+ break;
+ case OpSetModulo:
+ opStr = "%=";
+ break;
+ case OpSetXor:
+ opStr = "^=";
+ break;
+ case OpSetBinaryAnd:
+ opStr = "&=";
+ break;
+ case OpSetBinaryOr:
+ opStr = "|=";
+ break;
+ default:
+ VERIFY(!"Invalid opcode.");
+ }
+
+ auto varName (var->GetVariable());
+
+ Log(LogWarning, "config")
+ << "Global variable '" << varName << "' has been set implicitly via '" << varName << ' ' << opStr << " ...' " << debug << "."
+ " Please set it explicitly via 'globals." << varName << ' ' << opStr << " ...' instead.";
+ }
+ }
+}
+
+ExpressionResult SetExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("Assignments are not allowed in sandbox mode.", m_DebugInfo));
+
+ DebugHint *psdhint = dhint;
+
+ Value parent;
+ String index;
+
+ if (!m_Operand1->GetReference(frame, true, &parent, &index, &psdhint))
+ BOOST_THROW_EXCEPTION(ScriptError("Expression cannot be assigned to.", m_DebugInfo));
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame, dhint);
+ CHECK_RESULT(operand2);
+
+ if (m_Op != OpSetLiteral) {
+ Value object = VMOps::GetField(parent, index, frame.Sandboxed, m_DebugInfo);
+
+ switch (m_Op) {
+ case OpSetAdd:
+ operand2 = object + operand2;
+ break;
+ case OpSetSubtract:
+ operand2 = object - operand2;
+ break;
+ case OpSetMultiply:
+ operand2 = object * operand2;
+ break;
+ case OpSetDivide:
+ operand2 = object / operand2;
+ break;
+ case OpSetModulo:
+ operand2 = object % operand2;
+ break;
+ case OpSetXor:
+ operand2 = object ^ operand2;
+ break;
+ case OpSetBinaryAnd:
+ operand2 = object & operand2;
+ break;
+ case OpSetBinaryOr:
+ operand2 = object | operand2;
+ break;
+ default:
+ VERIFY(!"Invalid opcode.");
+ }
+ }
+
+ VMOps::SetField(parent, index, operand2.GetValue(), m_OverrideFrozen, m_DebugInfo);
+
+ if (psdhint) {
+ psdhint->AddMessage("=", m_DebugInfo);
+
+ if (psdhint != dhint)
+ delete psdhint;
+ }
+
+ WarnOnImplicitlySetGlobalVar(m_Operand1, parent, m_Op, m_DebugInfo);
+
+ return Empty;
+}
+
+void SetExpression::SetOverrideFrozen()
+{
+ m_OverrideFrozen = true;
+}
+
+ExpressionResult SetConstExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ auto globals = ScriptGlobal::GetGlobals();
+
+ ExpressionResult operandres = m_Operand->Evaluate(frame);
+ CHECK_RESULT(operandres);
+ Value operand = operandres.GetValue();
+
+ globals->Set(m_Name, operand, true);
+
+ return Empty;
+}
+
+ExpressionResult ConditionalExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult condition = m_Condition->Evaluate(frame, dhint);
+ CHECK_RESULT(condition);
+
+ if (condition.GetValue().ToBool())
+ return m_TrueBranch->Evaluate(frame, dhint);
+ else if (m_FalseBranch)
+ return m_FalseBranch->Evaluate(frame, dhint);
+
+ return Empty;
+}
+
+ExpressionResult WhileExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("While loops are not allowed in sandbox mode.", m_DebugInfo));
+
+ for (;;) {
+ ExpressionResult condition = m_Condition->Evaluate(frame, dhint);
+ CHECK_RESULT(condition);
+
+ if (!condition.GetValue().ToBool())
+ break;
+
+ ExpressionResult loop_body = m_LoopBody->Evaluate(frame, dhint);
+ CHECK_RESULT_LOOP(loop_body);
+ }
+
+ return Empty;
+}
+
+ExpressionResult ReturnExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand = m_Operand->Evaluate(frame);
+ CHECK_RESULT(operand);
+
+ return ExpressionResult(operand.GetValue(), ResultReturn);
+}
+
+ExpressionResult BreakExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ return ExpressionResult(Empty, ResultBreak);
+}
+
+ExpressionResult ContinueExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ return ExpressionResult(Empty, ResultContinue);
+}
+
+ExpressionResult IndexerExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame, dhint);
+ CHECK_RESULT(operand1);
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame, dhint);
+ CHECK_RESULT(operand2);
+
+ return VMOps::GetField(operand1.GetValue(), operand2.GetValue(), frame.Sandboxed, m_DebugInfo);
+}
+
+bool IndexerExpression::GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint) const
+{
+ Value vparent;
+ String vindex;
+ DebugHint *psdhint = nullptr;
+ bool free_psd = false;
+
+ if (dhint)
+ psdhint = *dhint;
+
+ if (frame.Sandboxed)
+ init_dict = false;
+
+ if (m_Operand1->GetReference(frame, init_dict, &vparent, &vindex, &psdhint)) {
+ if (init_dict) {
+ Value old_value;
+ bool has_field = true;
+
+ if (vparent.IsObject()) {
+ Object::Ptr oparent = vparent;
+ has_field = oparent->HasOwnField(vindex);
+ }
+
+ if (has_field)
+ old_value = VMOps::GetField(vparent, vindex, frame.Sandboxed, m_Operand1->GetDebugInfo());
+
+ if (old_value.IsEmpty() && !old_value.IsString())
+ VMOps::SetField(vparent, vindex, new Dictionary(), m_OverrideFrozen, m_Operand1->GetDebugInfo());
+ }
+
+ *parent = VMOps::GetField(vparent, vindex, frame.Sandboxed, m_DebugInfo);
+ free_psd = true;
+ } else {
+ ExpressionResult operand1 = m_Operand1->Evaluate(frame);
+ *parent = operand1.GetValue();
+ }
+
+ ExpressionResult operand2 = m_Operand2->Evaluate(frame);
+ *index = operand2.GetValue();
+
+ if (dhint) {
+ if (psdhint)
+ *dhint = new DebugHint(psdhint->GetChild(*index));
+ else
+ *dhint = nullptr;
+ }
+
+ if (free_psd)
+ delete psdhint;
+
+ return true;
+}
+
+void IndexerExpression::SetOverrideFrozen()
+{
+ m_OverrideFrozen = true;
+}
+
+void icinga::BindToScope(std::unique_ptr<Expression>& expr, ScopeSpecifier scopeSpec)
+{
+ auto *dexpr = dynamic_cast<DictExpression *>(expr.get());
+
+ if (dexpr) {
+ for (auto& expr : dexpr->m_Expressions)
+ BindToScope(expr, scopeSpec);
+
+ return;
+ }
+
+ auto *aexpr = dynamic_cast<SetExpression *>(expr.get());
+
+ if (aexpr) {
+ BindToScope(aexpr->m_Operand1, scopeSpec);
+
+ return;
+ }
+
+ auto *iexpr = dynamic_cast<IndexerExpression *>(expr.get());
+
+ if (iexpr) {
+ BindToScope(iexpr->m_Operand1, scopeSpec);
+ return;
+ }
+
+ auto *lexpr = dynamic_cast<LiteralExpression *>(expr.get());
+
+ if (lexpr && lexpr->GetValue().IsString()) {
+ std::unique_ptr<Expression> scope{new GetScopeExpression(scopeSpec)};
+ expr.reset(new IndexerExpression(std::move(scope), std::move(expr), lexpr->GetDebugInfo()));
+ }
+
+ auto *vexpr = dynamic_cast<VariableExpression *>(expr.get());
+
+ if (vexpr) {
+ std::unique_ptr<Expression> scope{new GetScopeExpression(scopeSpec)};
+ expr.reset(new IndexerExpression(std::move(scope), MakeLiteral(vexpr->GetVariable()), vexpr->GetDebugInfo()));
+ }
+}
+
+ExpressionResult ThrowExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ExpressionResult messageres = m_Message->Evaluate(frame);
+ CHECK_RESULT(messageres);
+ Value message = messageres.GetValue();
+ BOOST_THROW_EXCEPTION(ScriptError(message, m_DebugInfo, m_IncompleteExpr));
+}
+
+ExpressionResult ImportExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("Imports are not allowed in sandbox mode.", m_DebugInfo));
+
+ String type = VMOps::GetField(frame.Self, "type", frame.Sandboxed, m_DebugInfo);
+ ExpressionResult nameres = m_Name->Evaluate(frame);
+ CHECK_RESULT(nameres);
+ Value name = nameres.GetValue();
+
+ if (!name.IsString())
+ BOOST_THROW_EXCEPTION(ScriptError("Template/object name must be a string", m_DebugInfo));
+
+ ConfigItem::Ptr item = ConfigItem::GetByTypeAndName(Type::GetByName(type), name);
+
+ if (!item)
+ BOOST_THROW_EXCEPTION(ScriptError("Import references unknown template: '" + name + "'", m_DebugInfo));
+
+ Dictionary::Ptr scope = item->GetScope();
+
+ if (scope)
+ scope->CopyTo(frame.Locals);
+
+ ExpressionResult result = item->GetExpression()->Evaluate(frame, dhint);
+ CHECK_RESULT(result);
+
+ return Empty;
+}
+
+ExpressionResult ImportDefaultTemplatesExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("Imports are not allowed in sandbox mode.", m_DebugInfo));
+
+ String type = VMOps::GetField(frame.Self, "type", frame.Sandboxed, m_DebugInfo);
+ Type::Ptr ptype = Type::GetByName(type);
+
+ for (const ConfigItem::Ptr& item : ConfigItem::GetDefaultTemplates(ptype)) {
+ Dictionary::Ptr scope = item->GetScope();
+
+ if (scope)
+ scope->CopyTo(frame.Locals);
+
+ ExpressionResult result = item->GetExpression()->Evaluate(frame, dhint);
+ CHECK_RESULT(result);
+ }
+
+ return Empty;
+}
+
+ExpressionResult FunctionExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ return VMOps::NewFunction(frame, m_Name, m_Args, m_ClosedVars, m_Expression);
+}
+
+ExpressionResult ApplyExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("Apply rules are not allowed in sandbox mode.", m_DebugInfo));
+
+ ExpressionResult nameres = m_Name->Evaluate(frame);
+ CHECK_RESULT(nameres);
+
+ return VMOps::NewApply(frame, m_Type, m_Target, nameres.GetValue(), m_Filter,
+ m_Package, m_FKVar, m_FVVar, m_FTerm, m_ClosedVars, m_IgnoreOnError, m_Expression, m_DebugInfo);
+}
+
+ExpressionResult NamespaceExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ Namespace::Ptr ns = new Namespace(true);
+
+ ScriptFrame innerFrame(true, ns);
+ ExpressionResult result = m_Expression->Evaluate(innerFrame);
+ CHECK_RESULT(result);
+
+ return ns;
+}
+
+ExpressionResult ObjectExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("Object definitions are not allowed in sandbox mode.", m_DebugInfo));
+
+ ExpressionResult typeres = m_Type->Evaluate(frame, dhint);
+ CHECK_RESULT(typeres);
+ Type::Ptr type = typeres.GetValue();
+
+ String name;
+
+ if (m_Name) {
+ ExpressionResult nameres = m_Name->Evaluate(frame, dhint);
+ CHECK_RESULT(nameres);
+
+ name = nameres.GetValue();
+ }
+
+ return VMOps::NewObject(frame, m_Abstract, type, name, m_Filter, m_Zone,
+ m_Package, m_DefaultTmpl, m_IgnoreOnError, m_ClosedVars, m_Expression, m_DebugInfo);
+}
+
+ExpressionResult ForExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("For loops are not allowed in sandbox mode.", m_DebugInfo));
+
+ ExpressionResult valueres = m_Value->Evaluate(frame, dhint);
+ CHECK_RESULT(valueres);
+
+ return VMOps::For(frame, m_FKVar, m_FVVar, valueres.GetValue(), m_Expression, m_DebugInfo);
+}
+
+ExpressionResult LibraryExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("Loading libraries is not allowed in sandbox mode.", m_DebugInfo));
+
+ ExpressionResult libres = m_Operand->Evaluate(frame, dhint);
+ CHECK_RESULT(libres);
+
+ Log(LogNotice, "config")
+ << "Ignoring explicit load request for library \"" << libres << "\".";
+
+ return Empty;
+}
+
+ExpressionResult IncludeExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ if (frame.Sandboxed)
+ BOOST_THROW_EXCEPTION(ScriptError("Includes are not allowed in sandbox mode.", m_DebugInfo));
+
+ std::unique_ptr<Expression> expr;
+ String name, path, pattern;
+
+ switch (m_Type) {
+ case IncludeRegular:
+ {
+ ExpressionResult pathres = m_Path->Evaluate(frame, dhint);
+ CHECK_RESULT(pathres);
+ path = pathres.GetValue();
+ }
+
+ expr = ConfigCompiler::HandleInclude(m_RelativeBase, path, m_SearchIncludes, m_Zone, m_Package, m_DebugInfo);
+ break;
+
+ case IncludeRecursive:
+ {
+ ExpressionResult pathres = m_Path->Evaluate(frame, dhint);
+ CHECK_RESULT(pathres);
+ path = pathres.GetValue();
+ }
+
+ {
+ ExpressionResult patternres = m_Pattern->Evaluate(frame, dhint);
+ CHECK_RESULT(patternres);
+ pattern = patternres.GetValue();
+ }
+
+ expr = ConfigCompiler::HandleIncludeRecursive(m_RelativeBase, path, pattern, m_Zone, m_Package, m_DebugInfo);
+ break;
+
+ case IncludeZones:
+ {
+ ExpressionResult nameres = m_Name->Evaluate(frame, dhint);
+ CHECK_RESULT(nameres);
+ name = nameres.GetValue();
+ }
+
+ {
+ ExpressionResult pathres = m_Path->Evaluate(frame, dhint);
+ CHECK_RESULT(pathres);
+ path = pathres.GetValue();
+ }
+
+ {
+ ExpressionResult patternres = m_Pattern->Evaluate(frame, dhint);
+ CHECK_RESULT(patternres);
+ pattern = patternres.GetValue();
+ }
+
+ expr = ConfigCompiler::HandleIncludeZones(m_RelativeBase, name, path, pattern, m_Package, m_DebugInfo);
+ break;
+ }
+
+ ExpressionResult res(Empty);
+
+ try {
+ res = expr->Evaluate(frame, dhint);
+ } catch (const std::exception&) {
+ throw;
+ }
+
+ return res;
+}
+
+ExpressionResult BreakpointExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ ScriptBreakpoint(frame, nullptr, GetDebugInfo());
+
+ return Empty;
+}
+
+ExpressionResult TryExceptExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
+{
+ try {
+ ExpressionResult tryResult = m_TryBody->Evaluate(frame, dhint);
+ CHECK_RESULT(tryResult);
+ } catch (const std::exception&) {
+ ExpressionResult exceptResult = m_ExceptBody->Evaluate(frame, dhint);
+ CHECK_RESULT(exceptResult);
+ }
+
+ return Empty;
+}
+
diff --git a/lib/config/expression.hpp b/lib/config/expression.hpp
new file mode 100644
index 0000000..644548d
--- /dev/null
+++ b/lib/config/expression.hpp
@@ -0,0 +1,986 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EXPRESSION_H
+#define EXPRESSION_H
+
+#include "config/i2-config.hpp"
+#include "base/debuginfo.hpp"
+#include "base/array.hpp"
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/exception.hpp"
+#include "base/scriptframe.hpp"
+#include "base/shared-object.hpp"
+#include "base/convert.hpp"
+#include <map>
+
+namespace icinga
+{
+
+struct DebugHint
+{
+public:
+ DebugHint(Dictionary::Ptr hints = nullptr)
+ : m_Hints(std::move(hints))
+ { }
+
+ DebugHint(Dictionary::Ptr&& hints)
+ : m_Hints(std::move(hints))
+ { }
+
+ void AddMessage(const String& message, const DebugInfo& di)
+ {
+ GetMessages()->Add(new Array({ message, di.Path, di.FirstLine, di.FirstColumn, di.LastLine, di.LastColumn }));
+ }
+
+ DebugHint GetChild(const String& name)
+ {
+ const Dictionary::Ptr& children = GetChildren();
+
+ Value vchild;
+ Dictionary::Ptr child;
+
+ if (!children->Get(name, &vchild)) {
+ child = new Dictionary();
+ children->Set(name, child);
+ } else
+ child = vchild;
+
+ return DebugHint(child);
+ }
+
+ Dictionary::Ptr ToDictionary() const
+ {
+ return m_Hints;
+ }
+
+private:
+ Dictionary::Ptr m_Hints;
+ Array::Ptr m_Messages;
+ Dictionary::Ptr m_Children;
+
+ const Array::Ptr& GetMessages()
+ {
+ if (m_Messages)
+ return m_Messages;
+
+ if (!m_Hints)
+ m_Hints = new Dictionary();
+
+ Value vmessages;
+
+ if (!m_Hints->Get("messages", &vmessages)) {
+ m_Messages = new Array();
+ m_Hints->Set("messages", m_Messages);
+ } else
+ m_Messages = vmessages;
+
+ return m_Messages;
+ }
+
+ const Dictionary::Ptr& GetChildren()
+ {
+ if (m_Children)
+ return m_Children;
+
+ if (!m_Hints)
+ m_Hints = new Dictionary();
+
+ Value vchildren;
+
+ if (!m_Hints->Get("properties", &vchildren)) {
+ m_Children = new Dictionary();
+ m_Hints->Set("properties", m_Children);
+ } else
+ m_Children = vchildren;
+
+ return m_Children;
+ }
+};
+
+enum CombinedSetOp
+{
+ OpSetLiteral,
+ OpSetAdd,
+ OpSetSubtract,
+ OpSetMultiply,
+ OpSetDivide,
+ OpSetModulo,
+ OpSetXor,
+ OpSetBinaryAnd,
+ OpSetBinaryOr
+};
+
+enum ScopeSpecifier
+{
+ ScopeLocal,
+ ScopeThis,
+ ScopeGlobal
+};
+
+typedef std::map<String, String> DefinitionMap;
+
+/**
+ * @ingroup config
+ */
+enum ExpressionResultCode
+{
+ ResultOK,
+ ResultReturn,
+ ResultContinue,
+ ResultBreak
+};
+
+/**
+ * @ingroup config
+ */
+struct ExpressionResult
+{
+public:
+ template<typename T>
+ ExpressionResult(T value, ExpressionResultCode code = ResultOK)
+ : m_Value(std::move(value)), m_Code(code)
+ { }
+
+ operator const Value&() const
+ {
+ return m_Value;
+ }
+
+ const Value& GetValue() const
+ {
+ return m_Value;
+ }
+
+ ExpressionResultCode GetCode() const
+ {
+ return m_Code;
+ }
+
+private:
+ Value m_Value;
+ ExpressionResultCode m_Code;
+};
+
+#define CHECK_RESULT(res) \
+ do { \
+ if (res.GetCode() != ResultOK) \
+ return res; \
+ } while (0);
+
+#define CHECK_RESULT_LOOP(res) \
+ if (res.GetCode() == ResultReturn) \
+ return res; \
+ if (res.GetCode() == ResultContinue) \
+ continue; \
+ if (res.GetCode() == ResultBreak) \
+ break; \
+
+/**
+ * @ingroup config
+ */
+class Expression : public SharedObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Expression);
+
+ Expression() = default;
+ Expression(const Expression&) = delete;
+ virtual ~Expression();
+
+ Expression& operator=(const Expression&) = delete;
+
+ ExpressionResult Evaluate(ScriptFrame& frame, DebugHint *dhint = nullptr) const;
+ virtual bool GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint = nullptr) const;
+ virtual const DebugInfo& GetDebugInfo() const;
+
+ virtual ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const = 0;
+
+ static boost::signals2::signal<void (ScriptFrame& frame, ScriptError *ex, const DebugInfo& di)> OnBreakpoint;
+
+ static void ScriptBreakpoint(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di);
+};
+
+std::unique_ptr<Expression> MakeIndexer(ScopeSpecifier scopeSpec, const String& index);
+
+class OwnedExpression final : public Expression
+{
+public:
+ OwnedExpression(Expression::Ptr expression)
+ : m_Expression(std::move(expression))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override
+ {
+ return m_Expression->DoEvaluate(frame, dhint);
+ }
+
+ const DebugInfo& GetDebugInfo() const override
+ {
+ return m_Expression->GetDebugInfo();
+ }
+
+private:
+ Expression::Ptr m_Expression;
+};
+
+class LiteralExpression final : public Expression
+{
+public:
+ LiteralExpression(Value value = Value());
+
+ const Value& GetValue() const
+ {
+ return m_Value;
+ }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ Value m_Value;
+};
+
+inline LiteralExpression *MakeLiteralRaw(const Value& literal = Value())
+{
+ return new LiteralExpression(literal);
+}
+
+inline std::unique_ptr<LiteralExpression> MakeLiteral(const Value& literal = Value())
+{
+ return std::unique_ptr<LiteralExpression>(MakeLiteralRaw(literal));
+}
+
+class DebuggableExpression : public Expression
+{
+public:
+ DebuggableExpression(DebugInfo debugInfo = DebugInfo())
+ : m_DebugInfo(std::move(debugInfo))
+ { }
+
+protected:
+ const DebugInfo& GetDebugInfo() const final;
+
+ DebugInfo m_DebugInfo;
+};
+
+class UnaryExpression : public DebuggableExpression
+{
+public:
+ UnaryExpression(std::unique_ptr<Expression> operand, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Operand(std::move(operand))
+ { }
+
+protected:
+ std::unique_ptr<Expression> m_Operand;
+};
+
+class BinaryExpression : public DebuggableExpression
+{
+public:
+ BinaryExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Operand1(std::move(operand1)), m_Operand2(std::move(operand2))
+ { }
+
+ inline const std::unique_ptr<Expression>& GetOperand1() const noexcept
+ {
+ return m_Operand1;
+ }
+
+ inline const std::unique_ptr<Expression>& GetOperand2() const noexcept
+ {
+ return m_Operand2;
+ }
+
+protected:
+ std::unique_ptr<Expression> m_Operand1;
+ std::unique_ptr<Expression> m_Operand2;
+};
+
+class VariableExpression final : public DebuggableExpression
+{
+public:
+ VariableExpression(String variable, std::vector<Expression::Ptr> imports, const DebugInfo& debugInfo = DebugInfo());
+
+ inline const String& GetVariable() const
+ {
+ return m_Variable;
+ }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+ bool GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint) const override;
+
+private:
+ String m_Variable;
+ std::vector<Expression::Ptr> m_Imports;
+
+ friend void BindToScope(std::unique_ptr<Expression>& expr, ScopeSpecifier scopeSpec);
+};
+
+class DerefExpression final : public UnaryExpression
+{
+public:
+ DerefExpression(std::unique_ptr<Expression> operand, const DebugInfo& debugInfo = DebugInfo())
+ : UnaryExpression(std::move(operand), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+ bool GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint) const override;
+};
+
+class RefExpression final : public UnaryExpression
+{
+public:
+ RefExpression(std::unique_ptr<Expression> operand, const DebugInfo& debugInfo = DebugInfo())
+ : UnaryExpression(std::move(operand), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class NegateExpression final : public UnaryExpression
+{
+public:
+ NegateExpression(std::unique_ptr<Expression> operand, const DebugInfo& debugInfo = DebugInfo())
+ : UnaryExpression(std::move(operand), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class LogicalNegateExpression final : public UnaryExpression
+{
+public:
+ LogicalNegateExpression(std::unique_ptr<Expression> operand, const DebugInfo& debugInfo = DebugInfo())
+ : UnaryExpression(std::move(operand), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class AddExpression final : public BinaryExpression
+{
+public:
+ AddExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class SubtractExpression final : public BinaryExpression
+{
+public:
+ SubtractExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class MultiplyExpression final : public BinaryExpression
+{
+public:
+ MultiplyExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class DivideExpression final : public BinaryExpression
+{
+public:
+ DivideExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class ModuloExpression final : public BinaryExpression
+{
+public:
+ ModuloExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class XorExpression final : public BinaryExpression
+{
+public:
+ XorExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class BinaryAndExpression final : public BinaryExpression
+{
+public:
+ BinaryAndExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class BinaryOrExpression final : public BinaryExpression
+{
+public:
+ BinaryOrExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class ShiftLeftExpression final : public BinaryExpression
+{
+public:
+ ShiftLeftExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class ShiftRightExpression final : public BinaryExpression
+{
+public:
+ ShiftRightExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class EqualExpression final : public BinaryExpression
+{
+public:
+ EqualExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class NotEqualExpression final : public BinaryExpression
+{
+public:
+ NotEqualExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class LessThanExpression final : public BinaryExpression
+{
+public:
+ LessThanExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class GreaterThanExpression final : public BinaryExpression
+{
+public:
+ GreaterThanExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class LessThanOrEqualExpression final : public BinaryExpression
+{
+public:
+ LessThanOrEqualExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class GreaterThanOrEqualExpression final : public BinaryExpression
+{
+public:
+ GreaterThanOrEqualExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class InExpression final : public BinaryExpression
+{
+public:
+ InExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class NotInExpression final : public BinaryExpression
+{
+public:
+ NotInExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class LogicalAndExpression final : public BinaryExpression
+{
+public:
+ LogicalAndExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class LogicalOrExpression final : public BinaryExpression
+{
+public:
+ LogicalOrExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class FunctionCallExpression final : public DebuggableExpression
+{
+public:
+ FunctionCallExpression(std::unique_ptr<Expression> fname, std::vector<std::unique_ptr<Expression> >&& args, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_FName(std::move(fname)), m_Args(std::move(args))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+public:
+ std::unique_ptr<Expression> m_FName;
+ std::vector<std::unique_ptr<Expression> > m_Args;
+};
+
+class ArrayExpression final : public DebuggableExpression
+{
+public:
+ ArrayExpression(std::vector<std::unique_ptr<Expression > >&& expressions, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Expressions(std::move(expressions))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ std::vector<std::unique_ptr<Expression> > m_Expressions;
+};
+
+class DictExpression final : public DebuggableExpression
+{
+public:
+ DictExpression(std::vector<std::unique_ptr<Expression> >&& expressions = {}, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Expressions(std::move(expressions))
+ { }
+
+ void MakeInline();
+
+ inline const std::vector<std::unique_ptr<Expression>>& GetExpressions() const noexcept
+ {
+ return m_Expressions;
+ }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ std::vector<std::unique_ptr<Expression> > m_Expressions;
+ bool m_Inline{false};
+
+ friend void BindToScope(std::unique_ptr<Expression>& expr, ScopeSpecifier scopeSpec);
+};
+
+class SetConstExpression final : public UnaryExpression
+{
+public:
+ SetConstExpression(const String& name, std::unique_ptr<Expression> operand, const DebugInfo& debugInfo = DebugInfo())
+ : UnaryExpression(std::move(operand), debugInfo), m_Name(name)
+ { }
+
+protected:
+ String m_Name;
+
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class SetExpression final : public BinaryExpression
+{
+public:
+ SetExpression(std::unique_ptr<Expression> operand1, CombinedSetOp op, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo), m_Op(op)
+ { }
+
+ void SetOverrideFrozen();
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ CombinedSetOp m_Op;
+ bool m_OverrideFrozen{false};
+
+ friend void BindToScope(std::unique_ptr<Expression>& expr, ScopeSpecifier scopeSpec);
+};
+
+class ConditionalExpression final : public DebuggableExpression
+{
+public:
+ ConditionalExpression(std::unique_ptr<Expression> condition, std::unique_ptr<Expression> true_branch, std::unique_ptr<Expression> false_branch, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Condition(std::move(condition)), m_TrueBranch(std::move(true_branch)), m_FalseBranch(std::move(false_branch))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ std::unique_ptr<Expression> m_Condition;
+ std::unique_ptr<Expression> m_TrueBranch;
+ std::unique_ptr<Expression> m_FalseBranch;
+};
+
+class WhileExpression final : public DebuggableExpression
+{
+public:
+ WhileExpression(std::unique_ptr<Expression> condition, std::unique_ptr<Expression> loop_body, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Condition(std::move(condition)), m_LoopBody(std::move(loop_body))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ std::unique_ptr<Expression> m_Condition;
+ std::unique_ptr<Expression> m_LoopBody;
+};
+
+
+class ReturnExpression final : public UnaryExpression
+{
+public:
+ ReturnExpression(std::unique_ptr<Expression> expression, const DebugInfo& debugInfo = DebugInfo())
+ : UnaryExpression(std::move(expression), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class BreakExpression final : public DebuggableExpression
+{
+public:
+ BreakExpression(const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class ContinueExpression final : public DebuggableExpression
+{
+public:
+ ContinueExpression(const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class GetScopeExpression final : public Expression
+{
+public:
+ GetScopeExpression(ScopeSpecifier scopeSpec)
+ : m_ScopeSpec(scopeSpec)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ ScopeSpecifier m_ScopeSpec;
+};
+
+class IndexerExpression final : public BinaryExpression
+{
+public:
+ IndexerExpression(std::unique_ptr<Expression> operand1, std::unique_ptr<Expression> operand2, const DebugInfo& debugInfo = DebugInfo())
+ : BinaryExpression(std::move(operand1), std::move(operand2), debugInfo)
+ { }
+
+ void SetOverrideFrozen();
+
+protected:
+ bool m_OverrideFrozen{false};
+
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+ bool GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint) const override;
+
+ friend void BindToScope(std::unique_ptr<Expression>& expr, ScopeSpecifier scopeSpec);
+};
+
+void BindToScope(std::unique_ptr<Expression>& expr, ScopeSpecifier scopeSpec);
+
+class ThrowExpression final : public DebuggableExpression
+{
+public:
+ ThrowExpression(std::unique_ptr<Expression> message, bool incompleteExpr, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Message(std::move(message)), m_IncompleteExpr(incompleteExpr)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ std::unique_ptr<Expression> m_Message;
+ bool m_IncompleteExpr;
+};
+
+class ImportExpression final : public DebuggableExpression
+{
+public:
+ ImportExpression(std::unique_ptr<Expression> name, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Name(std::move(name))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ std::unique_ptr<Expression> m_Name;
+};
+
+class ImportDefaultTemplatesExpression final : public DebuggableExpression
+{
+public:
+ ImportDefaultTemplatesExpression(const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class FunctionExpression final : public DebuggableExpression
+{
+public:
+ FunctionExpression(String name, std::vector<String> args,
+ std::map<String, std::unique_ptr<Expression> >&& closedVars, std::unique_ptr<Expression> expression, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Name(std::move(name)), m_Args(std::move(args)), m_ClosedVars(std::move(closedVars)), m_Expression(expression.release())
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ String m_Name;
+ std::vector<String> m_Args;
+ std::map<String, std::unique_ptr<Expression> > m_ClosedVars;
+ Expression::Ptr m_Expression;
+};
+
+class ApplyExpression final : public DebuggableExpression
+{
+public:
+ ApplyExpression(String type, String target, std::unique_ptr<Expression> name,
+ std::unique_ptr<Expression> filter, String package, String fkvar, String fvvar,
+ std::unique_ptr<Expression> fterm, std::map<String, std::unique_ptr<Expression> >&& closedVars, bool ignoreOnError,
+ std::unique_ptr<Expression> expression, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Type(std::move(type)), m_Target(std::move(target)),
+ m_Name(std::move(name)), m_Filter(filter.release()), m_Package(std::move(package)), m_FKVar(std::move(fkvar)), m_FVVar(std::move(fvvar)),
+ m_FTerm(fterm.release()), m_IgnoreOnError(ignoreOnError), m_ClosedVars(std::move(closedVars)),
+ m_Expression(expression.release())
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ String m_Type;
+ String m_Target;
+ std::unique_ptr<Expression> m_Name;
+ Expression::Ptr m_Filter;
+ String m_Package;
+ String m_FKVar;
+ String m_FVVar;
+ Expression::Ptr m_FTerm;
+ bool m_IgnoreOnError;
+ std::map<String, std::unique_ptr<Expression> > m_ClosedVars;
+ Expression::Ptr m_Expression;
+};
+
+class NamespaceExpression final : public DebuggableExpression
+{
+public:
+ NamespaceExpression(std::unique_ptr<Expression> expression, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Expression(expression.release())
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ Expression::Ptr m_Expression;
+};
+
+class ObjectExpression final : public DebuggableExpression
+{
+public:
+ ObjectExpression(bool abstract, std::unique_ptr<Expression> type, std::unique_ptr<Expression> name, std::unique_ptr<Expression> filter,
+ String zone, String package, std::map<String, std::unique_ptr<Expression> >&& closedVars,
+ bool defaultTmpl, bool ignoreOnError, std::unique_ptr<Expression> expression, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_Abstract(abstract), m_Type(std::move(type)),
+ m_Name(std::move(name)), m_Filter(filter.release()), m_Zone(std::move(zone)), m_Package(std::move(package)), m_DefaultTmpl(defaultTmpl),
+ m_IgnoreOnError(ignoreOnError), m_ClosedVars(std::move(closedVars)), m_Expression(expression.release())
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ bool m_Abstract;
+ std::unique_ptr<Expression> m_Type;
+ std::unique_ptr<Expression> m_Name;
+ Expression::Ptr m_Filter;
+ String m_Zone;
+ String m_Package;
+ bool m_DefaultTmpl;
+ bool m_IgnoreOnError;
+ std::map<String, std::unique_ptr<Expression> > m_ClosedVars;
+ Expression::Ptr m_Expression;
+};
+
+class ForExpression final : public DebuggableExpression
+{
+public:
+ ForExpression(String fkvar, String fvvar, std::unique_ptr<Expression> value, std::unique_ptr<Expression> expression, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_FKVar(std::move(fkvar)), m_FVVar(std::move(fvvar)), m_Value(std::move(value)), m_Expression(std::move(expression))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ String m_FKVar;
+ String m_FVVar;
+ std::unique_ptr<Expression> m_Value;
+ std::unique_ptr<Expression> m_Expression;
+};
+
+class LibraryExpression final : public UnaryExpression
+{
+public:
+ LibraryExpression(std::unique_ptr<Expression> expression, const DebugInfo& debugInfo = DebugInfo())
+ : UnaryExpression(std::move(expression), debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+enum IncludeType
+{
+ IncludeRegular,
+ IncludeRecursive,
+ IncludeZones
+};
+
+class IncludeExpression final : public DebuggableExpression
+{
+public:
+ IncludeExpression(String relativeBase, std::unique_ptr<Expression> path, std::unique_ptr<Expression> pattern, std::unique_ptr<Expression> name,
+ IncludeType type, bool searchIncludes, String zone, String package, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_RelativeBase(std::move(relativeBase)), m_Path(std::move(path)), m_Pattern(std::move(pattern)),
+ m_Name(std::move(name)), m_Type(type), m_SearchIncludes(searchIncludes), m_Zone(std::move(zone)), m_Package(std::move(package))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ String m_RelativeBase;
+ std::unique_ptr<Expression> m_Path;
+ std::unique_ptr<Expression> m_Pattern;
+ std::unique_ptr<Expression> m_Name;
+ IncludeType m_Type;
+ bool m_SearchIncludes;
+ String m_Zone;
+ String m_Package;
+};
+
+class BreakpointExpression final : public DebuggableExpression
+{
+public:
+ BreakpointExpression(const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo)
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+};
+
+class TryExceptExpression final : public DebuggableExpression
+{
+public:
+ TryExceptExpression(std::unique_ptr<Expression> tryBody, std::unique_ptr<Expression> exceptBody, const DebugInfo& debugInfo = DebugInfo())
+ : DebuggableExpression(debugInfo), m_TryBody(std::move(tryBody)), m_ExceptBody(std::move(exceptBody))
+ { }
+
+protected:
+ ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override;
+
+private:
+ std::unique_ptr<Expression> m_TryBody;
+ std::unique_ptr<Expression> m_ExceptBody;
+};
+
+}
+
+#endif /* EXPRESSION_H */
diff --git a/lib/config/i2-config.hpp b/lib/config/i2-config.hpp
new file mode 100644
index 0000000..8c26287
--- /dev/null
+++ b/lib/config/i2-config.hpp
@@ -0,0 +1,16 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2CONFIG_H
+#define I2CONFIG_H
+
+/**
+ * @defgroup config Configuration library
+ *
+ * The configuration library implements a compiler for Icinga 2's configuration
+ * format. It also provides functionality for creating configuration objects
+ * at runtime.
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2CONFIG_H */
diff --git a/lib/config/objectrule.cpp b/lib/config/objectrule.cpp
new file mode 100644
index 0000000..6a74a40
--- /dev/null
+++ b/lib/config/objectrule.cpp
@@ -0,0 +1,18 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/objectrule.hpp"
+#include <set>
+
+using namespace icinga;
+
+ObjectRule::TypeSet ObjectRule::m_Types;
+
+void ObjectRule::RegisterType(const String& sourceType)
+{
+ m_Types.insert(sourceType);
+}
+
+bool ObjectRule::IsValidSourceType(const String& sourceType)
+{
+ return m_Types.find(sourceType) != m_Types.end();
+}
diff --git a/lib/config/objectrule.hpp b/lib/config/objectrule.hpp
new file mode 100644
index 0000000..d093c9f
--- /dev/null
+++ b/lib/config/objectrule.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTRULE_H
+#define OBJECTRULE_H
+
+#include "config/i2-config.hpp"
+#include "config/expression.hpp"
+#include "base/debuginfo.hpp"
+#include <set>
+
+namespace icinga
+{
+
+/**
+ * @ingroup config
+ */
+class ObjectRule
+{
+public:
+ typedef std::set<String> TypeSet;
+
+ static void RegisterType(const String& sourceType);
+ static bool IsValidSourceType(const String& sourceType);
+
+private:
+ ObjectRule();
+
+ static TypeSet m_Types;
+};
+
+}
+
+#endif /* OBJECTRULE_H */
diff --git a/lib/config/vmops.hpp b/lib/config/vmops.hpp
new file mode 100644
index 0000000..ea30983
--- /dev/null
+++ b/lib/config/vmops.hpp
@@ -0,0 +1,274 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VMOPS_H
+#define VMOPS_H
+
+#include "config/i2-config.hpp"
+#include "config/expression.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "config/objectrule.hpp"
+#include "base/debuginfo.hpp"
+#include "base/array.hpp"
+#include "base/dictionary.hpp"
+#include "base/namespace.hpp"
+#include "base/function.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include <map>
+#include <vector>
+
+namespace icinga
+{
+
+class VMOps
+{
+public:
+ static inline bool FindVarImportRef(ScriptFrame& frame, const std::vector<Expression::Ptr>& imports, const String& name, Value *result, const DebugInfo& debugInfo = DebugInfo())
+ {
+ for (const auto& import : imports) {
+ ExpressionResult res = import->Evaluate(frame);
+ Object::Ptr obj = res.GetValue();
+ if (obj->HasOwnField(name)) {
+ *result = obj;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static inline bool FindVarImport(ScriptFrame& frame, const std::vector<Expression::Ptr>& imports, const String& name, Value *result, const DebugInfo& debugInfo = DebugInfo())
+ {
+ Value parent;
+
+ if (FindVarImportRef(frame, imports, name, &parent, debugInfo)) {
+ *result = GetField(parent, name, frame.Sandboxed, debugInfo);
+ return true;
+ }
+
+ return false;
+ }
+
+ static inline Value ConstructorCall(const Type::Ptr& type, const std::vector<Value>& args, const DebugInfo& debugInfo = DebugInfo())
+ {
+ if (type->GetName() == "String") {
+ if (args.empty())
+ return "";
+ else if (args.size() == 1)
+ return Convert::ToString(args[0]);
+ else
+ BOOST_THROW_EXCEPTION(ScriptError("Too many arguments for constructor."));
+ } else if (type->GetName() == "Number") {
+ if (args.empty())
+ return 0;
+ else if (args.size() == 1)
+ return Convert::ToDouble(args[0]);
+ else
+ BOOST_THROW_EXCEPTION(ScriptError("Too many arguments for constructor."));
+ } else if (type->GetName() == "Boolean") {
+ if (args.empty())
+ return 0;
+ else if (args.size() == 1)
+ return Convert::ToBool(args[0]);
+ else
+ BOOST_THROW_EXCEPTION(ScriptError("Too many arguments for constructor."));
+ } else if (args.size() == 1 && type->IsAssignableFrom(args[0].GetReflectionType()))
+ return args[0];
+ else
+ return type->Instantiate(args);
+ }
+
+ static inline Value FunctionCall(ScriptFrame& frame, const Value& self, const Function::Ptr& func, const std::vector<Value>& arguments)
+ {
+ if (!self.IsEmpty() || self.IsString())
+ return func->InvokeThis(self, arguments);
+ else
+ return func->Invoke(arguments);
+
+ }
+
+ static inline Value NewFunction(ScriptFrame& frame, const String& name, const std::vector<String>& argNames,
+ const std::map<String, std::unique_ptr<Expression> >& closedVars, const Expression::Ptr& expression)
+ {
+ auto evaluatedClosedVars = EvaluateClosedVars(frame, closedVars);
+
+ auto wrapper = [argNames, evaluatedClosedVars, expression](const std::vector<Value>& arguments) -> Value {
+ if (arguments.size() < argNames.size())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments for function"));
+
+ ScriptFrame *frame = ScriptFrame::GetCurrentFrame();
+
+ frame->Locals = new Dictionary();
+
+ if (evaluatedClosedVars)
+ evaluatedClosedVars->CopyTo(frame->Locals);
+
+ for (std::vector<Value>::size_type i = 0; i < std::min(arguments.size(), argNames.size()); i++)
+ frame->Locals->Set(argNames[i], arguments[i]);
+
+ return expression->Evaluate(*frame);
+ };
+
+ return new Function(name, wrapper, argNames);
+ }
+
+ static inline Value NewApply(ScriptFrame& frame, const String& type, const String& target, const String& name, const Expression::Ptr& filter,
+ const String& package, const String& fkvar, const String& fvvar, const Expression::Ptr& fterm, const std::map<String, std::unique_ptr<Expression> >& closedVars,
+ bool ignoreOnError, const Expression::Ptr& expression, const DebugInfo& debugInfo = DebugInfo())
+ {
+ ApplyRule::AddRule(type, target, name, expression, filter, package, fkvar,
+ fvvar, fterm, ignoreOnError, debugInfo, EvaluateClosedVars(frame, closedVars));
+
+ return Empty;
+ }
+
+ static inline Value NewObject(ScriptFrame& frame, bool abstract, const Type::Ptr& type, const String& name, const Expression::Ptr& filter,
+ const String& zone, const String& package, bool defaultTmpl, bool ignoreOnError, const std::map<String, std::unique_ptr<Expression> >& closedVars, const Expression::Ptr& expression, const DebugInfo& debugInfo = DebugInfo())
+ {
+ ConfigItemBuilder item{debugInfo};
+
+ String checkName = name;
+
+ if (!abstract) {
+ auto *nc = dynamic_cast<NameComposer *>(type.get());
+
+ if (nc)
+ checkName = nc->MakeName(name, nullptr);
+ }
+
+ if (!checkName.IsEmpty()) {
+ ConfigItem::Ptr oldItem = ConfigItem::GetByTypeAndName(type, checkName);
+
+ if (oldItem) {
+ std::ostringstream msgbuf;
+ msgbuf << "Object '" << name << "' of type '" << type->GetName() << "' re-defined: " << debugInfo << "; previous definition: " << oldItem->GetDebugInfo();
+ BOOST_THROW_EXCEPTION(ScriptError(msgbuf.str(), debugInfo));
+ }
+ }
+
+ if (filter && !ObjectRule::IsValidSourceType(type->GetName())) {
+ std::ostringstream msgbuf;
+ msgbuf << "Object '" << name << "' of type '" << type->GetName() << "' must not have 'assign where' and 'ignore where' rules: " << debugInfo;
+ BOOST_THROW_EXCEPTION(ScriptError(msgbuf.str(), debugInfo));
+ }
+
+ item.SetType(type);
+ item.SetName(name);
+
+ if (!abstract)
+ item.AddExpression(new ImportDefaultTemplatesExpression());
+
+ item.AddExpression(new OwnedExpression(expression));
+ item.SetAbstract(abstract);
+ item.SetScope(EvaluateClosedVars(frame, closedVars));
+ item.SetZone(zone);
+ item.SetPackage(package);
+ item.SetFilter(filter);
+ item.SetDefaultTemplate(defaultTmpl);
+ item.SetIgnoreOnError(ignoreOnError);
+ item.Compile()->Register();
+
+ return Empty;
+ }
+
+ static inline ExpressionResult For(ScriptFrame& frame, const String& fkvar, const String& fvvar, const Value& value, const std::unique_ptr<Expression>& expression, const DebugInfo& debugInfo = DebugInfo())
+ {
+ if (value.IsObjectType<Array>()) {
+ if (!fvvar.IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot use dictionary iterator for array.", debugInfo));
+
+ Array::Ptr arr = value;
+
+ for (Array::SizeType i = 0; i < arr->GetLength(); i++) {
+ frame.Locals->Set(fkvar, arr->Get(i));
+ ExpressionResult res = expression->Evaluate(frame);
+ CHECK_RESULT_LOOP(res);
+ }
+ } else if (value.IsObjectType<Dictionary>()) {
+ if (fvvar.IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot use array iterator for dictionary.", debugInfo));
+
+ Dictionary::Ptr dict = value;
+ std::vector<String> keys;
+
+ {
+ ObjectLock olock(dict);
+ for (const Dictionary::Pair& kv : dict) {
+ keys.push_back(kv.first);
+ }
+ }
+
+ for (const String& key : keys) {
+ frame.Locals->Set(fkvar, key);
+ frame.Locals->Set(fvvar, dict->Get(key));
+ ExpressionResult res = expression->Evaluate(frame);
+ CHECK_RESULT_LOOP(res);
+ }
+ } else if (value.IsObjectType<Namespace>()) {
+ if (fvvar.IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot use array iterator for namespace.", debugInfo));
+
+ Namespace::Ptr ns = value;
+ std::vector<String> keys;
+
+ {
+ ObjectLock olock(ns);
+ for (const Namespace::Pair& kv : ns) {
+ keys.push_back(kv.first);
+ }
+ }
+
+ for (const String& key : keys) {
+ frame.Locals->Set(fkvar, key);
+ frame.Locals->Set(fvvar, ns->Get(key));
+ ExpressionResult res = expression->Evaluate(frame);
+ CHECK_RESULT_LOOP(res);
+ }
+ } else
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid type in for expression: " + value.GetTypeName(), debugInfo));
+
+ return Empty;
+ }
+
+ static inline Value GetField(const Value& context, const String& field, bool sandboxed = false, const DebugInfo& debugInfo = DebugInfo())
+ {
+ if (BOOST_UNLIKELY(context.IsEmpty() && !context.IsString()))
+ return Empty;
+
+ if (BOOST_UNLIKELY(!context.IsObject()))
+ return GetPrototypeField(context, field, true, debugInfo);
+
+ Object::Ptr object = context;
+
+ return object->GetFieldByName(field, sandboxed, debugInfo);
+ }
+
+ static inline void SetField(const Object::Ptr& context, const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo = DebugInfo())
+ {
+ if (!context)
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot set field '" + field + "' on a value that is not an object.", debugInfo));
+
+ return context->SetFieldByName(field, value, overrideFrozen, debugInfo);
+ }
+
+private:
+ static inline Dictionary::Ptr EvaluateClosedVars(ScriptFrame& frame, const std::map<String, std::unique_ptr<Expression> >& closedVars)
+ {
+ if (closedVars.empty())
+ return nullptr;
+
+ DictionaryData locals;
+
+ for (const auto& cvar : closedVars)
+ locals.emplace_back(cvar.first, cvar.second->Evaluate(frame));
+
+ return new Dictionary(std::move(locals));
+ }
+};
+
+}
+
+#endif /* VMOPS_H */
diff --git a/lib/db_ido/CMakeLists.txt b/lib/db_ido/CMakeLists.txt
new file mode 100644
index 0000000..7a97d27
--- /dev/null
+++ b/lib/db_ido/CMakeLists.txt
@@ -0,0 +1,40 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(dbconnection.ti dbconnection-ti.cpp dbconnection-ti.hpp)
+
+mkembedconfig_target(db_ido-itl.conf db_ido-itl.cpp)
+
+set(db_ido_SOURCES
+ i2-db_ido.hpp db_ido-itl.cpp
+ commanddbobject.cpp commanddbobject.hpp
+ dbconnection.cpp dbconnection.hpp dbconnection-ti.hpp
+ dbevents.cpp dbevents.hpp
+ dbobject.cpp dbobject.hpp
+ dbquery.cpp dbquery.hpp
+ dbreference.cpp dbreference.hpp
+ dbtype.cpp dbtype.hpp
+ dbvalue.cpp dbvalue.hpp
+ endpointdbobject.cpp endpointdbobject.hpp
+ hostdbobject.cpp hostdbobject.hpp
+ hostgroupdbobject.cpp hostgroupdbobject.hpp
+ idochecktask.cpp idochecktask.hpp
+ servicedbobject.cpp servicedbobject.hpp
+ servicegroupdbobject.cpp servicegroupdbobject.hpp
+ timeperioddbobject.cpp timeperioddbobject.hpp
+ userdbobject.cpp userdbobject.hpp
+ usergroupdbobject.cpp usergroupdbobject.hpp
+ zonedbobject.cpp zonedbobject.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(db_ido db_ido db_ido_SOURCES)
+endif()
+
+add_library(db_ido OBJECT ${db_ido_SOURCES})
+
+add_dependencies(db_ido base config icinga remote)
+
+set_target_properties (
+ db_ido PROPERTIES
+ FOLDER Lib
+)
diff --git a/lib/db_ido/commanddbobject.cpp b/lib/db_ido/commanddbobject.cpp
new file mode 100644
index 0000000..2ac167a
--- /dev/null
+++ b/lib/db_ido/commanddbobject.cpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/commanddbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "icinga/command.hpp"
+#include "icinga/compatutility.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+REGISTER_DBTYPE(Command, "command", DbObjectTypeCommand, "object_id", CommandDbObject);
+
+CommandDbObject::CommandDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr CommandDbObject::GetConfigFields() const
+{
+ Command::Ptr command = static_pointer_cast<Command>(GetObject());
+
+ return new Dictionary({
+ { "command_line", CompatUtility::GetCommandLine(command) }
+ });
+}
+
+Dictionary::Ptr CommandDbObject::GetStatusFields() const
+{
+ return nullptr;
+}
diff --git a/lib/db_ido/commanddbobject.hpp b/lib/db_ido/commanddbobject.hpp
new file mode 100644
index 0000000..6d22747
--- /dev/null
+++ b/lib/db_ido/commanddbobject.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMMANDDBOBJECT_H
+#define COMMANDDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+/**
+ * A Command database object.
+ *
+ * @ingroup ido
+ */
+class CommandDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CommandDbObject);
+
+ CommandDbObject(const DbType::Ptr& type, const String& name1, const String& name2);
+
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+};
+
+}
+
+#endif /* COMMANDDBOBJECT_H */
diff --git a/lib/db_ido/db_ido-itl.conf b/lib/db_ido/db_ido-itl.conf
new file mode 100644
index 0000000..e2c42c3
--- /dev/null
+++ b/lib/db_ido/db_ido-itl.conf
@@ -0,0 +1,19 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+System.assert(Internal.run_with_activation_context(function() {
+ template CheckCommand "ido-check-command" use (checkFunc = Internal.IdoCheck) {
+ execute = checkFunc
+ }
+
+ object CheckCommand "ido" {
+ import "ido-check-command"
+ }
+}))
+
+var methods = [
+ "IdoCheck"
+]
+
+for (method in methods) {
+ Internal.remove(method)
+}
diff --git a/lib/db_ido/dbconnection.cpp b/lib/db_ido/dbconnection.cpp
new file mode 100644
index 0000000..a8534c4
--- /dev/null
+++ b/lib/db_ido/dbconnection.cpp
@@ -0,0 +1,583 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbconnection.hpp"
+#include "db_ido/dbconnection-ti.cpp"
+#include "db_ido/dbvalue.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "base/configtype.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(DbConnection);
+
+Timer::Ptr DbConnection::m_ProgramStatusTimer;
+boost::once_flag DbConnection::m_OnceFlag = BOOST_ONCE_INIT;
+
+void DbConnection::OnConfigLoaded()
+{
+ ConfigObject::OnConfigLoaded();
+
+ Value categories = GetCategories();
+
+ SetCategoryFilter(FilterArrayToInt(categories, DbQuery::GetCategoryFilterMap(), DbCatEverything));
+
+ if (!GetEnableHa()) {
+ Log(LogDebug, "DbConnection")
+ << "HA functionality disabled. Won't pause IDO connection: " << GetName();
+
+ SetHAMode(HARunEverywhere);
+ }
+
+ boost::call_once(m_OnceFlag, InitializeDbTimer);
+}
+
+void DbConnection::Start(bool runtimeCreated)
+{
+ ObjectImpl<DbConnection>::Start(runtimeCreated);
+
+ Log(LogInformation, "DbConnection")
+ << "'" << GetName() << "' started.";
+
+ auto onQuery = [this](const DbQuery& query) { ExecuteQuery(query); };
+ DbObject::OnQuery.connect(onQuery);
+
+ auto onMultipleQueries = [this](const std::vector<DbQuery>& multiQueries) { ExecuteMultipleQueries(multiQueries); };
+ DbObject::OnMultipleQueries.connect(onMultipleQueries);
+
+ DbObject::QueryCallbacks queryCallbacks;
+ queryCallbacks.Query = onQuery;
+ queryCallbacks.MultipleQueries = onMultipleQueries;
+
+ DbObject::OnMakeQueries.connect([queryCallbacks](const std::function<void (const DbObject::QueryCallbacks&)>& queryFunc) {
+ queryFunc(queryCallbacks);
+ });
+}
+
+void DbConnection::Stop(bool runtimeRemoved)
+{
+ Log(LogInformation, "DbConnection")
+ << "'" << GetName() << "' stopped.";
+
+ ObjectImpl<DbConnection>::Stop(runtimeRemoved);
+}
+
+void DbConnection::EnableActiveChangedHandler()
+{
+ if (!m_ActiveChangedHandler) {
+ ConfigObject::OnActiveChanged.connect([this](const ConfigObject::Ptr& object, const Value&) { UpdateObject(object); });
+ m_ActiveChangedHandler = true;
+ }
+}
+
+void DbConnection::Resume()
+{
+ ConfigObject::Resume();
+
+ Log(LogInformation, "DbConnection")
+ << "Resuming IDO connection: " << GetName();
+
+ m_CleanUpTimer = Timer::Create();
+ m_CleanUpTimer->SetInterval(60);
+ m_CleanUpTimer->OnTimerExpired.connect([this](const Timer * const&) { CleanUpHandler(); });
+ m_CleanUpTimer->Start();
+
+ m_LogStatsTimeout = 0;
+
+ m_LogStatsTimer = Timer::Create();
+ m_LogStatsTimer->SetInterval(10);
+ m_LogStatsTimer->OnTimerExpired.connect([this](const Timer * const&) { LogStatsHandler(); });
+ m_LogStatsTimer->Start();
+}
+
+void DbConnection::Pause()
+{
+ Log(LogInformation, "DbConnection")
+ << "Pausing IDO connection: " << GetName();
+
+ m_LogStatsTimer->Stop(true);
+ m_CleanUpTimer->Stop(true);
+
+ DbQuery query1;
+ query1.Table = "programstatus";
+ query1.IdColumn = "programstatus_id";
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatProgramStatus;
+ query1.WhereCriteria = new Dictionary({
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+
+ query1.Fields = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "program_end_time", DbValue::FromTimestamp(Utility::GetTime()) },
+ { "is_currently_running", 0 },
+ { "process_id", Empty }
+ });
+
+ query1.Priority = PriorityHigh;
+
+ ExecuteQuery(query1);
+
+ NewTransaction();
+
+ m_QueryQueue.Enqueue([this]() { Disconnect(); }, PriorityLow);
+
+ /* Work on remaining tasks but never delete the threads, for HA resuming later. */
+ m_QueryQueue.Join();
+
+ ConfigObject::Pause();
+}
+
+void DbConnection::InitializeDbTimer()
+{
+ m_ProgramStatusTimer = Timer::Create();
+ m_ProgramStatusTimer->SetInterval(10);
+ m_ProgramStatusTimer->OnTimerExpired.connect([](const Timer * const&) { UpdateProgramStatus(); });
+ m_ProgramStatusTimer->Start();
+}
+
+void DbConnection::InsertRuntimeVariable(const String& key, const Value& value)
+{
+ DbQuery query;
+ query.Table = "runtimevariables";
+ query.Type = DbQueryInsert;
+ query.Category = DbCatProgramStatus;
+ query.Fields = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "varname", key },
+ { "varvalue", value }
+ });
+ DbObject::OnQuery(query);
+}
+
+void DbConnection::UpdateProgramStatus()
+{
+ IcingaApplication::Ptr icingaApplication = IcingaApplication::GetInstance();
+
+ if (!icingaApplication)
+ return;
+
+ Log(LogNotice, "DbConnection")
+ << "Updating programstatus table.";
+
+ std::vector<DbQuery> queries;
+
+ DbQuery query1;
+ query1.Type = DbQueryNewTransaction;
+ query1.Priority = PriorityImmediate;
+ queries.emplace_back(std::move(query1));
+
+ DbQuery query2;
+ query2.Table = "programstatus";
+ query2.IdColumn = "programstatus_id";
+ query2.Type = DbQueryInsert | DbQueryDelete;
+ query2.Category = DbCatProgramStatus;
+
+ query2.Fields = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "program_version", Application::GetAppVersion() },
+ { "status_update_time", DbValue::FromTimestamp(Utility::GetTime()) },
+ { "program_start_time", DbValue::FromTimestamp(Application::GetStartTime()) },
+ { "is_currently_running", 1 },
+ { "endpoint_name", icingaApplication->GetNodeName() },
+ { "process_id", Utility::GetPid() },
+ { "daemon_mode", 1 },
+ { "last_command_check", DbValue::FromTimestamp(Utility::GetTime()) },
+ { "notifications_enabled", (icingaApplication->GetEnableNotifications() ? 1 : 0) },
+ { "active_host_checks_enabled", (icingaApplication->GetEnableHostChecks() ? 1 : 0) },
+ { "passive_host_checks_enabled", 1 },
+ { "active_service_checks_enabled", (icingaApplication->GetEnableServiceChecks() ? 1 : 0) },
+ { "passive_service_checks_enabled", 1 },
+ { "event_handlers_enabled", (icingaApplication->GetEnableEventHandlers() ? 1 : 0) },
+ { "flap_detection_enabled", (icingaApplication->GetEnableFlapping() ? 1 : 0) },
+ { "process_performance_data", (icingaApplication->GetEnablePerfdata() ? 1 : 0) }
+ });
+
+ query2.WhereCriteria = new Dictionary({
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+
+ queries.emplace_back(std::move(query2));
+
+ DbQuery query3;
+ query3.Type = DbQueryNewTransaction;
+ queries.emplace_back(std::move(query3));
+
+ DbObject::OnMultipleQueries(queries);
+
+ DbQuery query4;
+ query4.Table = "runtimevariables";
+ query4.Type = DbQueryDelete;
+ query4.Category = DbCatProgramStatus;
+ query4.WhereCriteria = new Dictionary({
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+ DbObject::OnQuery(query4);
+
+ InsertRuntimeVariable("total_services", ConfigType::Get<Service>()->GetObjectCount());
+ InsertRuntimeVariable("total_scheduled_services", ConfigType::Get<Service>()->GetObjectCount());
+ InsertRuntimeVariable("total_hosts", ConfigType::Get<Host>()->GetObjectCount());
+ InsertRuntimeVariable("total_scheduled_hosts", ConfigType::Get<Host>()->GetObjectCount());
+}
+
+void DbConnection::CleanUpHandler()
+{
+ auto now = static_cast<long>(Utility::GetTime());
+
+ struct {
+ String name;
+ String time_column;
+ } tables[] = {
+ { "acknowledgements", "entry_time" },
+ { "commenthistory", "entry_time" },
+ { "contactnotifications", "start_time" },
+ { "contactnotificationmethods", "start_time" },
+ { "downtimehistory", "entry_time" },
+ { "eventhandlers", "start_time" },
+ { "externalcommands", "entry_time" },
+ { "flappinghistory", "event_time" },
+ { "hostchecks", "start_time" },
+ { "logentries", "logentry_time" },
+ { "notifications", "start_time" },
+ { "processevents", "event_time" },
+ { "statehistory", "state_time" },
+ { "servicechecks", "start_time" },
+ { "systemcommands", "start_time" }
+ };
+
+ for (auto& table : tables) {
+ double max_age = GetCleanup()->Get(table.name + "_age");
+
+ if (max_age == 0)
+ continue;
+
+ CleanUpExecuteQuery(table.name, table.time_column, now - max_age);
+ Log(LogNotice, "DbConnection")
+ << "Cleanup (" << table.name << "): " << max_age
+ << " now: " << now
+ << " old: " << now - max_age;
+ }
+
+}
+
+void DbConnection::LogStatsHandler()
+{
+ if (!GetConnected() || IsPaused())
+ return;
+
+ auto pending = m_PendingQueries.load();
+
+ auto now = Utility::GetTime();
+ bool timeoutReached = m_LogStatsTimeout < now;
+
+ if (pending == 0u && !timeoutReached) {
+ return;
+ }
+
+ auto output = round(m_OutputQueries.CalculateRate(now, 10));
+
+ if (pending < output * 5 && !timeoutReached) {
+ return;
+ }
+
+ auto input = round(m_InputQueries.CalculateRate(now, 10));
+
+ Log(LogInformation, GetReflectionType()->GetName())
+ << "Pending queries: " << pending << " (Input: " << input
+ << "/s; Output: " << output << "/s)";
+
+ /* Reschedule next log entry in 5 minutes. */
+ if (timeoutReached) {
+ m_LogStatsTimeout = now + 60 * 5;
+ }
+}
+
+void DbConnection::CleanUpExecuteQuery(const String&, const String&, double)
+{
+ /* Default handler does nothing. */
+}
+
+void DbConnection::SetConfigHash(const DbObject::Ptr& dbobj, const String& hash)
+{
+ SetConfigHash(dbobj->GetType(), GetObjectID(dbobj), hash);
+}
+
+void DbConnection::SetConfigHash(const DbType::Ptr& type, const DbReference& objid, const String& hash)
+{
+ if (!objid.IsValid())
+ return;
+
+ if (!hash.IsEmpty())
+ m_ConfigHashes[std::make_pair(type, objid)] = hash;
+ else
+ m_ConfigHashes.erase(std::make_pair(type, objid));
+}
+
+String DbConnection::GetConfigHash(const DbObject::Ptr& dbobj) const
+{
+ return GetConfigHash(dbobj->GetType(), GetObjectID(dbobj));
+}
+
+String DbConnection::GetConfigHash(const DbType::Ptr& type, const DbReference& objid) const
+{
+ if (!objid.IsValid())
+ return String();
+
+ auto it = m_ConfigHashes.find(std::make_pair(type, objid));
+
+ if (it == m_ConfigHashes.end())
+ return String();
+
+ return it->second;
+}
+
+void DbConnection::SetObjectID(const DbObject::Ptr& dbobj, const DbReference& dbref)
+{
+ if (dbref.IsValid())
+ m_ObjectIDs[dbobj] = dbref;
+ else
+ m_ObjectIDs.erase(dbobj);
+}
+
+DbReference DbConnection::GetObjectID(const DbObject::Ptr& dbobj) const
+{
+ auto it = m_ObjectIDs.find(dbobj);
+
+ if (it == m_ObjectIDs.end())
+ return {};
+
+ return it->second;
+}
+
+void DbConnection::SetInsertID(const DbObject::Ptr& dbobj, const DbReference& dbref)
+{
+ SetInsertID(dbobj->GetType(), GetObjectID(dbobj), dbref);
+}
+
+void DbConnection::SetInsertID(const DbType::Ptr& type, const DbReference& objid, const DbReference& dbref)
+{
+ if (!objid.IsValid())
+ return;
+
+ if (dbref.IsValid())
+ m_InsertIDs[std::make_pair(type, objid)] = dbref;
+ else
+ m_InsertIDs.erase(std::make_pair(type, objid));
+}
+
+DbReference DbConnection::GetInsertID(const DbObject::Ptr& dbobj) const
+{
+ return GetInsertID(dbobj->GetType(), GetObjectID(dbobj));
+}
+
+DbReference DbConnection::GetInsertID(const DbType::Ptr& type, const DbReference& objid) const
+{
+ if (!objid.IsValid())
+ return {};
+
+ auto it = m_InsertIDs.find(std::make_pair(type, objid));
+
+ if (it == m_InsertIDs.end())
+ return DbReference();
+
+ return it->second;
+}
+
+void DbConnection::SetObjectActive(const DbObject::Ptr& dbobj, bool active)
+{
+ if (active)
+ m_ActiveObjects.insert(dbobj);
+ else
+ m_ActiveObjects.erase(dbobj);
+}
+
+bool DbConnection::GetObjectActive(const DbObject::Ptr& dbobj) const
+{
+ return (m_ActiveObjects.find(dbobj) != m_ActiveObjects.end());
+}
+
+void DbConnection::ClearIDCache()
+{
+ SetIDCacheValid(false);
+
+ m_ObjectIDs.clear();
+ m_InsertIDs.clear();
+ m_ActiveObjects.clear();
+ m_ConfigUpdates.clear();
+ m_StatusUpdates.clear();
+ m_ConfigHashes.clear();
+}
+
+void DbConnection::SetConfigUpdate(const DbObject::Ptr& dbobj, bool hasupdate)
+{
+ if (hasupdate)
+ m_ConfigUpdates.insert(dbobj);
+ else
+ m_ConfigUpdates.erase(dbobj);
+}
+
+bool DbConnection::GetConfigUpdate(const DbObject::Ptr& dbobj) const
+{
+ return (m_ConfigUpdates.find(dbobj) != m_ConfigUpdates.end());
+}
+
+void DbConnection::SetStatusUpdate(const DbObject::Ptr& dbobj, bool hasupdate)
+{
+ if (hasupdate)
+ m_StatusUpdates.insert(dbobj);
+ else
+ m_StatusUpdates.erase(dbobj);
+}
+
+bool DbConnection::GetStatusUpdate(const DbObject::Ptr& dbobj) const
+{
+ return (m_StatusUpdates.find(dbobj) != m_StatusUpdates.end());
+}
+
+void DbConnection::UpdateObject(const ConfigObject::Ptr& object)
+{
+ bool isShuttingDown = Application::IsShuttingDown();
+ bool isRestarting = Application::IsRestarting();
+
+#ifdef I2_DEBUG
+ if (isShuttingDown || isRestarting) {
+ //Log(LogDebug, "DbConnection")
+ // << "Updating object '" << object->GetName() << "' \t\t active '" << Convert::ToLong(object->IsActive())
+ // << "' shutting down '" << Convert::ToLong(isShuttingDown) << "' restarting '" << Convert::ToLong(isRestarting) << "'.";
+ }
+#endif /* I2_DEBUG */
+
+ /* Wait until a database connection is established on reconnect. */
+ if (!GetConnected())
+ return;
+
+ /* Don't update inactive objects during shutdown/reload/restart.
+ * They would be marked as deleted. This gets triggered with ConfigObject::StopObjects().
+ * During startup/reconnect this is fine, the handler is not active there.
+ */
+ if (isShuttingDown || isRestarting)
+ return;
+
+ DbObject::Ptr dbobj = DbObject::GetOrCreateByObject(object);
+
+ if (dbobj) {
+ bool dbActive = GetObjectActive(dbobj);
+ bool active = object->IsActive();
+
+ if (active) {
+ if (!dbActive)
+ ActivateObject(dbobj);
+
+ Dictionary::Ptr configFields = dbobj->GetConfigFields();
+ String configHash = dbobj->CalculateConfigHash(configFields);
+ ASSERT(configHash.GetLength() <= 64);
+ configFields->Set("config_hash", configHash);
+
+ String cachedHash = GetConfigHash(dbobj);
+
+ if (cachedHash != configHash) {
+ dbobj->SendConfigUpdateHeavy(configFields);
+ dbobj->SendStatusUpdate();
+ } else {
+ dbobj->SendConfigUpdateLight();
+ }
+ } else if (!active) {
+ /* This may happen on reload/restart actions too
+ * and is blocked above already.
+ *
+ * Deactivate the deleted object no matter
+ * which state it had in the database.
+ */
+ DeactivateObject(dbobj);
+ }
+ }
+}
+
+void DbConnection::UpdateAllObjects()
+{
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+ m_QueryQueue.Enqueue([this, object](){ UpdateObject(object); }, PriorityHigh);
+ }
+ }
+}
+
+void DbConnection::PrepareDatabase()
+{
+ for (const DbType::Ptr& type : DbType::GetAllTypes()) {
+ FillIDCache(type);
+ }
+}
+
+void DbConnection::ValidateFailoverTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<DbConnection>::ValidateFailoverTimeout(lvalue, utils);
+
+ if (lvalue() < 30)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "failover_timeout" }, "Failover timeout minimum is 30s."));
+}
+
+void DbConnection::ValidateCategories(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<DbConnection>::ValidateCategories(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), DbQuery::GetCategoryFilterMap(), 0);
+
+ if (filter != DbCatEverything && (filter & ~(DbCatInvalid | DbCatEverything | DbCatConfig | DbCatState |
+ DbCatAcknowledgement | DbCatComment | DbCatDowntime | DbCatEventHandler | DbCatExternalCommand |
+ DbCatFlapping | DbCatLog | DbCatNotification | DbCatProgramStatus | DbCatRetention |
+ DbCatStateHistory)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "categories" }, "categories filter is invalid."));
+}
+
+void DbConnection::IncreaseQueryCount()
+{
+ double now = Utility::GetTime();
+
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ m_QueryStats.InsertValue(now, 1);
+}
+
+int DbConnection::GetQueryCount(RingBuffer::SizeType span)
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ return m_QueryStats.UpdateAndGetValues(Utility::GetTime(), span);
+}
+
+bool DbConnection::IsIDCacheValid() const
+{
+ return m_IDCacheValid;
+}
+
+void DbConnection::SetIDCacheValid(bool valid)
+{
+ m_IDCacheValid = valid;
+}
+
+int DbConnection::GetSessionToken()
+{
+ return Application::GetStartTime();
+}
+
+void DbConnection::IncreasePendingQueries(int count)
+{
+ m_PendingQueries.fetch_add(count);
+ m_InputQueries.InsertValue(Utility::GetTime(), count);
+}
+
+void DbConnection::DecreasePendingQueries(int count)
+{
+ m_PendingQueries.fetch_sub(count);
+ m_OutputQueries.InsertValue(Utility::GetTime(), count);
+}
diff --git a/lib/db_ido/dbconnection.hpp b/lib/db_ido/dbconnection.hpp
new file mode 100644
index 0000000..517a8a4
--- /dev/null
+++ b/lib/db_ido/dbconnection.hpp
@@ -0,0 +1,138 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DBCONNECTION_H
+#define DBCONNECTION_H
+
+#include "db_ido/i2-db_ido.hpp"
+#include "db_ido/dbconnection-ti.hpp"
+#include "db_ido/dbobject.hpp"
+#include "db_ido/dbquery.hpp"
+#include "base/timer.hpp"
+#include "base/ringbuffer.hpp"
+#include <boost/thread/once.hpp>
+#include <mutex>
+
+namespace icinga
+{
+
+/**
+ * A database connection.
+ *
+ * @ingroup db_ido
+ */
+class DbConnection : public ObjectImpl<DbConnection>
+{
+public:
+ DECLARE_OBJECT(DbConnection);
+
+ static void InitializeDbTimer();
+
+ virtual const char * GetLatestSchemaVersion() const noexcept = 0;
+ virtual const char * GetCompatSchemaVersion() const noexcept = 0;
+
+ void SetConfigHash(const DbObject::Ptr& dbobj, const String& hash);
+ void SetConfigHash(const DbType::Ptr& type, const DbReference& objid, const String& hash);
+ String GetConfigHash(const DbObject::Ptr& dbobj) const;
+ String GetConfigHash(const DbType::Ptr& type, const DbReference& objid) const;
+
+ void SetObjectID(const DbObject::Ptr& dbobj, const DbReference& dbref);
+ DbReference GetObjectID(const DbObject::Ptr& dbobj) const;
+
+ void SetInsertID(const DbObject::Ptr& dbobj, const DbReference& dbref);
+ void SetInsertID(const DbType::Ptr& type, const DbReference& objid, const DbReference& dbref);
+ DbReference GetInsertID(const DbObject::Ptr& dbobj) const;
+ DbReference GetInsertID(const DbType::Ptr& type, const DbReference& objid) const;
+
+ void SetObjectActive(const DbObject::Ptr& dbobj, bool active);
+ bool GetObjectActive(const DbObject::Ptr& dbobj) const;
+
+ void ClearIDCache();
+
+ void SetConfigUpdate(const DbObject::Ptr& dbobj, bool hasupdate);
+ bool GetConfigUpdate(const DbObject::Ptr& dbobj) const;
+
+ void SetStatusUpdate(const DbObject::Ptr& dbobj, bool hasupdate);
+ bool GetStatusUpdate(const DbObject::Ptr& dbobj) const;
+
+ int GetQueryCount(RingBuffer::SizeType span);
+ virtual int GetPendingQueryCount() const = 0;
+
+ void ValidateFailoverTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) final;
+ void ValidateCategories(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) final;
+
+protected:
+ void OnConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+ void Resume() override;
+ void Pause() override;
+
+ virtual void ExecuteQuery(const DbQuery& query) = 0;
+ virtual void ExecuteMultipleQueries(const std::vector<DbQuery>&) = 0;
+ virtual void ActivateObject(const DbObject::Ptr& dbobj) = 0;
+ virtual void DeactivateObject(const DbObject::Ptr& dbobj) = 0;
+
+ virtual void CleanUpExecuteQuery(const String& table, const String& time_column, double max_age);
+ virtual void FillIDCache(const DbType::Ptr& type) = 0;
+ virtual void NewTransaction() = 0;
+ virtual void Disconnect() = 0;
+
+ void UpdateObject(const ConfigObject::Ptr& object);
+ void UpdateAllObjects();
+
+ void PrepareDatabase();
+
+ void IncreaseQueryCount();
+
+ bool IsIDCacheValid() const;
+ void SetIDCacheValid(bool valid);
+
+ void EnableActiveChangedHandler();
+
+ static void UpdateProgramStatus();
+
+ static int GetSessionToken();
+
+ void IncreasePendingQueries(int count);
+ void DecreasePendingQueries(int count);
+
+ WorkQueue m_QueryQueue{10000000, 1, LogNotice};
+
+private:
+ bool m_IDCacheValid{false};
+ std::map<std::pair<DbType::Ptr, DbReference>, String> m_ConfigHashes;
+ std::map<DbObject::Ptr, DbReference> m_ObjectIDs;
+ std::map<std::pair<DbType::Ptr, DbReference>, DbReference> m_InsertIDs;
+ std::set<DbObject::Ptr> m_ActiveObjects;
+ std::set<DbObject::Ptr> m_ConfigUpdates;
+ std::set<DbObject::Ptr> m_StatusUpdates;
+ Timer::Ptr m_CleanUpTimer;
+ Timer::Ptr m_LogStatsTimer;
+
+ double m_LogStatsTimeout;
+
+ void CleanUpHandler();
+ void LogStatsHandler();
+
+ static Timer::Ptr m_ProgramStatusTimer;
+ static boost::once_flag m_OnceFlag;
+
+ static void InsertRuntimeVariable(const String& key, const Value& value);
+
+ mutable std::mutex m_StatsMutex;
+ RingBuffer m_QueryStats{15 * 60};
+ bool m_ActiveChangedHandler{false};
+
+ RingBuffer m_InputQueries{10};
+ RingBuffer m_OutputQueries{10};
+ Atomic<uint_fast64_t> m_PendingQueries{0};
+};
+
+struct database_error : virtual std::exception, virtual boost::exception { };
+
+struct errinfo_database_query_;
+typedef boost::error_info<struct errinfo_database_query_, std::string> errinfo_database_query;
+
+}
+
+#endif /* DBCONNECTION_H */
diff --git a/lib/db_ido/dbconnection.ti b/lib/db_ido/dbconnection.ti
new file mode 100644
index 0000000..ad02b40
--- /dev/null
+++ b/lib/db_ido/dbconnection.ti
@@ -0,0 +1,82 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbquery.hpp"
+#include "base/configobject.hpp"
+
+library db_ido;
+
+namespace icinga
+{
+
+abstract class DbConnection : ConfigObject
+{
+ [config] String table_prefix {
+ default {{{ return "icinga_"; }}}
+ };
+
+ [config, required] Dictionary::Ptr cleanup {
+ default {{{ return new Dictionary(); }}}
+ };
+
+ [config] Array::Ptr categories {
+ default {{{
+ return new Array({
+ "DbCatConfig",
+ "DbCatState",
+ "DbCatAcknowledgement",
+ "DbCatComment",
+ "DbCatDowntime",
+ "DbCatEventHandler",
+ "DbCatFlapping",
+ "DbCatNotification",
+ "DbCatProgramStatus",
+ "DbCatRetention",
+ "DbCatStateHistory"
+ });
+ }}}
+ };
+ [no_user_view, no_user_modify] int categories_filter_real (CategoryFilter);
+
+ [config] bool enable_ha {
+ default {{{ return true; }}}
+ };
+
+ [config] double failover_timeout {
+ default {{{ return 30; }}}
+ };
+
+ [state, no_user_modify] double last_failover;
+
+ [no_user_modify] String schema_version;
+ [no_user_modify] bool connected;
+ [no_user_modify] bool should_connect {
+ default {{{ return true; }}}
+ };
+};
+
+
+validator DbConnection {
+ Dictionary cleanup {
+ Number acknowledgements_age;
+ Number commenthistory_age;
+ Number contactnotifications_age;
+ Number contactnotificationmethods_age;
+ Number downtimehistory_age;
+ Number eventhandlers_age;
+ Number externalcommands_age;
+ Number flappinghistory_age;
+ Number hostchecks_age;
+ Number logentries_age;
+ Number notifications_age;
+ Number processevents_age;
+ Number statehistory_age;
+ Number servicechecks_age;
+ Number systemcommands_age;
+ };
+
+ Array categories {
+ String "*";
+ };
+};
+
+}
diff --git a/lib/db_ido/dbevents.cpp b/lib/db_ido/dbevents.cpp
new file mode 100644
index 0000000..8358824
--- /dev/null
+++ b/lib/db_ido/dbevents.cpp
@@ -0,0 +1,1884 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbevents.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "remote/endpoint.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/externalcommandprocessor.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/icingaapplication.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <utility>
+
+using namespace icinga;
+
+INITIALIZE_ONCE(&DbEvents::StaticInitialize);
+
+void DbEvents::StaticInitialize()
+{
+ /* Status */
+ Comment::OnCommentAdded.connect([](const Comment::Ptr& comment) { DbEvents::AddComment(comment); });
+ Comment::OnCommentRemoved.connect([](const Comment::Ptr& comment) { DbEvents::RemoveComment(comment); });
+ Downtime::OnDowntimeAdded.connect([](const Downtime::Ptr& downtime) { DbEvents::AddDowntime(downtime); });
+ Downtime::OnDowntimeRemoved.connect([](const Downtime::Ptr& downtime) { DbEvents::RemoveDowntime(downtime); });
+ Downtime::OnDowntimeTriggered.connect([](const Downtime::Ptr& downtime) { DbEvents::TriggerDowntime(downtime); });
+ Checkable::OnAcknowledgementSet.connect([](const Checkable::Ptr& checkable, const String&, const String&,
+ AcknowledgementType type, bool, bool, double, double, const MessageOrigin::Ptr&) {
+ DbEvents::AddAcknowledgement(checkable, type);
+ });
+ Checkable::OnAcknowledgementCleared.connect([](const Checkable::Ptr& checkable, const String&, double, const MessageOrigin::Ptr&) {
+ DbEvents::RemoveAcknowledgement(checkable);
+ });
+
+ Checkable::OnNextCheckUpdated.connect([](const Checkable::Ptr& checkable) { NextCheckUpdatedHandler(checkable); });
+ Checkable::OnFlappingChanged.connect([](const Checkable::Ptr& checkable, const Value&) { FlappingChangedHandler(checkable); });
+ Checkable::OnNotificationSentToAllUsers.connect([](const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const std::set<User::Ptr>&, const NotificationType&, const CheckResult::Ptr&, const String&, const String&,
+ const MessageOrigin::Ptr&) {
+ DbEvents::LastNotificationChangedHandler(notification, checkable);
+ });
+
+ Checkable::OnEnableActiveChecksChanged.connect([](const Checkable::Ptr& checkable, const Value&) {
+ DbEvents::EnableActiveChecksChangedHandler(checkable);
+ });
+ Checkable::OnEnablePassiveChecksChanged.connect([](const Checkable::Ptr& checkable, const Value&) {
+ DbEvents::EnablePassiveChecksChangedHandler(checkable);
+ });
+ Checkable::OnEnableNotificationsChanged.connect([](const Checkable::Ptr& checkable, const Value&) {
+ DbEvents::EnableNotificationsChangedHandler(checkable);
+ });
+ Checkable::OnEnablePerfdataChanged.connect([](const Checkable::Ptr& checkable, const Value&) {
+ DbEvents::EnablePerfdataChangedHandler(checkable);
+ });
+ Checkable::OnEnableFlappingChanged.connect([](const Checkable::Ptr& checkable, const Value&) {
+ DbEvents::EnableFlappingChangedHandler(checkable);
+ });
+
+ Checkable::OnReachabilityChanged.connect([](const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ std::set<Checkable::Ptr> children, const MessageOrigin::Ptr&) {
+ DbEvents::ReachabilityChangedHandler(checkable, cr, std::move(children));
+ });
+
+ /* History */
+ Comment::OnCommentAdded.connect([](const Comment::Ptr& comment) { AddCommentHistory(comment); });
+ Downtime::OnDowntimeAdded.connect([](const Downtime::Ptr& downtime) { AddDowntimeHistory(downtime); });
+ Checkable::OnAcknowledgementSet.connect([](const Checkable::Ptr& checkable, const String& author, const String& comment,
+ AcknowledgementType type, bool notify, bool, double expiry, double, const MessageOrigin::Ptr&) {
+ DbEvents::AddAcknowledgementHistory(checkable, author, comment, type, notify, expiry);
+ });
+
+ Checkable::OnNotificationSentToAllUsers.connect([](const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const std::set<User::Ptr>& users, const NotificationType& type, const CheckResult::Ptr& cr, const String& author,
+ const String& text, const MessageOrigin::Ptr&) {
+ DbEvents::AddNotificationHistory(notification, checkable, users, type, cr, author, text);
+ });
+
+ Checkable::OnStateChange.connect([](const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr&) {
+ DbEvents::AddStateChangeHistory(checkable, cr, type);
+ });
+
+ Checkable::OnNewCheckResult.connect([](const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ DbEvents::AddCheckResultLogHistory(checkable, cr);
+ });
+ Checkable::OnNotificationSentToUser.connect([](const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const User::Ptr& users, const NotificationType& type, const CheckResult::Ptr& cr, const String& author, const String& text,
+ const String&, const MessageOrigin::Ptr&) {
+ DbEvents::AddNotificationSentLogHistory(notification, checkable, users, type, cr, author, text);
+ });
+ Checkable::OnFlappingChanged.connect([](const Checkable::Ptr& checkable, const Value&) {
+ DbEvents::AddFlappingChangedLogHistory(checkable);
+ });
+ Checkable::OnEnableFlappingChanged.connect([](const Checkable::Ptr& checkable, const Value&) {
+ DbEvents::AddEnableFlappingChangedLogHistory(checkable);
+ });
+ Downtime::OnDowntimeTriggered.connect([](const Downtime::Ptr& downtime) { DbEvents::AddTriggerDowntimeLogHistory(downtime); });
+ Downtime::OnDowntimeRemoved.connect([](const Downtime::Ptr& downtime) { DbEvents::AddRemoveDowntimeLogHistory(downtime); });
+
+ Checkable::OnFlappingChanged.connect([](const Checkable::Ptr& checkable, const Value&) { DbEvents::AddFlappingChangedHistory(checkable); });
+ Checkable::OnEnableFlappingChanged.connect([](const Checkable::Ptr& checkable, const Value&) { DbEvents::AddEnableFlappingChangedHistory(checkable); });
+ Checkable::OnNewCheckResult.connect([](const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ DbEvents::AddCheckableCheckHistory(checkable, cr);
+ });
+
+ Checkable::OnEventCommandExecuted.connect([](const Checkable::Ptr& checkable) { DbEvents::AddEventHandlerHistory(checkable); });
+
+ ExternalCommandProcessor::OnNewExternalCommand.connect([](double time, const String& command, const std::vector<String>& arguments) {
+ DbEvents::AddExternalCommandHistory(time, command, arguments);
+ });
+}
+
+/* check events */
+void DbEvents::NextCheckUpdatedHandler(const Checkable::Ptr& checkable)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query1;
+ query1.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query1.Table = "servicestatus";
+ query1.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query1.Table = "hoststatus";
+ query1.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatState;
+ query1.StatusUpdate = true;
+ query1.Object = DbObject::GetOrCreateByObject(checkable);
+
+ query1.Fields = new Dictionary({
+ { "next_check", DbValue::FromTimestamp(checkable->GetNextCheck()) }
+ });
+
+ DbObject::OnQuery(query1);
+}
+
+void DbEvents::FlappingChangedHandler(const Checkable::Ptr& checkable)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query1;
+ query1.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query1.Table = "servicestatus";
+ query1.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query1.Table = "hoststatus";
+ query1.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatState;
+ query1.StatusUpdate = true;
+ query1.Object = DbObject::GetOrCreateByObject(checkable);
+
+ Dictionary::Ptr fields1 = new Dictionary();
+ fields1->Set("is_flapping", checkable->IsFlapping());
+ fields1->Set("percent_state_change", checkable->GetFlappingCurrent());
+
+ query1.Fields = new Dictionary({
+ { "is_flapping", checkable->IsFlapping() },
+ { "percent_state_change", checkable->GetFlappingCurrent() }
+ });
+
+ query1.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ DbObject::OnQuery(query1);
+}
+
+void DbEvents::LastNotificationChangedHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable)
+{
+ std::pair<unsigned long, unsigned long> now_bag = ConvertTimestamp(Utility::GetTime());
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(notification->GetNextNotification());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query1;
+ query1.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query1.Table = "servicestatus";
+ query1.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query1.Table = "hoststatus";
+ query1.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatState;
+ query1.StatusUpdate = true;
+ query1.Object = DbObject::GetOrCreateByObject(checkable);
+
+ query1.Fields = new Dictionary({
+ { "last_notification", DbValue::FromTimestamp(now_bag.first) },
+ { "next_notification", DbValue::FromTimestamp(timeBag.first) },
+ { "current_notification_number", notification->GetNotificationNumber() }
+ });
+
+ query1.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ DbObject::OnQuery(query1);
+}
+
+void DbEvents::ReachabilityChangedHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, std::set<Checkable::Ptr> children)
+{
+ int is_reachable = 0;
+
+ if (cr->GetState() == ServiceOK)
+ is_reachable = 1;
+
+ for (const Checkable::Ptr& child : children) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(child);
+
+ DbQuery query1;
+ query1.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query1.Table = "servicestatus";
+ query1.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query1.Table = "hoststatus";
+ query1.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatState;
+ query1.StatusUpdate = true;
+ query1.Object = DbObject::GetOrCreateByObject(child);
+
+ query1.Fields = new Dictionary({
+ { "is_reachable", is_reachable }
+ });
+
+ query1.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ DbObject::OnQuery(query1);
+ }
+}
+
+/* enable changed events */
+void DbEvents::EnableActiveChecksChangedHandler(const Checkable::Ptr& checkable)
+{
+ EnableChangedHandlerInternal(checkable, "active_checks_enabled", checkable->GetEnableActiveChecks());
+}
+
+void DbEvents::EnablePassiveChecksChangedHandler(const Checkable::Ptr& checkable)
+{
+ EnableChangedHandlerInternal(checkable, "passive_checks_enabled", checkable->GetEnablePassiveChecks());
+}
+
+void DbEvents::EnableNotificationsChangedHandler(const Checkable::Ptr& checkable)
+{
+ EnableChangedHandlerInternal(checkable, "notifications_enabled", checkable->GetEnableNotifications());
+}
+
+void DbEvents::EnablePerfdataChangedHandler(const Checkable::Ptr& checkable)
+{
+ EnableChangedHandlerInternal(checkable, "process_performance_data", checkable->GetEnablePerfdata());
+}
+
+void DbEvents::EnableFlappingChangedHandler(const Checkable::Ptr& checkable)
+{
+ EnableChangedHandlerInternal(checkable, "flap_detection_enabled", checkable->GetEnableFlapping());
+}
+
+void DbEvents::EnableChangedHandlerInternal(const Checkable::Ptr& checkable, const String& fieldName, bool enabled)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query1;
+ query1.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query1.Table = "servicestatus";
+ query1.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query1.Table = "hoststatus";
+ query1.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatState;
+ query1.StatusUpdate = true;
+ query1.Object = DbObject::GetOrCreateByObject(checkable);
+
+ query1.Fields = new Dictionary({
+ { fieldName, enabled }
+ });
+
+ query1.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ DbObject::OnQuery(query1);
+}
+
+
+/* comments */
+void DbEvents::AddComments(const Checkable::Ptr& checkable)
+{
+ std::set<Comment::Ptr> comments = checkable->GetComments();
+
+ std::vector<DbQuery> queries;
+
+ for (const Comment::Ptr& comment : comments) {
+ AddCommentInternal(queries, comment, false);
+ }
+
+ DbObject::OnMultipleQueries(queries);
+}
+
+void DbEvents::AddComment(const Comment::Ptr& comment)
+{
+ std::vector<DbQuery> queries;
+ AddCommentInternal(queries, comment, false);
+ DbObject::OnMultipleQueries(queries);
+}
+
+void DbEvents::AddCommentHistory(const Comment::Ptr& comment)
+{
+ std::vector<DbQuery> queries;
+ AddCommentInternal(queries, comment, true);
+ DbObject::OnMultipleQueries(queries);
+}
+
+void DbEvents::AddCommentInternal(std::vector<DbQuery>& queries, const Comment::Ptr& comment, bool historical)
+{
+ Checkable::Ptr checkable = comment->GetCheckable();
+
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(comment->GetEntryTime());
+
+ Dictionary::Ptr fields1 = new Dictionary();
+ fields1->Set("entry_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("entry_time_usec", timeBag.second);
+ fields1->Set("entry_type", comment->GetEntryType());
+ fields1->Set("object_id", checkable);
+
+ int commentType = 0;
+
+ if (checkable->GetReflectionType() == Host::TypeInstance)
+ commentType = 2;
+ else if (checkable->GetReflectionType() == Service::TypeInstance)
+ commentType = 1;
+ else {
+ return;
+ }
+
+ fields1->Set("comment_type", commentType);
+ fields1->Set("internal_comment_id", comment->GetLegacyId());
+ fields1->Set("name", comment->GetName());
+ fields1->Set("comment_time", DbValue::FromTimestamp(timeBag.first)); /* same as entry_time */
+ fields1->Set("author_name", comment->GetAuthor());
+ fields1->Set("comment_data", comment->GetText());
+ fields1->Set("is_persistent", comment->GetPersistent());
+ fields1->Set("comment_source", 1); /* external */
+ fields1->Set("expires", (comment->GetExpireTime() > 0));
+ fields1->Set("expiration_time", DbValue::FromTimestamp(comment->GetExpireTime()));
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ DbQuery query1;
+
+ if (!historical) {
+ query1.Table = "comments";
+ query1.Type = DbQueryInsert | DbQueryUpdate;
+
+ fields1->Set("session_token", 0); /* DbConnection class fills in real ID */
+
+ query1.WhereCriteria = new Dictionary({
+ { "object_id", checkable },
+ { "name", comment->GetName() },
+ { "entry_time", DbValue::FromTimestamp(timeBag.first) }
+ });
+ } else {
+ query1.Table = "commenthistory";
+ query1.Type = DbQueryInsert;
+ }
+
+ query1.Category = DbCatComment;
+ query1.Fields = fields1;
+ queries.emplace_back(std::move(query1));
+}
+
+void DbEvents::RemoveComment(const Comment::Ptr& comment)
+{
+ std::vector<DbQuery> queries;
+ RemoveCommentInternal(queries, comment);
+ DbObject::OnMultipleQueries(queries);
+}
+
+void DbEvents::RemoveCommentInternal(std::vector<DbQuery>& queries, const Comment::Ptr& comment)
+{
+ Checkable::Ptr checkable = comment->GetCheckable();
+
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(comment->GetEntryTime());
+
+ /* Status */
+ DbQuery query1;
+ query1.Table = "comments";
+ query1.Type = DbQueryDelete;
+ query1.Category = DbCatComment;
+
+ query1.WhereCriteria = new Dictionary({
+ { "object_id", checkable },
+ { "entry_time", DbValue::FromTimestamp(timeBag.first) },
+ { "name", comment->GetName() }
+ });
+
+ queries.emplace_back(std::move(query1));
+
+ /* History - update deletion time for service/host */
+ std::pair<unsigned long, unsigned long> timeBagNow = ConvertTimestamp(Utility::GetTime());
+
+ DbQuery query2;
+ query2.Table = "commenthistory";
+ query2.Type = DbQueryUpdate;
+ query2.Category = DbCatComment;
+
+ query2.Fields = new Dictionary({
+ { "deletion_time", DbValue::FromTimestamp(timeBagNow.first) },
+ { "deletion_time_usec", timeBagNow.second }
+ });
+
+ query2.WhereCriteria = new Dictionary({
+ { "object_id", checkable },
+ { "entry_time", DbValue::FromTimestamp(timeBag.first) },
+ { "name", comment->GetName() }
+ });
+
+ queries.emplace_back(std::move(query2));
+}
+
+/* downtimes */
+void DbEvents::AddDowntimes(const Checkable::Ptr& checkable)
+{
+ std::set<Downtime::Ptr> downtimes = checkable->GetDowntimes();
+
+ std::vector<DbQuery> queries;
+
+ for (const Downtime::Ptr& downtime : downtimes) {
+ AddDowntimeInternal(queries, downtime, false);
+ }
+
+ DbObject::OnMultipleQueries(queries);
+}
+
+void DbEvents::AddDowntime(const Downtime::Ptr& downtime)
+{
+ std::vector<DbQuery> queries;
+ AddDowntimeInternal(queries, downtime, false);
+ DbObject::OnMultipleQueries(queries);
+}
+
+void DbEvents::AddDowntimeHistory(const Downtime::Ptr& downtime)
+{
+ std::vector<DbQuery> queries;
+ AddDowntimeInternal(queries, downtime, true);
+ DbObject::OnMultipleQueries(queries);
+}
+
+void DbEvents::AddDowntimeInternal(std::vector<DbQuery>& queries, const Downtime::Ptr& downtime, bool historical)
+{
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ Dictionary::Ptr fields1 = new Dictionary();
+ fields1->Set("entry_time", DbValue::FromTimestamp(downtime->GetEntryTime()));
+ fields1->Set("object_id", checkable);
+
+ int downtimeType = 0;
+
+ if (checkable->GetReflectionType() == Host::TypeInstance)
+ downtimeType = 2;
+ else if (checkable->GetReflectionType() == Service::TypeInstance)
+ downtimeType = 1;
+ else {
+ return;
+ }
+
+ fields1->Set("downtime_type", downtimeType);
+ fields1->Set("internal_downtime_id", downtime->GetLegacyId());
+ fields1->Set("author_name", downtime->GetAuthor());
+ fields1->Set("comment_data", downtime->GetComment());
+ fields1->Set("triggered_by_id", Downtime::GetByName(downtime->GetTriggeredBy()));
+ fields1->Set("is_fixed", downtime->GetFixed());
+ fields1->Set("duration", downtime->GetDuration());
+ fields1->Set("scheduled_start_time", DbValue::FromTimestamp(downtime->GetStartTime()));
+ fields1->Set("scheduled_end_time", DbValue::FromTimestamp(downtime->GetEndTime()));
+ fields1->Set("name", downtime->GetName());
+
+ /* flexible downtimes are started at trigger time */
+ if (downtime->GetFixed()) {
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(downtime->GetStartTime());
+
+ fields1->Set("actual_start_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("actual_start_time_usec", timeBag.second);
+ fields1->Set("was_started", ((downtime->GetStartTime() <= Utility::GetTime()) ? 1 : 0));
+ }
+
+ fields1->Set("is_in_effect", downtime->IsInEffect());
+ fields1->Set("trigger_time", DbValue::FromTimestamp(downtime->GetTriggerTime()));
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ DbQuery query1;
+
+ if (!historical) {
+ query1.Table = "scheduleddowntime";
+ query1.Type = DbQueryInsert | DbQueryUpdate;
+
+ fields1->Set("session_token", 0); /* DbConnection class fills in real ID */
+
+ query1.WhereCriteria = new Dictionary({
+ { "object_id", checkable },
+ { "name", downtime->GetName() },
+ { "entry_time", DbValue::FromTimestamp(downtime->GetEntryTime()) }
+ });
+ } else {
+ query1.Table = "downtimehistory";
+ query1.Type = DbQueryInsert;
+ }
+
+ query1.Category = DbCatDowntime;
+ query1.Fields = fields1;
+ queries.emplace_back(std::move(query1));
+
+ /* host/service status */
+ if (!historical) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query2;
+ query2.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query2.Table = "servicestatus";
+ query2.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query2.Table = "hoststatus";
+ query2.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query2.Type = DbQueryUpdate;
+ query2.Category = DbCatState;
+ query2.StatusUpdate = true;
+ query2.Object = DbObject::GetOrCreateByObject(checkable);
+
+ Dictionary::Ptr fields2 = new Dictionary();
+ fields2->Set("scheduled_downtime_depth", checkable->GetDowntimeDepth());
+
+ query2.Fields = fields2;
+ query2.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ queries.emplace_back(std::move(query2));
+ }
+}
+
+void DbEvents::RemoveDowntime(const Downtime::Ptr& downtime)
+{
+ std::vector<DbQuery> queries;
+ RemoveDowntimeInternal(queries, downtime);
+ DbObject::OnMultipleQueries(queries);
+}
+
+void DbEvents::RemoveDowntimeInternal(std::vector<DbQuery>& queries, const Downtime::Ptr& downtime)
+{
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ /* Status */
+ DbQuery query1;
+ query1.Table = "scheduleddowntime";
+ query1.Type = DbQueryDelete;
+ query1.Category = DbCatDowntime;
+ query1.WhereCriteria = new Dictionary();
+
+ query1.WhereCriteria->Set("object_id", checkable);
+ query1.WhereCriteria->Set("entry_time", DbValue::FromTimestamp(downtime->GetEntryTime()));
+ query1.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+ query1.WhereCriteria->Set("scheduled_start_time", DbValue::FromTimestamp(downtime->GetStartTime()));
+ query1.WhereCriteria->Set("scheduled_end_time", DbValue::FromTimestamp(downtime->GetEndTime()));
+ query1.WhereCriteria->Set("name", downtime->GetName());
+ queries.emplace_back(std::move(query1));
+
+ /* History - update actual_end_time, was_cancelled for service (and host in case) */
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(Utility::GetTime());
+
+ DbQuery query3;
+ query3.Table = "downtimehistory";
+ query3.Type = DbQueryUpdate;
+ query3.Category = DbCatDowntime;
+
+ Dictionary::Ptr fields3 = new Dictionary();
+ fields3->Set("was_cancelled", downtime->GetWasCancelled() ? 1 : 0);
+
+ if (downtime->GetFixed() || (!downtime->GetFixed() && downtime->GetTriggerTime() > 0)) {
+ fields3->Set("actual_end_time", DbValue::FromTimestamp(timeBag.first));
+ fields3->Set("actual_end_time_usec", timeBag.second);
+ }
+
+ fields3->Set("is_in_effect", 0);
+ query3.Fields = fields3;
+
+ query3.WhereCriteria = new Dictionary({
+ { "object_id", checkable },
+ { "entry_time", DbValue::FromTimestamp(downtime->GetEntryTime()) },
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "scheduled_start_time", DbValue::FromTimestamp(downtime->GetStartTime()) },
+ { "scheduled_end_time", DbValue::FromTimestamp(downtime->GetEndTime()) },
+ { "name", downtime->GetName() }
+ });
+
+ queries.emplace_back(std::move(query3));
+
+ /* host/service status */
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query4;
+ query4.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query4.Table = "servicestatus";
+ query4.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query4.Table = "hoststatus";
+ query4.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query4.Type = DbQueryUpdate;
+ query4.Category = DbCatState;
+ query4.StatusUpdate = true;
+ query4.Object = DbObject::GetOrCreateByObject(checkable);
+
+ Dictionary::Ptr fields4 = new Dictionary();
+ fields4->Set("scheduled_downtime_depth", checkable->GetDowntimeDepth());
+
+ query4.Fields = fields4;
+ query4.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ queries.emplace_back(std::move(query4));
+}
+
+void DbEvents::TriggerDowntime(const Downtime::Ptr& downtime)
+{
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(Utility::GetTime());
+
+ /* Status */
+ DbQuery query1;
+ query1.Table = "scheduleddowntime";
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatDowntime;
+
+ query1.Fields = new Dictionary({
+ { "was_started", 1 },
+ { "actual_start_time", DbValue::FromTimestamp(timeBag.first) },
+ { "actual_start_time_usec", timeBag.second },
+ { "is_in_effect", (downtime->IsInEffect() ? 1 : 0) },
+ { "trigger_time", DbValue::FromTimestamp(downtime->GetTriggerTime()) },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+
+ query1.WhereCriteria = new Dictionary({
+ { "object_id", checkable },
+ { "entry_time", DbValue::FromTimestamp(downtime->GetEntryTime()) },
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "scheduled_start_time", DbValue::FromTimestamp(downtime->GetStartTime()) },
+ { "scheduled_end_time", DbValue::FromTimestamp(downtime->GetEndTime()) },
+ { "name", downtime->GetName() }
+ });
+
+ DbObject::OnQuery(query1);
+
+ /* History - downtime was started for service (and host in case) */
+ DbQuery query3;
+ query3.Table = "downtimehistory";
+ query3.Type = DbQueryUpdate;
+ query3.Category = DbCatDowntime;
+
+ query3.Fields = new Dictionary({
+ { "was_started", 1 },
+ { "is_in_effect", 1 },
+ { "actual_start_time", DbValue::FromTimestamp(timeBag.first) },
+ { "actual_start_time_usec", timeBag.second },
+ { "trigger_time", DbValue::FromTimestamp(downtime->GetTriggerTime()) }
+ });
+
+ query3.WhereCriteria = query1.WhereCriteria;
+
+ DbObject::OnQuery(query3);
+
+ /* host/service status */
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query4;
+ query4.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query4.Table = "servicestatus";
+ query4.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query4.Table = "hoststatus";
+ query4.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query4.Type = DbQueryUpdate;
+ query4.Category = DbCatState;
+ query4.StatusUpdate = true;
+ query4.Object = DbObject::GetOrCreateByObject(checkable);
+
+ query4.Fields = new Dictionary({
+ { "scheduled_downtime_depth", checkable->GetDowntimeDepth() }
+ });
+ query4.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ DbObject::OnQuery(query4);
+}
+
+/* acknowledgements */
+void DbEvents::AddAcknowledgementHistory(const Checkable::Ptr& checkable, const String& author, const String& comment,
+ AcknowledgementType type, bool notify, double expiry)
+{
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(Utility::GetTime());
+
+ DbQuery query1;
+ query1.Table = "acknowledgements";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatAcknowledgement;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr fields1 = new Dictionary();
+
+ fields1->Set("entry_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("entry_time_usec", timeBag.second);
+ fields1->Set("acknowledgement_type", type);
+ fields1->Set("object_id", checkable);
+ fields1->Set("author_name", author);
+ fields1->Set("comment_data", comment);
+ fields1->Set("persistent_comment", 1);
+ fields1->Set("notify_contacts", notify);
+ fields1->Set("is_sticky", type == AcknowledgementSticky);
+ fields1->Set("end_time", DbValue::FromTimestamp(expiry));
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ if (service)
+ fields1->Set("state", service->GetState());
+ else
+ fields1->Set("state", GetHostState(host));
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ DbObject::OnQuery(query1);
+}
+
+void DbEvents::AddAcknowledgement(const Checkable::Ptr& checkable, AcknowledgementType type)
+{
+ AddAcknowledgementInternal(checkable, type, true);
+}
+
+void DbEvents::RemoveAcknowledgement(const Checkable::Ptr& checkable)
+{
+ AddAcknowledgementInternal(checkable, AcknowledgementNone, false);
+}
+
+void DbEvents::AddAcknowledgementInternal(const Checkable::Ptr& checkable, AcknowledgementType type, bool add)
+{
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query1;
+ query1.WhereCriteria = new Dictionary();
+
+ if (service) {
+ query1.Table = "servicestatus";
+ query1.WhereCriteria->Set("service_object_id", service);
+ } else {
+ query1.Table = "hoststatus";
+ query1.WhereCriteria->Set("host_object_id", host);
+ }
+
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatState;
+ query1.StatusUpdate = true;
+ query1.Object = DbObject::GetOrCreateByObject(checkable);
+
+ query1.Fields = new Dictionary({
+ { "acknowledgement_type", type },
+ { "problem_has_been_acknowledged", add ? 1 : 0 }
+ });
+
+ query1.WhereCriteria->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ DbObject::OnQuery(query1);
+}
+
+/* notifications */
+void DbEvents::AddNotificationHistory(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text)
+{
+ /* NotificationInsertID has to be tracked per IDO instance, therefore the OnQuery and OnMultipleQueries signals
+ * cannot be called directly as all IDO instances would insert rows with the same ID which is (most likely) only
+ * correct in one database. Instead, pass a lambda which generates the queries with new DbValue for
+ * NotificationInsertID each IDO instance.
+ */
+ DbObject::OnMakeQueries([&checkable, &users, &type, &cr](const DbObject::QueryCallbacks& callbacks) {
+ /* start and end happen at the same time */
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(Utility::GetTime());
+
+ DbQuery query1;
+ query1.Table = "notifications";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatNotification;
+ query1.NotificationInsertID = new DbValue(DbValueObjectInsertID, -1);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr fields1 = new Dictionary();
+ fields1->Set("notification_type", 1); /* service */
+ fields1->Set("notification_reason", MapNotificationReasonType(type));
+ fields1->Set("object_id", checkable);
+ fields1->Set("start_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("start_time_usec", timeBag.second);
+ fields1->Set("end_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("end_time_usec", timeBag.second);
+
+ if (service)
+ fields1->Set("state", service->GetState());
+ else
+ fields1->Set("state", GetHostState(host));
+
+ if (cr) {
+ fields1->Set("output", CompatUtility::GetCheckResultOutput(cr));
+ fields1->Set("long_output", CompatUtility::GetCheckResultLongOutput(cr));
+ }
+
+ fields1->Set("escalated", 0);
+ fields1->Set("contacts_notified", static_cast<long>(users.size()));
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ callbacks.Query(query1);
+
+ std::vector<DbQuery> queries;
+
+ for (const User::Ptr& user : users) {
+ DbQuery query2;
+ query2.Table = "contactnotifications";
+ query2.Type = DbQueryInsert;
+ query2.Category = DbCatNotification;
+
+ query2.Fields = new Dictionary({
+ { "contact_object_id", user },
+ { "start_time", DbValue::FromTimestamp(timeBag.first) },
+ { "start_time_usec", timeBag.second },
+ { "end_time", DbValue::FromTimestamp(timeBag.first) },
+ { "end_time_usec", timeBag.second },
+ { "notification_id", query1.NotificationInsertID },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+
+ queries.emplace_back(std::move(query2));
+ }
+
+ callbacks.MultipleQueries(queries);
+ });
+}
+
+/* statehistory */
+void DbEvents::AddStateChangeHistory(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type)
+{
+ double ts = cr->GetExecutionEnd();
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(ts);
+
+ DbQuery query1;
+ query1.Table = "statehistory";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatStateHistory;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr fields1 = new Dictionary();
+ fields1->Set("state_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("state_time_usec", timeBag.second);
+ fields1->Set("object_id", checkable);
+ fields1->Set("state_change", 1); /* service */
+ fields1->Set("state_type", checkable->GetStateType());
+ fields1->Set("current_check_attempt", checkable->GetCheckAttempt());
+ fields1->Set("max_check_attempts", checkable->GetMaxCheckAttempts());
+
+ if (service) {
+ fields1->Set("state", service->GetState());
+ fields1->Set("last_state", service->GetLastState());
+ fields1->Set("last_hard_state", service->GetLastHardState());
+ } else {
+ fields1->Set("state", GetHostState(host));
+ fields1->Set("last_state", host->GetLastState());
+ fields1->Set("last_hard_state", host->GetLastHardState());
+ }
+
+ if (cr) {
+ fields1->Set("output", CompatUtility::GetCheckResultOutput(cr));
+ fields1->Set("long_output", CompatUtility::GetCheckResultLongOutput(cr));
+ fields1->Set("check_source", cr->GetCheckSource());
+ }
+
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ DbObject::OnQuery(query1);
+}
+
+/* logentries */
+void DbEvents::AddCheckResultLogHistory(const Checkable::Ptr& checkable, const CheckResult::Ptr &cr)
+{
+ if (!cr)
+ return;
+
+ Dictionary::Ptr varsBefore = cr->GetVarsBefore();
+ Dictionary::Ptr varsAfter = cr->GetVarsAfter();
+
+ if (varsBefore && varsAfter) {
+ if (varsBefore->Get("state") == varsAfter->Get("state") &&
+ varsBefore->Get("state_type") == varsAfter->Get("state_type") &&
+ varsBefore->Get("attempt") == varsAfter->Get("attempt") &&
+ varsBefore->Get("reachable") == varsAfter->Get("reachable"))
+ return; /* Nothing changed, ignore this checkresult. */
+ }
+
+ LogEntryType type;
+ String output = CompatUtility::GetCheckResultOutput(cr);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << Service::StateToString(service->GetState()) << ";"
+ << Service::StateTypeToString(service->GetStateType()) << ";"
+ << service->GetCheckAttempt() << ";"
+ << output << ""
+ << "";
+
+ switch (service->GetState()) {
+ case ServiceOK:
+ type = LogEntryTypeServiceOk;
+ break;
+ case ServiceUnknown:
+ type = LogEntryTypeServiceUnknown;
+ break;
+ case ServiceWarning:
+ type = LogEntryTypeServiceWarning;
+ break;
+ case ServiceCritical:
+ type = LogEntryTypeServiceCritical;
+ break;
+ default:
+ Log(LogCritical, "DbEvents")
+ << "Unknown service state: " << service->GetState();
+ return;
+ }
+ } else {
+ msgbuf << "HOST ALERT: "
+ << host->GetName() << ";"
+ << GetHostStateString(host) << ";"
+ << Host::StateTypeToString(host->GetStateType()) << ";"
+ << host->GetCheckAttempt() << ";"
+ << output << ""
+ << "";
+
+ switch (host->GetState()) {
+ case HostUp:
+ type = LogEntryTypeHostUp;
+ break;
+ case HostDown:
+ type = LogEntryTypeHostDown;
+ break;
+ default:
+ Log(LogCritical, "DbEvents")
+ << "Unknown host state: " << host->GetState();
+ return;
+ }
+
+ if (!host->IsReachable())
+ type = LogEntryTypeHostUnreachable;
+ }
+
+ AddLogHistory(checkable, msgbuf.str(), type);
+}
+
+void DbEvents::AddTriggerDowntimeLogHistory(const Downtime::Ptr& downtime)
+{
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE DOWNTIME ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << "STARTED" << "; "
+ << "Service has entered a period of scheduled downtime."
+ << "";
+ } else {
+ msgbuf << "HOST DOWNTIME ALERT: "
+ << host->GetName() << ";"
+ << "STARTED" << "; "
+ << "Service has entered a period of scheduled downtime."
+ << "";
+ }
+
+ AddLogHistory(checkable, msgbuf.str(), LogEntryTypeInfoMessage);
+}
+
+void DbEvents::AddRemoveDowntimeLogHistory(const Downtime::Ptr& downtime)
+{
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ String downtimeOutput;
+ String downtimeStateStr;
+
+ if (downtime->GetWasCancelled()) {
+ downtimeOutput = "Scheduled downtime for service has been cancelled.";
+ downtimeStateStr = "CANCELLED";
+ } else {
+ downtimeOutput = "Service has exited from a period of scheduled downtime.";
+ downtimeStateStr = "STOPPED";
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE DOWNTIME ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << downtimeStateStr << "; "
+ << downtimeOutput
+ << "";
+ } else {
+ msgbuf << "HOST DOWNTIME ALERT: "
+ << host->GetName() << ";"
+ << downtimeStateStr << "; "
+ << downtimeOutput
+ << "";
+ }
+
+ AddLogHistory(checkable, msgbuf.str(), LogEntryTypeInfoMessage);
+}
+
+void DbEvents::AddNotificationSentLogHistory(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const User::Ptr& user,
+ NotificationType notification_type, const CheckResult::Ptr& cr,
+ const String& author, const String& comment_text)
+{
+ CheckCommand::Ptr commandObj = checkable->GetCheckCommand();
+
+ String checkCommandName;
+
+ if (commandObj)
+ checkCommandName = commandObj->GetName();
+
+ String notificationTypeStr = Notification::NotificationTypeToStringCompat(notification_type); //TODO: Change that to our own types.
+
+ String author_comment = "";
+ if (notification_type == NotificationCustom || notification_type == NotificationAcknowledgement) {
+ author_comment = ";" + author + ";" + comment_text;
+ }
+
+ if (!cr)
+ return;
+
+ String output = CompatUtility::GetCheckResultOutput(cr);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE NOTIFICATION: "
+ << user->GetName() << ";"
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << notificationTypeStr << " "
+ << "(" << Service::StateToString(service->GetState()) << ");"
+ << checkCommandName << ";"
+ << output << author_comment
+ << "";
+ } else {
+ msgbuf << "HOST NOTIFICATION: "
+ << user->GetName() << ";"
+ << host->GetName() << ";"
+ << notificationTypeStr << " "
+ << "(" << Host::StateToString(host->GetState()) << ");"
+ << checkCommandName << ";"
+ << output << author_comment
+ << "";
+ }
+
+ AddLogHistory(checkable, msgbuf.str(), LogEntryTypeHostNotification);
+}
+
+void DbEvents::AddFlappingChangedLogHistory(const Checkable::Ptr& checkable)
+{
+ String flappingStateStr;
+ String flappingOutput;
+
+ if (checkable->IsFlapping()) {
+ flappingOutput = "Service appears to have started flapping (" + Convert::ToString(checkable->GetFlappingCurrent()) + "% change >= " + Convert::ToString(checkable->GetFlappingThresholdHigh()) + "% threshold)";
+ flappingStateStr = "STARTED";
+ } else {
+ flappingOutput = "Service appears to have stopped flapping (" + Convert::ToString(checkable->GetFlappingCurrent()) + "% change < " + Convert::ToString(checkable->GetFlappingThresholdLow()) + "% threshold)";
+ flappingStateStr = "STOPPED";
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE FLAPPING ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << flappingStateStr << "; "
+ << flappingOutput
+ << "";
+ } else {
+ msgbuf << "HOST FLAPPING ALERT: "
+ << host->GetName() << ";"
+ << flappingStateStr << "; "
+ << flappingOutput
+ << "";
+ }
+
+ AddLogHistory(checkable, msgbuf.str(), LogEntryTypeInfoMessage);
+}
+
+void DbEvents::AddEnableFlappingChangedLogHistory(const Checkable::Ptr& checkable)
+{
+ if (!checkable->GetEnableFlapping())
+ return;
+
+ String flappingOutput = "Flap detection has been disabled";
+ String flappingStateStr = "DISABLED";
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::ostringstream msgbuf;
+
+ if (service) {
+ msgbuf << "SERVICE FLAPPING ALERT: "
+ << host->GetName() << ";"
+ << service->GetShortName() << ";"
+ << flappingStateStr << "; "
+ << flappingOutput
+ << "";
+ } else {
+ msgbuf << "HOST FLAPPING ALERT: "
+ << host->GetName() << ";"
+ << flappingStateStr << "; "
+ << flappingOutput
+ << "";
+ }
+
+ AddLogHistory(checkable, msgbuf.str(), LogEntryTypeInfoMessage);
+}
+
+void DbEvents::AddLogHistory(const Checkable::Ptr& checkable, const String& buffer, LogEntryType type)
+{
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(Utility::GetTime());
+
+ DbQuery query1;
+ query1.Table = "logentries";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatLog;
+
+ Dictionary::Ptr fields1 = new Dictionary();
+
+ fields1->Set("logentry_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("entry_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("entry_time_usec", timeBag.second);
+ fields1->Set("object_id", checkable);
+ fields1->Set("logentry_type", type);
+ fields1->Set("logentry_data", buffer);
+
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ DbObject::OnQuery(query1);
+}
+
+/* flappinghistory */
+void DbEvents::AddFlappingChangedHistory(const Checkable::Ptr& checkable)
+{
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(Utility::GetTime());
+
+ DbQuery query1;
+ query1.Table = "flappinghistory";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatFlapping;
+
+ Dictionary::Ptr fields1 = new Dictionary();
+
+ fields1->Set("event_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("event_time_usec", timeBag.second);
+
+ if (checkable->IsFlapping())
+ fields1->Set("event_type", 1000);
+ else {
+ fields1->Set("event_type", 1001);
+ fields1->Set("reason_type", 1);
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ fields1->Set("flapping_type", service ? 1 : 0);
+ fields1->Set("object_id", checkable);
+ fields1->Set("percent_state_change", checkable->GetFlappingCurrent());
+ fields1->Set("low_threshold", checkable->GetFlappingThresholdLow());
+ fields1->Set("high_threshold", checkable->GetFlappingThresholdHigh());
+
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ DbObject::OnQuery(query1);
+}
+
+void DbEvents::AddEnableFlappingChangedHistory(const Checkable::Ptr& checkable)
+{
+ if (!checkable->GetEnableFlapping())
+ return;
+
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(Utility::GetTime());
+
+ DbQuery query1;
+ query1.Table = "flappinghistory";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatFlapping;
+
+ Dictionary::Ptr fields1 = new Dictionary();
+
+ fields1->Set("event_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("event_time_usec", timeBag.second);
+
+ fields1->Set("event_type", 1001);
+ fields1->Set("reason_type", 2);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ fields1->Set("flapping_type", service ? 1 : 0);
+ fields1->Set("object_id", checkable);
+ fields1->Set("percent_state_change", checkable->GetFlappingCurrent());
+ fields1->Set("low_threshold", checkable->GetFlappingThresholdLow());
+ fields1->Set("high_threshold", checkable->GetFlappingThresholdHigh());
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ DbObject::OnQuery(query1);
+}
+
+/* servicechecks */
+void DbEvents::AddCheckableCheckHistory(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ if (!cr)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DbQuery query1;
+ query1.Table = service ? "servicechecks" : "hostchecks";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatCheck;
+
+ Dictionary::Ptr fields1 = new Dictionary();
+ fields1->Set("check_type", !checkable->GetEnableActiveChecks()); /* 0 .. active, 1 .. passive */
+ fields1->Set("current_check_attempt", checkable->GetCheckAttempt());
+ fields1->Set("max_check_attempts", checkable->GetMaxCheckAttempts());
+ fields1->Set("state_type", checkable->GetStateType());
+
+ double start = cr->GetExecutionStart();
+ double end = cr->GetExecutionEnd();
+ double executionTime = cr->CalculateExecutionTime();
+
+ std::pair<unsigned long, unsigned long> timeBagStart = ConvertTimestamp(start);
+ std::pair<unsigned long, unsigned long> timeBagEnd = ConvertTimestamp(end);
+
+ fields1->Set("start_time", DbValue::FromTimestamp(timeBagStart.first));
+ fields1->Set("start_time_usec", timeBagStart.second);
+ fields1->Set("end_time", DbValue::FromTimestamp(timeBagEnd.first));
+ fields1->Set("end_time_usec", timeBagEnd.second);
+ fields1->Set("command_object_id", checkable->GetCheckCommand());
+ fields1->Set("execution_time", executionTime);
+ fields1->Set("latency", cr->CalculateLatency());
+ fields1->Set("return_code", cr->GetExitStatus());
+ fields1->Set("perfdata", PluginUtility::FormatPerfdata(cr->GetPerformanceData()));
+
+ fields1->Set("output", CompatUtility::GetCheckResultOutput(cr));
+ fields1->Set("long_output", CompatUtility::GetCheckResultLongOutput(cr));
+ fields1->Set("command_line", CompatUtility::GetCommandLine(checkable->GetCheckCommand()));
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ if (service) {
+ fields1->Set("service_object_id", service);
+ fields1->Set("state", service->GetState());
+ } else {
+ fields1->Set("host_object_id", host);
+ fields1->Set("state", GetHostState(host));
+ }
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ DbObject::OnQuery(query1);
+}
+
+/* eventhandlers */
+void DbEvents::AddEventHandlerHistory(const Checkable::Ptr& checkable)
+{
+ DbQuery query1;
+ query1.Table = "eventhandlers";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatEventHandler;
+
+ Dictionary::Ptr fields1 = new Dictionary();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ fields1->Set("object_id", checkable);
+ fields1->Set("state_type", checkable->GetStateType());
+ fields1->Set("command_object_id", checkable->GetEventCommand());
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ if (service) {
+ fields1->Set("state", service->GetState());
+ fields1->Set("eventhandler_type", 1);
+ } else {
+ fields1->Set("state", GetHostState(host));
+ fields1->Set("eventhandler_type", 0);
+ }
+
+ std::pair<unsigned long, unsigned long> timeBag = ConvertTimestamp(Utility::GetTime());
+
+ fields1->Set("start_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("start_time_usec", timeBag.second);
+ fields1->Set("end_time", DbValue::FromTimestamp(timeBag.first));
+ fields1->Set("end_time_usec", timeBag.second);
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ DbObject::OnQuery(query1);
+}
+
+/* externalcommands */
+void DbEvents::AddExternalCommandHistory(double time, const String& command, const std::vector<String>& arguments)
+{
+ DbQuery query1;
+ query1.Table = "externalcommands";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatExternalCommand;
+
+ Dictionary::Ptr fields1 = new Dictionary();
+
+ fields1->Set("entry_time", DbValue::FromTimestamp(time));
+ fields1->Set("command_type", MapExternalCommandType(command));
+ fields1->Set("command_name", command);
+ fields1->Set("command_args", boost::algorithm::join(arguments, ";"));
+ fields1->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(IcingaApplication::GetInstance()->GetNodeName());
+
+ if (endpoint)
+ fields1->Set("endpoint_object_id", endpoint);
+
+ query1.Fields = fields1;
+ DbObject::OnQuery(query1);
+}
+
+int DbEvents::GetHostState(const Host::Ptr& host)
+{
+ int currentState = host->GetState();
+
+ if (currentState != HostUp && !host->IsReachable())
+ currentState = 2; /* hardcoded compat state */
+
+ return currentState;
+}
+
+String DbEvents::GetHostStateString(const Host::Ptr& host)
+{
+ if (host->GetState() != HostUp && !host->IsReachable())
+ return "UNREACHABLE"; /* hardcoded compat state */
+
+ return Host::StateToString(host->GetState());
+}
+
+std::pair<unsigned long, unsigned long> DbEvents::ConvertTimestamp(double time)
+{
+ unsigned long time_sec = static_cast<long>(time);
+ unsigned long time_usec = (time - time_sec) * 1000 * 1000;
+
+ return std::make_pair(time_sec, time_usec);
+}
+
+int DbEvents::MapNotificationReasonType(NotificationType type)
+{
+ switch (type) {
+ case NotificationDowntimeStart:
+ return 5;
+ case NotificationDowntimeEnd:
+ return 6;
+ case NotificationDowntimeRemoved:
+ return 7;
+ case NotificationCustom:
+ return 8;
+ case NotificationAcknowledgement:
+ return 1;
+ case NotificationProblem:
+ return 0;
+ case NotificationRecovery:
+ return 0;
+ case NotificationFlappingStart:
+ return 2;
+ case NotificationFlappingEnd:
+ return 3;
+ default:
+ return 0;
+ }
+}
+
+int DbEvents::MapExternalCommandType(const String& name)
+{
+ if (name == "NONE")
+ return 0;
+ if (name == "ADD_HOST_COMMENT")
+ return 1;
+ if (name == "DEL_HOST_COMMENT")
+ return 2;
+ if (name == "ADD_SVC_COMMENT")
+ return 3;
+ if (name == "DEL_SVC_COMMENT")
+ return 4;
+ if (name == "ENABLE_SVC_CHECK")
+ return 5;
+ if (name == "DISABLE_SVC_CHECK")
+ return 6;
+ if (name == "SCHEDULE_SVC_CHECK")
+ return 7;
+ if (name == "DELAY_SVC_NOTIFICATION")
+ return 9;
+ if (name == "DELAY_HOST_NOTIFICATION")
+ return 10;
+ if (name == "DISABLE_NOTIFICATIONS")
+ return 11;
+ if (name == "ENABLE_NOTIFICATIONS")
+ return 12;
+ if (name == "RESTART_PROCESS")
+ return 13;
+ if (name == "SHUTDOWN_PROCESS")
+ return 14;
+ if (name == "ENABLE_HOST_SVC_CHECKS")
+ return 15;
+ if (name == "DISABLE_HOST_SVC_CHECKS")
+ return 16;
+ if (name == "SCHEDULE_HOST_SVC_CHECKS")
+ return 17;
+ if (name == "DELAY_HOST_SVC_NOTIFICATIONS")
+ return 19;
+ if (name == "DEL_ALL_HOST_COMMENTS")
+ return 20;
+ if (name == "DEL_ALL_SVC_COMMENTS")
+ return 21;
+ if (name == "ENABLE_SVC_NOTIFICATIONS")
+ return 22;
+ if (name == "DISABLE_SVC_NOTIFICATIONS")
+ return 23;
+ if (name == "ENABLE_HOST_NOTIFICATIONS")
+ return 24;
+ if (name == "DISABLE_HOST_NOTIFICATIONS")
+ return 25;
+ if (name == "ENABLE_ALL_NOTIFICATIONS_BEYOND_HOST")
+ return 26;
+ if (name == "DISABLE_ALL_NOTIFICATIONS_BEYOND_HOST")
+ return 27;
+ if (name == "ENABLE_HOST_SVC_NOTIFICATIONS")
+ return 28;
+ if (name == "DISABLE_HOST_SVC_NOTIFICATIONS")
+ return 29;
+ if (name == "PROCESS_SERVICE_CHECK_RESULT")
+ return 30;
+ if (name == "SAVE_STATE_INFORMATION")
+ return 31;
+ if (name == "READ_STATE_INFORMATION")
+ return 32;
+ if (name == "ACKNOWLEDGE_HOST_PROBLEM")
+ return 33;
+ if (name == "ACKNOWLEDGE_SVC_PROBLEM")
+ return 34;
+ if (name == "START_EXECUTING_SVC_CHECKS")
+ return 35;
+ if (name == "STOP_EXECUTING_SVC_CHECKS")
+ return 36;
+ if (name == "START_ACCEPTING_PASSIVE_SVC_CHECKS")
+ return 37;
+ if (name == "STOP_ACCEPTING_PASSIVE_SVC_CHECKS")
+ return 38;
+ if (name == "ENABLE_PASSIVE_SVC_CHECKS")
+ return 39;
+ if (name == "DISABLE_PASSIVE_SVC_CHECKS")
+ return 40;
+ if (name == "ENABLE_EVENT_HANDLERS")
+ return 41;
+ if (name == "DISABLE_EVENT_HANDLERS")
+ return 42;
+ if (name == "ENABLE_HOST_EVENT_HANDLER")
+ return 43;
+ if (name == "DISABLE_HOST_EVENT_HANDLER")
+ return 44;
+ if (name == "ENABLE_SVC_EVENT_HANDLER")
+ return 45;
+ if (name == "DISABLE_SVC_EVENT_HANDLER")
+ return 46;
+ if (name == "ENABLE_HOST_CHECK")
+ return 47;
+ if (name == "DISABLE_HOST_CHECK")
+ return 48;
+ if (name == "START_OBSESSING_OVER_SVC_CHECKS")
+ return 49;
+ if (name == "STOP_OBSESSING_OVER_SVC_CHECKS")
+ return 50;
+ if (name == "REMOVE_HOST_ACKNOWLEDGEMENT")
+ return 51;
+ if (name == "REMOVE_SVC_ACKNOWLEDGEMENT")
+ return 52;
+ if (name == "SCHEDULE_FORCED_HOST_SVC_CHECKS")
+ return 53;
+ if (name == "SCHEDULE_FORCED_SVC_CHECK")
+ return 54;
+ if (name == "SCHEDULE_HOST_DOWNTIME")
+ return 55;
+ if (name == "SCHEDULE_SVC_DOWNTIME")
+ return 56;
+ if (name == "ENABLE_HOST_FLAP_DETECTION")
+ return 57;
+ if (name == "DISABLE_HOST_FLAP_DETECTION")
+ return 58;
+ if (name == "ENABLE_SVC_FLAP_DETECTION")
+ return 59;
+ if (name == "DISABLE_SVC_FLAP_DETECTION")
+ return 60;
+ if (name == "ENABLE_FLAP_DETECTION")
+ return 61;
+ if (name == "DISABLE_FLAP_DETECTION")
+ return 62;
+ if (name == "ENABLE_HOSTGROUP_SVC_NOTIFICATIONS")
+ return 63;
+ if (name == "DISABLE_HOSTGROUP_SVC_NOTIFICATIONS")
+ return 64;
+ if (name == "ENABLE_HOSTGROUP_HOST_NOTIFICATIONS")
+ return 65;
+ if (name == "DISABLE_HOSTGROUP_HOST_NOTIFICATIONS")
+ return 66;
+ if (name == "ENABLE_HOSTGROUP_SVC_CHECKS")
+ return 67;
+ if (name == "DISABLE_HOSTGROUP_SVC_CHECKS")
+ return 68;
+ if (name == "CANCEL_HOST_DOWNTIME")
+ return 69;
+ if (name == "CANCEL_SVC_DOWNTIME")
+ return 70;
+ if (name == "CANCEL_ACTIVE_HOST_DOWNTIME")
+ return 71;
+ if (name == "CANCEL_PENDING_HOST_DOWNTIME")
+ return 72;
+ if (name == "CANCEL_ACTIVE_SVC_DOWNTIME")
+ return 73;
+ if (name == "CANCEL_PENDING_SVC_DOWNTIME")
+ return 74;
+ if (name == "CANCEL_ACTIVE_HOST_SVC_DOWNTIME")
+ return 75;
+ if (name == "CANCEL_PENDING_HOST_SVC_DOWNTIME")
+ return 76;
+ if (name == "FLUSH_PENDING_COMMANDS")
+ return 77;
+ if (name == "DEL_HOST_DOWNTIME")
+ return 78;
+ if (name == "DEL_SVC_DOWNTIME")
+ return 79;
+ if (name == "ENABLE_FAILURE_PREDICTION")
+ return 80;
+ if (name == "DISABLE_FAILURE_PREDICTION")
+ return 81;
+ if (name == "ENABLE_PERFORMANCE_DATA")
+ return 82;
+ if (name == "DISABLE_PERFORMANCE_DATA")
+ return 83;
+ if (name == "SCHEDULE_HOSTGROUP_HOST_DOWNTIME")
+ return 84;
+ if (name == "SCHEDULE_HOSTGROUP_SVC_DOWNTIME")
+ return 85;
+ if (name == "SCHEDULE_HOST_SVC_DOWNTIME")
+ return 86;
+ if (name == "PROCESS_HOST_CHECK_RESULT")
+ return 87;
+ if (name == "START_EXECUTING_HOST_CHECKS")
+ return 88;
+ if (name == "STOP_EXECUTING_HOST_CHECKS")
+ return 89;
+ if (name == "START_ACCEPTING_PASSIVE_HOST_CHECKS")
+ return 90;
+ if (name == "STOP_ACCEPTING_PASSIVE_HOST_CHECKS")
+ return 91;
+ if (name == "ENABLE_PASSIVE_HOST_CHECKS")
+ return 92;
+ if (name == "DISABLE_PASSIVE_HOST_CHECKS")
+ return 93;
+ if (name == "START_OBSESSING_OVER_HOST_CHECKS")
+ return 94;
+ if (name == "STOP_OBSESSING_OVER_HOST_CHECKS")
+ return 95;
+ if (name == "SCHEDULE_HOST_CHECK")
+ return 96;
+ if (name == "SCHEDULE_FORCED_HOST_CHECK")
+ return 98;
+ if (name == "START_OBSESSING_OVER_SVC")
+ return 99;
+ if (name == "STOP_OBSESSING_OVER_SVC")
+ return 100;
+ if (name == "START_OBSESSING_OVER_HOST")
+ return 101;
+ if (name == "STOP_OBSESSING_OVER_HOST")
+ return 102;
+ if (name == "ENABLE_HOSTGROUP_HOST_CHECKS")
+ return 103;
+ if (name == "DISABLE_HOSTGROUP_HOST_CHECKS")
+ return 104;
+ if (name == "ENABLE_HOSTGROUP_PASSIVE_SVC_CHECKS")
+ return 105;
+ if (name == "DISABLE_HOSTGROUP_PASSIVE_SVC_CHECKS")
+ return 106;
+ if (name == "ENABLE_HOSTGROUP_PASSIVE_HOST_CHECKS")
+ return 107;
+ if (name == "DISABLE_HOSTGROUP_PASSIVE_HOST_CHECKS")
+ return 108;
+ if (name == "ENABLE_SERVICEGROUP_SVC_NOTIFICATIONS")
+ return 109;
+ if (name == "DISABLE_SERVICEGROUP_SVC_NOTIFICATIONS")
+ return 110;
+ if (name == "ENABLE_SERVICEGROUP_HOST_NOTIFICATIONS")
+ return 111;
+ if (name == "DISABLE_SERVICEGROUP_HOST_NOTIFICATIONS")
+ return 112;
+ if (name == "ENABLE_SERVICEGROUP_SVC_CHECKS")
+ return 113;
+ if (name == "DISABLE_SERVICEGROUP_SVC_CHECKS")
+ return 114;
+ if (name == "ENABLE_SERVICEGROUP_HOST_CHECKS")
+ return 115;
+ if (name == "DISABLE_SERVICEGROUP_HOST_CHECKS")
+ return 116;
+ if (name == "ENABLE_SERVICEGROUP_PASSIVE_SVC_CHECKS")
+ return 117;
+ if (name == "DISABLE_SERVICEGROUP_PASSIVE_SVC_CHECKS")
+ return 118;
+ if (name == "ENABLE_SERVICEGROUP_PASSIVE_HOST_CHECKS")
+ return 119;
+ if (name == "DISABLE_SERVICEGROUP_PASSIVE_HOST_CHECKS")
+ return 120;
+ if (name == "SCHEDULE_SERVICEGROUP_HOST_DOWNTIME")
+ return 121;
+ if (name == "SCHEDULE_SERVICEGROUP_SVC_DOWNTIME")
+ return 122;
+ if (name == "CHANGE_GLOBAL_HOST_EVENT_HANDLER")
+ return 123;
+ if (name == "CHANGE_GLOBAL_SVC_EVENT_HANDLER")
+ return 124;
+ if (name == "CHANGE_HOST_EVENT_HANDLER")
+ return 125;
+ if (name == "CHANGE_SVC_EVENT_HANDLER")
+ return 126;
+ if (name == "CHANGE_HOST_CHECK_COMMAND")
+ return 127;
+ if (name == "CHANGE_SVC_CHECK_COMMAND")
+ return 128;
+ if (name == "CHANGE_NORMAL_HOST_CHECK_INTERVAL")
+ return 129;
+ if (name == "CHANGE_NORMAL_SVC_CHECK_INTERVAL")
+ return 130;
+ if (name == "CHANGE_RETRY_SVC_CHECK_INTERVAL")
+ return 131;
+ if (name == "CHANGE_MAX_HOST_CHECK_ATTEMPTS")
+ return 132;
+ if (name == "CHANGE_MAX_SVC_CHECK_ATTEMPTS")
+ return 133;
+ if (name == "SCHEDULE_AND_PROPAGATE_TRIGGERED_HOST_DOWNTIME")
+ return 134;
+ if (name == "ENABLE_HOST_AND_CHILD_NOTIFICATIONS")
+ return 135;
+ if (name == "DISABLE_HOST_AND_CHILD_NOTIFICATIONS")
+ return 136;
+ if (name == "SCHEDULE_AND_PROPAGATE_HOST_DOWNTIME")
+ return 137;
+ if (name == "ENABLE_SERVICE_FRESHNESS_CHECKS")
+ return 138;
+ if (name == "DISABLE_SERVICE_FRESHNESS_CHECKS")
+ return 139;
+ if (name == "ENABLE_HOST_FRESHNESS_CHECKS")
+ return 140;
+ if (name == "DISABLE_HOST_FRESHNESS_CHECKS")
+ return 141;
+ if (name == "SET_HOST_NOTIFICATION_NUMBER")
+ return 142;
+ if (name == "SET_SVC_NOTIFICATION_NUMBER")
+ return 143;
+ if (name == "CHANGE_HOST_CHECK_TIMEPERIOD")
+ return 144;
+ if (name == "CHANGE_SVC_CHECK_TIMEPERIOD")
+ return 145;
+ if (name == "PROCESS_FILE")
+ return 146;
+ if (name == "CHANGE_CUSTOM_HOST_VAR")
+ return 147;
+ if (name == "CHANGE_CUSTOM_SVC_VAR")
+ return 148;
+ if (name == "CHANGE_CUSTOM_CONTACT_VAR")
+ return 149;
+ if (name == "ENABLE_CONTACT_HOST_NOTIFICATIONS")
+ return 150;
+ if (name == "DISABLE_CONTACT_HOST_NOTIFICATIONS")
+ return 151;
+ if (name == "ENABLE_CONTACT_SVC_NOTIFICATIONS")
+ return 152;
+ if (name == "DISABLE_CONTACT_SVC_NOTIFICATIONS")
+ return 153;
+ if (name == "ENABLE_CONTACTGROUP_HOST_NOTIFICATIONS")
+ return 154;
+ if (name == "DISABLE_CONTACTGROUP_HOST_NOTIFICATIONS")
+ return 155;
+ if (name == "ENABLE_CONTACTGROUP_SVC_NOTIFICATIONS")
+ return 156;
+ if (name == "DISABLE_CONTACTGROUP_SVC_NOTIFICATIONS")
+ return 157;
+ if (name == "CHANGE_RETRY_HOST_CHECK_INTERVAL")
+ return 158;
+ if (name == "SEND_CUSTOM_HOST_NOTIFICATION")
+ return 159;
+ if (name == "SEND_CUSTOM_SVC_NOTIFICATION")
+ return 160;
+ if (name == "CHANGE_HOST_NOTIFICATION_TIMEPERIOD")
+ return 161;
+ if (name == "CHANGE_SVC_NOTIFICATION_TIMEPERIOD")
+ return 162;
+ if (name == "CHANGE_CONTACT_HOST_NOTIFICATION_TIMEPERIOD")
+ return 163;
+ if (name == "CHANGE_CONTACT_SVC_NOTIFICATION_TIMEPERIOD")
+ return 164;
+ if (name == "CHANGE_HOST_MODATTR")
+ return 165;
+ if (name == "CHANGE_SVC_MODATTR")
+ return 166;
+ if (name == "CHANGE_CONTACT_MODATTR")
+ return 167;
+ if (name == "CHANGE_CONTACT_MODHATTR")
+ return 168;
+ if (name == "CHANGE_CONTACT_MODSATTR")
+ return 169;
+ if (name == "SYNC_STATE_INFORMATION")
+ return 170;
+ if (name == "DEL_DOWNTIME_BY_HOST_NAME")
+ return 171;
+ if (name == "DEL_DOWNTIME_BY_HOSTGROUP_NAME")
+ return 172;
+ if (name == "DEL_DOWNTIME_BY_START_TIME_COMMENT")
+ return 173;
+ if (name == "ACKNOWLEDGE_HOST_PROBLEM_EXPIRE")
+ return 174;
+ if (name == "ACKNOWLEDGE_SVC_PROBLEM_EXPIRE")
+ return 175;
+ if (name == "DISABLE_NOTIFICATIONS_EXPIRE_TIME")
+ return 176;
+ if (name == "CUSTOM_COMMAND")
+ return 999;
+
+ return 0;
+}
diff --git a/lib/db_ido/dbevents.hpp b/lib/db_ido/dbevents.hpp
new file mode 100644
index 0000000..858f3b3
--- /dev/null
+++ b/lib/db_ido/dbevents.hpp
@@ -0,0 +1,128 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DBEVENTS_H
+#define DBEVENTS_H
+
+#include "db_ido/dbobject.hpp"
+#include "base/configobject.hpp"
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+enum LogEntryType
+{
+ LogEntryTypeRuntimeError = 1,
+ LogEntryTypeRuntimeWarning = 2,
+ LogEntryTypeVerificationError = 4,
+ LogEntryTypeVerificationWarning = 8,
+ LogEntryTypeConfigError = 16,
+ LogEntryTypeConfigWarning = 32,
+ LogEntryTypeProcessInfo = 64,
+ LogEntryTypeEventHandler = 128,
+ LogEntryTypeExternalCommand = 512,
+ LogEntryTypeHostUp = 1024,
+ LogEntryTypeHostDown = 2048,
+ LogEntryTypeHostUnreachable = 4096,
+ LogEntryTypeServiceOk = 8192,
+ LogEntryTypeServiceUnknown = 16384,
+ LogEntryTypeServiceWarning = 32768,
+ LogEntryTypeServiceCritical = 65536,
+ LogEntryTypePassiveCheck = 1231072,
+ LogEntryTypeInfoMessage = 262144,
+ LogEntryTypeHostNotification = 524288,
+ LogEntryTypeServiceNotification = 1048576
+};
+
+/**
+ * IDO events
+ *
+ * @ingroup ido
+ */
+class DbEvents
+{
+public:
+ static void StaticInitialize();
+
+ static void AddComments(const Checkable::Ptr& checkable);
+
+ static void AddDowntimes(const Checkable::Ptr& checkable);
+ static void RemoveDowntimes(const Checkable::Ptr& checkable);
+
+ static void AddLogHistory(const Checkable::Ptr& checkable, const String& buffer, LogEntryType type);
+
+ /* Status */
+ static void NextCheckUpdatedHandler(const Checkable::Ptr& checkable);
+ static void FlappingChangedHandler(const Checkable::Ptr& checkable);
+ static void LastNotificationChangedHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable);
+
+ static void EnableActiveChecksChangedHandler(const Checkable::Ptr& checkable);
+ static void EnablePassiveChecksChangedHandler(const Checkable::Ptr& checkable);
+ static void EnableNotificationsChangedHandler(const Checkable::Ptr& checkable);
+ static void EnablePerfdataChangedHandler(const Checkable::Ptr& checkable);
+ static void EnableFlappingChangedHandler(const Checkable::Ptr& checkable);
+
+ static void AddComment(const Comment::Ptr& comment);
+ static void RemoveComment(const Comment::Ptr& comment);
+
+ static void AddDowntime(const Downtime::Ptr& downtime);
+ static void RemoveDowntime(const Downtime::Ptr& downtime);
+ static void TriggerDowntime(const Downtime::Ptr& downtime);
+
+ static void AddAcknowledgement(const Checkable::Ptr& checkable, AcknowledgementType type);
+ static void RemoveAcknowledgement(const Checkable::Ptr& checkable);
+ static void AddAcknowledgementInternal(const Checkable::Ptr& checkable, AcknowledgementType type, bool add);
+
+ static void ReachabilityChangedHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, std::set<Checkable::Ptr> children);
+
+ /* comment, downtime, acknowledgement history */
+ static void AddCommentHistory(const Comment::Ptr& comment);
+ static void AddDowntimeHistory(const Downtime::Ptr& downtime);
+ static void AddAcknowledgementHistory(const Checkable::Ptr& checkable, const String& author, const String& comment,
+ AcknowledgementType type, bool notify, double expiry);
+
+ /* notification & contactnotification history */
+ static void AddNotificationHistory(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const std::set<User::Ptr>& users, NotificationType type, const CheckResult::Ptr& cr, const String& author,
+ const String& text);
+
+ /* statehistory */
+ static void AddStateChangeHistory(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type);
+
+ /* logentries */
+ static void AddCheckResultLogHistory(const Checkable::Ptr& checkable, const CheckResult::Ptr &cr);
+ static void AddTriggerDowntimeLogHistory(const Downtime::Ptr& downtime);
+ static void AddRemoveDowntimeLogHistory(const Downtime::Ptr& downtime);
+ static void AddNotificationSentLogHistory(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const User::Ptr& user, NotificationType notification_type, const CheckResult::Ptr& cr, const String& author,
+ const String& comment_text);
+
+ static void AddFlappingChangedLogHistory(const Checkable::Ptr& checkable);
+ static void AddEnableFlappingChangedLogHistory(const Checkable::Ptr& checkable);
+
+ /* other history */
+ static void AddFlappingChangedHistory(const Checkable::Ptr& checkable);
+ static void AddEnableFlappingChangedHistory(const Checkable::Ptr& checkable);
+ static void AddCheckableCheckHistory(const Checkable::Ptr& checkable, const CheckResult::Ptr &cr);
+ static void AddEventHandlerHistory(const Checkable::Ptr& checkable);
+ static void AddExternalCommandHistory(double time, const String& command, const std::vector<String>& arguments);
+
+private:
+ DbEvents();
+
+ static void AddCommentInternal(std::vector<DbQuery>& queries, const Comment::Ptr& comment, bool historical);
+ static void RemoveCommentInternal(std::vector<DbQuery>& queries, const Comment::Ptr& comment);
+ static void AddDowntimeInternal(std::vector<DbQuery>& queries, const Downtime::Ptr& downtime, bool historical);
+ static void RemoveDowntimeInternal(std::vector<DbQuery>& queries, const Downtime::Ptr& downtime);
+ static void EnableChangedHandlerInternal(const Checkable::Ptr& checkable, const String& fieldName, bool enabled);
+
+ static int GetHostState(const Host::Ptr& host);
+ static String GetHostStateString(const Host::Ptr& host);
+ static std::pair<unsigned long, unsigned long> ConvertTimestamp(double time);
+ static int MapNotificationReasonType(NotificationType type);
+ static int MapExternalCommandType(const String& name);
+};
+
+}
+
+#endif /* DBEVENTS_H */
diff --git a/lib/db_ido/dbobject.cpp b/lib/db_ido/dbobject.cpp
new file mode 100644
index 0000000..406bf52
--- /dev/null
+++ b/lib/db_ido/dbobject.cpp
@@ -0,0 +1,430 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/service.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "remote/endpoint.hpp"
+#include "base/configobject.hpp"
+#include "base/configtype.hpp"
+#include "base/json.hpp"
+#include "base/serializer.hpp"
+#include "base/json.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/initialize.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+boost::signals2::signal<void (const DbQuery&)> DbObject::OnQuery;
+boost::signals2::signal<void (const std::vector<DbQuery>&)> DbObject::OnMultipleQueries;
+boost::signals2::signal<void (const std::function<void (const DbObject::QueryCallbacks&)>&)> DbObject::OnMakeQueries;
+
+INITIALIZE_ONCE(&DbObject::StaticInitialize);
+
+DbObject::DbObject(intrusive_ptr<DbType> type, String name1, String name2)
+ : m_Name1(std::move(name1)), m_Name2(std::move(name2)), m_Type(std::move(type)), m_LastConfigUpdate(0), m_LastStatusUpdate(0)
+{ }
+
+void DbObject::StaticInitialize()
+{
+ /* triggered in ProcessCheckResult(), requires UpdateNextCheck() to be called before */
+ ConfigObject::OnStateChanged.connect([](const ConfigObject::Ptr& object) { StateChangedHandler(object); });
+ CustomVarObject::OnVarsChanged.connect([](const CustomVarObject::Ptr& customVar, const Value&) { VarsChangedHandler(customVar); });
+
+ /* triggered on create, update and delete objects */
+ ConfigObject::OnVersionChanged.connect([](const ConfigObject::Ptr& object, const Value&) { VersionChangedHandler(object); });
+}
+
+void DbObject::SetObject(const ConfigObject::Ptr& object)
+{
+ m_Object = object;
+}
+
+ConfigObject::Ptr DbObject::GetObject() const
+{
+ return m_Object;
+}
+
+String DbObject::GetName1() const
+{
+ return m_Name1;
+}
+
+String DbObject::GetName2() const
+{
+ return m_Name2;
+}
+
+DbType::Ptr DbObject::GetType() const
+{
+ return m_Type;
+}
+
+String DbObject::CalculateConfigHash(const Dictionary::Ptr& configFields) const
+{
+ Dictionary::Ptr configFieldsDup = configFields->ShallowClone();
+
+ {
+ ObjectLock olock(configFieldsDup);
+
+ for (const Dictionary::Pair& kv : configFieldsDup) {
+ if (kv.second.IsObjectType<ConfigObject>()) {
+ ConfigObject::Ptr obj = kv.second;
+ configFieldsDup->Set(kv.first, obj->GetName());
+ }
+ }
+ }
+
+ Array::Ptr data = new Array();
+ data->Add(configFieldsDup);
+
+ CustomVarObject::Ptr custom_var_object = dynamic_pointer_cast<CustomVarObject>(GetObject());
+
+ if (custom_var_object)
+ data->Add(custom_var_object->GetVars());
+
+ return HashValue(data);
+}
+
+String DbObject::HashValue(const Value& value)
+{
+ Value temp;
+
+ Type::Ptr type = value.GetReflectionType();
+
+ if (ConfigObject::TypeInstance->IsAssignableFrom(type))
+ temp = Serialize(value, FAConfig);
+ else
+ temp = value;
+
+ return SHA256(JsonEncode(temp));
+}
+
+void DbObject::SendConfigUpdateHeavy(const Dictionary::Ptr& configFields)
+{
+ /* update custom var config and status */
+ SendVarsConfigUpdateHeavy();
+
+ /* config attributes */
+ if (!configFields)
+ return;
+
+ ASSERT(configFields->Contains("config_hash"));
+
+ ConfigObject::Ptr object = GetObject();
+
+ DbQuery query;
+ query.Table = GetType()->GetTable() + "s";
+ query.Type = DbQueryInsert | DbQueryUpdate;
+ query.Category = DbCatConfig;
+ query.Fields = configFields;
+ query.Fields->Set(GetType()->GetIDColumn(), object);
+ query.Fields->Set("instance_id", 0); /* DbConnection class fills in real ID */
+ query.Fields->Set("config_type", 1);
+ query.WhereCriteria = new Dictionary({
+ { GetType()->GetIDColumn(), object }
+ });
+ query.Object = this;
+ query.ConfigUpdate = true;
+ OnQuery(query);
+
+ m_LastConfigUpdate = Utility::GetTime();
+
+ OnConfigUpdateHeavy();
+}
+
+void DbObject::SendConfigUpdateLight()
+{
+ OnConfigUpdateLight();
+}
+
+void DbObject::SendStatusUpdate()
+{
+ /* status attributes */
+ Dictionary::Ptr fields = GetStatusFields();
+
+ if (!fields)
+ return;
+
+ DbQuery query;
+ query.Table = GetType()->GetTable() + "status";
+ query.Type = DbQueryInsert | DbQueryUpdate;
+ query.Category = DbCatState;
+ query.Fields = fields;
+ query.Fields->Set(GetType()->GetIDColumn(), GetObject());
+
+ /* do not override endpoint_object_id for endpoints & zones */
+ if (query.Table != "endpointstatus" && query.Table != "zonestatus") {
+ String node = IcingaApplication::GetInstance()->GetNodeName();
+
+ Endpoint::Ptr endpoint = Endpoint::GetByName(node);
+ if (endpoint)
+ query.Fields->Set("endpoint_object_id", endpoint);
+ }
+
+ query.Fields->Set("instance_id", 0); /* DbConnection class fills in real ID */
+
+ query.Fields->Set("status_update_time", DbValue::FromTimestamp(Utility::GetTime()));
+ query.WhereCriteria = new Dictionary({
+ { GetType()->GetIDColumn(), GetObject() }
+ });
+ query.Object = this;
+ query.StatusUpdate = true;
+ OnQuery(query);
+
+ m_LastStatusUpdate = Utility::GetTime();
+
+ OnStatusUpdate();
+}
+
+void DbObject::SendVarsConfigUpdateHeavy()
+{
+ ConfigObject::Ptr obj = GetObject();
+
+ CustomVarObject::Ptr custom_var_object = dynamic_pointer_cast<CustomVarObject>(obj);
+
+ if (!custom_var_object)
+ return;
+
+ std::vector<DbQuery> queries;
+
+ DbQuery query1;
+ query1.Table = "customvariables";
+ query1.Type = DbQueryDelete;
+ query1.Category = DbCatConfig;
+ query1.WhereCriteria = new Dictionary({
+ { "object_id", obj }
+ });
+ queries.emplace_back(std::move(query1));
+
+ DbQuery query2;
+ query2.Table = "customvariablestatus";
+ query2.Type = DbQueryDelete;
+ query2.Category = DbCatConfig;
+ query2.WhereCriteria = new Dictionary({
+ { "object_id", obj }
+ });
+ queries.emplace_back(std::move(query2));
+
+ Dictionary::Ptr vars = custom_var_object->GetVars();
+
+ if (vars) {
+ ObjectLock olock (vars);
+
+ for (const Dictionary::Pair& kv : vars) {
+ if (kv.first.IsEmpty())
+ continue;
+
+ String value;
+ int is_json = 0;
+
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>()) {
+ value = JsonEncode(kv.second);
+ is_json = 1;
+ } else
+ value = kv.second;
+
+ DbQuery query3;
+ query3.Table = "customvariables";
+ query3.Type = DbQueryInsert;
+ query3.Category = DbCatConfig;
+ query3.Fields = new Dictionary({
+ { "varname", kv.first },
+ { "varvalue", value },
+ { "is_json", is_json },
+ { "config_type", 1 },
+ { "object_id", obj },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+ queries.emplace_back(std::move(query3));
+
+ DbQuery query4;
+ query4.Table = "customvariablestatus";
+ query4.Type = DbQueryInsert;
+ query4.Category = DbCatState;
+
+ query4.Fields = new Dictionary({
+ { "varname", kv.first },
+ { "varvalue", value },
+ { "is_json", is_json },
+ { "status_update_time", DbValue::FromTimestamp(Utility::GetTime()) },
+ { "object_id", obj },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+
+ queries.emplace_back(std::move(query4));
+ }
+ }
+
+ OnMultipleQueries(queries);
+}
+
+void DbObject::SendVarsStatusUpdate()
+{
+ ConfigObject::Ptr obj = GetObject();
+
+ CustomVarObject::Ptr custom_var_object = dynamic_pointer_cast<CustomVarObject>(obj);
+
+ if (!custom_var_object)
+ return;
+
+ Dictionary::Ptr vars = custom_var_object->GetVars();
+
+ if (vars) {
+ std::vector<DbQuery> queries;
+ ObjectLock olock (vars);
+
+ for (const Dictionary::Pair& kv : vars) {
+ if (kv.first.IsEmpty())
+ continue;
+
+ String value;
+ int is_json = 0;
+
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>()) {
+ value = JsonEncode(kv.second);
+ is_json = 1;
+ } else
+ value = kv.second;
+
+ DbQuery query;
+ query.Table = "customvariablestatus";
+ query.Type = DbQueryInsert | DbQueryUpdate;
+ query.Category = DbCatState;
+
+ query.Fields = new Dictionary({
+ { "varname", kv.first },
+ { "varvalue", value },
+ { "is_json", is_json },
+ { "status_update_time", DbValue::FromTimestamp(Utility::GetTime()) },
+ { "object_id", obj },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+
+ query.WhereCriteria = new Dictionary({
+ { "object_id", obj },
+ { "varname", kv.first }
+ });
+
+ queries.emplace_back(std::move(query));
+ }
+
+ OnMultipleQueries(queries);
+ }
+}
+
+double DbObject::GetLastConfigUpdate() const
+{
+ return m_LastConfigUpdate;
+}
+
+double DbObject::GetLastStatusUpdate() const
+{
+ return m_LastStatusUpdate;
+}
+
+void DbObject::OnConfigUpdateHeavy()
+{
+ /* Default handler does nothing. */
+}
+
+void DbObject::OnConfigUpdateLight()
+{
+ /* Default handler does nothing. */
+}
+
+void DbObject::OnStatusUpdate()
+{
+ /* Default handler does nothing. */
+}
+
+DbObject::Ptr DbObject::GetOrCreateByObject(const ConfigObject::Ptr& object)
+{
+ std::unique_lock<std::mutex> lock(GetStaticMutex());
+
+ DbObject::Ptr dbobj = object->GetExtension("DbObject");
+
+ if (dbobj)
+ return dbobj;
+
+ DbType::Ptr dbtype = DbType::GetByName(object->GetReflectionType()->GetName());
+
+ if (!dbtype)
+ return nullptr;
+
+ Service::Ptr service;
+ String name1, name2;
+
+ service = dynamic_pointer_cast<Service>(object);
+
+ if (service) {
+ Host::Ptr host = service->GetHost();
+
+ name1 = service->GetHost()->GetName();
+ name2 = service->GetShortName();
+ } else {
+ if (object->GetReflectionType() == CheckCommand::TypeInstance ||
+ object->GetReflectionType() == EventCommand::TypeInstance ||
+ object->GetReflectionType() == NotificationCommand::TypeInstance) {
+ Command::Ptr command = dynamic_pointer_cast<Command>(object);
+ name1 = CompatUtility::GetCommandName(command);
+ }
+ else
+ name1 = object->GetName();
+ }
+
+ dbobj = dbtype->GetOrCreateObjectByName(name1, name2);
+
+ dbobj->SetObject(object);
+ object->SetExtension("DbObject", dbobj);
+
+ return dbobj;
+}
+
+void DbObject::StateChangedHandler(const ConfigObject::Ptr& object)
+{
+ DbObject::Ptr dbobj = GetOrCreateByObject(object);
+
+ if (!dbobj)
+ return;
+
+ dbobj->SendStatusUpdate();
+}
+
+void DbObject::VarsChangedHandler(const CustomVarObject::Ptr& object)
+{
+ DbObject::Ptr dbobj = GetOrCreateByObject(object);
+
+ if (!dbobj)
+ return;
+
+ dbobj->SendVarsStatusUpdate();
+}
+
+void DbObject::VersionChangedHandler(const ConfigObject::Ptr& object)
+{
+ DbObject::Ptr dbobj = DbObject::GetOrCreateByObject(object);
+
+ if (dbobj) {
+ Dictionary::Ptr configFields = dbobj->GetConfigFields();
+ String configHash = dbobj->CalculateConfigHash(configFields);
+ configFields->Set("config_hash", configHash);
+
+ dbobj->SendConfigUpdateHeavy(configFields);
+ dbobj->SendStatusUpdate();
+ }
+}
+
+std::mutex& DbObject::GetStaticMutex()
+{
+ static std::mutex mutex;
+ return mutex;
+}
diff --git a/lib/db_ido/dbobject.hpp b/lib/db_ido/dbobject.hpp
new file mode 100644
index 0000000..399b77d
--- /dev/null
+++ b/lib/db_ido/dbobject.hpp
@@ -0,0 +1,112 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DBOBJECT_H
+#define DBOBJECT_H
+
+#include "db_ido/i2-db_ido.hpp"
+#include "db_ido/dbreference.hpp"
+#include "db_ido/dbquery.hpp"
+#include "db_ido/dbtype.hpp"
+#include "icinga/customvarobject.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+enum DbObjectUpdateType
+{
+ DbObjectCreated,
+ DbObjectRemoved
+};
+
+enum DbObjectType
+{
+ DbObjectTypeHost = 1,
+ DbObjectTypeService = 2,
+ DbObjectTypeHostGroup = 3,
+ DbObjectTypeServiceGroup = 4,
+ DbObjectTypeHostEscalation = 5,
+ DbObjectTypeServiceEscalation = 6,
+ DbObjectTypeHostDependency = 7,
+ DbObjectTypeServiceDependency = 8,
+ DbObjectTypeTimePeriod = 9,
+ DbObjectTypeContact = 10,
+ DbObjectTypeContactGroup = 11,
+ DbObjectTypeCommand = 12,
+ DbObjectTypeEndpoint = 13,
+ DbObjectTypeZone = 14,
+};
+
+/**
+ * A database object.
+ *
+ * @ingroup ido
+ */
+class DbObject : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(DbObject);
+
+ static void StaticInitialize();
+
+ void SetObject(const ConfigObject::Ptr& object);
+ ConfigObject::Ptr GetObject() const;
+
+ String GetName1() const;
+ String GetName2() const;
+ intrusive_ptr<DbType> GetType() const;
+
+ virtual Dictionary::Ptr GetConfigFields() const = 0;
+ virtual Dictionary::Ptr GetStatusFields() const = 0;
+
+ static DbObject::Ptr GetOrCreateByObject(const ConfigObject::Ptr& object);
+
+ struct QueryCallbacks {
+ std::function<void(const DbQuery&)> Query;
+ std::function<void(const std::vector<DbQuery>&)> MultipleQueries;
+ };
+
+ static boost::signals2::signal<void (const DbQuery&)> OnQuery;
+ static boost::signals2::signal<void (const std::vector<DbQuery>&)> OnMultipleQueries;
+ static boost::signals2::signal<void (const std::function<void (const QueryCallbacks&)>&)> OnMakeQueries;
+
+ void SendConfigUpdateHeavy(const Dictionary::Ptr& configFields);
+ void SendConfigUpdateLight();
+ void SendStatusUpdate();
+ void SendVarsConfigUpdateHeavy();
+ void SendVarsStatusUpdate();
+
+ double GetLastConfigUpdate() const;
+ double GetLastStatusUpdate() const;
+
+ virtual String CalculateConfigHash(const Dictionary::Ptr& configFields) const;
+
+protected:
+ DbObject(intrusive_ptr<DbType> type, String name1, String name2);
+
+ virtual void OnConfigUpdateHeavy();
+ virtual void OnConfigUpdateLight();
+ virtual void OnStatusUpdate();
+
+ static String HashValue(const Value& value);
+
+private:
+ String m_Name1;
+ String m_Name2;
+ intrusive_ptr<DbType> m_Type;
+ ConfigObject::Ptr m_Object;
+ double m_LastConfigUpdate;
+ double m_LastStatusUpdate;
+
+ static void StateChangedHandler(const ConfigObject::Ptr& object);
+ static void VarsChangedHandler(const CustomVarObject::Ptr& object);
+ static void VersionChangedHandler(const ConfigObject::Ptr& object);
+
+ static std::mutex& GetStaticMutex();
+
+ friend class DbType;
+};
+
+}
+
+#endif /* DBOBJECT_H */
diff --git a/lib/db_ido/dbquery.cpp b/lib/db_ido/dbquery.cpp
new file mode 100644
index 0000000..1de2928
--- /dev/null
+++ b/lib/db_ido/dbquery.cpp
@@ -0,0 +1,52 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbquery.hpp"
+#include "base/initialize.hpp"
+#include "base/scriptglobal.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE(&DbQuery::StaticInitialize);
+
+std::map<String, int> DbQuery::m_CategoryFilterMap;
+
+void DbQuery::StaticInitialize()
+{
+ ScriptGlobal::Set("Icinga.DbCatConfig", DbCatConfig);
+ ScriptGlobal::Set("Icinga.DbCatState", DbCatState);
+ ScriptGlobal::Set("Icinga.DbCatAcknowledgement", DbCatAcknowledgement);
+ ScriptGlobal::Set("Icinga.DbCatComment", DbCatComment);
+ ScriptGlobal::Set("Icinga.DbCatDowntime", DbCatDowntime);
+ ScriptGlobal::Set("Icinga.DbCatEventHandler", DbCatEventHandler);
+ ScriptGlobal::Set("Icinga.DbCatExternalCommand", DbCatExternalCommand);
+ ScriptGlobal::Set("Icinga.DbCatFlapping", DbCatFlapping);
+ ScriptGlobal::Set("Icinga.DbCatCheck", DbCatCheck);
+ ScriptGlobal::Set("Icinga.DbCatLog", DbCatLog);
+ ScriptGlobal::Set("Icinga.DbCatNotification", DbCatNotification);
+ ScriptGlobal::Set("Icinga.DbCatProgramStatus", DbCatProgramStatus);
+ ScriptGlobal::Set("Icinga.DbCatRetention", DbCatRetention);
+ ScriptGlobal::Set("Icinga.DbCatStateHistory", DbCatStateHistory);
+
+ ScriptGlobal::Set("Icinga.DbCatEverything", DbCatEverything);
+
+ m_CategoryFilterMap["DbCatConfig"] = DbCatConfig;
+ m_CategoryFilterMap["DbCatState"] = DbCatState;
+ m_CategoryFilterMap["DbCatAcknowledgement"] = DbCatAcknowledgement;
+ m_CategoryFilterMap["DbCatComment"] = DbCatComment;
+ m_CategoryFilterMap["DbCatDowntime"] = DbCatDowntime;
+ m_CategoryFilterMap["DbCatEventHandler"] = DbCatEventHandler;
+ m_CategoryFilterMap["DbCatExternalCommand"] = DbCatExternalCommand;
+ m_CategoryFilterMap["DbCatFlapping"] = DbCatFlapping;
+ m_CategoryFilterMap["DbCatCheck"] = DbCatCheck;
+ m_CategoryFilterMap["DbCatLog"] = DbCatLog;
+ m_CategoryFilterMap["DbCatNotification"] = DbCatNotification;
+ m_CategoryFilterMap["DbCatProgramStatus"] = DbCatProgramStatus;
+ m_CategoryFilterMap["DbCatRetention"] = DbCatRetention;
+ m_CategoryFilterMap["DbCatStateHistory"] = DbCatStateHistory;
+ m_CategoryFilterMap["DbCatEverything"] = DbCatEverything;
+}
+
+const std::map<String, int>& DbQuery::GetCategoryFilterMap()
+{
+ return m_CategoryFilterMap;
+}
diff --git a/lib/db_ido/dbquery.hpp b/lib/db_ido/dbquery.hpp
new file mode 100644
index 0000000..fecb2e3
--- /dev/null
+++ b/lib/db_ido/dbquery.hpp
@@ -0,0 +1,72 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DBQUERY_H
+#define DBQUERY_H
+
+#include "db_ido/i2-db_ido.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "icinga/customvarobject.hpp"
+#include "base/dictionary.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+enum DbQueryType
+{
+ DbQueryInsert = 1,
+ DbQueryUpdate = 2,
+ DbQueryDelete = 4,
+ DbQueryNewTransaction = 8
+};
+
+enum DbQueryCategory
+{
+ DbCatInvalid = 0, //-1 is required for DbCatEverything
+ DbCatEverything = ~0,
+
+ DbCatConfig = 1,
+ DbCatState = 2,
+ DbCatAcknowledgement = 4,
+ DbCatComment = 8,
+ DbCatDowntime = 16,
+ DbCatEventHandler = 32,
+ DbCatExternalCommand = 64,
+ DbCatFlapping = 128,
+ DbCatCheck = 256,
+ DbCatLog = 512,
+ DbCatNotification = 1024,
+ DbCatProgramStatus = 2048,
+ DbCatRetention = 4096,
+ DbCatStateHistory = 8192
+};
+
+class DbObject;
+
+struct DbQuery
+{
+ int Type{0};
+ DbQueryCategory Category{DbCatInvalid};
+ String Table;
+ String IdColumn;
+ Dictionary::Ptr Fields;
+ Dictionary::Ptr WhereCriteria;
+ intrusive_ptr<DbObject> Object;
+ DbValue::Ptr NotificationInsertID;
+ bool ConfigUpdate{false};
+ bool StatusUpdate{false};
+ WorkQueuePriority Priority{PriorityNormal};
+
+ static void StaticInitialize();
+
+ static const std::map<String, int>& GetCategoryFilterMap();
+
+private:
+ static std::map<String, int> m_CategoryFilterMap;
+};
+
+}
+
+#endif /* DBQUERY_H */
+
+#include "db_ido/dbobject.hpp"
diff --git a/lib/db_ido/dbreference.cpp b/lib/db_ido/dbreference.cpp
new file mode 100644
index 0000000..e8f13c0
--- /dev/null
+++ b/lib/db_ido/dbreference.cpp
@@ -0,0 +1,19 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "dbreference.hpp"
+
+using namespace icinga;
+
+DbReference::DbReference(long id)
+ : m_Id(id)
+{ }
+
+bool DbReference::IsValid() const
+{
+ return (m_Id != -1);
+}
+
+DbReference::operator long() const
+{
+ return m_Id;
+}
diff --git a/lib/db_ido/dbreference.hpp b/lib/db_ido/dbreference.hpp
new file mode 100644
index 0000000..70edf9a
--- /dev/null
+++ b/lib/db_ido/dbreference.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DBREFERENCE_H
+#define DBREFERENCE_H
+
+#include "db_ido/i2-db_ido.hpp"
+
+namespace icinga
+{
+
+/**
+ * A database reference.
+ *
+ * @ingroup ido
+ */
+struct DbReference
+{
+public:
+ DbReference() = default;
+ DbReference(long id);
+
+ bool IsValid() const;
+ operator long() const;
+private:
+ long m_Id{-1};
+};
+
+}
+
+#endif /* DBREFERENCE_H */
diff --git a/lib/db_ido/dbtype.cpp b/lib/db_ido/dbtype.cpp
new file mode 100644
index 0000000..bc45dcb
--- /dev/null
+++ b/lib/db_ido/dbtype.cpp
@@ -0,0 +1,141 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbconnection.hpp"
+#include "base/objectlock.hpp"
+#include "base/debug.hpp"
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+DbType::DbType(String name, String table, long tid, String idcolumn, DbType::ObjectFactory factory)
+ : m_Name(std::move(name)), m_Table(std::move(table)), m_TypeID(tid), m_IDColumn(std::move(idcolumn)), m_ObjectFactory(std::move(factory))
+{ }
+
+String DbType::GetName() const
+{
+ return m_Name;
+}
+
+String DbType::GetTable() const
+{
+ return m_Table;
+}
+
+long DbType::GetTypeID() const
+{
+ return m_TypeID;
+}
+
+String DbType::GetIDColumn() const
+{
+ return m_IDColumn;
+}
+
+void DbType::RegisterType(const DbType::Ptr& type)
+{
+ std::unique_lock<std::mutex> lock(GetStaticMutex());
+ GetTypes()[type->GetName()] = type;
+}
+
+DbType::Ptr DbType::GetByName(const String& name)
+{
+ String typeName;
+
+ if (name == "CheckCommand" || name == "NotificationCommand" || name == "EventCommand")
+ typeName = "Command";
+ else
+ typeName = name;
+
+ std::unique_lock<std::mutex> lock(GetStaticMutex());
+ auto it = GetTypes().find(typeName);
+
+ if (it == GetTypes().end())
+ return nullptr;
+
+ return it->second;
+}
+
+DbType::Ptr DbType::GetByID(long tid)
+{
+ std::unique_lock<std::mutex> lock(GetStaticMutex());
+
+ for (const TypeMap::value_type& kv : GetTypes()) {
+ if (kv.second->GetTypeID() == tid)
+ return kv.second;
+ }
+
+ return nullptr;
+}
+
+DbObject::Ptr DbType::GetOrCreateObjectByName(const String& name1, const String& name2)
+{
+ ObjectLock olock(this);
+
+ auto it = m_Objects.find(std::make_pair(name1, name2));
+
+ if (it != m_Objects.end())
+ return it->second;
+
+ DbObject::Ptr dbobj = m_ObjectFactory(this, name1, name2);
+ m_Objects[std::make_pair(name1, name2)] = dbobj;
+
+ String objName = name1;
+
+ if (!name2.IsEmpty())
+ objName += "!" + name2;
+
+ String objType = m_Name;
+
+ if (m_TypeID == DbObjectTypeCommand) {
+ if (objName.SubStr(0, 6) == "check_") {
+ objType = "CheckCommand";
+ objName = objName.SubStr(6);
+ } else if (objName.SubStr(0, 13) == "notification_") {
+ objType = "NotificationCommand";
+ objName = objName.SubStr(13);
+ } else if (objName.SubStr(0, 6) == "event_") {
+ objType = "EventCommand";
+ objName = objName.SubStr(6);
+ }
+ }
+
+ dbobj->SetObject(ConfigObject::GetObject(objType, objName));
+
+ return dbobj;
+}
+
+std::mutex& DbType::GetStaticMutex()
+{
+ static std::mutex mutex;
+ return mutex;
+}
+
+/**
+ * Caller must hold static mutex.
+ */
+DbType::TypeMap& DbType::GetTypes()
+{
+ static DbType::TypeMap tm;
+ return tm;
+}
+
+std::set<DbType::Ptr> DbType::GetAllTypes()
+{
+ std::set<DbType::Ptr> result;
+
+ {
+ std::unique_lock<std::mutex> lock(GetStaticMutex());
+ for (const auto& kv : GetTypes()) {
+ result.insert(kv.second);
+ }
+ }
+
+ return result;
+}
+
+DbTypeRegistry *DbTypeRegistry::GetInstance()
+{
+ return Singleton<DbTypeRegistry>::GetInstance();
+}
+
diff --git a/lib/db_ido/dbtype.hpp b/lib/db_ido/dbtype.hpp
new file mode 100644
index 0000000..c8ebc45
--- /dev/null
+++ b/lib/db_ido/dbtype.hpp
@@ -0,0 +1,90 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DBTYPE_H
+#define DBTYPE_H
+
+#include "db_ido/i2-db_ido.hpp"
+#include "base/object.hpp"
+#include "base/registry.hpp"
+#include "base/singleton.hpp"
+#include <set>
+
+namespace icinga
+{
+
+class DbObject;
+
+/**
+ * A database object type.
+ *
+ * @ingroup ido
+ */
+class DbType final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(DbType);
+
+ typedef std::function<intrusive_ptr<DbObject> (const intrusive_ptr<DbType>&, const String&, const String&)> ObjectFactory;
+ typedef std::map<String, DbType::Ptr> TypeMap;
+ typedef std::map<std::pair<String, String>, intrusive_ptr<DbObject> > ObjectMap;
+
+ DbType(String name, String table, long tid, String idcolumn, ObjectFactory factory);
+
+ String GetName() const;
+ String GetTable() const;
+ long GetTypeID() const;
+ String GetIDColumn() const;
+
+ static void RegisterType(const DbType::Ptr& type);
+
+ static DbType::Ptr GetByName(const String& name);
+ static DbType::Ptr GetByID(long tid);
+
+ intrusive_ptr<DbObject> GetOrCreateObjectByName(const String& name1, const String& name2);
+
+ static std::set<DbType::Ptr> GetAllTypes();
+
+private:
+ String m_Name;
+ String m_Table;
+ long m_TypeID;
+ String m_IDColumn;
+ ObjectFactory m_ObjectFactory;
+
+ static std::mutex& GetStaticMutex();
+ static TypeMap& GetTypes();
+
+ ObjectMap m_Objects;
+};
+
+/**
+ * A registry for DbType objects.
+ *
+ * @ingroup ido
+ */
+class DbTypeRegistry : public Registry<DbTypeRegistry, DbType::Ptr>
+{
+public:
+ static DbTypeRegistry *GetInstance();
+};
+
+/**
+ * Factory function for DbObject-based classes.
+ *
+ * @ingroup ido
+ */
+template<typename T>
+intrusive_ptr<T> DbObjectFactory(const DbType::Ptr& type, const String& name1, const String& name2)
+{
+ return new T(type, name1, name2);
+}
+
+#define REGISTER_DBTYPE(name, table, tid, idcolumn, type) \
+ INITIALIZE_ONCE([]() { \
+ DbType::Ptr dbtype = new DbType(#name, table, tid, idcolumn, DbObjectFactory<type>); \
+ DbType::RegisterType(dbtype); \
+ })
+
+}
+
+#endif /* DBTYPE_H */
diff --git a/lib/db_ido/dbvalue.cpp b/lib/db_ido/dbvalue.cpp
new file mode 100644
index 0000000..e1e3e6c
--- /dev/null
+++ b/lib/db_ido/dbvalue.cpp
@@ -0,0 +1,69 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbvalue.hpp"
+
+using namespace icinga;
+
+DbValue::DbValue(DbValueType type, Value value)
+ : m_Type(type), m_Value(std::move(value))
+{ }
+
+Value DbValue::FromTimestamp(const Value& ts)
+{
+ if (ts.IsEmpty() || ts == 0)
+ return Empty;
+
+ return new DbValue(DbValueTimestamp, ts);
+}
+
+Value DbValue::FromValue(const Value& value)
+{
+ return value;
+}
+
+Value DbValue::FromObjectInsertID(const Value& value)
+{
+ return new DbValue(DbValueObjectInsertID, value);
+}
+
+bool DbValue::IsTimestamp(const Value& value)
+{
+ if (!value.IsObjectType<DbValue>())
+ return false;
+
+ DbValue::Ptr dbv = value;
+ return dbv->GetType() == DbValueTimestamp;
+}
+
+bool DbValue::IsObjectInsertID(const Value& value)
+{
+ if (!value.IsObjectType<DbValue>())
+ return false;
+
+ DbValue::Ptr dbv = value;
+ return dbv->GetType() == DbValueObjectInsertID;
+}
+
+Value DbValue::ExtractValue(const Value& value)
+{
+ if (!value.IsObjectType<DbValue>())
+ return value;
+
+ DbValue::Ptr dbv = value;
+ return dbv->GetValue();
+}
+
+DbValueType DbValue::GetType() const
+{
+ return m_Type;
+}
+
+Value DbValue::GetValue() const
+{
+ return m_Value;
+}
+
+void DbValue::SetValue(const Value& value)
+{
+ m_Value = value;
+}
diff --git a/lib/db_ido/dbvalue.hpp b/lib/db_ido/dbvalue.hpp
new file mode 100644
index 0000000..cb59e3a
--- /dev/null
+++ b/lib/db_ido/dbvalue.hpp
@@ -0,0 +1,52 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DBVALUE_H
+#define DBVALUE_H
+
+#include "db_ido/i2-db_ido.hpp"
+#include "base/object.hpp"
+#include "base/value.hpp"
+
+namespace icinga
+{
+
+enum DbValueType
+{
+ DbValueTimestamp,
+ DbValueObjectInsertID
+};
+
+/**
+ * A database value.
+ *
+ * @ingroup ido
+ */
+struct DbValue final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(DbValue);
+
+ DbValue(DbValueType type, Value value);
+
+ static Value FromTimestamp(const Value& ts);
+ static Value FromValue(const Value& value);
+ static Value FromObjectInsertID(const Value& value);
+
+ static bool IsTimestamp(const Value& value);
+ static bool IsObjectInsertID(const Value& value);
+
+ static Value ExtractValue(const Value& value);
+
+ DbValueType GetType() const;
+
+ Value GetValue() const;
+ void SetValue(const Value& value);
+
+private:
+ DbValueType m_Type;
+ Value m_Value;
+};
+
+}
+
+#endif /* DBVALUE_H */
diff --git a/lib/db_ido/endpointdbobject.cpp b/lib/db_ido/endpointdbobject.cpp
new file mode 100644
index 0000000..ea16dd7
--- /dev/null
+++ b/lib/db_ido/endpointdbobject.cpp
@@ -0,0 +1,91 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/endpointdbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/objectlock.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_DBTYPE(Endpoint, "endpoint", DbObjectTypeEndpoint, "endpoint_object_id", EndpointDbObject);
+
+INITIALIZE_ONCE(&EndpointDbObject::StaticInitialize);
+
+void EndpointDbObject::StaticInitialize()
+{
+ Endpoint::OnConnected.connect([](const Endpoint::Ptr& endpoint, const JsonRpcConnection::Ptr&) { EndpointDbObject::UpdateConnectedStatus(endpoint); });
+ Endpoint::OnDisconnected.connect([](const Endpoint::Ptr& endpoint, const JsonRpcConnection::Ptr&) { EndpointDbObject::UpdateConnectedStatus(endpoint); });
+}
+
+EndpointDbObject::EndpointDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr EndpointDbObject::GetConfigFields() const
+{
+ Endpoint::Ptr endpoint = static_pointer_cast<Endpoint>(GetObject());
+
+ return new Dictionary({
+ { "identity", endpoint->GetName() },
+ { "node", IcingaApplication::GetInstance()->GetNodeName() },
+ { "zone_object_id", endpoint->GetZone() }
+ });
+}
+
+Dictionary::Ptr EndpointDbObject::GetStatusFields() const
+{
+ Endpoint::Ptr endpoint = static_pointer_cast<Endpoint>(GetObject());
+
+
+ Log(LogDebug, "EndpointDbObject")
+ << "update status for endpoint '" << endpoint->GetName() << "'";
+
+ return new Dictionary({
+ { "identity", endpoint->GetName() },
+ { "node", IcingaApplication::GetInstance()->GetNodeName() },
+ { "zone_object_id", endpoint->GetZone() },
+ { "is_connected", EndpointIsConnected(endpoint) }
+ });
+}
+
+void EndpointDbObject::UpdateConnectedStatus(const Endpoint::Ptr& endpoint)
+{
+ bool connected = EndpointIsConnected(endpoint);
+
+ Log(LogDebug, "EndpointDbObject")
+ << "update is_connected=" << connected << " for endpoint '" << endpoint->GetName() << "'";
+
+ DbQuery query1;
+ query1.Table = "endpointstatus";
+ query1.Type = DbQueryUpdate;
+ query1.Category = DbCatState;
+
+ query1.Fields = new Dictionary({
+ { "is_connected", (connected ? 1 : 0) },
+ { "status_update_time", DbValue::FromTimestamp(Utility::GetTime()) }
+ });
+
+ query1.WhereCriteria = new Dictionary({
+ { "endpoint_object_id", endpoint },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+
+ OnQuery(query1);
+}
+
+int EndpointDbObject::EndpointIsConnected(const Endpoint::Ptr& endpoint)
+{
+ unsigned int is_connected = endpoint->GetConnected() ? 1 : 0;
+
+ /* if identity is equal to node, fake is_connected */
+ if (endpoint->GetName() == IcingaApplication::GetInstance()->GetNodeName())
+ is_connected = 1;
+
+ return is_connected;
+}
diff --git a/lib/db_ido/endpointdbobject.hpp b/lib/db_ido/endpointdbobject.hpp
new file mode 100644
index 0000000..e4fba36
--- /dev/null
+++ b/lib/db_ido/endpointdbobject.hpp
@@ -0,0 +1,37 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ENDPOINTDBOBJECT_H
+#define ENDPOINTDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "base/configobject.hpp"
+#include "remote/endpoint.hpp"
+
+namespace icinga
+{
+
+/**
+ * A Command database object.
+ *
+ * @ingroup ido
+ */
+class EndpointDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EndpointDbObject);
+
+ EndpointDbObject(const intrusive_ptr<DbType>& type, const String& name1, const String& name2);
+
+ static void StaticInitialize();
+
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+
+private:
+ static void UpdateConnectedStatus(const Endpoint::Ptr& endpoint);
+ static int EndpointIsConnected(const Endpoint::Ptr& endpoint);
+};
+
+}
+
+#endif /* ENDPOINTDBOBJECT_H */
diff --git a/lib/db_ido/hostdbobject.cpp b/lib/db_ido/hostdbobject.cpp
new file mode 100644
index 0000000..60d1a99
--- /dev/null
+++ b/lib/db_ido/hostdbobject.cpp
@@ -0,0 +1,423 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/hostdbobject.hpp"
+#include "db_ido/hostgroupdbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "db_ido/dbevents.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/dependency.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/json.hpp"
+
+using namespace icinga;
+
+REGISTER_DBTYPE(Host, "host", DbObjectTypeHost, "host_object_id", HostDbObject);
+
+HostDbObject::HostDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr HostDbObject::GetConfigFields() const
+{
+ Dictionary::Ptr fields = new Dictionary();
+ Host::Ptr host = static_pointer_cast<Host>(GetObject());
+
+ /* Compatibility fallback. */
+ String displayName = host->GetDisplayName();
+
+ unsigned long notificationStateFilter = CompatUtility::GetCheckableNotificationTypeFilter(host);
+ unsigned long notificationTypeFilter = CompatUtility::GetCheckableNotificationTypeFilter(host);
+
+ return new Dictionary({
+ { "alias", !displayName.IsEmpty() ? displayName : host->GetName() },
+ { "display_name", displayName },
+ { "address", host->GetAddress() },
+ { "address6", host->GetAddress6() },
+ { "check_command_object_id", host->GetCheckCommand() },
+ { "eventhandler_command_object_id", host->GetEventCommand() },
+ { "check_timeperiod_object_id", host->GetCheckPeriod() },
+ { "check_interval", host->GetCheckInterval() / 60.0 },
+ { "retry_interval", host->GetRetryInterval() / 60.0 },
+ { "max_check_attempts", host->GetMaxCheckAttempts() },
+ { "flap_detection_enabled", host->GetEnableFlapping() },
+ { "low_flap_threshold", host->GetFlappingThresholdLow() },
+ { "high_flap_threshold", host->GetFlappingThresholdLow() },
+ { "process_performance_data", host->GetEnablePerfdata() },
+ { "freshness_checks_enabled", 1 },
+ { "freshness_threshold", Convert::ToLong(host->GetCheckInterval()) },
+ { "event_handler_enabled", host->GetEnableEventHandler() },
+ { "passive_checks_enabled", host->GetEnablePassiveChecks() },
+ { "active_checks_enabled", host->GetEnableActiveChecks() },
+ { "notifications_enabled", host->GetEnableNotifications() },
+ { "notes", host->GetNotes() },
+ { "notes_url", host->GetNotesUrl() },
+ { "action_url", host->GetActionUrl() },
+ { "icon_image", host->GetIconImage() },
+ { "icon_image_alt", host->GetIconImageAlt() },
+ { "notification_interval", CompatUtility::GetCheckableNotificationNotificationInterval(host) },
+ { "notify_on_down", (notificationStateFilter & (ServiceWarning | ServiceCritical)) ? 1 : 0 },
+ { "notify_on_unreachable", 1 }, /* We don't have this filter and state, and as such we don't filter such notifications. */
+ { "notify_on_recovery", (notificationTypeFilter & NotificationRecovery) ? 1 : 0 },
+ { "notify_on_flapping", (notificationTypeFilter & (NotificationFlappingStart | NotificationFlappingEnd)) ? 1 : 0 },
+ { "notify_on_downtime", (notificationTypeFilter & (NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved)) ? 1 : 0 }
+ });
+}
+
+Dictionary::Ptr HostDbObject::GetStatusFields() const
+{
+ Dictionary::Ptr fields = new Dictionary();
+ Host::Ptr host = static_pointer_cast<Host>(GetObject());
+
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (cr) {
+ fields->Set("output", CompatUtility::GetCheckResultOutput(cr));
+ fields->Set("long_output", CompatUtility::GetCheckResultLongOutput(cr));
+ fields->Set("perfdata", PluginUtility::FormatPerfdata(cr->GetPerformanceData()));
+ fields->Set("check_source", cr->GetCheckSource());
+ fields->Set("latency", cr->CalculateLatency());
+ fields->Set("execution_time", cr->CalculateExecutionTime());
+ }
+
+ int currentState = host->GetState();
+
+ if (currentState != HostUp && !host->GetLastReachable())
+ currentState = 2; /* hardcoded compat state */
+
+ fields->Set("current_state", currentState);
+ fields->Set("has_been_checked", host->HasBeenChecked());
+ fields->Set("should_be_scheduled", host->GetEnableActiveChecks());
+ fields->Set("current_check_attempt", host->GetCheckAttempt());
+ fields->Set("max_check_attempts", host->GetMaxCheckAttempts());
+ fields->Set("last_check", DbValue::FromTimestamp(host->GetLastCheck()));
+ fields->Set("next_check", DbValue::FromTimestamp(host->GetNextCheck()));
+ fields->Set("check_type", !host->GetEnableActiveChecks()); /* 0 .. active, 1 .. passive */
+ fields->Set("last_state_change", DbValue::FromTimestamp(host->GetLastStateChange()));
+ fields->Set("last_hard_state_change", DbValue::FromTimestamp(host->GetLastHardStateChange()));
+ fields->Set("last_hard_state", host->GetLastHardState());
+ fields->Set("last_time_up", DbValue::FromTimestamp(host->GetLastStateUp()));
+ fields->Set("last_time_down", DbValue::FromTimestamp(host->GetLastStateDown()));
+ fields->Set("last_time_unreachable", DbValue::FromTimestamp(host->GetLastStateUnreachable()));
+ fields->Set("state_type", host->GetStateType());
+ fields->Set("notifications_enabled", host->GetEnableNotifications());
+ fields->Set("problem_has_been_acknowledged", host->GetAcknowledgement() != AcknowledgementNone);
+ fields->Set("acknowledgement_type", host->GetAcknowledgement());
+ fields->Set("passive_checks_enabled", host->GetEnablePassiveChecks());
+ fields->Set("active_checks_enabled", host->GetEnableActiveChecks());
+ fields->Set("event_handler_enabled", host->GetEnableEventHandler());
+ fields->Set("flap_detection_enabled", host->GetEnableFlapping());
+ fields->Set("is_flapping", host->IsFlapping());
+ fields->Set("percent_state_change", host->GetFlappingCurrent());
+ fields->Set("scheduled_downtime_depth", host->GetDowntimeDepth());
+ fields->Set("process_performance_data", host->GetEnablePerfdata());
+ fields->Set("normal_check_interval", host->GetCheckInterval() / 60.0);
+ fields->Set("retry_check_interval", host->GetRetryInterval() / 60.0);
+ fields->Set("check_timeperiod_object_id", host->GetCheckPeriod());
+ fields->Set("is_reachable", host->GetLastReachable());
+ fields->Set("original_attributes", JsonEncode(host->GetOriginalAttributes()));
+
+ fields->Set("current_notification_number", CompatUtility::GetCheckableNotificationNotificationNumber(host));
+ fields->Set("last_notification", DbValue::FromTimestamp(CompatUtility::GetCheckableNotificationLastNotification(host)));
+ fields->Set("next_notification", DbValue::FromTimestamp(CompatUtility::GetCheckableNotificationNextNotification(host)));
+
+ EventCommand::Ptr eventCommand = host->GetEventCommand();
+
+ if (eventCommand)
+ fields->Set("event_handler", eventCommand->GetName());
+
+ CheckCommand::Ptr checkCommand = host->GetCheckCommand();
+
+ if (checkCommand)
+ fields->Set("check_command", checkCommand->GetName());
+
+ return fields;
+}
+
+void HostDbObject::OnConfigUpdateHeavy()
+{
+ Host::Ptr host = static_pointer_cast<Host>(GetObject());
+
+ /* groups */
+ Array::Ptr groups = host->GetGroups();
+
+ std::vector<DbQuery> queries;
+
+ DbQuery query1;
+ query1.Table = DbType::GetByName("HostGroup")->GetTable() + "_members";
+ query1.Type = DbQueryDelete;
+ query1.Category = DbCatConfig;
+ query1.WhereCriteria = new Dictionary();
+ query1.WhereCriteria->Set("host_object_id", host);
+ queries.emplace_back(std::move(query1));
+
+ if (groups) {
+ ObjectLock olock(groups);
+ for (const String& groupName : groups) {
+ HostGroup::Ptr group = HostGroup::GetByName(groupName);
+
+ DbQuery query2;
+ query2.Table = DbType::GetByName("HostGroup")->GetTable() + "_members";
+ query2.Type = DbQueryInsert;
+ query2.Category = DbCatConfig;
+ query2.Fields = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "hostgroup_id", DbValue::FromObjectInsertID(group) },
+ { "host_object_id", host }
+ });
+ query2.WhereCriteria = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "hostgroup_id", DbValue::FromObjectInsertID(group) },
+ { "host_object_id", host }
+ });
+ queries.emplace_back(std::move(query2));
+ }
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ queries.clear();
+
+ DbQuery query2;
+ query2.Table = GetType()->GetTable() + "_parenthosts";
+ query2.Type = DbQueryDelete;
+ query2.Category = DbCatConfig;
+ query2.WhereCriteria = new Dictionary({
+ { GetType()->GetTable() + "_id", DbValue::FromObjectInsertID(GetObject()) }
+ });
+ queries.emplace_back(std::move(query2));
+
+ /* parents */
+ for (const Checkable::Ptr& checkable : host->GetParents()) {
+ Host::Ptr parent = dynamic_pointer_cast<Host>(checkable);
+
+ if (!parent)
+ continue;
+
+ Log(LogDebug, "HostDbObject")
+ << "host parents: " << parent->GetName();
+
+ /* parents: host_id, parent_host_object_id */
+ DbQuery query1;
+ query1.Table = GetType()->GetTable() + "_parenthosts";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatConfig;
+ query1.Fields = new Dictionary({
+ { GetType()->GetTable() + "_id", DbValue::FromObjectInsertID(GetObject()) },
+ { "parent_host_object_id", parent },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+ queries.emplace_back(std::move(query1));
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ /* host dependencies */
+ Log(LogDebug, "HostDbObject")
+ << "host dependencies for '" << host->GetName() << "'";
+
+ queries.clear();
+
+ DbQuery query3;
+ query3.Table = GetType()->GetTable() + "dependencies";
+ query3.Type = DbQueryDelete;
+ query3.Category = DbCatConfig;
+ query3.WhereCriteria = new Dictionary({
+ { "dependent_host_object_id", host }
+ });
+ queries.emplace_back(std::move(query3));
+
+ for (const Dependency::Ptr& dep : host->GetDependencies()) {
+ Checkable::Ptr parent = dep->GetParent();
+
+ if (!parent) {
+ Log(LogDebug, "HostDbObject")
+ << "Missing parent for dependency '" << dep->GetName() << "'.";
+ continue;
+ }
+
+ int stateFilter = dep->GetStateFilter();
+
+ Log(LogDebug, "HostDbObject")
+ << "parent host: " << parent->GetName();
+
+ DbQuery query2;
+ query2.Table = GetType()->GetTable() + "dependencies";
+ query2.Type = DbQueryInsert;
+ query2.Category = DbCatConfig;
+ query2.Fields = new Dictionary({
+ { "host_object_id", parent },
+ { "dependent_host_object_id", host },
+ { "inherits_parent", 1 },
+ { "timeperiod_object_id", dep->GetPeriod() },
+ { "fail_on_up", (stateFilter & StateFilterUp) ? 1 : 0 },
+ { "fail_on_down", (stateFilter & StateFilterDown) ? 1 : 0 },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+ queries.emplace_back(std::move(query2));
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ Log(LogDebug, "HostDbObject")
+ << "host contacts: " << host->GetName();
+
+ queries.clear();
+
+ DbQuery query4;
+ query4.Table = GetType()->GetTable() + "_contacts";
+ query4.Type = DbQueryDelete;
+ query4.Category = DbCatConfig;
+ query4.WhereCriteria = new Dictionary({
+ { "host_id", DbValue::FromObjectInsertID(host) }
+ });
+ queries.emplace_back(std::move(query4));
+
+ for (const User::Ptr& user : CompatUtility::GetCheckableNotificationUsers(host)) {
+ Log(LogDebug, "HostDbObject")
+ << "host contacts: " << user->GetName();
+
+ DbQuery query_contact;
+ query_contact.Table = GetType()->GetTable() + "_contacts";
+ query_contact.Type = DbQueryInsert;
+ query_contact.Category = DbCatConfig;
+ query_contact.Fields = new Dictionary({
+ { "host_id", DbValue::FromObjectInsertID(host) },
+ { "contact_object_id", user },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+ queries.emplace_back(std::move(query_contact));
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ Log(LogDebug, "HostDbObject")
+ << "host contactgroups: " << host->GetName();
+
+ queries.clear();
+
+ DbQuery query5;
+ query5.Table = GetType()->GetTable() + "_contactgroups";
+ query5.Type = DbQueryDelete;
+ query5.Category = DbCatConfig;
+ query5.WhereCriteria = new Dictionary({
+ { "host_id", DbValue::FromObjectInsertID(host) }
+ });
+ queries.emplace_back(std::move(query5));
+
+ for (const UserGroup::Ptr& usergroup : CompatUtility::GetCheckableNotificationUserGroups(host)) {
+ Log(LogDebug, "HostDbObject")
+ << "host contactgroups: " << usergroup->GetName();
+
+ DbQuery query_contact;
+ query_contact.Table = GetType()->GetTable() + "_contactgroups";
+ query_contact.Type = DbQueryInsert;
+ query_contact.Category = DbCatConfig;
+ query_contact.Fields = new Dictionary({
+ { "host_id", DbValue::FromObjectInsertID(host) },
+ { "contactgroup_object_id", usergroup },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+ queries.emplace_back(std::move(query_contact));
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ DoCommonConfigUpdate();
+}
+
+void HostDbObject::OnConfigUpdateLight()
+{
+ DoCommonConfigUpdate();
+}
+
+void HostDbObject::DoCommonConfigUpdate()
+{
+ Host::Ptr host = static_pointer_cast<Host>(GetObject());
+
+ /* update comments and downtimes on config change */
+ DbEvents::AddComments(host);
+ DbEvents::AddDowntimes(host);
+}
+
+String HostDbObject::CalculateConfigHash(const Dictionary::Ptr& configFields) const
+{
+ String hashData = DbObject::CalculateConfigHash(configFields);
+
+ Host::Ptr host = static_pointer_cast<Host>(GetObject());
+
+ Array::Ptr groups = host->GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+ ObjectLock oLock (groups);
+ std::sort(groups->Begin(), groups->End());
+ hashData += DbObject::HashValue(groups);
+ }
+
+ ArrayData parents;
+
+ /* parents */
+ for (const Checkable::Ptr& checkable : host->GetParents()) {
+ Host::Ptr parent = dynamic_pointer_cast<Host>(checkable);
+
+ if (!parent)
+ continue;
+
+ parents.push_back(parent->GetName());
+ }
+
+ std::sort(parents.begin(), parents.end());
+
+ hashData += DbObject::HashValue(new Array(std::move(parents)));
+
+ ArrayData dependencies;
+
+ /* dependencies */
+ for (const Dependency::Ptr& dep : host->GetDependencies()) {
+ Checkable::Ptr parent = dep->GetParent();
+
+ if (!parent)
+ continue;
+
+ dependencies.push_back(new Array({
+ parent->GetName(),
+ dep->GetStateFilter(),
+ dep->GetPeriodRaw()
+ }));
+ }
+
+ std::sort(dependencies.begin(), dependencies.end());
+
+ hashData += DbObject::HashValue(new Array(std::move(dependencies)));
+
+ ArrayData users;
+
+ for (const User::Ptr& user : CompatUtility::GetCheckableNotificationUsers(host)) {
+ users.push_back(user->GetName());
+ }
+
+ std::sort(users.begin(), users.end());
+
+ hashData += DbObject::HashValue(new Array(std::move(users)));
+
+ ArrayData userGroups;
+
+ for (const UserGroup::Ptr& usergroup : CompatUtility::GetCheckableNotificationUserGroups(host)) {
+ userGroups.push_back(usergroup->GetName());
+ }
+
+ std::sort(userGroups.begin(), userGroups.end());
+
+ hashData += DbObject::HashValue(new Array(std::move(userGroups)));
+
+ return SHA256(hashData);
+}
diff --git a/lib/db_ido/hostdbobject.hpp b/lib/db_ido/hostdbobject.hpp
new file mode 100644
index 0000000..9fff10a
--- /dev/null
+++ b/lib/db_ido/hostdbobject.hpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HOSTDBOBJECT_H
+#define HOSTDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+/**
+ * A Host database object.
+ *
+ * @ingroup ido
+ */
+class HostDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(HostDbObject);
+
+ HostDbObject(const DbType::Ptr& type, const String& name1, const String& name2);
+
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+
+ void OnConfigUpdateHeavy() override;
+ void OnConfigUpdateLight() override;
+
+ String CalculateConfigHash(const Dictionary::Ptr& configFields) const override;
+
+private:
+ void DoCommonConfigUpdate();
+};
+
+}
+
+#endif /* HOSTDBOBJECT_H */
diff --git a/lib/db_ido/hostgroupdbobject.cpp b/lib/db_ido/hostgroupdbobject.cpp
new file mode 100644
index 0000000..cef6aa2
--- /dev/null
+++ b/lib/db_ido/hostgroupdbobject.cpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/hostgroupdbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "base/objectlock.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+REGISTER_DBTYPE(HostGroup, "hostgroup", DbObjectTypeHostGroup, "hostgroup_object_id", HostGroupDbObject);
+
+HostGroupDbObject::HostGroupDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr HostGroupDbObject::GetConfigFields() const
+{
+ HostGroup::Ptr group = static_pointer_cast<HostGroup>(GetObject());
+
+ return new Dictionary({
+ { "alias", group->GetDisplayName() },
+ { "notes", group->GetNotes() },
+ { "notes_url", group->GetNotesUrl() },
+ { "action_url", group->GetActionUrl() }
+ });
+}
+
+Dictionary::Ptr HostGroupDbObject::GetStatusFields() const
+{
+ return nullptr;
+}
diff --git a/lib/db_ido/hostgroupdbobject.hpp b/lib/db_ido/hostgroupdbobject.hpp
new file mode 100644
index 0000000..9c48f29
--- /dev/null
+++ b/lib/db_ido/hostgroupdbobject.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HOSTGROUPDBOBJECT_H
+#define HOSTGROUPDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "icinga/hostgroup.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+/**
+ * A HostGroup database object.
+ *
+ * @ingroup ido
+ */
+class HostGroupDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(HostGroupDbObject);
+
+ HostGroupDbObject(const DbType::Ptr& type, const String& name1, const String& name2);
+
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+
+private:
+ static void MembersChangedHandler(const HostGroup::Ptr& hgfilter);
+};
+
+}
+
+#endif /* HOSTGROUPDBOBJECT_H */
diff --git a/lib/db_ido/i2-db_ido.hpp b/lib/db_ido/i2-db_ido.hpp
new file mode 100644
index 0000000..1da9fdc
--- /dev/null
+++ b/lib/db_ido/i2-db_ido.hpp
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2DB_IDO_H
+#define I2DB_IDO_H
+
+/**
+ * @defgroup db_ido IDO library
+ *
+ * The Icinga library implements database-agnostic IDO functionality.
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2DB_IDO_H */
diff --git a/lib/db_ido/idochecktask.cpp b/lib/db_ido/idochecktask.cpp
new file mode 100644
index 0000000..3b5856a
--- /dev/null
+++ b/lib/db_ido/idochecktask.cpp
@@ -0,0 +1,197 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/idochecktask.hpp"
+#include "icinga/host.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/zone.hpp"
+#include "base/function.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/configtype.hpp"
+#include "base/convert.hpp"
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, IdoCheck, &IdoCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+static void ReportIdoCheck(
+ const Checkable::Ptr& checkable, const CheckCommand::Ptr& commandObj,
+ const CheckResult::Ptr& cr, String output, ServiceState state = ServiceUnknown
+)
+{
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = std::move(output);
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandObj->GetName(), pr);
+ } else {
+ cr->SetState(state);
+ cr->SetOutput(output);
+ checkable->ProcessCheckResult(cr);
+ }
+}
+
+void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ ServiceState state;
+ CheckCommand::Ptr commandObj = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+ Value raw_command = commandObj->GetCommandLine();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", commandObj);
+
+ String idoType = MacroProcessor::ResolveMacros("$ido_type$", resolvers, checkable->GetLastCheckResult(),
+ nullptr, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ String idoName = MacroProcessor::ResolveMacros("$ido_name$", resolvers, checkable->GetLastCheckResult(),
+ nullptr, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ String missingQueriesWarning;
+ String missingQueriesCritical;
+ String missingPendingQueriesWarning;
+ String missingPendingQueriesCritical;
+
+ double queriesWarning = MacroProcessor::ResolveMacros("$ido_queries_warning$", resolvers, checkable->GetLastCheckResult(),
+ &missingQueriesWarning, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ double queriesCritical = MacroProcessor::ResolveMacros("$ido_queries_critical$", resolvers, checkable->GetLastCheckResult(),
+ &missingQueriesCritical, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ double pendingQueriesWarning = MacroProcessor::ResolveMacros("$ido_pending_queries_warning$", resolvers, checkable->GetLastCheckResult(),
+ &missingPendingQueriesWarning, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ double pendingQueriesCritical = MacroProcessor::ResolveMacros("$ido_pending_queries_critical$", resolvers, checkable->GetLastCheckResult(),
+ &missingPendingQueriesCritical, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ if (idoType.IsEmpty()) {
+ ReportIdoCheck(checkable, commandObj, cr, "Attribute 'ido_type' must be set.");
+ return;
+ }
+
+ if (idoName.IsEmpty()) {
+ ReportIdoCheck(checkable, commandObj, cr, "Attribute 'ido_name' must be set.");
+ return;
+ }
+
+ Type::Ptr type = Type::GetByName(idoType);
+
+ if (!type || !DbConnection::TypeInstance->IsAssignableFrom(type)) {
+ ReportIdoCheck(checkable, commandObj, cr, "DB IDO type '" + idoType + "' is invalid.");
+ return;
+ }
+
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+ VERIFY(dtype);
+
+ DbConnection::Ptr conn = static_pointer_cast<DbConnection>(dtype->GetObject(idoName));
+
+ if (!conn) {
+ ReportIdoCheck(checkable, commandObj, cr, "DB IDO connection '" + idoName + "' does not exist.");
+ return;
+ }
+
+ double qps = conn->GetQueryCount(60) / 60.0;
+
+ if (conn->IsPaused()) {
+ ReportIdoCheck(checkable, commandObj, cr, "DB IDO connection is temporarily disabled on this cluster instance.", ServiceOK);
+ return;
+ }
+
+ double pendingQueries = conn->GetPendingQueryCount();
+
+ if (!conn->GetConnected()) {
+ if (conn->GetShouldConnect()) {
+ ReportIdoCheck(checkable, commandObj, cr, "Could not connect to the database server.", ServiceCritical);
+ } else {
+ ReportIdoCheck(
+ checkable, commandObj, cr,
+ "Not currently enabled: Another cluster instance is responsible for the IDO database.", ServiceOK
+ );
+ }
+ return;
+ }
+
+ /* Schema versions. */
+ String schema_version = conn->GetSchemaVersion();
+ std::ostringstream msgbuf;
+
+ if (Utility::CompareVersion(conn->GetLatestSchemaVersion(), schema_version) < 0) {
+ msgbuf << "Outdated schema version: '" << schema_version << "'. Latest version: '"
+ << conn->GetLatestSchemaVersion() << "'."
+ << " Queries per second: " << std::fixed << std::setprecision(3) << qps
+ << " Pending queries: " << std::fixed << std::setprecision(3) << pendingQueries << ".";
+
+ state = ServiceWarning;
+ } else {
+ msgbuf << "Connected to the database server (Schema version: '" << schema_version << "')."
+ << " Queries per second: " << std::fixed << std::setprecision(3) << qps
+ << " Pending queries: " << std::fixed << std::setprecision(3) << pendingQueries << ".";
+
+ state = ServiceOK;
+ }
+
+ if (conn->GetEnableHa()) {
+ double failoverTs = conn->GetLastFailover();
+
+ msgbuf << " Last failover: " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", failoverTs) << ".";
+ }
+
+ /* Check whether the thresholds have been defined and match. */
+ if (missingQueriesCritical.IsEmpty() && qps < queriesCritical) {
+ msgbuf << " " << qps << " queries/s lower than critical threshold (" << queriesCritical << " queries/s).";
+
+ state = ServiceCritical;
+ } else if (missingQueriesWarning.IsEmpty() && qps < queriesWarning) {
+ msgbuf << " " << qps << " queries/s lower than warning threshold (" << queriesWarning << " queries/s).";
+
+ state = ServiceWarning;
+ }
+
+ if (missingPendingQueriesCritical.IsEmpty() && pendingQueries > pendingQueriesCritical) {
+ msgbuf << " " << pendingQueries << " pending queries greater than critical threshold ("
+ << pendingQueriesCritical << " queries).";
+
+ state = ServiceCritical;
+ } else if (missingPendingQueriesWarning.IsEmpty() && pendingQueries > pendingQueriesWarning) {
+ msgbuf << " " << pendingQueries << " pending queries greater than warning threshold ("
+ << pendingQueriesWarning << " queries).";
+
+ if (state == ServiceOK) {
+ state = ServiceWarning;
+ }
+ }
+
+ cr->SetPerformanceData(new Array({
+ { new PerfdataValue("queries", qps, false, "", queriesWarning, queriesCritical) },
+ { new PerfdataValue("queries_1min", conn->GetQueryCount(60)) },
+ { new PerfdataValue("queries_5mins", conn->GetQueryCount(5 * 60)) },
+ { new PerfdataValue("queries_15mins", conn->GetQueryCount(15 * 60)) },
+ { new PerfdataValue("pending_queries", pendingQueries, false, "", pendingQueriesWarning, pendingQueriesCritical) }
+ }));
+
+ ReportIdoCheck(checkable, commandObj, cr, msgbuf.str(), state);
+}
diff --git a/lib/db_ido/idochecktask.hpp b/lib/db_ido/idochecktask.hpp
new file mode 100644
index 0000000..5868c38
--- /dev/null
+++ b/lib/db_ido/idochecktask.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef IDOCHECKTASK_H
+#define IDOCHECKTASK_H
+
+#include "db_ido/dbconnection.hpp"
+#include "icinga/checkable.hpp"
+
+namespace icinga
+{
+
+/**
+ * IDO check type.
+ *
+ * @ingroup db_ido
+ */
+class IdoCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ IdoCheckTask();
+};
+
+}
+
+#endif /* IDOCHECKTASK_H */
diff --git a/lib/db_ido/servicedbobject.cpp b/lib/db_ido/servicedbobject.cpp
new file mode 100644
index 0000000..7f711df
--- /dev/null
+++ b/lib/db_ido/servicedbobject.cpp
@@ -0,0 +1,359 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/servicedbobject.hpp"
+#include "db_ido/servicegroupdbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "db_ido/dbevents.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/dependency.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/externalcommandprocessor.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "remote/endpoint.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/json.hpp"
+#include <boost/algorithm/string/join.hpp>
+
+using namespace icinga;
+
+REGISTER_DBTYPE(Service, "service", DbObjectTypeService, "service_object_id", ServiceDbObject);
+
+ServiceDbObject::ServiceDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr ServiceDbObject::GetConfigFields() const
+{
+ Service::Ptr service = static_pointer_cast<Service>(GetObject());
+ Host::Ptr host = service->GetHost();
+
+ unsigned long notificationStateFilter = CompatUtility::GetCheckableNotificationTypeFilter(service);
+ unsigned long notificationTypeFilter = CompatUtility::GetCheckableNotificationTypeFilter(service);
+
+ return new Dictionary({
+ { "host_object_id", host },
+ { "display_name", service->GetDisplayName() },
+ { "check_command_object_id", service->GetCheckCommand() },
+ { "eventhandler_command_object_id", service->GetEventCommand() },
+ { "check_timeperiod_object_id", service->GetCheckPeriod() },
+ { "check_interval", service->GetCheckInterval() / 60.0 },
+ { "retry_interval", service->GetRetryInterval() / 60.0 },
+ { "max_check_attempts", service->GetMaxCheckAttempts() },
+ { "is_volatile", service->GetVolatile() },
+ { "flap_detection_enabled", service->GetEnableFlapping() },
+ { "low_flap_threshold", service->GetFlappingThresholdLow() },
+ { "high_flap_threshold", service->GetFlappingThresholdLow() },
+ { "process_performance_data", service->GetEnablePerfdata() },
+ { "freshness_checks_enabled", 1 },
+ { "freshness_threshold", Convert::ToLong(service->GetCheckInterval()) },
+ { "event_handler_enabled", service->GetEnableEventHandler() },
+ { "passive_checks_enabled", service->GetEnablePassiveChecks() },
+ { "active_checks_enabled", service->GetEnableActiveChecks() },
+ { "notifications_enabled", service->GetEnableNotifications() },
+ { "notes", service->GetNotes() },
+ { "notes_url", service->GetNotesUrl() },
+ { "action_url", service->GetActionUrl() },
+ { "icon_image", service->GetIconImage() },
+ { "icon_image_alt", service->GetIconImageAlt() },
+ { "notification_interval", CompatUtility::GetCheckableNotificationNotificationInterval(service) },
+ { "notify_on_warning", (notificationStateFilter & ServiceWarning) ? 1 : 0 },
+ { "notify_on_unknown", (notificationStateFilter & ServiceUnknown) ? 1 : 0 },
+ { "notify_on_critical", (notificationStateFilter & ServiceCritical) ? 1 : 0 },
+ { "notify_on_recovery", (notificationTypeFilter & NotificationRecovery) ? 1 : 0 },
+ { "notify_on_flapping", (notificationTypeFilter & (NotificationFlappingStart | NotificationFlappingEnd)) ? 1 : 0 },
+ { "notify_on_downtime", (notificationTypeFilter & (NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved)) ? 1 : 0 }
+ });
+}
+
+Dictionary::Ptr ServiceDbObject::GetStatusFields() const
+{
+ Dictionary::Ptr fields = new Dictionary();
+ Service::Ptr service = static_pointer_cast<Service>(GetObject());
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (cr) {
+ fields->Set("output", CompatUtility::GetCheckResultOutput(cr));
+ fields->Set("long_output", CompatUtility::GetCheckResultLongOutput(cr));
+ fields->Set("perfdata", PluginUtility::FormatPerfdata(cr->GetPerformanceData()));
+ fields->Set("check_source", cr->GetCheckSource());
+ fields->Set("latency", cr->CalculateLatency());
+ fields->Set("execution_time", cr->CalculateExecutionTime());
+ }
+
+ fields->Set("current_state", service->GetState());
+ fields->Set("has_been_checked", service->HasBeenChecked());
+ fields->Set("should_be_scheduled", service->GetEnableActiveChecks());
+ fields->Set("current_check_attempt", service->GetCheckAttempt());
+ fields->Set("max_check_attempts", service->GetMaxCheckAttempts());
+ fields->Set("last_check", DbValue::FromTimestamp(service->GetLastCheck()));
+ fields->Set("next_check", DbValue::FromTimestamp(service->GetNextCheck()));
+ fields->Set("check_type", !service->GetEnableActiveChecks()); /* 0 .. active, 1 .. passive */
+ fields->Set("last_state_change", DbValue::FromTimestamp(service->GetLastStateChange()));
+ fields->Set("last_hard_state_change", DbValue::FromTimestamp(service->GetLastHardStateChange()));
+ fields->Set("last_hard_state", service->GetLastHardState());
+ fields->Set("last_time_ok", DbValue::FromTimestamp(service->GetLastStateOK()));
+ fields->Set("last_time_warning", DbValue::FromTimestamp(service->GetLastStateWarning()));
+ fields->Set("last_time_critical", DbValue::FromTimestamp(service->GetLastStateCritical()));
+ fields->Set("last_time_unknown", DbValue::FromTimestamp(service->GetLastStateUnknown()));
+ fields->Set("state_type", service->GetStateType());
+ fields->Set("notifications_enabled", service->GetEnableNotifications());
+ fields->Set("problem_has_been_acknowledged", service->GetAcknowledgement() != AcknowledgementNone);
+ fields->Set("acknowledgement_type", service->GetAcknowledgement());
+ fields->Set("passive_checks_enabled", service->GetEnablePassiveChecks());
+ fields->Set("active_checks_enabled", service->GetEnableActiveChecks());
+ fields->Set("event_handler_enabled", service->GetEnableEventHandler());
+ fields->Set("flap_detection_enabled", service->GetEnableFlapping());
+ fields->Set("is_flapping", service->IsFlapping());
+ fields->Set("percent_state_change", service->GetFlappingCurrent());
+ fields->Set("scheduled_downtime_depth", service->GetDowntimeDepth());
+ fields->Set("process_performance_data", service->GetEnablePerfdata());
+ fields->Set("normal_check_interval", service->GetCheckInterval() / 60.0);
+ fields->Set("retry_check_interval", service->GetRetryInterval() / 60.0);
+ fields->Set("check_timeperiod_object_id", service->GetCheckPeriod());
+ fields->Set("is_reachable", service->GetLastReachable());
+ fields->Set("original_attributes", JsonEncode(service->GetOriginalAttributes()));
+
+ fields->Set("current_notification_number", CompatUtility::GetCheckableNotificationNotificationNumber(service));
+ fields->Set("last_notification", DbValue::FromTimestamp(CompatUtility::GetCheckableNotificationLastNotification(service)));
+ fields->Set("next_notification", DbValue::FromTimestamp(CompatUtility::GetCheckableNotificationNextNotification(service)));
+
+ EventCommand::Ptr eventCommand = service->GetEventCommand();
+
+ if (eventCommand)
+ fields->Set("event_handler", eventCommand->GetName());
+
+ CheckCommand::Ptr checkCommand = service->GetCheckCommand();
+
+ if (checkCommand)
+ fields->Set("check_command", checkCommand->GetName());
+
+ return fields;
+}
+
+void ServiceDbObject::OnConfigUpdateHeavy()
+{
+ Service::Ptr service = static_pointer_cast<Service>(GetObject());
+
+ /* groups */
+ Array::Ptr groups = service->GetGroups();
+
+ std::vector<DbQuery> queries;
+
+ DbQuery query1;
+ query1.Table = DbType::GetByName("ServiceGroup")->GetTable() + "_members";
+ query1.Type = DbQueryDelete;
+ query1.Category = DbCatConfig;
+ query1.WhereCriteria = new Dictionary({
+ { "service_object_id", service }
+ });
+ queries.emplace_back(std::move(query1));
+
+ if (groups) {
+ ObjectLock olock(groups);
+ for (const String& groupName : groups) {
+ ServiceGroup::Ptr group = ServiceGroup::GetByName(groupName);
+
+ DbQuery query2;
+ query2.Table = DbType::GetByName("ServiceGroup")->GetTable() + "_members";
+ query2.Type = DbQueryInsert;
+ query2.Category = DbCatConfig;
+ query2.Fields = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "servicegroup_id", DbValue::FromObjectInsertID(group) },
+ { "service_object_id", service }
+ });
+ query2.WhereCriteria = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "servicegroup_id", DbValue::FromObjectInsertID(group) },
+ { "service_object_id", service }
+ });
+ queries.emplace_back(std::move(query2));
+ }
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ /* service dependencies */
+ queries.clear();
+
+ DbQuery query2;
+ query2.Table = GetType()->GetTable() + "dependencies";
+ query2.Type = DbQueryDelete;
+ query2.Category = DbCatConfig;
+ query2.WhereCriteria = new Dictionary({
+ { "dependent_service_object_id", service }
+ });
+ queries.emplace_back(std::move(query2));
+
+ for (const Dependency::Ptr& dep : service->GetDependencies()) {
+ Checkable::Ptr parent = dep->GetParent();
+
+ if (!parent) {
+ Log(LogDebug, "ServiceDbObject")
+ << "Missing parent for dependency '" << dep->GetName() << "'.";
+ continue;
+ }
+
+ Log(LogDebug, "ServiceDbObject")
+ << "service parents: " << parent->GetName();
+
+ int stateFilter = dep->GetStateFilter();
+
+ /* service dependencies */
+ DbQuery query1;
+ query1.Table = GetType()->GetTable() + "dependencies";
+ query1.Type = DbQueryInsert;
+ query1.Category = DbCatConfig;
+ query1.Fields = new Dictionary({
+ { "service_object_id", parent },
+ { "dependent_service_object_id", service },
+ { "inherits_parent", 1 },
+ { "timeperiod_object_id", dep->GetPeriod() },
+ { "fail_on_ok", (stateFilter & StateFilterOK) ? 1 : 0 },
+ { "fail_on_warning", (stateFilter & StateFilterWarning) ? 1 : 0 },
+ { "fail_on_critical", (stateFilter & StateFilterCritical) ? 1 : 0 },
+ { "fail_on_unknown", (stateFilter & StateFilterUnknown) ? 1 : 0 },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+ queries.emplace_back(std::move(query1));
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ /* service contacts, contactgroups */
+ queries.clear();
+
+ DbQuery query3;
+ query3.Table = GetType()->GetTable() + "_contacts";
+ query3.Type = DbQueryDelete;
+ query3.Category = DbCatConfig;
+ query3.WhereCriteria = new Dictionary({
+ { "service_id", DbValue::FromObjectInsertID(service) }
+ });
+ queries.emplace_back(std::move(query3));
+
+ for (const User::Ptr& user : CompatUtility::GetCheckableNotificationUsers(service)) {
+ DbQuery query_contact;
+ query_contact.Table = GetType()->GetTable() + "_contacts";
+ query_contact.Type = DbQueryInsert;
+ query_contact.Category = DbCatConfig;
+ query_contact.Fields = new Dictionary({
+ { "service_id", DbValue::FromObjectInsertID(service) },
+ { "contact_object_id", user },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+
+ });
+ queries.emplace_back(std::move(query_contact));
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ queries.clear();
+
+ DbQuery query4;
+ query4.Table = GetType()->GetTable() + "_contactgroups";
+ query4.Type = DbQueryDelete;
+ query4.Category = DbCatConfig;
+ query4.WhereCriteria = new Dictionary({
+ { "service_id", DbValue::FromObjectInsertID(service) }
+ });
+ queries.emplace_back(std::move(query4));
+
+ for (const UserGroup::Ptr& usergroup : CompatUtility::GetCheckableNotificationUserGroups(service)) {
+ DbQuery query_contact;
+ query_contact.Table = GetType()->GetTable() + "_contactgroups";
+ query_contact.Type = DbQueryInsert;
+ query_contact.Category = DbCatConfig;
+ query_contact.Fields = new Dictionary({
+ { "service_id", DbValue::FromObjectInsertID(service) },
+ { "contactgroup_object_id", usergroup },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+ });
+ queries.emplace_back(std::move(query_contact));
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ DoCommonConfigUpdate();
+}
+
+void ServiceDbObject::OnConfigUpdateLight()
+{
+ DoCommonConfigUpdate();
+}
+
+void ServiceDbObject::DoCommonConfigUpdate()
+{
+ Service::Ptr service = static_pointer_cast<Service>(GetObject());
+
+ /* update comments and downtimes on config change */
+ DbEvents::AddComments(service);
+ DbEvents::AddDowntimes(service);
+}
+
+String ServiceDbObject::CalculateConfigHash(const Dictionary::Ptr& configFields) const
+{
+ String hashData = DbObject::CalculateConfigHash(configFields);
+
+ Service::Ptr service = static_pointer_cast<Service>(GetObject());
+
+ Array::Ptr groups = service->GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+ ObjectLock oLock (groups);
+ std::sort(groups->Begin(), groups->End());
+ hashData += DbObject::HashValue(groups);
+ }
+
+ ArrayData dependencies;
+
+ /* dependencies */
+ for (const Dependency::Ptr& dep : service->GetDependencies()) {
+ Checkable::Ptr parent = dep->GetParent();
+
+ if (!parent)
+ continue;
+
+ dependencies.push_back(new Array({
+ parent->GetName(),
+ dep->GetStateFilter(),
+ dep->GetPeriodRaw()
+ }));
+ }
+
+ std::sort(dependencies.begin(), dependencies.end());
+
+ hashData += DbObject::HashValue(new Array(std::move(dependencies)));
+
+ ArrayData users;
+
+ for (const User::Ptr& user : CompatUtility::GetCheckableNotificationUsers(service)) {
+ users.push_back(user->GetName());
+ }
+
+ std::sort(users.begin(), users.end());
+
+ hashData += DbObject::HashValue(new Array(std::move(users)));
+
+ ArrayData userGroups;
+
+ for (const UserGroup::Ptr& usergroup : CompatUtility::GetCheckableNotificationUserGroups(service)) {
+ userGroups.push_back(usergroup->GetName());
+ }
+
+ std::sort(userGroups.begin(), userGroups.end());
+
+ hashData += DbObject::HashValue(new Array(std::move(userGroups)));
+
+ return SHA256(hashData);
+}
diff --git a/lib/db_ido/servicedbobject.hpp b/lib/db_ido/servicedbobject.hpp
new file mode 100644
index 0000000..19824be
--- /dev/null
+++ b/lib/db_ido/servicedbobject.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERVICEDBOBJECT_H
+#define SERVICEDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "base/configobject.hpp"
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+/**
+ * A Service database object.
+ *
+ * @ingroup ido
+ */
+class ServiceDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ServiceDbObject);
+
+ ServiceDbObject(const DbType::Ptr& type, const String& name1, const String& name2);
+
+ static void StaticInitialize();
+
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+
+ void OnConfigUpdateHeavy() override;
+ void OnConfigUpdateLight() override;
+
+ String CalculateConfigHash(const Dictionary::Ptr& configFields) const override;
+
+private:
+ void DoCommonConfigUpdate();
+};
+
+}
+
+#endif /* SERVICEDBOBJECT_H */
diff --git a/lib/db_ido/servicegroupdbobject.cpp b/lib/db_ido/servicegroupdbobject.cpp
new file mode 100644
index 0000000..ea4d40c
--- /dev/null
+++ b/lib/db_ido/servicegroupdbobject.cpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/servicegroupdbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "base/objectlock.hpp"
+#include "base/initialize.hpp"
+
+using namespace icinga;
+
+REGISTER_DBTYPE(ServiceGroup, "servicegroup", DbObjectTypeServiceGroup, "servicegroup_object_id", ServiceGroupDbObject);
+
+ServiceGroupDbObject::ServiceGroupDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr ServiceGroupDbObject::GetConfigFields() const
+{
+ ServiceGroup::Ptr group = static_pointer_cast<ServiceGroup>(GetObject());
+
+ return new Dictionary({
+ { "alias", group->GetDisplayName() },
+ { "notes", group->GetNotes() },
+ { "notes_url", group->GetNotesUrl() },
+ { "action_url", group->GetActionUrl() }
+ });
+}
+
+Dictionary::Ptr ServiceGroupDbObject::GetStatusFields() const
+{
+ return nullptr;
+}
diff --git a/lib/db_ido/servicegroupdbobject.hpp b/lib/db_ido/servicegroupdbobject.hpp
new file mode 100644
index 0000000..7f0d6c1
--- /dev/null
+++ b/lib/db_ido/servicegroupdbobject.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERVICEGROUPDBOBJECT_H
+#define SERVICEGROUPDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "icinga/servicegroup.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+/**
+ * A ServiceGroup database object.
+ *
+ * @ingroup ido
+ */
+class ServiceGroupDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ServiceGroupDbObject);
+
+ ServiceGroupDbObject(const DbType::Ptr& type, const String& name1, const String& name2);
+
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+};
+
+}
+
+#endif /* SERVICEGROUPDBOBJECT_H */
diff --git a/lib/db_ido/timeperioddbobject.cpp b/lib/db_ido/timeperioddbobject.cpp
new file mode 100644
index 0000000..98997f5
--- /dev/null
+++ b/lib/db_ido/timeperioddbobject.cpp
@@ -0,0 +1,85 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/timeperioddbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/legacytimeperiod.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+REGISTER_DBTYPE(TimePeriod, "timeperiod", DbObjectTypeTimePeriod, "timeperiod_object_id", TimePeriodDbObject);
+
+TimePeriodDbObject::TimePeriodDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr TimePeriodDbObject::GetConfigFields() const
+{
+ TimePeriod::Ptr tp = static_pointer_cast<TimePeriod>(GetObject());
+
+ return new Dictionary({
+ { "alias", tp->GetDisplayName() }
+ });
+}
+
+Dictionary::Ptr TimePeriodDbObject::GetStatusFields() const
+{
+ return Empty;
+}
+
+void TimePeriodDbObject::OnConfigUpdateHeavy()
+{
+ TimePeriod::Ptr tp = static_pointer_cast<TimePeriod>(GetObject());
+
+ DbQuery query_del1;
+ query_del1.Table = GetType()->GetTable() + "_timeranges";
+ query_del1.Type = DbQueryDelete;
+ query_del1.Category = DbCatConfig;
+ query_del1.WhereCriteria = new Dictionary({
+ { "timeperiod_id", DbValue::FromObjectInsertID(tp) }
+ });
+ OnQuery(query_del1);
+
+ Dictionary::Ptr ranges = tp->GetRanges();
+
+ if (!ranges)
+ return;
+
+ time_t refts = Utility::GetTime();
+ ObjectLock olock(ranges);
+ for (const Dictionary::Pair& kv : ranges) {
+ int wday = LegacyTimePeriod::WeekdayFromString(kv.first);
+
+ if (wday == -1)
+ continue;
+
+ tm reference = Utility::LocalTime(refts);
+
+ Array::Ptr segments = new Array();
+ LegacyTimePeriod::ProcessTimeRanges(kv.second, &reference, segments);
+
+ ObjectLock olock(segments);
+ for (const Value& vsegment : segments) {
+ Dictionary::Ptr segment = vsegment;
+ int begin = segment->Get("begin");
+ int end = segment->Get("end");
+
+ DbQuery query;
+ query.Table = GetType()->GetTable() + "_timeranges";
+ query.Type = DbQueryInsert;
+ query.Category = DbCatConfig;
+ query.Fields = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "timeperiod_id", DbValue::FromObjectInsertID(tp) },
+ { "day", wday },
+ { "start_sec", begin % 86400 },
+ { "end_sec", end % 86400 }
+ });
+ OnQuery(query);
+ }
+ }
+}
diff --git a/lib/db_ido/timeperioddbobject.hpp b/lib/db_ido/timeperioddbobject.hpp
new file mode 100644
index 0000000..e3cc13c
--- /dev/null
+++ b/lib/db_ido/timeperioddbobject.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TIMEPERIODDBOBJECT_H
+#define TIMEPERIODDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+/**
+ * A TimePeriod database object.
+ *
+ * @ingroup ido
+ */
+class TimePeriodDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TimePeriodDbObject);
+
+ TimePeriodDbObject(const DbType::Ptr& type, const String& name1, const String& name2);
+
+protected:
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+
+ void OnConfigUpdateHeavy() override;
+};
+
+}
+
+#endif /* TIMEPERIODDBOBJECT_H */
diff --git a/lib/db_ido/userdbobject.cpp b/lib/db_ido/userdbobject.cpp
new file mode 100644
index 0000000..439b8fb
--- /dev/null
+++ b/lib/db_ido/userdbobject.cpp
@@ -0,0 +1,161 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/userdbobject.hpp"
+#include "db_ido/usergroupdbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "icinga/user.hpp"
+#include "icinga/notification.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_DBTYPE(User, "contact", DbObjectTypeContact, "contact_object_id", UserDbObject);
+
+UserDbObject::UserDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr UserDbObject::GetConfigFields() const
+{
+ User::Ptr user = static_pointer_cast<User>(GetObject());
+
+ int typeFilter = user->GetTypeFilter();
+ int stateFilter = user->GetStateFilter();
+
+ return new Dictionary({
+ { "alias", user->GetDisplayName() },
+ { "email_address", user->GetEmail() },
+ { "pager_address", user->GetPager() },
+ { "host_timeperiod_object_id", user->GetPeriod() },
+ { "service_timeperiod_object_id", user->GetPeriod() },
+ { "host_notifications_enabled", user->GetEnableNotifications() },
+ { "service_notifications_enabled", user->GetEnableNotifications() },
+ { "can_submit_commands", 1 },
+ { "notify_service_recovery", (typeFilter & NotificationRecovery) ? 1 : 0 },
+ { "notify_service_warning", (stateFilter & StateFilterWarning) ? 1 : 0 },
+ { "notify_service_unknown", (stateFilter & StateFilterUnknown) ? 1 : 0 },
+ { "notify_service_critical", (stateFilter & StateFilterCritical) ? 1 : 0 },
+ { "notify_service_flapping", (typeFilter & (NotificationFlappingStart | NotificationFlappingEnd)) ? 1 : 0 },
+ { "notify_service_downtime", (typeFilter & (NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved)) ? 1 : 0 },
+ { "notify_host_recovery", (typeFilter & NotificationRecovery) ? 1 : 0 },
+ { "notify_host_down", (stateFilter & StateFilterDown) ? 1 : 0 },
+ { "notify_host_flapping", (typeFilter & (NotificationFlappingStart | NotificationFlappingEnd)) ? 1 : 0 },
+ { "notify_host_downtime", (typeFilter & (NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved)) ? 1 : 0 }
+ });
+}
+
+Dictionary::Ptr UserDbObject::GetStatusFields() const
+{
+ User::Ptr user = static_pointer_cast<User>(GetObject());
+
+ return new Dictionary({
+ { "host_notifications_enabled", user->GetEnableNotifications() },
+ { "service_notifications_enabled", user->GetEnableNotifications() },
+ { "last_host_notification", DbValue::FromTimestamp(user->GetLastNotification()) },
+ { "last_service_notification", DbValue::FromTimestamp(user->GetLastNotification()) }
+ });
+}
+
+void UserDbObject::OnConfigUpdateHeavy()
+{
+ User::Ptr user = static_pointer_cast<User>(GetObject());
+
+ /* groups */
+ Array::Ptr groups = user->GetGroups();
+
+ std::vector<DbQuery> queries;
+
+ DbQuery query1;
+ query1.Table = DbType::GetByName("UserGroup")->GetTable() + "_members";
+ query1.Type = DbQueryDelete;
+ query1.Category = DbCatConfig;
+ query1.WhereCriteria = new Dictionary({
+ { "contact_object_id", user }
+ });
+ queries.emplace_back(std::move(query1));
+
+ if (groups) {
+ ObjectLock olock(groups);
+ for (const String& groupName : groups) {
+ UserGroup::Ptr group = UserGroup::GetByName(groupName);
+
+ DbQuery query2;
+ query2.Table = DbType::GetByName("UserGroup")->GetTable() + "_members";
+ query2.Type = DbQueryInsert | DbQueryUpdate;
+ query2.Category = DbCatConfig;
+ query2.Fields = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "contactgroup_id", DbValue::FromObjectInsertID(group) },
+ { "contact_object_id", user }
+ });
+ query2.WhereCriteria = new Dictionary({
+ { "instance_id", 0 }, /* DbConnection class fills in real ID */
+ { "contactgroup_id", DbValue::FromObjectInsertID(group) },
+ { "contact_object_id", user }
+ });
+ queries.emplace_back(std::move(query2));
+ }
+ }
+
+ DbObject::OnMultipleQueries(queries);
+
+ queries.clear();
+
+ DbQuery query2;
+ query2.Table = "contact_addresses";
+ query2.Type = DbQueryDelete;
+ query2.Category = DbCatConfig;
+ query2.WhereCriteria = new Dictionary({
+ { "contact_id", DbValue::FromObjectInsertID(user) }
+ });
+ queries.emplace_back(std::move(query2));
+
+ Dictionary::Ptr vars = user->GetVars();
+
+ if (vars) { /* This is sparta. */
+ for (int i = 1; i <= 6; i++) {
+ String key = "address" + Convert::ToString(i);
+
+ if (!vars->Contains(key))
+ continue;
+
+ String val = vars->Get(key);
+
+ DbQuery query;
+ query.Type = DbQueryInsert;
+ query.Table = "contact_addresses";
+ query.Category = DbCatConfig;
+ query.Fields = new Dictionary({
+ { "contact_id", DbValue::FromObjectInsertID(user) },
+ { "address_number", i },
+ { "address", val },
+ { "instance_id", 0 } /* DbConnection class fills in real ID */
+
+ });
+ queries.emplace_back(std::move(query));
+ }
+ }
+
+ DbObject::OnMultipleQueries(queries);
+}
+
+String UserDbObject::CalculateConfigHash(const Dictionary::Ptr& configFields) const
+{
+ String hashData = DbObject::CalculateConfigHash(configFields);
+
+ User::Ptr user = static_pointer_cast<User>(GetObject());
+
+ Array::Ptr groups = user->GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+ ObjectLock oLock (groups);
+ std::sort(groups->Begin(), groups->End());
+ hashData += DbObject::HashValue(groups);
+ }
+
+ return SHA256(hashData);
+}
diff --git a/lib/db_ido/userdbobject.hpp b/lib/db_ido/userdbobject.hpp
new file mode 100644
index 0000000..e0f36c5
--- /dev/null
+++ b/lib/db_ido/userdbobject.hpp
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef USERDBOBJECT_H
+#define USERDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+/**
+ * A User database object.
+ *
+ * @ingroup ido
+ */
+class UserDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(UserDbObject);
+
+ UserDbObject(const DbType::Ptr& type, const String& name1, const String& name2);
+
+protected:
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+
+ void OnConfigUpdateHeavy() override;
+
+ String CalculateConfigHash(const Dictionary::Ptr& configFields) const override;
+};
+
+}
+
+#endif /* USERDBOBJECT_H */
diff --git a/lib/db_ido/usergroupdbobject.cpp b/lib/db_ido/usergroupdbobject.cpp
new file mode 100644
index 0000000..23b3581
--- /dev/null
+++ b/lib/db_ido/usergroupdbobject.cpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/usergroupdbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "base/objectlock.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+REGISTER_DBTYPE(UserGroup, "contactgroup", DbObjectTypeContactGroup, "contactgroup_object_id", UserGroupDbObject);
+
+UserGroupDbObject::UserGroupDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr UserGroupDbObject::GetConfigFields() const
+{
+ UserGroup::Ptr group = static_pointer_cast<UserGroup>(GetObject());
+
+ return new Dictionary({
+ { "alias", group->GetDisplayName() }
+ });
+}
+
+Dictionary::Ptr UserGroupDbObject::GetStatusFields() const
+{
+ return nullptr;
+}
diff --git a/lib/db_ido/usergroupdbobject.hpp b/lib/db_ido/usergroupdbobject.hpp
new file mode 100644
index 0000000..9469823
--- /dev/null
+++ b/lib/db_ido/usergroupdbobject.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef USERGROUPDBOBJECT_H
+#define USERGROUPDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "icinga/usergroup.hpp"
+#include "base/configobject.hpp"
+
+namespace icinga
+{
+
+/**
+ * A UserGroup database object.
+ *
+ * @ingroup ido
+ */
+class UserGroupDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(UserGroupDbObject);
+
+ UserGroupDbObject(const DbType::Ptr& type, const String& name1, const String& name2);
+
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+};
+
+}
+
+#endif /* USERGROUPDBOBJECT_H */
diff --git a/lib/db_ido/zonedbobject.cpp b/lib/db_ido/zonedbobject.cpp
new file mode 100644
index 0000000..b8ad0c1
--- /dev/null
+++ b/lib/db_ido/zonedbobject.cpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/zonedbobject.hpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+
+REGISTER_DBTYPE(Zone, "zone", DbObjectTypeZone, "zone_object_id", ZoneDbObject);
+
+ZoneDbObject::ZoneDbObject(const DbType::Ptr& type, const String& name1, const String& name2)
+ : DbObject(type, name1, name2)
+{ }
+
+Dictionary::Ptr ZoneDbObject::GetConfigFields() const
+{
+ Zone::Ptr zone = static_pointer_cast<Zone>(GetObject());
+
+ return new Dictionary({
+ { "is_global", zone->IsGlobal() ? 1 : 0 },
+ { "parent_zone_object_id", zone->GetParent() }
+
+ });
+}
+
+Dictionary::Ptr ZoneDbObject::GetStatusFields() const
+{
+ Zone::Ptr zone = static_pointer_cast<Zone>(GetObject());
+
+ Log(LogDebug, "ZoneDbObject")
+ << "update status for zone '" << zone->GetName() << "'";
+
+ return new Dictionary({
+ { "parent_zone_object_id", zone->GetParent() }
+ });
+}
diff --git a/lib/db_ido/zonedbobject.hpp b/lib/db_ido/zonedbobject.hpp
new file mode 100644
index 0000000..3901c81
--- /dev/null
+++ b/lib/db_ido/zonedbobject.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ZONEDBOBJECT_H
+#define ZONEDBOBJECT_H
+
+#include "db_ido/dbobject.hpp"
+#include "base/configobject.hpp"
+#include "remote/zone.hpp"
+
+namespace icinga
+{
+
+/**
+ * An Endpoint database object.
+ *
+ * @ingroup ido
+ */
+class ZoneDbObject final : public DbObject
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ZoneDbObject);
+
+ ZoneDbObject(const intrusive_ptr<DbType>& type, const String& name1, const String& name2);
+
+ Dictionary::Ptr GetConfigFields() const override;
+ Dictionary::Ptr GetStatusFields() const override;
+};
+
+}
+
+#endif /* ZONEDBOBJECT_H */
diff --git a/lib/db_ido_mysql/CMakeLists.txt b/lib/db_ido_mysql/CMakeLists.txt
new file mode 100644
index 0000000..70cb90d
--- /dev/null
+++ b/lib/db_ido_mysql/CMakeLists.txt
@@ -0,0 +1,41 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(idomysqlconnection.ti idomysqlconnection-ti.cpp idomysqlconnection-ti.hpp)
+
+set(db_ido_mysql_SOURCES
+ idomysqlconnection.cpp idomysqlconnection.hpp idomysqlconnection-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(db_ido_mysql db_ido_mysql db_ido_mysql_SOURCES)
+endif()
+
+add_library(db_ido_mysql OBJECT ${db_ido_mysql_SOURCES})
+
+include_directories(${MYSQL_INCLUDE_DIR})
+
+add_dependencies(db_ido_mysql base config icinga db_ido)
+
+set_target_properties (
+ db_ido_mysql PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/ido-mysql.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install(
+ DIRECTORY schema
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/icinga2-ido-mysql
+ FILES_MATCHING PATTERN "*.sql"
+)
+
+install(
+ DIRECTORY schema/upgrade
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/icinga2-ido-mysql/schema
+ FILES_MATCHING PATTERN "*.sql"
+)
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/db_ido_mysql/idomysqlconnection.cpp b/lib/db_ido_mysql/idomysqlconnection.cpp
new file mode 100644
index 0000000..aacb7d7
--- /dev/null
+++ b/lib/db_ido_mysql/idomysqlconnection.cpp
@@ -0,0 +1,1269 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido_mysql/idomysqlconnection.hpp"
+#include "db_ido_mysql/idomysqlconnection-ti.cpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/application.hpp"
+#include "base/configtype.hpp"
+#include "base/exception.hpp"
+#include "base/statsfunction.hpp"
+#include "base/defer.hpp"
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(IdoMysqlConnection);
+REGISTER_STATSFUNCTION(IdoMysqlConnection, &IdoMysqlConnection::StatsFunc);
+
+const char * IdoMysqlConnection::GetLatestSchemaVersion() const noexcept
+{
+ return "1.15.1";
+}
+
+const char * IdoMysqlConnection::GetCompatSchemaVersion() const noexcept
+{
+ return "1.14.3";
+}
+
+void IdoMysqlConnection::OnConfigLoaded()
+{
+ ObjectImpl<IdoMysqlConnection>::OnConfigLoaded();
+
+ m_QueryQueue.SetName("IdoMysqlConnection, " + GetName());
+
+ Library shimLibrary{"mysql_shim"};
+
+ auto create_mysql_shim = shimLibrary.GetSymbolAddress<create_mysql_shim_ptr>("create_mysql_shim");
+
+ m_Mysql.reset(create_mysql_shim());
+
+ std::swap(m_Library, shimLibrary);
+}
+
+void IdoMysqlConnection::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const IdoMysqlConnection::Ptr& idomysqlconnection : ConfigType::GetObjectsByType<IdoMysqlConnection>()) {
+ size_t queryQueueItems = idomysqlconnection->m_QueryQueue.GetLength();
+ double queryQueueItemRate = idomysqlconnection->m_QueryQueue.GetTaskCount(60) / 60.0;
+
+ nodes.emplace_back(idomysqlconnection->GetName(), new Dictionary({
+ { "version", idomysqlconnection->GetSchemaVersion() },
+ { "instance_name", idomysqlconnection->GetInstanceName() },
+ { "connected", idomysqlconnection->GetConnected() },
+ { "query_queue_items", queryQueueItems },
+ { "query_queue_item_rate", queryQueueItemRate }
+ }));
+
+ perfdata->Add(new PerfdataValue("idomysqlconnection_" + idomysqlconnection->GetName() + "_queries_rate", idomysqlconnection->GetQueryCount(60) / 60.0));
+ perfdata->Add(new PerfdataValue("idomysqlconnection_" + idomysqlconnection->GetName() + "_queries_1min", idomysqlconnection->GetQueryCount(60)));
+ perfdata->Add(new PerfdataValue("idomysqlconnection_" + idomysqlconnection->GetName() + "_queries_5mins", idomysqlconnection->GetQueryCount(5 * 60)));
+ perfdata->Add(new PerfdataValue("idomysqlconnection_" + idomysqlconnection->GetName() + "_queries_15mins", idomysqlconnection->GetQueryCount(15 * 60)));
+ perfdata->Add(new PerfdataValue("idomysqlconnection_" + idomysqlconnection->GetName() + "_query_queue_items", queryQueueItems));
+ perfdata->Add(new PerfdataValue("idomysqlconnection_" + idomysqlconnection->GetName() + "_query_queue_item_rate", queryQueueItemRate));
+ }
+
+ status->Set("idomysqlconnection", new Dictionary(std::move(nodes)));
+}
+
+void IdoMysqlConnection::Resume()
+{
+ Log(LogInformation, "IdoMysqlConnection")
+ << "'" << GetName() << "' resumed.";
+
+ SetConnected(false);
+
+ m_QueryQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); });
+
+ /* Immediately try to connect on Resume() without timer. */
+ m_QueryQueue.Enqueue([this]() { Reconnect(); }, PriorityImmediate);
+
+ m_TxTimer = Timer::Create();
+ m_TxTimer->SetInterval(1);
+ m_TxTimer->OnTimerExpired.connect([this](const Timer * const&) { NewTransaction(); });
+ m_TxTimer->Start();
+
+ m_ReconnectTimer = Timer::Create();
+ m_ReconnectTimer->SetInterval(10);
+ m_ReconnectTimer->OnTimerExpired.connect([this](const Timer * const&){ ReconnectTimerHandler(); });
+ m_ReconnectTimer->Start();
+
+ /* Start with queries after connect. */
+ DbConnection::Resume();
+
+ ASSERT(m_Mysql->thread_safe());
+}
+
+void IdoMysqlConnection::Pause()
+{
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Attempting to pause '" << GetName() << "'.";
+
+ DbConnection::Pause();
+
+ m_ReconnectTimer->Stop(true);
+ m_TxTimer->Stop(true);
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Rescheduling disconnect task.";
+#endif /* I2_DEBUG */
+
+ Log(LogInformation, "IdoMysqlConnection")
+ << "'" << GetName() << "' paused.";
+
+}
+
+void IdoMysqlConnection::ExceptionHandler(boost::exception_ptr exp)
+{
+ Log(LogCritical, "IdoMysqlConnection", "Exception during database operation: Verify that your database is operational!");
+
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Exception during database operation: " << DiagnosticInformation(std::move(exp));
+
+ if (GetConnected()) {
+ m_Mysql->close(&m_Connection);
+
+ SetConnected(false);
+ }
+}
+
+void IdoMysqlConnection::AssertOnWorkQueue()
+{
+ ASSERT(m_QueryQueue.IsWorkerThread());
+}
+
+void IdoMysqlConnection::Disconnect()
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected())
+ return;
+
+ Query("COMMIT");
+ m_Mysql->close(&m_Connection);
+
+ SetConnected(false);
+
+ Log(LogInformation, "IdoMysqlConnection")
+ << "Disconnected from '" << GetName() << "' database '" << GetDatabase() << "'.";
+}
+
+void IdoMysqlConnection::NewTransaction()
+{
+ if (IsPaused() && GetPauseCalled())
+ return;
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling new transaction and finishing async queries.";
+#endif /* I2_DEBUG */
+
+ m_QueryQueue.Enqueue([this]() { InternalNewTransaction(); }, PriorityHigh);
+}
+
+void IdoMysqlConnection::InternalNewTransaction()
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected())
+ return;
+
+ IncreasePendingQueries(2);
+
+ AsyncQuery("COMMIT");
+ AsyncQuery("BEGIN");
+
+ FinishAsyncQueries();
+}
+
+void IdoMysqlConnection::ReconnectTimerHandler()
+{
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling reconnect task.";
+#endif /* I2_DEBUG */
+
+ /* Only allow Reconnect events with high priority. */
+ m_QueryQueue.Enqueue([this]() { Reconnect(); }, PriorityImmediate);
+}
+
+void IdoMysqlConnection::Reconnect()
+{
+ AssertOnWorkQueue();
+
+ if (!IsActive())
+ return;
+
+ CONTEXT("Reconnecting to MySQL IDO database '" << GetName() << "'");
+
+ double startTime = Utility::GetTime();
+
+ SetShouldConnect(true);
+
+ bool reconnect = false;
+
+ /* Ensure to close old connections first. */
+ if (GetConnected()) {
+ /* Check if we're really still connected */
+ if (m_Mysql->ping(&m_Connection) == 0)
+ return;
+
+ m_Mysql->close(&m_Connection);
+ SetConnected(false);
+ reconnect = true;
+ }
+
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Reconnect: Clearing ID cache.";
+
+ ClearIDCache();
+
+ String ihost, isocket_path, iuser, ipasswd, idb;
+ String isslKey, isslCert, isslCa, isslCaPath, isslCipher;
+ const char *host, *socket_path, *user , *passwd, *db;
+ const char *sslKey, *sslCert, *sslCa, *sslCaPath, *sslCipher;
+ bool enableSsl;
+ long port;
+
+ ihost = GetHost();
+ isocket_path = GetSocketPath();
+ iuser = GetUser();
+ ipasswd = GetPassword();
+ idb = GetDatabase();
+
+ enableSsl = GetEnableSsl();
+ isslKey = GetSslKey();
+ isslCert = GetSslCert();
+ isslCa = GetSslCa();
+ isslCaPath = GetSslCapath();
+ isslCipher = GetSslCipher();
+
+ host = (!ihost.IsEmpty()) ? ihost.CStr() : nullptr;
+ port = GetPort();
+ socket_path = (!isocket_path.IsEmpty()) ? isocket_path.CStr() : nullptr;
+ user = (!iuser.IsEmpty()) ? iuser.CStr() : nullptr;
+ passwd = (!ipasswd.IsEmpty()) ? ipasswd.CStr() : nullptr;
+ db = (!idb.IsEmpty()) ? idb.CStr() : nullptr;
+
+ sslKey = (!isslKey.IsEmpty()) ? isslKey.CStr() : nullptr;
+ sslCert = (!isslCert.IsEmpty()) ? isslCert.CStr() : nullptr;
+ sslCa = (!isslCa.IsEmpty()) ? isslCa.CStr() : nullptr;
+ sslCaPath = (!isslCaPath.IsEmpty()) ? isslCaPath.CStr() : nullptr;
+ sslCipher = (!isslCipher.IsEmpty()) ? isslCipher.CStr() : nullptr;
+
+ /* connection */
+ if (!m_Mysql->init(&m_Connection)) {
+ Log(LogCritical, "IdoMysqlConnection")
+ << "mysql_init() failed: out of memory";
+
+ BOOST_THROW_EXCEPTION(std::bad_alloc());
+ }
+
+ /* Read "latin1" (here, in the schema and in Icinga Web) as "bytes".
+ Icinga 2 and Icinga Web use byte-strings everywhere and every byte-string is a valid latin1 string.
+ This way the (actually mostly UTF-8) bytes are transferred end-to-end as-is. */
+ m_Mysql->options(&m_Connection, MYSQL_SET_CHARSET_NAME, "latin1");
+
+ if (enableSsl)
+ m_Mysql->ssl_set(&m_Connection, sslKey, sslCert, sslCa, sslCaPath, sslCipher);
+
+ if (!m_Mysql->real_connect(&m_Connection, host, user, passwd, db, port, socket_path, CLIENT_FOUND_ROWS | CLIENT_MULTI_STATEMENTS)) {
+ Log(LogCritical, "IdoMysqlConnection")
+ << "Connection to database '" << db << "' with user '" << user << "' on '" << host << ":" << port
+ << "' " << (enableSsl ? "(SSL enabled) " : "") << "failed: \"" << m_Mysql->error(&m_Connection) << "\"";
+
+ BOOST_THROW_EXCEPTION(std::runtime_error(m_Mysql->error(&m_Connection)));
+ }
+
+ Log(LogNotice, "IdoMysqlConnection")
+ << "Reconnect: '" << GetName() << "' is now connected to database '" << GetDatabase() << "'.";
+
+ SetConnected(true);
+
+ IdoMysqlResult result = Query("SELECT @@global.max_allowed_packet AS max_allowed_packet");
+
+ Dictionary::Ptr row = FetchRow(result);
+
+ if (row)
+ m_MaxPacketSize = row->Get("max_allowed_packet");
+ else
+ m_MaxPacketSize = 64 * 1024;
+
+ DiscardRows(result);
+
+ String dbVersionName = "idoutils";
+ result = Query("SELECT version FROM " + GetTablePrefix() + "dbversion WHERE name='" + Escape(dbVersionName) + "'");
+
+ row = FetchRow(result);
+
+ if (!row) {
+ m_Mysql->close(&m_Connection);
+ SetConnected(false);
+
+ Log(LogCritical, "IdoMysqlConnection", "Schema does not provide any valid version! Verify your schema installation.");
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid schema."));
+ }
+
+ DiscardRows(result);
+
+ String version = row->Get("version");
+
+ SetSchemaVersion(version);
+
+ if (Utility::CompareVersion(GetCompatSchemaVersion(), version) < 0) {
+ m_Mysql->close(&m_Connection);
+ SetConnected(false);
+
+ Log(LogCritical, "IdoMysqlConnection")
+ << "Schema version '" << version << "' does not match the required version '"
+ << GetCompatSchemaVersion() << "' (or newer)! Please check the upgrade documentation at "
+ << "https://icinga.com/docs/icinga2/latest/doc/16-upgrading-icinga-2/#upgrading-mysql-db";
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Schema version mismatch."));
+ }
+
+ String instanceName = GetInstanceName();
+
+ result = Query("SELECT instance_id FROM " + GetTablePrefix() + "instances WHERE instance_name = '" + Escape(instanceName) + "'");
+ row = FetchRow(result);
+
+ if (!row) {
+ Query("INSERT INTO " + GetTablePrefix() + "instances (instance_name, instance_description) VALUES ('" + Escape(instanceName) + "', '" + Escape(GetInstanceDescription()) + "')");
+ m_InstanceID = GetLastInsertID();
+ } else {
+ m_InstanceID = DbReference(row->Get("instance_id"));
+ }
+
+ DiscardRows(result);
+
+ Endpoint::Ptr my_endpoint = Endpoint::GetLocalEndpoint();
+
+ /* we have an endpoint in a cluster setup, so decide if we can proceed here */
+ if (my_endpoint && GetHAMode() == HARunOnce) {
+ /* get the current endpoint writing to programstatus table */
+ result = Query("SELECT UNIX_TIMESTAMP(status_update_time) AS status_update_time, endpoint_name FROM " +
+ GetTablePrefix() + "programstatus WHERE instance_id = " + Convert::ToString(m_InstanceID));
+ row = FetchRow(result);
+ DiscardRows(result);
+
+ String endpoint_name;
+
+ if (row)
+ endpoint_name = row->Get("endpoint_name");
+ else
+ Log(LogNotice, "IdoMysqlConnection", "Empty program status table");
+
+ /* if we did not write into the database earlier, another instance is active */
+ if (endpoint_name != my_endpoint->GetName()) {
+ double status_update_time;
+
+ if (row)
+ status_update_time = row->Get("status_update_time");
+ else
+ status_update_time = 0;
+
+ double now = Utility::GetTime();
+
+ double status_update_age = now - status_update_time;
+ double failoverTimeout = GetFailoverTimeout();
+
+ if (status_update_age < failoverTimeout) {
+ Log(LogInformation, "IdoMysqlConnection")
+ << "Last update by endpoint '" << endpoint_name << "' was "
+ << status_update_age << "s ago (< failover timeout of " << failoverTimeout << "s). Retrying.";
+
+ m_Mysql->close(&m_Connection);
+ SetConnected(false);
+ SetShouldConnect(false);
+
+ return;
+ }
+
+ /* activate the IDO only, if we're authoritative in this zone */
+ if (IsPaused()) {
+ Log(LogNotice, "IdoMysqlConnection")
+ << "Local endpoint '" << my_endpoint->GetName() << "' is not authoritative, bailing out.";
+
+ m_Mysql->close(&m_Connection);
+ SetConnected(false);
+
+ return;
+ }
+
+ SetLastFailover(now);
+
+ Log(LogInformation, "IdoMysqlConnection")
+ << "Last update by endpoint '" << endpoint_name << "' was "
+ << status_update_age << "s ago. Taking over '" << GetName() << "' in HA zone '" << Zone::GetLocalZone()->GetName() << "'.";
+ }
+
+ Log(LogNotice, "IdoMysqlConnection", "Enabling IDO connection in HA zone.");
+ }
+
+ Log(LogInformation, "IdoMysqlConnection")
+ << "MySQL IDO instance id: " << static_cast<long>(m_InstanceID) << " (schema version: '" + version + "')";
+
+ /* set session time zone to utc */
+ Query("SET SESSION TIME_ZONE='+00:00'");
+
+ Query("SET SESSION SQL_MODE='NO_AUTO_VALUE_ON_ZERO'");
+
+ Query("BEGIN");
+
+ /* update programstatus table */
+ UpdateProgramStatus();
+
+ /* record connection */
+ Query("INSERT INTO " + GetTablePrefix() + "conninfo " +
+ "(instance_id, connect_time, last_checkin_time, agent_name, agent_version, connect_type, data_start_time) VALUES ("
+ + Convert::ToString(static_cast<long>(m_InstanceID)) + ", NOW(), NOW(), 'icinga2 db_ido_mysql', '" + Escape(Application::GetAppVersion())
+ + "', '" + (reconnect ? "RECONNECT" : "INITIAL") + "', NOW())");
+
+ /* clear config tables for the initial config dump */
+ PrepareDatabase();
+
+ std::ostringstream q1buf;
+ q1buf << "SELECT object_id, objecttype_id, name1, name2, is_active FROM " + GetTablePrefix() + "objects WHERE instance_id = " << static_cast<long>(m_InstanceID);
+ result = Query(q1buf.str());
+
+ std::vector<DbObject::Ptr> activeDbObjs;
+
+ while ((row = FetchRow(result))) {
+ DbType::Ptr dbtype = DbType::GetByID(row->Get("objecttype_id"));
+
+ if (!dbtype)
+ continue;
+
+ DbObject::Ptr dbobj = dbtype->GetOrCreateObjectByName(row->Get("name1"), row->Get("name2"));
+ SetObjectID(dbobj, DbReference(row->Get("object_id")));
+ bool active = row->Get("is_active");
+ SetObjectActive(dbobj, active);
+
+ if (active)
+ activeDbObjs.emplace_back(std::move(dbobj));
+ }
+
+ SetIDCacheValid(true);
+
+ EnableActiveChangedHandler();
+
+ for (const DbObject::Ptr& dbobj : activeDbObjs) {
+ if (dbobj->GetObject())
+ continue;
+
+ Log(LogNotice, "IdoMysqlConnection")
+ << "Deactivate deleted object name1: '" << dbobj->GetName1()
+ << "' name2: '" << dbobj->GetName2() + "'.";
+ DeactivateObject(dbobj);
+ }
+
+ UpdateAllObjects();
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling session table clear and finish connect task.";
+#endif /* I2_DEBUG */
+
+ m_QueryQueue.Enqueue([this]() { ClearTablesBySession(); }, PriorityNormal);
+
+ m_QueryQueue.Enqueue([this, startTime]() { FinishConnect(startTime); }, PriorityNormal);
+}
+
+void IdoMysqlConnection::FinishConnect(double startTime)
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected() || IsPaused())
+ return;
+
+ FinishAsyncQueries();
+
+ Log(LogInformation, "IdoMysqlConnection")
+ << "Finished reconnecting to '" << GetName() << "' database '" << GetDatabase() << "' in "
+ << std::setw(2) << Utility::GetTime() - startTime << " second(s).";
+
+ Query("COMMIT");
+ Query("BEGIN");
+}
+
+void IdoMysqlConnection::ClearTablesBySession()
+{
+ /* delete all comments and downtimes without current session token */
+ ClearTableBySession("comments");
+ ClearTableBySession("scheduleddowntime");
+}
+
+void IdoMysqlConnection::ClearTableBySession(const String& table)
+{
+ Query("DELETE FROM " + GetTablePrefix() + table + " WHERE instance_id = " +
+ Convert::ToString(static_cast<long>(m_InstanceID)) + " AND session_token <> " +
+ Convert::ToString(GetSessionToken()));
+}
+
+void IdoMysqlConnection::AsyncQuery(const String& query, const std::function<void (const IdoMysqlResult&)>& callback)
+{
+ AssertOnWorkQueue();
+
+ IdoAsyncQuery aq;
+ aq.Query = query;
+ /* XXX: Important: The callback must not immediately execute a query, but enqueue it!
+ * See https://github.com/Icinga/icinga2/issues/4603 for details.
+ */
+ aq.Callback = callback;
+ m_AsyncQueries.emplace_back(std::move(aq));
+}
+
+void IdoMysqlConnection::FinishAsyncQueries()
+{
+ std::vector<IdoAsyncQuery> queries;
+ m_AsyncQueries.swap(queries);
+
+ std::vector<IdoAsyncQuery>::size_type offset = 0;
+
+ // This will be executed if there is a problem with executing the queries,
+ // at which point this function throws an exception and the queries should
+ // not be listed as still pending in the queue.
+ Defer decreaseQueries ([this, &offset, &queries]() {
+ auto lostQueries = queries.size() - offset;
+
+ if (lostQueries > 0) {
+ DecreasePendingQueries(lostQueries);
+ }
+ });
+
+ while (offset < queries.size()) {
+ std::ostringstream querybuf;
+
+ std::vector<IdoAsyncQuery>::size_type count = 0;
+ size_t num_bytes = 0;
+
+ Defer decreaseQueries ([this, &offset, &count]() {
+ offset += count;
+ DecreasePendingQueries(count);
+ m_UncommittedAsyncQueries += count;
+ });
+
+ for (std::vector<IdoAsyncQuery>::size_type i = offset; i < queries.size(); i++) {
+ const IdoAsyncQuery& aq = queries[i];
+
+ size_t size_query = aq.Query.GetLength() + 1;
+
+ if (count > 0) {
+ if (num_bytes + size_query > m_MaxPacketSize - 512)
+ break;
+
+ querybuf << ";";
+ }
+
+ IncreaseQueryCount();
+ count++;
+
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Query: " << aq.Query;
+
+ querybuf << aq.Query;
+ num_bytes += size_query;
+ }
+
+ String query = querybuf.str();
+
+ if (m_Mysql->query(&m_Connection, query.CStr()) != 0) {
+ std::ostringstream msgbuf;
+ String message = m_Mysql->error(&m_Connection);
+ msgbuf << "Error \"" << message << "\" when executing query \"" << query << "\"";
+ Log(LogCritical, "IdoMysqlConnection", msgbuf.str());
+
+ BOOST_THROW_EXCEPTION(
+ database_error()
+ << errinfo_message(m_Mysql->error(&m_Connection))
+ << errinfo_database_query(query)
+ );
+ }
+
+ for (std::vector<IdoAsyncQuery>::size_type i = offset; i < offset + count; i++) {
+ const IdoAsyncQuery& aq = queries[i];
+
+ MYSQL_RES *result = m_Mysql->store_result(&m_Connection);
+
+ m_AffectedRows = m_Mysql->affected_rows(&m_Connection);
+
+ IdoMysqlResult iresult;
+
+ if (!result) {
+ if (m_Mysql->field_count(&m_Connection) > 0) {
+ std::ostringstream msgbuf;
+ String message = m_Mysql->error(&m_Connection);
+ msgbuf << "Error \"" << message << "\" when executing query \"" << aq.Query << "\"";
+ Log(LogCritical, "IdoMysqlConnection", msgbuf.str());
+
+ BOOST_THROW_EXCEPTION(
+ database_error()
+ << errinfo_message(m_Mysql->error(&m_Connection))
+ << errinfo_database_query(query)
+ );
+ }
+ } else
+ iresult = IdoMysqlResult(result, [this](MYSQL_RES* result) { m_Mysql->free_result(result); });
+
+ if (aq.Callback)
+ aq.Callback(iresult);
+
+ if (m_Mysql->next_result(&m_Connection) > 0) {
+ std::ostringstream msgbuf;
+ String message = m_Mysql->error(&m_Connection);
+ msgbuf << "Error \"" << message << "\" when executing query \"" << query << "\"";
+ Log(LogCritical, "IdoMysqlConnection", msgbuf.str());
+
+ BOOST_THROW_EXCEPTION(
+ database_error()
+ << errinfo_message(m_Mysql->error(&m_Connection))
+ << errinfo_database_query(query)
+ );
+ }
+ }
+ }
+
+ if (m_UncommittedAsyncQueries > 25000) {
+ m_UncommittedAsyncQueries = 0;
+
+ Query("COMMIT");
+ Query("BEGIN");
+ }
+}
+
+IdoMysqlResult IdoMysqlConnection::Query(const String& query)
+{
+ AssertOnWorkQueue();
+
+ IncreasePendingQueries(1);
+ Defer decreaseQueries ([this]() { DecreasePendingQueries(1); });
+
+ /* finish all async queries to maintain the right order for queries */
+ FinishAsyncQueries();
+
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Query: " << query;
+
+ IncreaseQueryCount();
+
+ if (m_Mysql->query(&m_Connection, query.CStr()) != 0) {
+ std::ostringstream msgbuf;
+ String message = m_Mysql->error(&m_Connection);
+ msgbuf << "Error \"" << message << "\" when executing query \"" << query << "\"";
+ Log(LogCritical, "IdoMysqlConnection", msgbuf.str());
+
+ BOOST_THROW_EXCEPTION(
+ database_error()
+ << errinfo_message(m_Mysql->error(&m_Connection))
+ << errinfo_database_query(query)
+ );
+ }
+
+ MYSQL_RES *result = m_Mysql->store_result(&m_Connection);
+
+ m_AffectedRows = m_Mysql->affected_rows(&m_Connection);
+
+ if (!result) {
+ if (m_Mysql->field_count(&m_Connection) > 0) {
+ std::ostringstream msgbuf;
+ String message = m_Mysql->error(&m_Connection);
+ msgbuf << "Error \"" << message << "\" when executing query \"" << query << "\"";
+ Log(LogCritical, "IdoMysqlConnection", msgbuf.str());
+
+ BOOST_THROW_EXCEPTION(
+ database_error()
+ << errinfo_message(m_Mysql->error(&m_Connection))
+ << errinfo_database_query(query)
+ );
+ }
+
+ return IdoMysqlResult();
+ }
+
+ return IdoMysqlResult(result, [this](MYSQL_RES* result) { m_Mysql->free_result(result); });
+}
+
+DbReference IdoMysqlConnection::GetLastInsertID()
+{
+ AssertOnWorkQueue();
+
+ return {static_cast<long>(m_Mysql->insert_id(&m_Connection))};
+}
+
+int IdoMysqlConnection::GetAffectedRows()
+{
+ AssertOnWorkQueue();
+
+ return m_AffectedRows;
+}
+
+String IdoMysqlConnection::Escape(const String& s)
+{
+ AssertOnWorkQueue();
+
+ String utf8s = Utility::ValidateUTF8(s);
+
+ size_t length = utf8s.GetLength();
+ auto *to = new char[utf8s.GetLength() * 2 + 1];
+
+ m_Mysql->real_escape_string(&m_Connection, to, utf8s.CStr(), length);
+
+ String result = String(to);
+
+ delete [] to;
+
+ return result;
+}
+
+Dictionary::Ptr IdoMysqlConnection::FetchRow(const IdoMysqlResult& result)
+{
+ AssertOnWorkQueue();
+
+ MYSQL_ROW row;
+ MYSQL_FIELD *field;
+ unsigned long *lengths, i;
+
+ row = m_Mysql->fetch_row(result.get());
+
+ if (!row)
+ return nullptr;
+
+ lengths = m_Mysql->fetch_lengths(result.get());
+
+ if (!lengths)
+ return nullptr;
+
+ Dictionary::Ptr dict = new Dictionary();
+
+ m_Mysql->field_seek(result.get(), 0);
+ for (field = m_Mysql->fetch_field(result.get()), i = 0; field; field = m_Mysql->fetch_field(result.get()), i++)
+ dict->Set(field->name, String(row[i], row[i] + lengths[i]));
+
+ return dict;
+}
+
+void IdoMysqlConnection::DiscardRows(const IdoMysqlResult& result)
+{
+ Dictionary::Ptr row;
+
+ while ((row = FetchRow(result)))
+ ; /* empty loop body */
+}
+
+void IdoMysqlConnection::ActivateObject(const DbObject::Ptr& dbobj)
+{
+ if (IsPaused())
+ return;
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling object activation task for '" << dbobj->GetName1() << "!" << dbobj->GetName2() << "'.";
+#endif /* I2_DEBUG */
+
+ m_QueryQueue.Enqueue([this, dbobj]() { InternalActivateObject(dbobj); }, PriorityNormal);
+}
+
+void IdoMysqlConnection::InternalActivateObject(const DbObject::Ptr& dbobj)
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused())
+ return;
+
+ if (!GetConnected())
+ return;
+
+ DbReference dbref = GetObjectID(dbobj);
+ std::ostringstream qbuf;
+
+ if (!dbref.IsValid()) {
+ if (!dbobj->GetName2().IsEmpty()) {
+ qbuf << "INSERT INTO " + GetTablePrefix() + "objects (instance_id, objecttype_id, name1, name2, is_active) VALUES ("
+ << static_cast<long>(m_InstanceID) << ", " << dbobj->GetType()->GetTypeID() << ", "
+ << "'" << Escape(dbobj->GetName1()) << "', '" << Escape(dbobj->GetName2()) << "', 1)";
+ } else {
+ qbuf << "INSERT INTO " + GetTablePrefix() + "objects (instance_id, objecttype_id, name1, is_active) VALUES ("
+ << static_cast<long>(m_InstanceID) << ", " << dbobj->GetType()->GetTypeID() << ", "
+ << "'" << Escape(dbobj->GetName1()) << "', 1)";
+ }
+
+ Query(qbuf.str());
+ SetObjectID(dbobj, GetLastInsertID());
+ } else {
+ qbuf << "UPDATE " + GetTablePrefix() + "objects SET is_active = 1 WHERE object_id = " << static_cast<long>(dbref);
+ IncreasePendingQueries(1);
+ AsyncQuery(qbuf.str());
+ }
+}
+
+void IdoMysqlConnection::DeactivateObject(const DbObject::Ptr& dbobj)
+{
+ if (IsPaused())
+ return;
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling object deactivation task for '" << dbobj->GetName1() << "!" << dbobj->GetName2() << "'.";
+#endif /* I2_DEBUG */
+
+ m_QueryQueue.Enqueue([this, dbobj]() { InternalDeactivateObject(dbobj); }, PriorityNormal);
+}
+
+void IdoMysqlConnection::InternalDeactivateObject(const DbObject::Ptr& dbobj)
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused())
+ return;
+
+ if (!GetConnected())
+ return;
+
+ DbReference dbref = GetObjectID(dbobj);
+
+ if (!dbref.IsValid())
+ return;
+
+ std::ostringstream qbuf;
+ qbuf << "UPDATE " + GetTablePrefix() + "objects SET is_active = 0 WHERE object_id = " << static_cast<long>(dbref);
+ IncreasePendingQueries(1);
+ AsyncQuery(qbuf.str());
+
+ /* Note that we're _NOT_ clearing the db refs via SetReference/SetConfigUpdate/SetStatusUpdate
+ * because the object is still in the database. */
+
+ SetObjectActive(dbobj, false);
+}
+
+bool IdoMysqlConnection::FieldToEscapedString(const String& key, const Value& value, Value *result)
+{
+ if (key == "instance_id") {
+ *result = static_cast<long>(m_InstanceID);
+ return true;
+ } else if (key == "session_token") {
+ *result = GetSessionToken();
+ return true;
+ }
+
+ Value rawvalue = DbValue::ExtractValue(value);
+
+ if (rawvalue.GetType() == ValueEmpty) {
+ *result = "NULL";
+ } else if (rawvalue.IsObjectType<ConfigObject>()) {
+ DbObject::Ptr dbobjcol = DbObject::GetOrCreateByObject(rawvalue);
+
+ if (!dbobjcol) {
+ *result = 0;
+ return true;
+ }
+
+ if (!IsIDCacheValid())
+ return false;
+
+ DbReference dbrefcol;
+
+ if (DbValue::IsObjectInsertID(value)) {
+ dbrefcol = GetInsertID(dbobjcol);
+
+ if (!dbrefcol.IsValid())
+ return false;
+ } else {
+ dbrefcol = GetObjectID(dbobjcol);
+
+ if (!dbrefcol.IsValid()) {
+ InternalActivateObject(dbobjcol);
+
+ dbrefcol = GetObjectID(dbobjcol);
+
+ if (!dbrefcol.IsValid())
+ return false;
+ }
+ }
+
+ *result = static_cast<long>(dbrefcol);
+ } else if (DbValue::IsTimestamp(value)) {
+ long ts = rawvalue;
+ std::ostringstream msgbuf;
+ msgbuf << "FROM_UNIXTIME(" << ts << ")";
+ *result = Value(msgbuf.str());
+ } else if (DbValue::IsObjectInsertID(value)) {
+ auto id = static_cast<long>(rawvalue);
+
+ if (id <= 0)
+ return false;
+
+ *result = id;
+ return true;
+ } else {
+ Value fvalue;
+
+ if (rawvalue.IsBoolean())
+ fvalue = Convert::ToLong(rawvalue);
+ else
+ fvalue = rawvalue;
+
+ *result = "'" + Escape(fvalue) + "'";
+ }
+
+ return true;
+}
+
+void IdoMysqlConnection::ExecuteQuery(const DbQuery& query)
+{
+ if (IsPaused() && GetPauseCalled())
+ return;
+
+ ASSERT(query.Category != DbCatInvalid);
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling execute query task, type " << query.Type << ", table '" << query.Table << "'.";
+#endif /* I2_DEBUG */
+
+ IncreasePendingQueries(1);
+ m_QueryQueue.Enqueue([this, query]() { InternalExecuteQuery(query, -1); }, query.Priority, true);
+}
+
+void IdoMysqlConnection::ExecuteMultipleQueries(const std::vector<DbQuery>& queries)
+{
+ if (IsPaused())
+ return;
+
+ if (queries.empty())
+ return;
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling multiple execute query task, type " << queries[0].Type << ", table '" << queries[0].Table << "'.";
+#endif /* I2_DEBUG */
+
+ IncreasePendingQueries(queries.size());
+ m_QueryQueue.Enqueue([this, queries]() { InternalExecuteMultipleQueries(queries); }, queries[0].Priority, true);
+}
+
+bool IdoMysqlConnection::CanExecuteQuery(const DbQuery& query)
+{
+ if (query.Object && !IsIDCacheValid())
+ return false;
+
+ if (query.WhereCriteria) {
+ ObjectLock olock(query.WhereCriteria);
+ Value value;
+
+ for (const Dictionary::Pair& kv : query.WhereCriteria) {
+ if (!FieldToEscapedString(kv.first, kv.second, &value))
+ return false;
+ }
+ }
+
+ if (query.Fields) {
+ ObjectLock olock(query.Fields);
+
+ for (const Dictionary::Pair& kv : query.Fields) {
+ Value value;
+
+ if (!FieldToEscapedString(kv.first, kv.second, &value))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void IdoMysqlConnection::InternalExecuteMultipleQueries(const std::vector<DbQuery>& queries)
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused()) {
+ DecreasePendingQueries(queries.size());
+ return;
+ }
+
+ if (!GetConnected()) {
+ DecreasePendingQueries(queries.size());
+ return;
+ }
+
+
+ for (const DbQuery& query : queries) {
+ ASSERT(query.Type == DbQueryNewTransaction || query.Category != DbCatInvalid);
+
+ if (!CanExecuteQuery(query)) {
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling multiple execute query task again: Cannot execute query now. Type '"
+ << query.Type << "', table '" << query.Table << "', queue size: '" << GetPendingQueryCount() << "'.";
+#endif /* I2_DEBUG */
+
+ m_QueryQueue.Enqueue([this, queries]() { InternalExecuteMultipleQueries(queries); }, query.Priority);
+ return;
+ }
+ }
+
+ for (const DbQuery& query : queries) {
+ InternalExecuteQuery(query);
+ }
+}
+
+void IdoMysqlConnection::InternalExecuteQuery(const DbQuery& query, int typeOverride)
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused() && GetPauseCalled()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ if (!GetConnected()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ if (query.Type == DbQueryNewTransaction) {
+ DecreasePendingQueries(1);
+ InternalNewTransaction();
+ return;
+ }
+
+ /* check whether we're allowed to execute the query first */
+ if (GetCategoryFilter() != DbCatEverything && (query.Category & GetCategoryFilter()) == 0) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ if (query.Object && query.Object->GetObject()->GetExtension("agent_check").ToBool()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ /* check if there are missing object/insert ids and re-enqueue the query */
+ if (!CanExecuteQuery(query)) {
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling execute query task again: Cannot execute query now. Type '"
+ << typeOverride << "', table '" << query.Table << "', queue size: '" << GetPendingQueryCount() << "'.";
+#endif /* I2_DEBUG */
+
+ m_QueryQueue.Enqueue([this, query, typeOverride]() { InternalExecuteQuery(query, typeOverride); }, query.Priority);
+ return;
+ }
+
+ std::ostringstream qbuf, where;
+ int type;
+
+ if (query.WhereCriteria) {
+ where << " WHERE ";
+
+ ObjectLock olock(query.WhereCriteria);
+ Value value;
+ bool first = true;
+
+ for (const Dictionary::Pair& kv : query.WhereCriteria) {
+ if (!FieldToEscapedString(kv.first, kv.second, &value)) {
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling execute query task again: Cannot execute query now. Type '"
+ << typeOverride << "', table '" << query.Table << "', queue size: '" << GetPendingQueryCount() << "'.";
+#endif /* I2_DEBUG */
+
+ m_QueryQueue.Enqueue([this, query]() { InternalExecuteQuery(query, -1); }, query.Priority);
+ return;
+ }
+
+ if (!first)
+ where << " AND ";
+
+ where << kv.first << " = " << value;
+
+ if (first)
+ first = false;
+ }
+ }
+
+ type = (typeOverride != -1) ? typeOverride : query.Type;
+
+ bool upsert = false;
+
+ if ((type & DbQueryInsert) && (type & DbQueryUpdate)) {
+ bool hasid = false;
+
+ if (query.Object) {
+ if (query.ConfigUpdate)
+ hasid = GetConfigUpdate(query.Object);
+ else if (query.StatusUpdate)
+ hasid = GetStatusUpdate(query.Object);
+ }
+
+ if (!hasid)
+ upsert = true;
+
+ type = DbQueryUpdate;
+ }
+
+ if ((type & DbQueryInsert) && (type & DbQueryDelete)) {
+ std::ostringstream qdel;
+ qdel << "DELETE FROM " << GetTablePrefix() << query.Table << where.str();
+ IncreasePendingQueries(1);
+ AsyncQuery(qdel.str());
+
+ type = DbQueryInsert;
+ }
+
+ switch (type) {
+ case DbQueryInsert:
+ qbuf << "INSERT INTO " << GetTablePrefix() << query.Table;
+ break;
+ case DbQueryUpdate:
+ qbuf << "UPDATE " << GetTablePrefix() << query.Table << " SET";
+ break;
+ case DbQueryDelete:
+ qbuf << "DELETE FROM " << GetTablePrefix() << query.Table;
+ break;
+ default:
+ VERIFY(!"Invalid query type.");
+ }
+
+ if (type == DbQueryInsert || type == DbQueryUpdate) {
+ std::ostringstream colbuf, valbuf;
+
+ if (type == DbQueryUpdate && query.Fields->GetLength() == 0)
+ return;
+
+ ObjectLock olock(query.Fields);
+
+ bool first = true;
+ for (const Dictionary::Pair& kv : query.Fields) {
+ Value value;
+
+ if (!FieldToEscapedString(kv.first, kv.second, &value)) {
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Scheduling execute query task again: Cannot extract required INSERT/UPDATE fields, key '"
+ << kv.first << "', val '" << kv.second << "', type " << typeOverride << ", table '" << query.Table << "'.";
+#endif /* I2_DEBUG */
+
+ m_QueryQueue.Enqueue([this, query]() { InternalExecuteQuery(query, -1); }, query.Priority);
+ return;
+ }
+
+ if (type == DbQueryInsert) {
+ if (!first) {
+ colbuf << ", ";
+ valbuf << ", ";
+ }
+
+ colbuf << kv.first;
+ valbuf << value;
+ } else {
+ if (!first)
+ qbuf << ", ";
+
+ qbuf << " " << kv.first << " = " << value;
+ }
+
+ if (first)
+ first = false;
+ }
+
+ if (type == DbQueryInsert)
+ qbuf << " (" << colbuf.str() << ") VALUES (" << valbuf.str() << ")";
+ }
+
+ if (type != DbQueryInsert)
+ qbuf << where.str();
+
+ AsyncQuery(qbuf.str(), [this, query, type, upsert](const IdoMysqlResult&) { FinishExecuteQuery(query, type, upsert); });
+}
+
+void IdoMysqlConnection::FinishExecuteQuery(const DbQuery& query, int type, bool upsert)
+{
+ if (upsert && GetAffectedRows() == 0) {
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Rescheduling DELETE/INSERT query: Upsert UPDATE did not affect rows, type " << type << ", table '" << query.Table << "'.";
+#endif /* I2_DEBUG */
+
+ IncreasePendingQueries(1);
+ m_QueryQueue.Enqueue([this, query]() { InternalExecuteQuery(query, DbQueryDelete | DbQueryInsert); }, query.Priority);
+
+ return;
+ }
+
+ if (type == DbQueryInsert && query.Object) {
+ if (query.ConfigUpdate) {
+ SetInsertID(query.Object, GetLastInsertID());
+ SetConfigUpdate(query.Object, true);
+ } else if (query.StatusUpdate)
+ SetStatusUpdate(query.Object, true);
+ }
+
+ if (type == DbQueryInsert && query.Table == "notifications" && query.NotificationInsertID)
+ query.NotificationInsertID->SetValue(static_cast<long>(GetLastInsertID()));
+}
+
+void IdoMysqlConnection::CleanUpExecuteQuery(const String& table, const String& time_column, double max_age)
+{
+ if (IsPaused())
+ return;
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "IdoMysqlConnection")
+ << "Rescheduling cleanup query for table '" << table << "' and column '"
+ << time_column << "'. max_age is set to '" << max_age << "'.";
+#endif /* I2_DEBUG */
+
+ IncreasePendingQueries(1);
+ m_QueryQueue.Enqueue([this, table, time_column, max_age]() { InternalCleanUpExecuteQuery(table, time_column, max_age); }, PriorityLow, true);
+}
+
+void IdoMysqlConnection::InternalCleanUpExecuteQuery(const String& table, const String& time_column, double max_age)
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ if (!GetConnected()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ AsyncQuery("DELETE FROM " + GetTablePrefix() + table + " WHERE instance_id = " +
+ Convert::ToString(static_cast<long>(m_InstanceID)) + " AND " + time_column +
+ " < FROM_UNIXTIME(" + Convert::ToString(static_cast<long>(max_age)) + ")");
+}
+
+void IdoMysqlConnection::FillIDCache(const DbType::Ptr& type)
+{
+ String query = "SELECT " + type->GetIDColumn() + " AS object_id, " + type->GetTable() + "_id, config_hash FROM " + GetTablePrefix() + type->GetTable() + "s";
+ IdoMysqlResult result = Query(query);
+
+ Dictionary::Ptr row;
+
+ while ((row = FetchRow(result))) {
+ DbReference dbref(row->Get("object_id"));
+ SetInsertID(type, dbref, DbReference(row->Get(type->GetTable() + "_id")));
+ SetConfigHash(type, dbref, row->Get("config_hash"));
+ }
+}
+
+int IdoMysqlConnection::GetPendingQueryCount() const
+{
+ return m_QueryQueue.GetLength();
+}
diff --git a/lib/db_ido_mysql/idomysqlconnection.hpp b/lib/db_ido_mysql/idomysqlconnection.hpp
new file mode 100644
index 0000000..5a5c120
--- /dev/null
+++ b/lib/db_ido_mysql/idomysqlconnection.hpp
@@ -0,0 +1,114 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef IDOMYSQLCONNECTION_H
+#define IDOMYSQLCONNECTION_H
+
+#include "db_ido_mysql/idomysqlconnection-ti.hpp"
+#include "mysql_shim/mysqlinterface.hpp"
+#include "base/array.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include "base/library.hpp"
+#include <cstdint>
+
+namespace icinga
+{
+
+typedef std::shared_ptr<MYSQL_RES> IdoMysqlResult;
+
+typedef std::function<void (const IdoMysqlResult&)> IdoAsyncCallback;
+
+struct IdoAsyncQuery
+{
+ String Query;
+ IdoAsyncCallback Callback;
+};
+
+/**
+ * An IDO MySQL database connection.
+ *
+ * @ingroup ido
+ */
+class IdoMysqlConnection final : public ObjectImpl<IdoMysqlConnection>
+{
+public:
+ DECLARE_OBJECT(IdoMysqlConnection);
+ DECLARE_OBJECTNAME(IdoMysqlConnection);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ const char * GetLatestSchemaVersion() const noexcept override;
+ const char * GetCompatSchemaVersion() const noexcept override;
+
+ int GetPendingQueryCount() const override;
+
+protected:
+ void OnConfigLoaded() override;
+ void Resume() override;
+ void Pause() override;
+
+ void ActivateObject(const DbObject::Ptr& dbobj) override;
+ void DeactivateObject(const DbObject::Ptr& dbobj) override;
+ void ExecuteQuery(const DbQuery& query) override;
+ void ExecuteMultipleQueries(const std::vector<DbQuery>& queries) override;
+ void CleanUpExecuteQuery(const String& table, const String& time_key, double time_value) override;
+ void FillIDCache(const DbType::Ptr& type) override;
+ void NewTransaction() override;
+ void Disconnect() override;
+
+private:
+ DbReference m_InstanceID;
+
+ Library m_Library;
+ std::unique_ptr<MysqlInterface, MysqlInterfaceDeleter> m_Mysql;
+
+ MYSQL m_Connection;
+ int m_AffectedRows;
+ unsigned int m_MaxPacketSize;
+
+ std::vector<IdoAsyncQuery> m_AsyncQueries;
+ uint_fast32_t m_UncommittedAsyncQueries = 0;
+
+ Timer::Ptr m_ReconnectTimer;
+ Timer::Ptr m_TxTimer;
+
+ IdoMysqlResult Query(const String& query);
+ DbReference GetLastInsertID();
+ int GetAffectedRows();
+ String Escape(const String& s);
+ Dictionary::Ptr FetchRow(const IdoMysqlResult& result);
+ void DiscardRows(const IdoMysqlResult& result);
+
+ void AsyncQuery(const String& query, const IdoAsyncCallback& callback = IdoAsyncCallback());
+ void FinishAsyncQueries();
+
+ bool FieldToEscapedString(const String& key, const Value& value, Value *result);
+ void InternalActivateObject(const DbObject::Ptr& dbobj);
+ void InternalDeactivateObject(const DbObject::Ptr& dbobj);
+
+ void Reconnect();
+
+ void AssertOnWorkQueue();
+
+ void ReconnectTimerHandler();
+
+ bool CanExecuteQuery(const DbQuery& query);
+
+ void InternalExecuteQuery(const DbQuery& query, int typeOverride = -1);
+ void InternalExecuteMultipleQueries(const std::vector<DbQuery>& queries);
+
+ void FinishExecuteQuery(const DbQuery& query, int type, bool upsert);
+ void InternalCleanUpExecuteQuery(const String& table, const String& time_key, double time_value);
+ void InternalNewTransaction();
+
+ void ClearTableBySession(const String& table);
+ void ClearTablesBySession();
+
+ void ExceptionHandler(boost::exception_ptr exp);
+
+ void FinishConnect(double startTime);
+};
+
+}
+
+#endif /* IDOMYSQLCONNECTION_H */
diff --git a/lib/db_ido_mysql/idomysqlconnection.ti b/lib/db_ido_mysql/idomysqlconnection.ti
new file mode 100644
index 0000000..681148f
--- /dev/null
+++ b/lib/db_ido_mysql/idomysqlconnection.ti
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbconnection.hpp"
+
+library db_ido_mysql;
+
+namespace icinga
+{
+
+class IdoMysqlConnection : DbConnection
+{
+ activation_priority 100;
+
+ [config] String host {
+ default {{{ return "localhost"; }}}
+ };
+ [config] int port {
+ default {{{ return 3306; }}}
+ };
+ [config] String socket_path;
+ [config] String user {
+ default {{{ return "icinga"; }}}
+ };
+ [config, no_user_view, no_user_modify] String password {
+ default {{{ return "icinga"; }}}
+ };
+ [config] String database {
+ default {{{ return "icinga"; }}}
+ };
+ [config] bool enable_ssl;
+ [config] String ssl_key;
+ [config] String ssl_cert;
+ [config] String ssl_ca;
+ [config] String ssl_capath;
+ [config] String ssl_cipher;
+ [config] String instance_name {
+ default {{{ return "default"; }}}
+ };
+ [config] String instance_description;
+};
+
+}
diff --git a/lib/db_ido_mysql/schema/mysql.sql b/lib/db_ido_mysql/schema/mysql.sql
new file mode 100644
index 0000000..020ba3c
--- /dev/null
+++ b/lib/db_ido_mysql/schema/mysql.sql
@@ -0,0 +1,1666 @@
+-- --------------------------------------------------------
+-- mysql.sql
+-- DB definition for IDO MySQL
+--
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- -- --------------------------------------------------------
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+
+--
+-- Database: icinga
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_acknowledgements
+--
+
+CREATE TABLE IF NOT EXISTS icinga_acknowledgements (
+ acknowledgement_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ entry_time timestamp NULL,
+ entry_time_usec int default 0,
+ acknowledgement_type smallint default 0,
+ object_id bigint unsigned default 0,
+ state smallint default 0,
+ author_name varchar(64) character set latin1 default '',
+ comment_data TEXT character set latin1,
+ is_sticky smallint default 0,
+ persistent_comment smallint default 0,
+ notify_contacts smallint default 0,
+ end_time timestamp NULL,
+ PRIMARY KEY (acknowledgement_id)
+) ENGINE=InnoDB COMMENT='Current and historical host and service acknowledgements';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_commands
+--
+
+CREATE TABLE IF NOT EXISTS icinga_commands (
+ command_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ object_id bigint unsigned default 0,
+ command_line TEXT character set latin1,
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (command_id),
+ UNIQUE KEY instance_id (instance_id,object_id,config_type)
+) ENGINE=InnoDB COMMENT='Command definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_commenthistory
+--
+
+CREATE TABLE IF NOT EXISTS icinga_commenthistory (
+ commenthistory_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ entry_time timestamp NULL,
+ entry_time_usec int default 0,
+ comment_type smallint default 0,
+ entry_type smallint default 0,
+ object_id bigint unsigned default 0,
+ comment_time timestamp NULL,
+ internal_comment_id bigint unsigned default 0,
+ author_name varchar(64) character set latin1 default '',
+ comment_data TEXT character set latin1,
+ is_persistent smallint default 0,
+ comment_source smallint default 0,
+ expires smallint default 0,
+ expiration_time timestamp NULL,
+ deletion_time timestamp NULL,
+ deletion_time_usec int default 0,
+ name TEXT character set latin1 default NULL,
+ PRIMARY KEY (commenthistory_id)
+) ENGINE=InnoDB COMMENT='Historical host and service comments';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_comments
+--
+
+CREATE TABLE IF NOT EXISTS icinga_comments (
+ comment_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ entry_time timestamp NULL,
+ entry_time_usec int default 0,
+ comment_type smallint default 0,
+ entry_type smallint default 0,
+ object_id bigint unsigned default 0,
+ comment_time timestamp NULL,
+ internal_comment_id bigint unsigned default 0,
+ author_name varchar(64) character set latin1 default '',
+ comment_data TEXT character set latin1,
+ is_persistent smallint default 0,
+ comment_source smallint default 0,
+ expires smallint default 0,
+ expiration_time timestamp NULL,
+ name TEXT character set latin1 default NULL,
+ session_token int default NULL,
+ PRIMARY KEY (comment_id)
+) ENGINE=InnoDB COMMENT='Usercomments on Icinga objects';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_configfiles
+--
+
+CREATE TABLE IF NOT EXISTS icinga_configfiles (
+ configfile_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ configfile_type smallint default 0,
+ configfile_path varchar(255) character set latin1 default '',
+ PRIMARY KEY (configfile_id),
+ UNIQUE KEY instance_id (instance_id,configfile_type,configfile_path)
+) ENGINE=InnoDB COMMENT='Configuration files';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_configfilevariables
+--
+
+CREATE TABLE IF NOT EXISTS icinga_configfilevariables (
+ configfilevariable_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ configfile_id bigint unsigned default 0,
+ varname varchar(64) character set latin1 default '',
+ varvalue TEXT character set latin1,
+ PRIMARY KEY (configfilevariable_id)
+) ENGINE=InnoDB COMMENT='Configuration file variables';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_conninfo
+--
+
+CREATE TABLE IF NOT EXISTS icinga_conninfo (
+ conninfo_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ agent_name varchar(32) character set latin1 default '',
+ agent_version varchar(32) character set latin1 default '',
+ disposition varchar(32) character set latin1 default '',
+ connect_source varchar(32) character set latin1 default '',
+ connect_type varchar(32) character set latin1 default '',
+ connect_time timestamp NULL,
+ disconnect_time timestamp NULL,
+ last_checkin_time timestamp NULL,
+ data_start_time timestamp NULL,
+ data_end_time timestamp NULL,
+ bytes_processed bigint unsigned default '0',
+ lines_processed bigint unsigned default '0',
+ entries_processed bigint unsigned default '0',
+ PRIMARY KEY (conninfo_id)
+) ENGINE=InnoDB COMMENT='IDO2DB daemon connection information';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactgroups
+--
+
+CREATE TABLE IF NOT EXISTS icinga_contactgroups (
+ contactgroup_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ contactgroup_object_id bigint unsigned default 0,
+ alias varchar(255) character set latin1 default '',
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (contactgroup_id),
+ UNIQUE KEY instance_id (instance_id,config_type,contactgroup_object_id)
+) ENGINE=InnoDB COMMENT='Contactgroup definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactgroup_members
+--
+
+CREATE TABLE IF NOT EXISTS icinga_contactgroup_members (
+ contactgroup_member_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ contactgroup_id bigint unsigned default 0,
+ contact_object_id bigint unsigned default 0,
+ PRIMARY KEY (contactgroup_member_id)
+) ENGINE=InnoDB COMMENT='Contactgroup members';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactnotificationmethods
+--
+
+CREATE TABLE IF NOT EXISTS icinga_contactnotificationmethods (
+ contactnotificationmethod_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ contactnotification_id bigint unsigned default 0,
+ start_time timestamp NULL,
+ start_time_usec int default 0,
+ end_time timestamp NULL,
+ end_time_usec int default 0,
+ command_object_id bigint unsigned default 0,
+ command_args TEXT character set latin1,
+ PRIMARY KEY (contactnotificationmethod_id),
+ UNIQUE KEY instance_id (instance_id,contactnotification_id,start_time,start_time_usec)
+) ENGINE=InnoDB COMMENT='Historical record of contact notification methods';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactnotifications
+--
+
+CREATE TABLE IF NOT EXISTS icinga_contactnotifications (
+ contactnotification_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ notification_id bigint unsigned default 0,
+ contact_object_id bigint unsigned default 0,
+ start_time timestamp NULL,
+ start_time_usec int default 0,
+ end_time timestamp NULL,
+ end_time_usec int default 0,
+ PRIMARY KEY (contactnotification_id),
+ UNIQUE KEY instance_id (instance_id,contact_object_id,start_time,start_time_usec)
+) ENGINE=InnoDB COMMENT='Historical record of contact notifications';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contacts
+--
+
+CREATE TABLE IF NOT EXISTS icinga_contacts (
+ contact_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ contact_object_id bigint unsigned default 0,
+ alias varchar(255) character set latin1 default '',
+ email_address varchar(255) character set latin1 default '',
+ pager_address varchar(64) character set latin1 default '',
+ host_timeperiod_object_id bigint unsigned default 0,
+ service_timeperiod_object_id bigint unsigned default 0,
+ host_notifications_enabled smallint default 0,
+ service_notifications_enabled smallint default 0,
+ can_submit_commands smallint default 0,
+ notify_service_recovery smallint default 0,
+ notify_service_warning smallint default 0,
+ notify_service_unknown smallint default 0,
+ notify_service_critical smallint default 0,
+ notify_service_flapping smallint default 0,
+ notify_service_downtime smallint default 0,
+ notify_host_recovery smallint default 0,
+ notify_host_down smallint default 0,
+ notify_host_unreachable smallint default 0,
+ notify_host_flapping smallint default 0,
+ notify_host_downtime smallint default 0,
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (contact_id),
+ UNIQUE KEY instance_id (instance_id,config_type,contact_object_id)
+) ENGINE=InnoDB COMMENT='Contact definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactstatus
+--
+
+CREATE TABLE IF NOT EXISTS icinga_contactstatus (
+ contactstatus_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ contact_object_id bigint unsigned default 0,
+ status_update_time timestamp NULL,
+ host_notifications_enabled smallint default 0,
+ service_notifications_enabled smallint default 0,
+ last_host_notification timestamp NULL,
+ last_service_notification timestamp NULL,
+ modified_attributes int default 0,
+ modified_host_attributes int default 0,
+ modified_service_attributes int default 0,
+ PRIMARY KEY (contactstatus_id),
+ UNIQUE KEY contact_object_id (contact_object_id)
+) ENGINE=InnoDB COMMENT='Contact status';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contact_addresses
+--
+
+CREATE TABLE IF NOT EXISTS icinga_contact_addresses (
+ contact_address_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ contact_id bigint unsigned default 0,
+ address_number smallint default 0,
+ address varchar(255) character set latin1 default '',
+ PRIMARY KEY (contact_address_id),
+ UNIQUE KEY contact_id (contact_id,address_number)
+) ENGINE=InnoDB COMMENT='Contact addresses';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contact_notificationcommands
+--
+
+CREATE TABLE IF NOT EXISTS icinga_contact_notificationcommands (
+ contact_notificationcommand_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ contact_id bigint unsigned default 0,
+ notification_type smallint default 0,
+ command_object_id bigint unsigned default 0,
+ command_args varchar(255) character set latin1 default '',
+ PRIMARY KEY (contact_notificationcommand_id),
+ UNIQUE KEY contact_id (contact_id,notification_type,command_object_id,command_args)
+) ENGINE=InnoDB COMMENT='Contact host and service notification commands';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_customvariables
+--
+
+CREATE TABLE IF NOT EXISTS icinga_customvariables (
+ customvariable_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ object_id bigint unsigned default 0,
+ config_type smallint default 0,
+ has_been_modified smallint default 0,
+ varname varchar(255) character set latin1 collate latin1_general_cs default NULL,
+ varvalue TEXT character set latin1,
+ is_json smallint default 0,
+ PRIMARY KEY (customvariable_id),
+ UNIQUE KEY object_id_2 (object_id,config_type,varname),
+ KEY varname (varname)
+) ENGINE=InnoDB COMMENT='Custom variables';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_customvariablestatus
+--
+
+CREATE TABLE IF NOT EXISTS icinga_customvariablestatus (
+ customvariablestatus_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ object_id bigint unsigned default 0,
+ status_update_time timestamp NULL,
+ has_been_modified smallint default 0,
+ varname varchar(255) character set latin1 collate latin1_general_cs default NULL,
+ varvalue TEXT character set latin1,
+ is_json smallint default 0,
+ PRIMARY KEY (customvariablestatus_id),
+ UNIQUE KEY object_id_2 (object_id,varname),
+ KEY varname (varname)
+) ENGINE=InnoDB COMMENT='Custom variable status information';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_dbversion
+--
+
+CREATE TABLE IF NOT EXISTS icinga_dbversion (
+ dbversion_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ name varchar(10) character set latin1 default '',
+ version varchar(10) character set latin1 default '',
+ create_time timestamp NULL,
+ modify_time timestamp NULL,
+ PRIMARY KEY (dbversion_id),
+ UNIQUE KEY dbversion (name)
+) ENGINE=InnoDB;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_downtimehistory
+--
+
+CREATE TABLE IF NOT EXISTS icinga_downtimehistory (
+ downtimehistory_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ downtime_type smallint default 0,
+ object_id bigint unsigned default 0,
+ entry_time timestamp NULL,
+ author_name varchar(64) character set latin1 default '',
+ comment_data TEXT character set latin1,
+ internal_downtime_id bigint unsigned default 0,
+ triggered_by_id bigint unsigned default 0,
+ is_fixed smallint default 0,
+ duration bigint(20) default 0,
+ scheduled_start_time timestamp NULL,
+ scheduled_end_time timestamp NULL,
+ was_started smallint default 0,
+ actual_start_time timestamp NULL,
+ actual_start_time_usec int default 0,
+ actual_end_time timestamp NULL,
+ actual_end_time_usec int default 0,
+ was_cancelled smallint default 0,
+ is_in_effect smallint default 0,
+ trigger_time timestamp NULL,
+ name TEXT character set latin1 default NULL,
+ PRIMARY KEY (downtimehistory_id)
+) ENGINE=InnoDB COMMENT='Historical scheduled host and service downtime';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_eventhandlers
+--
+
+CREATE TABLE IF NOT EXISTS icinga_eventhandlers (
+ eventhandler_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ eventhandler_type smallint default 0,
+ object_id bigint unsigned default 0,
+ state smallint default 0,
+ state_type smallint default 0,
+ start_time timestamp NULL,
+ start_time_usec int default 0,
+ end_time timestamp NULL,
+ end_time_usec int default 0,
+ command_object_id bigint unsigned default 0,
+ command_args TEXT character set latin1,
+ command_line TEXT character set latin1,
+ timeout smallint default 0,
+ early_timeout smallint default 0,
+ execution_time double default '0',
+ return_code smallint default 0,
+ output TEXT character set latin1,
+ long_output TEXT,
+ PRIMARY KEY (eventhandler_id),
+ UNIQUE KEY instance_id (instance_id,object_id,start_time,start_time_usec)
+) ENGINE=InnoDB COMMENT='Historical host and service event handlers';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_externalcommands
+--
+
+CREATE TABLE IF NOT EXISTS icinga_externalcommands (
+ externalcommand_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ entry_time timestamp NULL,
+ command_type smallint default 0,
+ command_name varchar(128) character set latin1 default '',
+ command_args TEXT character set latin1,
+ PRIMARY KEY (externalcommand_id)
+) ENGINE=InnoDB COMMENT='Historical record of processed external commands';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_flappinghistory
+--
+
+CREATE TABLE IF NOT EXISTS icinga_flappinghistory (
+ flappinghistory_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ event_time timestamp NULL,
+ event_time_usec int default 0,
+ event_type smallint default 0,
+ reason_type smallint default 0,
+ flapping_type smallint default 0,
+ object_id bigint unsigned default 0,
+ percent_state_change double default '0',
+ low_threshold double default '0',
+ high_threshold double default '0',
+ comment_time timestamp NULL,
+ internal_comment_id bigint unsigned default 0,
+ PRIMARY KEY (flappinghistory_id)
+) ENGINE=InnoDB COMMENT='Current and historical record of host and service flapping';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostchecks
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hostchecks (
+ hostcheck_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ host_object_id bigint unsigned default 0,
+ check_type smallint default 0,
+ is_raw_check smallint default 0,
+ current_check_attempt smallint default 0,
+ max_check_attempts smallint default 0,
+ state smallint default 0,
+ state_type smallint default 0,
+ start_time timestamp NULL,
+ start_time_usec int default 0,
+ end_time timestamp NULL,
+ end_time_usec int default 0,
+ command_object_id bigint unsigned default 0,
+ command_args TEXT character set latin1,
+ command_line TEXT character set latin1,
+ timeout smallint default 0,
+ early_timeout smallint default 0,
+ execution_time double default '0',
+ latency double default '0',
+ return_code smallint default 0,
+ output TEXT character set latin1,
+ long_output TEXT,
+ perfdata TEXT character set latin1,
+ PRIMARY KEY (hostcheck_id)
+) ENGINE=InnoDB COMMENT='Historical host checks';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostdependencies
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hostdependencies (
+ hostdependency_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ host_object_id bigint unsigned default 0,
+ dependent_host_object_id bigint unsigned default 0,
+ dependency_type smallint default 0,
+ inherits_parent smallint default 0,
+ timeperiod_object_id bigint unsigned default 0,
+ fail_on_up smallint default 0,
+ fail_on_down smallint default 0,
+ fail_on_unreachable smallint default 0,
+ PRIMARY KEY (hostdependency_id),
+ KEY instance_id (instance_id,config_type,host_object_id,dependent_host_object_id,dependency_type,inherits_parent,fail_on_up,fail_on_down,fail_on_unreachable)
+) ENGINE=InnoDB COMMENT='Host dependency definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostescalations
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hostescalations (
+ hostescalation_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ host_object_id bigint unsigned default 0,
+ timeperiod_object_id bigint unsigned default 0,
+ first_notification smallint default 0,
+ last_notification smallint default 0,
+ notification_interval double default '0',
+ escalate_on_recovery smallint default 0,
+ escalate_on_down smallint default 0,
+ escalate_on_unreachable smallint default 0,
+ PRIMARY KEY (hostescalation_id),
+ UNIQUE KEY instance_id (instance_id,config_type,host_object_id,timeperiod_object_id,first_notification,last_notification)
+) ENGINE=InnoDB COMMENT='Host escalation definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostescalation_contactgroups
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hostescalation_contactgroups (
+ hostescalation_contactgroup_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ hostescalation_id bigint unsigned default 0,
+ contactgroup_object_id bigint unsigned default 0,
+ PRIMARY KEY (hostescalation_contactgroup_id),
+ UNIQUE KEY instance_id (hostescalation_id,contactgroup_object_id)
+) ENGINE=InnoDB COMMENT='Host escalation contact groups';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostescalation_contacts
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hostescalation_contacts (
+ hostescalation_contact_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ hostescalation_id bigint unsigned default 0,
+ contact_object_id bigint unsigned default 0,
+ PRIMARY KEY (hostescalation_contact_id),
+ UNIQUE KEY instance_id (instance_id,hostescalation_id,contact_object_id)
+) ENGINE=InnoDB COMMENT='Host escalation contacts';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostgroups
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hostgroups (
+ hostgroup_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ hostgroup_object_id bigint unsigned default 0,
+ alias varchar(255) character set latin1 default '',
+ notes TEXT character set latin1 default NULL,
+ notes_url TEXT character set latin1 default NULL,
+ action_url TEXT character set latin1 default NULL,
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (hostgroup_id),
+ UNIQUE KEY instance_id (instance_id,hostgroup_object_id)
+) ENGINE=InnoDB COMMENT='Hostgroup definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostgroup_members
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hostgroup_members (
+ hostgroup_member_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ hostgroup_id bigint unsigned default 0,
+ host_object_id bigint unsigned default 0,
+ PRIMARY KEY (hostgroup_member_id)
+) ENGINE=InnoDB COMMENT='Hostgroup members';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hosts
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hosts (
+ host_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ host_object_id bigint unsigned default 0,
+ alias varchar(255) character set latin1 default '',
+ display_name varchar(255) character set latin1 collate latin1_general_cs default '',
+ address varchar(128) character set latin1 default '',
+ address6 varchar(128) character set latin1 default '',
+ check_command_object_id bigint unsigned default 0,
+ check_command_args TEXT character set latin1,
+ eventhandler_command_object_id bigint unsigned default 0,
+ eventhandler_command_args TEXT character set latin1,
+ notification_timeperiod_object_id bigint unsigned default 0,
+ check_timeperiod_object_id bigint unsigned default 0,
+ failure_prediction_options varchar(128) character set latin1 default '',
+ check_interval double default '0',
+ retry_interval double default '0',
+ max_check_attempts smallint default 0,
+ first_notification_delay double default '0',
+ notification_interval double default '0',
+ notify_on_down smallint default 0,
+ notify_on_unreachable smallint default 0,
+ notify_on_recovery smallint default 0,
+ notify_on_flapping smallint default 0,
+ notify_on_downtime smallint default 0,
+ stalk_on_up smallint default 0,
+ stalk_on_down smallint default 0,
+ stalk_on_unreachable smallint default 0,
+ flap_detection_enabled smallint default 0,
+ flap_detection_on_up smallint default 0,
+ flap_detection_on_down smallint default 0,
+ flap_detection_on_unreachable smallint default 0,
+ low_flap_threshold double default '0',
+ high_flap_threshold double default '0',
+ process_performance_data smallint default 0,
+ freshness_checks_enabled smallint default 0,
+ freshness_threshold int default 0,
+ passive_checks_enabled smallint default 0,
+ event_handler_enabled smallint default 0,
+ active_checks_enabled smallint default 0,
+ retain_status_information smallint default 0,
+ retain_nonstatus_information smallint default 0,
+ notifications_enabled smallint default 0,
+ obsess_over_host smallint default 0,
+ failure_prediction_enabled smallint default 0,
+ notes TEXT character set latin1,
+ notes_url TEXT character set latin1,
+ action_url TEXT character set latin1,
+ icon_image TEXT character set latin1,
+ icon_image_alt TEXT character set latin1,
+ vrml_image TEXT character set latin1,
+ statusmap_image TEXT character set latin1,
+ have_2d_coords smallint default 0,
+ x_2d smallint default 0,
+ y_2d smallint default 0,
+ have_3d_coords smallint default 0,
+ x_3d double default '0',
+ y_3d double default '0',
+ z_3d double default '0',
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (host_id),
+ UNIQUE KEY instance_id (instance_id,config_type,host_object_id)
+) ENGINE=InnoDB COMMENT='Host definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hoststatus
+--
+
+CREATE TABLE IF NOT EXISTS icinga_hoststatus (
+ hoststatus_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ host_object_id bigint unsigned default 0,
+ status_update_time timestamp NULL,
+ output TEXT character set latin1,
+ long_output TEXT,
+ perfdata TEXT character set latin1,
+ check_source varchar(255) character set latin1 default '',
+ current_state smallint default 0,
+ has_been_checked smallint default 0,
+ should_be_scheduled smallint default 0,
+ current_check_attempt smallint default 0,
+ max_check_attempts smallint default 0,
+ last_check timestamp NULL,
+ next_check timestamp NULL,
+ check_type smallint default 0,
+ last_state_change timestamp NULL,
+ last_hard_state_change timestamp NULL,
+ last_hard_state smallint default 0,
+ last_time_up timestamp NULL,
+ last_time_down timestamp NULL,
+ last_time_unreachable timestamp NULL,
+ state_type smallint default 0,
+ last_notification timestamp NULL,
+ next_notification timestamp NULL,
+ no_more_notifications smallint default 0,
+ notifications_enabled smallint default 0,
+ problem_has_been_acknowledged smallint default 0,
+ acknowledgement_type smallint default 0,
+ current_notification_number int unsigned default 0,
+ passive_checks_enabled smallint default 0,
+ active_checks_enabled smallint default 0,
+ event_handler_enabled smallint default 0,
+ flap_detection_enabled smallint default 0,
+ is_flapping smallint default 0,
+ percent_state_change double default '0',
+ latency double default '0',
+ execution_time double default '0',
+ scheduled_downtime_depth smallint default 0,
+ failure_prediction_enabled smallint default 0,
+ process_performance_data smallint default 0,
+ obsess_over_host smallint default 0,
+ modified_host_attributes int default 0,
+ original_attributes TEXT character set latin1 default NULL,
+ event_handler TEXT character set latin1,
+ check_command TEXT character set latin1,
+ normal_check_interval double default '0',
+ retry_check_interval double default '0',
+ check_timeperiod_object_id bigint unsigned default 0,
+ is_reachable smallint default 0,
+ PRIMARY KEY (hoststatus_id),
+ UNIQUE KEY object_id (host_object_id)
+) ENGINE=InnoDB COMMENT='Current host status information';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_host_contactgroups
+--
+
+CREATE TABLE IF NOT EXISTS icinga_host_contactgroups (
+ host_contactgroup_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ host_id bigint unsigned default 0,
+ contactgroup_object_id bigint unsigned default 0,
+ PRIMARY KEY (host_contactgroup_id)
+) ENGINE=InnoDB COMMENT='Host contact groups';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_host_contacts
+--
+
+CREATE TABLE IF NOT EXISTS icinga_host_contacts (
+ host_contact_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ host_id bigint unsigned default 0,
+ contact_object_id bigint unsigned default 0,
+ PRIMARY KEY (host_contact_id)
+) ENGINE=InnoDB COMMENT='Host contacts';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_host_parenthosts
+--
+
+CREATE TABLE IF NOT EXISTS icinga_host_parenthosts (
+ host_parenthost_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ host_id bigint unsigned default 0,
+ parent_host_object_id bigint unsigned default 0,
+ PRIMARY KEY (host_parenthost_id)
+) ENGINE=InnoDB COMMENT='Parent hosts';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_instances
+--
+
+CREATE TABLE IF NOT EXISTS icinga_instances (
+ instance_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_name varchar(64) character set latin1 default '',
+ instance_description varchar(128) character set latin1 default '',
+ PRIMARY KEY (instance_id)
+) ENGINE=InnoDB COMMENT='Location names of various Icinga installations';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_logentries
+--
+
+CREATE TABLE IF NOT EXISTS icinga_logentries (
+ logentry_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ logentry_time timestamp NULL,
+ entry_time timestamp NULL,
+ entry_time_usec int default 0,
+ logentry_type int default 0,
+ logentry_data TEXT character set latin1,
+ realtime_data smallint default 0,
+ inferred_data_extracted smallint default 0,
+ object_id bigint unsigned default NULL,
+ PRIMARY KEY (logentry_id)
+) ENGINE=InnoDB COMMENT='Historical record of log entries';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_notifications
+--
+
+CREATE TABLE IF NOT EXISTS icinga_notifications (
+ notification_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ notification_type smallint default 0,
+ notification_reason smallint default 0,
+ object_id bigint unsigned default 0,
+ start_time timestamp NULL,
+ start_time_usec int default 0,
+ end_time timestamp NULL,
+ end_time_usec int default 0,
+ state smallint default 0,
+ output TEXT character set latin1,
+ long_output TEXT,
+ escalated smallint default 0,
+ contacts_notified smallint default 0,
+ PRIMARY KEY (notification_id),
+ UNIQUE KEY instance_id (instance_id,object_id,start_time,start_time_usec)
+) ENGINE=InnoDB COMMENT='Historical record of host and service notifications';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_objects
+--
+
+CREATE TABLE IF NOT EXISTS icinga_objects (
+ object_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ objecttype_id bigint unsigned default 0,
+ name1 varchar(255) character set latin1 collate latin1_general_cs default '',
+ name2 varchar(255) character set latin1 collate latin1_general_cs default NULL,
+ is_active smallint default 0,
+ PRIMARY KEY (object_id),
+ KEY objecttype_id (objecttype_id,name1,name2)
+) ENGINE=InnoDB COMMENT='Current and historical objects of all kinds';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_processevents
+--
+
+CREATE TABLE IF NOT EXISTS icinga_processevents (
+ processevent_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ event_type smallint default 0,
+ event_time timestamp NULL,
+ event_time_usec int default 0,
+ process_id bigint unsigned default 0,
+ program_name varchar(16) character set latin1 default '',
+ program_version varchar(20) character set latin1 default '',
+ program_date varchar(10) character set latin1 default '',
+ PRIMARY KEY (processevent_id)
+) ENGINE=InnoDB COMMENT='Historical Icinga process events';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_programstatus
+--
+
+CREATE TABLE IF NOT EXISTS icinga_programstatus (
+ programstatus_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ program_version varchar(64) character set latin1 collate latin1_general_cs default NULL,
+ status_update_time timestamp NULL,
+ program_start_time timestamp NULL,
+ program_end_time timestamp NULL,
+ endpoint_name varchar(255) character set latin1 collate latin1_general_cs default NULL,
+ is_currently_running smallint default 0,
+ process_id bigint unsigned default 0,
+ daemon_mode smallint default 0,
+ last_command_check timestamp NULL,
+ last_log_rotation timestamp NULL,
+ notifications_enabled smallint default 0,
+ disable_notif_expire_time timestamp NULL,
+ active_service_checks_enabled smallint default 0,
+ passive_service_checks_enabled smallint default 0,
+ active_host_checks_enabled smallint default 0,
+ passive_host_checks_enabled smallint default 0,
+ event_handlers_enabled smallint default 0,
+ flap_detection_enabled smallint default 0,
+ failure_prediction_enabled smallint default 0,
+ process_performance_data smallint default 0,
+ obsess_over_hosts smallint default 0,
+ obsess_over_services smallint default 0,
+ modified_host_attributes int default 0,
+ modified_service_attributes int default 0,
+ global_host_event_handler TEXT character set latin1,
+ global_service_event_handler TEXT character set latin1,
+ config_dump_in_progress smallint default 0,
+ PRIMARY KEY (programstatus_id),
+ UNIQUE KEY instance_id (instance_id)
+) ENGINE=InnoDB COMMENT='Current program status information';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_runtimevariables
+--
+
+CREATE TABLE IF NOT EXISTS icinga_runtimevariables (
+ runtimevariable_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ varname varchar(64) character set latin1 default '',
+ varvalue TEXT character set latin1,
+ PRIMARY KEY (runtimevariable_id)
+) ENGINE=InnoDB COMMENT='Runtime variables from the Icinga daemon';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_scheduleddowntime
+--
+
+CREATE TABLE IF NOT EXISTS icinga_scheduleddowntime (
+ scheduleddowntime_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ downtime_type smallint default 0,
+ object_id bigint unsigned default 0,
+ entry_time timestamp NULL,
+ author_name varchar(64) character set latin1 default '',
+ comment_data TEXT character set latin1,
+ internal_downtime_id bigint unsigned default 0,
+ triggered_by_id bigint unsigned default 0,
+ is_fixed smallint default 0,
+ duration bigint(20) default 0,
+ scheduled_start_time timestamp NULL,
+ scheduled_end_time timestamp NULL,
+ was_started smallint default 0,
+ actual_start_time timestamp NULL,
+ actual_start_time_usec int default 0,
+ is_in_effect smallint default 0,
+ trigger_time timestamp NULL,
+ name TEXT character set latin1 default NULL,
+ session_token int default NULL,
+ PRIMARY KEY (scheduleddowntime_id)
+) ENGINE=InnoDB COMMENT='Current scheduled host and service downtime';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicechecks
+--
+
+CREATE TABLE IF NOT EXISTS icinga_servicechecks (
+ servicecheck_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ service_object_id bigint unsigned default 0,
+ check_type smallint default 0,
+ current_check_attempt smallint default 0,
+ max_check_attempts smallint default 0,
+ state smallint default 0,
+ state_type smallint default 0,
+ start_time timestamp NULL,
+ start_time_usec int default 0,
+ end_time timestamp NULL,
+ end_time_usec int default 0,
+ command_object_id bigint unsigned default 0,
+ command_args TEXT character set latin1,
+ command_line TEXT character set latin1,
+ timeout smallint default 0,
+ early_timeout smallint default 0,
+ execution_time double default '0',
+ latency double default '0',
+ return_code smallint default 0,
+ output TEXT character set latin1,
+ long_output TEXT,
+ perfdata TEXT character set latin1,
+ PRIMARY KEY (servicecheck_id)
+) ENGINE=InnoDB COMMENT='Historical service checks';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicedependencies
+--
+
+CREATE TABLE IF NOT EXISTS icinga_servicedependencies (
+ servicedependency_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ service_object_id bigint unsigned default 0,
+ dependent_service_object_id bigint unsigned default 0,
+ dependency_type smallint default 0,
+ inherits_parent smallint default 0,
+ timeperiod_object_id bigint unsigned default 0,
+ fail_on_ok smallint default 0,
+ fail_on_warning smallint default 0,
+ fail_on_unknown smallint default 0,
+ fail_on_critical smallint default 0,
+ PRIMARY KEY (servicedependency_id),
+ KEY instance_id (instance_id,config_type,service_object_id,dependent_service_object_id,dependency_type,inherits_parent,fail_on_ok,fail_on_warning,fail_on_unknown,fail_on_critical)
+) ENGINE=InnoDB COMMENT='Service dependency definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_serviceescalations
+--
+
+CREATE TABLE IF NOT EXISTS icinga_serviceescalations (
+ serviceescalation_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ service_object_id bigint unsigned default 0,
+ timeperiod_object_id bigint unsigned default 0,
+ first_notification smallint default 0,
+ last_notification smallint default 0,
+ notification_interval double default '0',
+ escalate_on_recovery smallint default 0,
+ escalate_on_warning smallint default 0,
+ escalate_on_unknown smallint default 0,
+ escalate_on_critical smallint default 0,
+ PRIMARY KEY (serviceescalation_id),
+ UNIQUE KEY instance_id (instance_id,config_type,service_object_id,timeperiod_object_id,first_notification,last_notification)
+) ENGINE=InnoDB COMMENT='Service escalation definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_serviceescalation_contactgroups
+--
+
+CREATE TABLE IF NOT EXISTS icinga_serviceescalation_contactgroups (
+ serviceescalation_contactgroup_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ serviceescalation_id bigint unsigned default 0,
+ contactgroup_object_id bigint unsigned default 0,
+ PRIMARY KEY (serviceescalation_contactgroup_id),
+ UNIQUE KEY instance_id (serviceescalation_id,contactgroup_object_id)
+) ENGINE=InnoDB COMMENT='Service escalation contact groups';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_serviceescalation_contacts
+--
+
+CREATE TABLE IF NOT EXISTS icinga_serviceescalation_contacts (
+ serviceescalation_contact_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ serviceescalation_id bigint unsigned default 0,
+ contact_object_id bigint unsigned default 0,
+ PRIMARY KEY (serviceescalation_contact_id),
+ UNIQUE KEY instance_id (instance_id,serviceescalation_id,contact_object_id)
+) ENGINE=InnoDB COMMENT='Service escalation contacts';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicegroups
+--
+
+CREATE TABLE IF NOT EXISTS icinga_servicegroups (
+ servicegroup_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ servicegroup_object_id bigint unsigned default 0,
+ alias varchar(255) character set latin1 default '',
+ notes TEXT character set latin1 default NULL,
+ notes_url TEXT character set latin1 default NULL,
+ action_url TEXT character set latin1 default NULL,
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (servicegroup_id),
+ UNIQUE KEY instance_id (instance_id,config_type,servicegroup_object_id)
+) ENGINE=InnoDB COMMENT='Servicegroup definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicegroup_members
+--
+
+CREATE TABLE IF NOT EXISTS icinga_servicegroup_members (
+ servicegroup_member_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ servicegroup_id bigint unsigned default 0,
+ service_object_id bigint unsigned default 0,
+ PRIMARY KEY (servicegroup_member_id)
+) ENGINE=InnoDB COMMENT='Servicegroup members';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_services
+--
+
+CREATE TABLE IF NOT EXISTS icinga_services (
+ service_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ host_object_id bigint unsigned default 0,
+ service_object_id bigint unsigned default 0,
+ display_name varchar(255) character set latin1 collate latin1_general_cs default '',
+ check_command_object_id bigint unsigned default 0,
+ check_command_args TEXT character set latin1,
+ eventhandler_command_object_id bigint unsigned default 0,
+ eventhandler_command_args TEXT character set latin1,
+ notification_timeperiod_object_id bigint unsigned default 0,
+ check_timeperiod_object_id bigint unsigned default 0,
+ failure_prediction_options varchar(64) character set latin1 default '',
+ check_interval double default '0',
+ retry_interval double default '0',
+ max_check_attempts smallint default 0,
+ first_notification_delay double default '0',
+ notification_interval double default '0',
+ notify_on_warning smallint default 0,
+ notify_on_unknown smallint default 0,
+ notify_on_critical smallint default 0,
+ notify_on_recovery smallint default 0,
+ notify_on_flapping smallint default 0,
+ notify_on_downtime smallint default 0,
+ stalk_on_ok smallint default 0,
+ stalk_on_warning smallint default 0,
+ stalk_on_unknown smallint default 0,
+ stalk_on_critical smallint default 0,
+ is_volatile smallint default 0,
+ flap_detection_enabled smallint default 0,
+ flap_detection_on_ok smallint default 0,
+ flap_detection_on_warning smallint default 0,
+ flap_detection_on_unknown smallint default 0,
+ flap_detection_on_critical smallint default 0,
+ low_flap_threshold double default '0',
+ high_flap_threshold double default '0',
+ process_performance_data smallint default 0,
+ freshness_checks_enabled smallint default 0,
+ freshness_threshold int default 0,
+ passive_checks_enabled smallint default 0,
+ event_handler_enabled smallint default 0,
+ active_checks_enabled smallint default 0,
+ retain_status_information smallint default 0,
+ retain_nonstatus_information smallint default 0,
+ notifications_enabled smallint default 0,
+ obsess_over_service smallint default 0,
+ failure_prediction_enabled smallint default 0,
+ notes TEXT character set latin1,
+ notes_url TEXT character set latin1,
+ action_url TEXT character set latin1,
+ icon_image TEXT character set latin1,
+ icon_image_alt TEXT character set latin1,
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (service_id),
+ UNIQUE KEY instance_id (instance_id,config_type,service_object_id)
+) ENGINE=InnoDB COMMENT='Service definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicestatus
+--
+
+CREATE TABLE IF NOT EXISTS icinga_servicestatus (
+ servicestatus_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ service_object_id bigint unsigned default 0,
+ status_update_time timestamp NULL,
+ output TEXT character set latin1,
+ long_output TEXT,
+ perfdata TEXT character set latin1,
+ check_source varchar(255) character set latin1 default '',
+ current_state smallint default 0,
+ has_been_checked smallint default 0,
+ should_be_scheduled smallint default 0,
+ current_check_attempt smallint default 0,
+ max_check_attempts smallint default 0,
+ last_check timestamp NULL,
+ next_check timestamp NULL,
+ check_type smallint default 0,
+ last_state_change timestamp NULL,
+ last_hard_state_change timestamp NULL,
+ last_hard_state smallint default 0,
+ last_time_ok timestamp NULL,
+ last_time_warning timestamp NULL,
+ last_time_unknown timestamp NULL,
+ last_time_critical timestamp NULL,
+ state_type smallint default 0,
+ last_notification timestamp NULL,
+ next_notification timestamp NULL,
+ no_more_notifications smallint default 0,
+ notifications_enabled smallint default 0,
+ problem_has_been_acknowledged smallint default 0,
+ acknowledgement_type smallint default 0,
+ current_notification_number int unsigned default 0,
+ passive_checks_enabled smallint default 0,
+ active_checks_enabled smallint default 0,
+ event_handler_enabled smallint default 0,
+ flap_detection_enabled smallint default 0,
+ is_flapping smallint default 0,
+ percent_state_change double default '0',
+ latency double default '0',
+ execution_time double default '0',
+ scheduled_downtime_depth smallint default 0,
+ failure_prediction_enabled smallint default 0,
+ process_performance_data smallint default 0,
+ obsess_over_service smallint default 0,
+ modified_service_attributes int default 0,
+ original_attributes TEXT character set latin1 default NULL,
+ event_handler TEXT character set latin1,
+ check_command TEXT character set latin1,
+ normal_check_interval double default '0',
+ retry_check_interval double default '0',
+ check_timeperiod_object_id bigint unsigned default 0,
+ is_reachable smallint default 0,
+ PRIMARY KEY (servicestatus_id),
+ UNIQUE KEY object_id (service_object_id)
+) ENGINE=InnoDB COMMENT='Current service status information';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_service_contactgroups
+--
+
+CREATE TABLE IF NOT EXISTS icinga_service_contactgroups (
+ service_contactgroup_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ service_id bigint unsigned default 0,
+ contactgroup_object_id bigint unsigned default 0,
+ PRIMARY KEY (service_contactgroup_id)
+) ENGINE=InnoDB COMMENT='Service contact groups';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_service_contacts
+--
+
+CREATE TABLE IF NOT EXISTS icinga_service_contacts (
+ service_contact_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ service_id bigint unsigned default 0,
+ contact_object_id bigint unsigned default 0,
+ PRIMARY KEY (service_contact_id)
+) ENGINE=InnoDB COMMENT='Service contacts';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_statehistory
+--
+
+CREATE TABLE IF NOT EXISTS icinga_statehistory (
+ statehistory_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ state_time timestamp NULL,
+ state_time_usec int default 0,
+ object_id bigint unsigned default 0,
+ state_change smallint default 0,
+ state smallint default 0,
+ state_type smallint default 0,
+ current_check_attempt smallint default 0,
+ max_check_attempts smallint default 0,
+ last_state smallint default 0,
+ last_hard_state smallint default 0,
+ output TEXT character set latin1,
+ long_output TEXT,
+ check_source varchar(255) character set latin1 default NULL,
+ PRIMARY KEY (statehistory_id)
+) ENGINE=InnoDB COMMENT='Historical host and service state changes';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_systemcommands
+--
+
+CREATE TABLE IF NOT EXISTS icinga_systemcommands (
+ systemcommand_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ start_time timestamp NULL,
+ start_time_usec int default 0,
+ end_time timestamp NULL,
+ end_time_usec int default 0,
+ command_line TEXT character set latin1,
+ timeout smallint default 0,
+ early_timeout smallint default 0,
+ execution_time double default '0',
+ return_code smallint default 0,
+ output TEXT character set latin1,
+ long_output TEXT,
+ PRIMARY KEY (systemcommand_id),
+ UNIQUE KEY instance_id (instance_id,start_time,start_time_usec)
+) ENGINE=InnoDB COMMENT='Historical system commands that are executed';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_timeperiods
+--
+
+CREATE TABLE IF NOT EXISTS icinga_timeperiods (
+ timeperiod_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ config_type smallint default 0,
+ timeperiod_object_id bigint unsigned default 0,
+ alias varchar(255) character set latin1 default '',
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (timeperiod_id),
+ UNIQUE KEY instance_id (instance_id,config_type,timeperiod_object_id)
+) ENGINE=InnoDB COMMENT='Timeperiod definitions';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_timeperiod_timeranges
+--
+
+CREATE TABLE IF NOT EXISTS icinga_timeperiod_timeranges (
+ timeperiod_timerange_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ timeperiod_id bigint unsigned default 0,
+ day smallint default 0,
+ start_sec int default 0,
+ end_sec int default 0,
+ PRIMARY KEY (timeperiod_timerange_id)
+) ENGINE=InnoDB COMMENT='Timeperiod definitions';
+
+
+-- --------------------------------------------------------
+-- Icinga 2 specific schema extensions
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_endpoints
+--
+
+CREATE TABLE IF NOT EXISTS icinga_endpoints (
+ endpoint_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ endpoint_object_id bigint(20) unsigned DEFAULT '0',
+ zone_object_id bigint(20) unsigned DEFAULT '0',
+ config_type smallint(6) DEFAULT '0',
+ identity varchar(255) DEFAULT NULL,
+ node varchar(255) DEFAULT NULL,
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (endpoint_id)
+) ENGINE=InnoDB COMMENT='Endpoint configuration';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_endpointstatus
+--
+
+CREATE TABLE IF NOT EXISTS icinga_endpointstatus (
+ endpointstatus_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ endpoint_object_id bigint(20) unsigned DEFAULT '0',
+ zone_object_id bigint(20) unsigned DEFAULT '0',
+ status_update_time timestamp NULL,
+ identity varchar(255) DEFAULT NULL,
+ node varchar(255) DEFAULT NULL,
+ is_connected smallint(6),
+ PRIMARY KEY (endpointstatus_id)
+) ENGINE=InnoDB COMMENT='Endpoint status';
+
+--
+-- Table structure for table icinga_zones
+--
+
+CREATE TABLE IF NOT EXISTS icinga_zones (
+ zone_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ zone_object_id bigint(20) unsigned DEFAULT '0',
+ config_type smallint(6) DEFAULT '0',
+ parent_zone_object_id bigint(20) unsigned DEFAULT '0',
+ is_global smallint(6),
+ config_hash varchar(64) DEFAULT NULL,
+ PRIMARY KEY (zone_id)
+) ENGINE=InnoDB COMMENT='Zone configuration';
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_zonestatus
+--
+
+CREATE TABLE IF NOT EXISTS icinga_zonestatus (
+ zonestatus_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ zone_object_id bigint(20) unsigned DEFAULT '0',
+ status_update_time timestamp NULL,
+ parent_zone_object_id bigint(20) unsigned DEFAULT '0',
+ PRIMARY KEY (zonestatus_id)
+) ENGINE=InnoDB COMMENT='Zone status';
+
+
+
+
+ALTER TABLE icinga_servicestatus ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_hoststatus ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_contactstatus ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_programstatus ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_comments ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_scheduleddowntime ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_runtimevariables ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_customvariablestatus ADD COLUMN endpoint_object_id bigint default NULL;
+
+ALTER TABLE icinga_acknowledgements ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_commenthistory ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_contactnotifications ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_downtimehistory ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_eventhandlers ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_externalcommands ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_flappinghistory ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_hostchecks ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_logentries ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_notifications ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_processevents ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_servicechecks ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_statehistory ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_systemcommands ADD COLUMN endpoint_object_id bigint default NULL;
+
+-- -----------------------------------------
+-- add index (delete)
+-- -----------------------------------------
+
+-- for periodic delete
+-- instance_id and
+-- SYSTEMCOMMANDS, SERVICECHECKS, HOSTCHECKS, EVENTHANDLERS => start_time
+-- EXTERNALCOMMANDS => entry_time
+
+-- instance_id
+CREATE INDEX servicechecks_i_id_idx on icinga_servicechecks(instance_id);
+CREATE INDEX hostchecks_i_id_idx on icinga_hostchecks(instance_id);
+CREATE INDEX externalcommands_i_id_idx on icinga_externalcommands(instance_id);
+
+-- time
+CREATE INDEX systemcommands_time_id_idx on icinga_systemcommands(start_time);
+CREATE INDEX servicechecks_time_id_idx on icinga_servicechecks(start_time);
+CREATE INDEX hostchecks_time_id_idx on icinga_hostchecks(start_time);
+CREATE INDEX eventhandlers_time_id_idx on icinga_eventhandlers(start_time);
+CREATE INDEX externalcommands_time_id_idx on icinga_externalcommands(entry_time);
+
+
+-- for starting cleanup - referenced in dbhandler.c:882
+-- instance_id only
+
+-- realtime data
+CREATE INDEX hoststatus_i_id_idx on icinga_hoststatus(instance_id);
+CREATE INDEX servicestatus_i_id_idx on icinga_servicestatus(instance_id);
+CREATE INDEX contactstatus_i_id_idx on icinga_contactstatus(instance_id);
+CREATE INDEX customvariablestatus_i_id_idx on icinga_customvariablestatus(instance_id);
+
+-- config data
+CREATE INDEX configfilevariables_i_id_idx on icinga_configfilevariables(instance_id);
+CREATE INDEX customvariables_i_id_idx on icinga_customvariables(instance_id);
+CREATE INDEX timeperiod_timeranges_i_id_idx on icinga_timeperiod_timeranges(instance_id);
+CREATE INDEX contactgroup_members_i_id_idx on icinga_contactgroup_members(instance_id);
+CREATE INDEX hostgroup_members_i_id_idx on icinga_hostgroup_members(instance_id);
+CREATE INDEX servicegroup_members_i_id_idx on icinga_servicegroup_members(instance_id);
+CREATE INDEX contact_addresses_i_id_idx on icinga_contact_addresses(instance_id);
+CREATE INDEX contact_notifcommands_i_id_idx on icinga_contact_notificationcommands(instance_id);
+CREATE INDEX host_parenthosts_i_id_idx on icinga_host_parenthosts(instance_id);
+CREATE INDEX host_contacts_i_id_idx on icinga_host_contacts(instance_id);
+CREATE INDEX service_contacts_i_id_idx on icinga_service_contacts(instance_id);
+CREATE INDEX service_contactgroups_i_id_idx on icinga_service_contactgroups(instance_id);
+CREATE INDEX host_contactgroups_i_id_idx on icinga_host_contactgroups(instance_id);
+CREATE INDEX hostesc_cgroups_i_id_idx on icinga_hostescalation_contactgroups(instance_id);
+CREATE INDEX serviceesc_cgroups_i_id_idx on icinga_serviceescalation_contactgroups(instance_id);
+
+-- -----------------------------------------
+-- more index stuff (WHERE clauses)
+-- -----------------------------------------
+
+-- hosts
+CREATE INDEX hosts_host_object_id_idx on icinga_hosts(host_object_id);
+
+-- hoststatus
+CREATE INDEX hoststatus_stat_upd_time_idx on icinga_hoststatus(status_update_time);
+CREATE INDEX hoststatus_current_state_idx on icinga_hoststatus(current_state);
+CREATE INDEX hoststatus_check_type_idx on icinga_hoststatus(check_type);
+CREATE INDEX hoststatus_state_type_idx on icinga_hoststatus(state_type);
+CREATE INDEX hoststatus_last_state_chg_idx on icinga_hoststatus(last_state_change);
+CREATE INDEX hoststatus_notif_enabled_idx on icinga_hoststatus(notifications_enabled);
+CREATE INDEX hoststatus_problem_ack_idx on icinga_hoststatus(problem_has_been_acknowledged);
+CREATE INDEX hoststatus_act_chks_en_idx on icinga_hoststatus(active_checks_enabled);
+CREATE INDEX hoststatus_pas_chks_en_idx on icinga_hoststatus(passive_checks_enabled);
+CREATE INDEX hoststatus_event_hdl_en_idx on icinga_hoststatus(event_handler_enabled);
+CREATE INDEX hoststatus_flap_det_en_idx on icinga_hoststatus(flap_detection_enabled);
+CREATE INDEX hoststatus_is_flapping_idx on icinga_hoststatus(is_flapping);
+CREATE INDEX hoststatus_p_state_chg_idx on icinga_hoststatus(percent_state_change);
+CREATE INDEX hoststatus_latency_idx on icinga_hoststatus(latency);
+CREATE INDEX hoststatus_ex_time_idx on icinga_hoststatus(execution_time);
+CREATE INDEX hoststatus_sch_downt_d_idx on icinga_hoststatus(scheduled_downtime_depth);
+
+-- services
+CREATE INDEX services_host_object_id_idx on icinga_services(host_object_id);
+
+-- servicestatus
+CREATE INDEX srvcstatus_stat_upd_time_idx on icinga_servicestatus(status_update_time);
+CREATE INDEX srvcstatus_current_state_idx on icinga_servicestatus(current_state);
+CREATE INDEX srvcstatus_check_type_idx on icinga_servicestatus(check_type);
+CREATE INDEX srvcstatus_state_type_idx on icinga_servicestatus(state_type);
+CREATE INDEX srvcstatus_last_state_chg_idx on icinga_servicestatus(last_state_change);
+CREATE INDEX srvcstatus_notif_enabled_idx on icinga_servicestatus(notifications_enabled);
+CREATE INDEX srvcstatus_problem_ack_idx on icinga_servicestatus(problem_has_been_acknowledged);
+CREATE INDEX srvcstatus_act_chks_en_idx on icinga_servicestatus(active_checks_enabled);
+CREATE INDEX srvcstatus_pas_chks_en_idx on icinga_servicestatus(passive_checks_enabled);
+CREATE INDEX srvcstatus_event_hdl_en_idx on icinga_servicestatus(event_handler_enabled);
+CREATE INDEX srvcstatus_flap_det_en_idx on icinga_servicestatus(flap_detection_enabled);
+CREATE INDEX srvcstatus_is_flapping_idx on icinga_servicestatus(is_flapping);
+CREATE INDEX srvcstatus_p_state_chg_idx on icinga_servicestatus(percent_state_change);
+CREATE INDEX srvcstatus_latency_idx on icinga_servicestatus(latency);
+CREATE INDEX srvcstatus_ex_time_idx on icinga_servicestatus(execution_time);
+CREATE INDEX srvcstatus_sch_downt_d_idx on icinga_servicestatus(scheduled_downtime_depth);
+
+-- hostchecks
+CREATE INDEX hostchks_h_obj_id_idx on icinga_hostchecks(host_object_id);
+
+-- servicechecks
+CREATE INDEX servicechks_s_obj_id_idx on icinga_servicechecks(service_object_id);
+
+-- objects
+CREATE INDEX objects_name1_idx ON icinga_objects(name1);
+CREATE INDEX objects_name2_idx ON icinga_objects(name2);
+CREATE INDEX objects_inst_id_idx ON icinga_objects(instance_id);
+
+-- instances
+-- CREATE INDEX instances_name_idx on icinga_instances(instance_name);
+
+-- logentries
+-- CREATE INDEX loge_instance_id_idx on icinga_logentries(instance_id);
+-- #236
+CREATE INDEX loge_time_idx on icinga_logentries(logentry_time);
+-- CREATE INDEX loge_data_idx on icinga_logentries(logentry_data);
+CREATE INDEX loge_inst_id_time_idx on icinga_logentries (instance_id ASC, logentry_time DESC);
+
+-- commenthistory
+-- CREATE INDEX c_hist_instance_id_idx on icinga_logentries(instance_id);
+-- CREATE INDEX c_hist_c_time_idx on icinga_logentries(comment_time);
+-- CREATE INDEX c_hist_i_c_id_idx on icinga_logentries(internal_comment_id);
+
+-- downtimehistory
+-- CREATE INDEX d_t_hist_nstance_id_idx on icinga_downtimehistory(instance_id);
+-- CREATE INDEX d_t_hist_type_idx on icinga_downtimehistory(downtime_type);
+-- CREATE INDEX d_t_hist_object_id_idx on icinga_downtimehistory(object_id);
+-- CREATE INDEX d_t_hist_entry_time_idx on icinga_downtimehistory(entry_time);
+-- CREATE INDEX d_t_hist_sched_start_idx on icinga_downtimehistory(scheduled_start_time);
+-- CREATE INDEX d_t_hist_sched_end_idx on icinga_downtimehistory(scheduled_end_time);
+
+-- scheduleddowntime
+-- CREATE INDEX sched_d_t_downtime_type_idx on icinga_scheduleddowntime(downtime_type);
+-- CREATE INDEX sched_d_t_object_id_idx on icinga_scheduleddowntime(object_id);
+-- CREATE INDEX sched_d_t_entry_time_idx on icinga_scheduleddowntime(entry_time);
+-- CREATE INDEX sched_d_t_start_time_idx on icinga_scheduleddowntime(scheduled_start_time);
+-- CREATE INDEX sched_d_t_end_time_idx on icinga_scheduleddowntime(scheduled_end_time);
+
+-- statehistory
+CREATE INDEX statehist_i_id_o_id_s_ty_s_ti on icinga_statehistory(instance_id, object_id, state_type, state_time);
+-- #2274
+create index statehist_state_idx on icinga_statehistory(object_id,state);
+
+
+-- Icinga Web Notifications
+CREATE INDEX notification_idx ON icinga_notifications(notification_type, object_id, start_time);
+CREATE INDEX notification_object_id_idx ON icinga_notifications(object_id);
+CREATE INDEX contact_notification_idx ON icinga_contactnotifications(notification_id, contact_object_id);
+CREATE INDEX contacts_object_id_idx ON icinga_contacts(contact_object_id);
+CREATE INDEX contact_notif_meth_notif_idx ON icinga_contactnotificationmethods(contactnotification_id, command_object_id);
+CREATE INDEX command_object_idx ON icinga_commands(object_id);
+CREATE INDEX services_combined_object_idx ON icinga_services(service_object_id, host_object_id);
+
+
+-- #2618
+CREATE INDEX cntgrpmbrs_cgid_coid ON icinga_contactgroup_members (contactgroup_id,contact_object_id);
+CREATE INDEX hstgrpmbrs_hgid_hoid ON icinga_hostgroup_members (hostgroup_id,host_object_id);
+CREATE INDEX hstcntgrps_hid_cgoid ON icinga_host_contactgroups (host_id,contactgroup_object_id);
+CREATE INDEX hstprnthsts_hid_phoid ON icinga_host_parenthosts (host_id,parent_host_object_id);
+CREATE INDEX runtimevars_iid_varn ON icinga_runtimevariables (instance_id,varname);
+CREATE INDEX sgmbrs_sgid_soid ON icinga_servicegroup_members (servicegroup_id,service_object_id);
+CREATE INDEX scgrps_sid_cgoid ON icinga_service_contactgroups (service_id,contactgroup_object_id);
+CREATE INDEX tperiod_tid_d_ss_es ON icinga_timeperiod_timeranges (timeperiod_id,day,start_sec,end_sec);
+
+-- #3649
+CREATE INDEX sla_idx_sthist ON icinga_statehistory (object_id, state_time DESC);
+CREATE INDEX sla_idx_dohist ON icinga_downtimehistory (object_id, actual_start_time, actual_end_time);
+CREATE INDEX sla_idx_obj ON icinga_objects (objecttype_id, is_active, name1);
+
+-- #4985
+CREATE INDEX commenthistory_delete_idx ON icinga_commenthistory (instance_id, comment_time, internal_comment_id);
+
+-- #10066
+CREATE INDEX idx_endpoints_object_id on icinga_endpoints(endpoint_object_id);
+CREATE INDEX idx_endpointstatus_object_id on icinga_endpointstatus(endpoint_object_id);
+
+CREATE INDEX idx_endpoints_zone_object_id on icinga_endpoints(zone_object_id);
+CREATE INDEX idx_endpointstatus_zone_object_id on icinga_endpointstatus(zone_object_id);
+
+CREATE INDEX idx_zones_object_id on icinga_zones(zone_object_id);
+CREATE INDEX idx_zonestatus_object_id on icinga_zonestatus(zone_object_id);
+
+CREATE INDEX idx_zones_parent_object_id on icinga_zones(parent_zone_object_id);
+CREATE INDEX idx_zonestatus_parent_object_id on icinga_zonestatus(parent_zone_object_id);
+
+-- #12210
+CREATE INDEX idx_comments_session_del ON icinga_comments (instance_id, session_token);
+CREATE INDEX idx_downtimes_session_del ON icinga_scheduleddowntime (instance_id, session_token);
+
+-- #12107
+CREATE INDEX idx_statehistory_cleanup on icinga_statehistory(instance_id, state_time);
+
+-- #12435
+CREATE INDEX idx_contactgroup_members_object_id on icinga_contactgroup_members(contact_object_id);
+CREATE INDEX idx_hostgroup_members_object_id on icinga_hostgroup_members(host_object_id);
+CREATE INDEX idx_servicegroup_members_object_id on icinga_servicegroup_members(service_object_id);
+CREATE INDEX idx_servicedependencies_dependent_service_object_id on icinga_servicedependencies(dependent_service_object_id);
+CREATE INDEX idx_hostdependencies_dependent_host_object_id on icinga_hostdependencies(dependent_host_object_id);
+CREATE INDEX idx_service_contacts_service_id on icinga_service_contacts(service_id);
+CREATE INDEX idx_host_contacts_host_id on icinga_host_contacts(host_id);
+
+-- #5458
+create index idx_downtimehistory_remove on icinga_downtimehistory (object_id, entry_time, scheduled_start_time, scheduled_end_time);
+create index idx_scheduleddowntime_remove on icinga_scheduleddowntime (object_id, entry_time, scheduled_start_time, scheduled_end_time);
+
+-- #5492
+CREATE INDEX idx_commenthistory_remove ON icinga_commenthistory (object_id, entry_time);
+CREATE INDEX idx_comments_remove ON icinga_comments (object_id, entry_time);
+
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.15.1', NOW(), NOW())
+ON DUPLICATE KEY UPDATE version='1.15.1', modify_time=NOW();
+
+
diff --git a/lib/db_ido_mysql/schema/upgrade/2.0.2.sql b/lib/db_ido_mysql/schema/upgrade/2.0.2.sql
new file mode 100644
index 0000000..c622ae9
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.0.2.sql
@@ -0,0 +1,20 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.0.2
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+UPDATE icinga_objects SET name2 = NULL WHERE name2 = '';
+
+ALTER TABLE `icinga_customvariables` MODIFY COLUMN `varname` varchar(255) character set latin1 collate latin1_general_cs default NULL;
+ALTER TABLE `icinga_customvariablestatus` MODIFY COLUMN `varname` varchar(255) character set latin1 collate latin1_general_cs default NULL;
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.11.6', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.11.6', modify_time=NOW();
+
diff --git a/lib/db_ido_mysql/schema/upgrade/2.1.0.sql b/lib/db_ido_mysql/schema/upgrade/2.1.0.sql
new file mode 100644
index 0000000..7bbed72
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.1.0.sql
@@ -0,0 +1,17 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.1.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+ALTER TABLE `icinga_programstatus` ADD COLUMN `endpoint_name` varchar(255) character set latin1 collate latin1_general_cs default NULL;
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.11.7', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.11.7', modify_time=NOW();
+
diff --git a/lib/db_ido_mysql/schema/upgrade/2.11.0.sql b/lib/db_ido_mysql/schema/upgrade/2.11.0.sql
new file mode 100644
index 0000000..bafa93f
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.11.0.sql
@@ -0,0 +1,89 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.11.0
+--
+-- -----------------------------------------
+-- Copyright (c) 2019 Icinga Development Team (https://icinga.com/)
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+
+-- --------------------------------------------------------
+-- Helper functions and procedures for DROP INDEX IF EXISTS
+-- --------------------------------------------------------
+
+DELIMITER //
+DROP FUNCTION IF EXISTS ido_index_exists //
+CREATE FUNCTION ido_index_exists(
+ f_table_name varchar(64),
+ f_index_name varchar(64)
+)
+ RETURNS BOOL
+ DETERMINISTIC
+ READS SQL DATA
+ BEGIN
+ DECLARE index_exists BOOL DEFAULT FALSE;
+ SELECT EXISTS (
+ SELECT 1
+ FROM information_schema.statistics
+ WHERE table_schema = SCHEMA()
+ AND table_name = f_table_name
+ AND index_name = f_index_name
+ ) INTO index_exists;
+ RETURN index_exists;
+ END //
+
+DROP PROCEDURE IF EXISTS ido_drop_index_if_exists //
+CREATE PROCEDURE ido_drop_index_if_exists (
+ IN p_table_name varchar(64),
+ IN p_index_name varchar(64)
+)
+ DETERMINISTIC
+ MODIFIES SQL DATA
+ BEGIN
+ IF ido_index_exists(p_table_name, p_index_name)
+ THEN
+ SET @ido_drop_index_sql = CONCAT('ALTER TABLE `', SCHEMA(), '`.`', p_table_name, '` DROP INDEX `', p_index_name, '`');
+ PREPARE stmt FROM @ido_drop_index_sql;
+ EXECUTE stmt;
+ DEALLOCATE PREPARE stmt;
+ SET @ido_drop_index_sql = NULL;
+ END IF;
+ END //
+DELIMITER ;
+
+CALL ido_drop_index_if_exists('icinga_commands', 'commands_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_comments', 'idx_comments_object_id');
+CALL ido_drop_index_if_exists('icinga_comments', 'comments_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_configfiles', 'configfiles_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_contactgroups', 'contactgroups_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_contacts', 'contacts_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_customvariables', 'idx_customvariables_object_id');
+CALL ido_drop_index_if_exists('icinga_eventhandlers', 'eventhandlers_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_hostdependencies', 'hostdependencies_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_hostescalations', 'hostesc_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_hostescalation_contacts', 'hostesc_contacts_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_hostgroups', 'hostgroups_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_hosts', 'host_object_id');
+CALL ido_drop_index_if_exists('icinga_hosts', 'hosts_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_objects', 'objects_objtype_id_idx');
+CALL ido_drop_index_if_exists('icinga_programstatus', 'programstatus_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_runtimevariables', 'runtimevariables_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_scheduleddowntime', 'scheduleddowntime_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_scheduleddowntime', 'idx_scheduleddowntime_object_id');
+CALL ido_drop_index_if_exists('icinga_serviceescalations', 'serviceesc_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_serviceescalation_contacts', 'serviceesc_contacts_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_servicegroups', 'servicegroups_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_services', 'services_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_services', 'service_object_id');
+CALL ido_drop_index_if_exists('icinga_systemcommands', 'systemcommands_i_id_idx');
+CALL ido_drop_index_if_exists('icinga_timeperiods', 'timeperiods_i_id_idx');
+
+DROP FUNCTION ido_index_exists;
+DROP PROCEDURE ido_drop_index_if_exists;
+
+-- -----------------------------------------
+-- set dbversion (same as 2.11.0)
+-- -----------------------------------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.15.0', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.15.0', modify_time=NOW();
diff --git a/lib/db_ido_mysql/schema/upgrade/2.12.7.sql b/lib/db_ido_mysql/schema/upgrade/2.12.7.sql
new file mode 100644
index 0000000..6319b37
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.12.7.sql
@@ -0,0 +1,15 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.12.7
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+
+-- -------------
+-- set dbversion
+-- -------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.15.0', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.15.0', modify_time=NOW();
diff --git a/lib/db_ido_mysql/schema/upgrade/2.13.0.sql b/lib/db_ido_mysql/schema/upgrade/2.13.0.sql
new file mode 100644
index 0000000..462be6f
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.13.0.sql
@@ -0,0 +1,23 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.13.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+
+-- ----------------------------------------
+-- #7472 Support hosts with >128 characters
+-- ----------------------------------------
+
+ALTER TABLE icinga_objects
+ MODIFY COLUMN name1 varchar(255) character set latin1 collate latin1_general_cs default '',
+ MODIFY COLUMN name2 varchar(255) character set latin1 collate latin1_general_cs default NULL;
+
+-- -------------
+-- set dbversion
+-- -------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.15.1', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.15.1', modify_time=NOW();
diff --git a/lib/db_ido_mysql/schema/upgrade/2.13.3.sql b/lib/db_ido_mysql/schema/upgrade/2.13.3.sql
new file mode 100644
index 0000000..577eb0a
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.13.3.sql
@@ -0,0 +1,15 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.13.3
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+
+-- -------------
+-- set dbversion
+-- -------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.15.1', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.15.1', modify_time=NOW();
diff --git a/lib/db_ido_mysql/schema/upgrade/2.2.0.sql b/lib/db_ido_mysql/schema/upgrade/2.2.0.sql
new file mode 100644
index 0000000..22a6115
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.2.0.sql
@@ -0,0 +1,23 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.2.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+ALTER TABLE `icinga_programstatus` ADD COLUMN `program_version` varchar(64) character set latin1 collate latin1_general_cs default NULL;
+
+ALTER TABLE icinga_contacts MODIFY alias TEXT character set latin1;
+ALTER TABLE icinga_hosts MODIFY alias TEXT character set latin1;
+
+ALTER TABLE icinga_customvariables ADD COLUMN is_json smallint default 0;
+ALTER TABLE icinga_customvariablestatus ADD COLUMN is_json smallint default 0;
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.12.0', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.12.0', modify_time=NOW();
+
diff --git a/lib/db_ido_mysql/schema/upgrade/2.3.0.sql b/lib/db_ido_mysql/schema/upgrade/2.3.0.sql
new file mode 100644
index 0000000..f2fe463
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.3.0.sql
@@ -0,0 +1,26 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.3.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+-- -----------------------------------------
+-- #7765 drop unique constraint
+-- -----------------------------------------
+
+ALTER TABLE icinga_servicedependencies DROP KEY instance_id;
+ALTER TABLE icinga_hostdependencies DROP KEY instance_id;
+
+ALTER TABLE icinga_servicedependencies ADD KEY instance_id (instance_id,config_type,service_object_id,dependent_service_object_id,dependency_type,inherits_parent,fail_on_ok,fail_on_warning,fail_on_unknown,fail_on_critical);
+ALTER TABLE icinga_hostdependencies ADD KEY instance_id (instance_id,config_type,host_object_id,dependent_host_object_id,dependency_type,inherits_parent,fail_on_up,fail_on_down,fail_on_unreachable);
+
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.13.0', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.13.0', modify_time=NOW();
+
diff --git a/lib/db_ido_mysql/schema/upgrade/2.4.0.sql b/lib/db_ido_mysql/schema/upgrade/2.4.0.sql
new file mode 100644
index 0000000..f6803f7
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.4.0.sql
@@ -0,0 +1,75 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.4.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+-- -----------------------------------------
+-- #9286 - zone tables
+-- -----------------------------------------
+
+ALTER TABLE icinga_endpoints ADD COLUMN zone_object_id bigint(20) unsigned DEFAULT '0';
+ALTER TABLE icinga_endpointstatus ADD COLUMN zone_object_id bigint(20) unsigned DEFAULT '0';
+
+CREATE TABLE IF NOT EXISTS icinga_zones (
+ zone_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ zone_object_id bigint(20) unsigned DEFAULT '0',
+ config_type smallint(6) DEFAULT '0',
+ parent_zone_object_id bigint(20) unsigned DEFAULT '0',
+ is_global smallint(6),
+ PRIMARY KEY (zone_id)
+) ENGINE=InnoDB COMMENT='Zone configuration';
+
+CREATE TABLE IF NOT EXISTS icinga_zonestatus (
+ zonestatus_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ instance_id bigint unsigned default 0,
+ zone_object_id bigint(20) unsigned DEFAULT '0',
+ status_update_time timestamp NOT NULL,
+ parent_zone_object_id bigint(20) unsigned DEFAULT '0',
+ PRIMARY KEY (zonestatus_id)
+) ENGINE=InnoDB COMMENT='Zone status';
+
+
+-- -----------------------------------------
+-- #9576 - freshness_threshold
+-- -----------------------------------------
+
+ALTER TABLE icinga_services MODIFY freshness_threshold int;
+ALTER TABLE icinga_hosts MODIFY freshness_threshold int;
+
+-- -----------------------------------------
+-- #10392 - original attributes
+-- -----------------------------------------
+
+ALTER TABLE icinga_servicestatus ADD COLUMN original_attributes TEXT character set latin1 default NULL;
+ALTER TABLE icinga_hoststatus ADD COLUMN original_attributes TEXT character set latin1 default NULL;
+
+-- -----------------------------------------
+-- #10436 deleted custom vars
+-- -----------------------------------------
+
+ALTER TABLE icinga_customvariables ADD COLUMN session_token int default NULL;
+ALTER TABLE icinga_customvariablestatus ADD COLUMN session_token int default NULL;
+
+CREATE INDEX cv_session_del_idx ON icinga_customvariables (session_token);
+CREATE INDEX cvs_session_del_idx ON icinga_customvariablestatus (session_token);
+
+-- -----------------------------------------
+-- #10431 comment/downtime name
+-- -----------------------------------------
+
+ALTER TABLE icinga_comments ADD COLUMN name TEXT character set latin1 default NULL;
+ALTER TABLE icinga_commenthistory ADD COLUMN name TEXT character set latin1 default NULL;
+
+ALTER TABLE icinga_scheduleddowntime ADD COLUMN name TEXT character set latin1 default NULL;
+ALTER TABLE icinga_downtimehistory ADD COLUMN name TEXT character set latin1 default NULL;
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.14.0', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.14.0', modify_time=NOW();
diff --git a/lib/db_ido_mysql/schema/upgrade/2.5.0.sql b/lib/db_ido_mysql/schema/upgrade/2.5.0.sql
new file mode 100644
index 0000000..d5714a0
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.5.0.sql
@@ -0,0 +1,103 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.5.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+
+-- -----------------------------------------
+-- #10069 IDO: check_source should not be a TEXT field
+-- -----------------------------------------
+
+ALTER TABLE icinga_hoststatus MODIFY COLUMN check_source varchar(255) character set latin1 default '';
+ALTER TABLE icinga_servicestatus MODIFY COLUMN check_source varchar(255) character set latin1 default '';
+
+-- -----------------------------------------
+-- #10070
+-- -----------------------------------------
+
+CREATE INDEX idx_comments_object_id on icinga_comments(object_id);
+CREATE INDEX idx_scheduleddowntime_object_id on icinga_scheduleddowntime(object_id);
+
+-- -----------------------------------------
+-- #11962
+-- -----------------------------------------
+
+ALTER TABLE icinga_hoststatus MODIFY COLUMN current_notification_number int unsigned default 0;
+ALTER TABLE icinga_servicestatus MODIFY COLUMN current_notification_number int unsigned default 0;
+
+-- -----------------------------------------
+-- #10061
+-- -----------------------------------------
+
+ALTER TABLE icinga_contactgroups MODIFY COLUMN alias varchar(255) character set latin1 default '';
+ALTER TABLE icinga_contacts MODIFY COLUMN alias varchar(255) character set latin1 default '';
+ALTER TABLE icinga_hostgroups MODIFY COLUMN alias varchar(255) character set latin1 default '';
+ALTER TABLE icinga_hosts MODIFY COLUMN alias varchar(255) character set latin1 default '';
+ALTER TABLE icinga_servicegroups MODIFY COLUMN alias varchar(255) character set latin1 default '';
+ALTER TABLE icinga_timeperiods MODIFY COLUMN alias varchar(255) character set latin1 default '';
+
+-- -----------------------------------------
+-- #10066
+-- -----------------------------------------
+
+CREATE INDEX idx_endpoints_object_id on icinga_endpoints(endpoint_object_id);
+CREATE INDEX idx_endpointstatus_object_id on icinga_endpointstatus(endpoint_object_id);
+
+CREATE INDEX idx_endpoints_zone_object_id on icinga_endpoints(zone_object_id);
+CREATE INDEX idx_endpointstatus_zone_object_id on icinga_endpointstatus(zone_object_id);
+
+CREATE INDEX idx_zones_object_id on icinga_zones(zone_object_id);
+CREATE INDEX idx_zonestatus_object_id on icinga_zonestatus(zone_object_id);
+
+CREATE INDEX idx_zones_parent_object_id on icinga_zones(parent_zone_object_id);
+CREATE INDEX idx_zonestatus_parent_object_id on icinga_zonestatus(parent_zone_object_id);
+
+-- -----------------------------------------
+-- #12107
+-- -----------------------------------------
+CREATE INDEX idx_statehistory_cleanup on icinga_statehistory(instance_id, state_time);
+
+-- -----------------------------------------
+-- #12258
+-- -----------------------------------------
+ALTER TABLE icinga_comments ADD COLUMN session_token INTEGER default NULL;
+ALTER TABLE icinga_scheduleddowntime ADD COLUMN session_token INTEGER default NULL;
+
+CREATE INDEX idx_comments_session_del ON icinga_comments (instance_id, session_token);
+CREATE INDEX idx_downtimes_session_del ON icinga_scheduleddowntime (instance_id, session_token);
+
+-- -----------------------------------------
+-- #12435
+-- -----------------------------------------
+ALTER TABLE icinga_commands ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_contactgroups ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_contacts ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_hostgroups ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_hosts ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_servicegroups ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_services ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_timeperiods ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_endpoints ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_zones ADD config_hash VARCHAR(64) DEFAULT NULL;
+
+ALTER TABLE icinga_customvariables DROP session_token;
+ALTER TABLE icinga_customvariablestatus DROP session_token;
+
+CREATE INDEX idx_customvariables_object_id on icinga_customvariables(object_id);
+CREATE INDEX idx_contactgroup_members_object_id on icinga_contactgroup_members(contact_object_id);
+CREATE INDEX idx_hostgroup_members_object_id on icinga_hostgroup_members(host_object_id);
+CREATE INDEX idx_servicegroup_members_object_id on icinga_servicegroup_members(service_object_id);
+CREATE INDEX idx_servicedependencies_dependent_service_object_id on icinga_servicedependencies(dependent_service_object_id);
+CREATE INDEX idx_hostdependencies_dependent_host_object_id on icinga_hostdependencies(dependent_host_object_id);
+CREATE INDEX idx_service_contacts_service_id on icinga_service_contacts(service_id);
+CREATE INDEX idx_host_contacts_host_id on icinga_host_contacts(host_id);
+
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.14.1', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.14.1', modify_time=NOW();
diff --git a/lib/db_ido_mysql/schema/upgrade/2.6.0.sql b/lib/db_ido_mysql/schema/upgrade/2.6.0.sql
new file mode 100644
index 0000000..33dd780
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.6.0.sql
@@ -0,0 +1,151 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.6.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+-- -----------------------------------------
+-- #10502 IDO: Support NO_ZERO_DATE and NO_ZERO_IN_DATE SQL modes
+-- -----------------------------------------
+
+ALTER TABLE icinga_acknowledgements
+ MODIFY COLUMN entry_time timestamp NULL,
+ MODIFY COLUMN end_time timestamp NULL;
+
+ALTER TABLE icinga_commenthistory
+ MODIFY COLUMN entry_time timestamp NULL,
+ MODIFY COLUMN comment_time timestamp NULL,
+ MODIFY COLUMN expiration_time timestamp NULL,
+ MODIFY COLUMN deletion_time timestamp NULL;
+
+ALTER TABLE icinga_comments
+ MODIFY COLUMN entry_time timestamp NULL,
+ MODIFY COLUMN comment_time timestamp NULL,
+ MODIFY COLUMN expiration_time timestamp NULL;
+
+ALTER TABLE icinga_conninfo
+ MODIFY COLUMN connect_time timestamp NULL,
+ MODIFY COLUMN disconnect_time timestamp NULL,
+ MODIFY COLUMN last_checkin_time timestamp NULL,
+ MODIFY COLUMN data_start_time timestamp NULL,
+ MODIFY COLUMN data_end_time timestamp NULL;
+
+ALTER TABLE icinga_contactnotificationmethods
+ MODIFY COLUMN start_time timestamp NULL,
+ MODIFY COLUMN end_time timestamp NULL;
+
+ALTER TABLE icinga_contactnotifications
+ MODIFY COLUMN start_time timestamp NULL,
+ MODIFY COLUMN end_time timestamp NULL;
+
+ALTER TABLE icinga_contactstatus
+ MODIFY COLUMN status_update_time timestamp NULL,
+ MODIFY COLUMN last_host_notification timestamp NULL,
+ MODIFY COLUMN last_service_notification timestamp NULL;
+
+ALTER TABLE icinga_customvariablestatus
+ MODIFY COLUMN status_update_time timestamp NULL;
+
+ALTER TABLE icinga_dbversion
+ MODIFY COLUMN create_time timestamp NULL,
+ MODIFY COLUMN modify_time timestamp NULL;
+
+ALTER TABLE icinga_downtimehistory
+ MODIFY COLUMN entry_time timestamp NULL,
+ MODIFY COLUMN scheduled_start_time timestamp NULL,
+ MODIFY COLUMN scheduled_end_time timestamp NULL,
+ MODIFY COLUMN actual_start_time timestamp NULL,
+ MODIFY COLUMN actual_end_time timestamp NULL,
+ MODIFY COLUMN trigger_time timestamp NULL;
+
+ALTER TABLE icinga_eventhandlers
+ MODIFY COLUMN start_time timestamp NULL,
+ MODIFY COLUMN end_time timestamp NULL;
+
+ALTER TABLE icinga_externalcommands
+ MODIFY COLUMN entry_time timestamp NULL;
+
+ALTER TABLE icinga_flappinghistory
+ MODIFY COLUMN event_time timestamp NULL,
+ MODIFY COLUMN comment_time timestamp NULL;
+
+ALTER TABLE icinga_hostchecks
+ MODIFY COLUMN start_time timestamp NULL,
+ MODIFY COLUMN end_time timestamp NULL;
+
+ALTER TABLE icinga_hoststatus
+ MODIFY COLUMN status_update_time timestamp NULL,
+ MODIFY COLUMN last_check timestamp NULL,
+ MODIFY COLUMN next_check timestamp NULL,
+ MODIFY COLUMN last_state_change timestamp NULL,
+ MODIFY COLUMN last_hard_state_change timestamp NULL,
+ MODIFY COLUMN last_time_up timestamp NULL,
+ MODIFY COLUMN last_time_down timestamp NULL,
+ MODIFY COLUMN last_time_unreachable timestamp NULL,
+ MODIFY COLUMN last_notification timestamp NULL,
+ MODIFY COLUMN next_notification timestamp NULL;
+
+ALTER TABLE icinga_logentries
+ MODIFY COLUMN logentry_time timestamp NULL,
+ MODIFY COLUMN entry_time timestamp NULL;
+
+ALTER TABLE icinga_notifications
+ MODIFY COLUMN start_time timestamp NULL,
+ MODIFY COLUMN end_time timestamp NULL;
+
+ALTER TABLE icinga_processevents
+ MODIFY COLUMN event_time timestamp NULL;
+
+ALTER TABLE icinga_programstatus
+ MODIFY COLUMN status_update_time timestamp NULL,
+ MODIFY COLUMN program_start_time timestamp NULL,
+ MODIFY COLUMN program_end_time timestamp NULL,
+ MODIFY COLUMN last_command_check timestamp NULL,
+ MODIFY COLUMN last_log_rotation timestamp NULL,
+ MODIFY COLUMN disable_notif_expire_time timestamp NULL;
+
+ALTER TABLE icinga_scheduleddowntime
+ MODIFY COLUMN entry_time timestamp NULL,
+ MODIFY COLUMN scheduled_start_time timestamp NULL,
+ MODIFY COLUMN scheduled_end_time timestamp NULL,
+ MODIFY COLUMN actual_start_time timestamp NULL,
+ MODIFY COLUMN trigger_time timestamp NULL;
+
+ALTER TABLE icinga_servicechecks
+ MODIFY COLUMN start_time timestamp NULL,
+ MODIFY COLUMN end_time timestamp NULL;
+
+ALTER TABLE icinga_servicestatus
+ MODIFY COLUMN status_update_time timestamp NULL,
+ MODIFY COLUMN last_check timestamp NULL,
+ MODIFY COLUMN next_check timestamp NULL,
+ MODIFY COLUMN last_state_change timestamp NULL,
+ MODIFY COLUMN last_hard_state_change timestamp NULL,
+ MODIFY COLUMN last_time_ok timestamp NULL,
+ MODIFY COLUMN last_time_warning timestamp NULL,
+ MODIFY COLUMN last_time_unknown timestamp NULL,
+ MODIFY COLUMN last_time_critical timestamp NULL,
+ MODIFY COLUMN last_notification timestamp NULL,
+ MODIFY COLUMN next_notification timestamp NULL;
+
+ALTER TABLE icinga_statehistory
+ MODIFY COLUMN state_time timestamp NULL;
+
+ALTER TABLE icinga_systemcommands
+ MODIFY COLUMN start_time timestamp NULL,
+ MODIFY COLUMN end_time timestamp NULL;
+
+ALTER TABLE icinga_endpointstatus
+ MODIFY COLUMN status_update_time timestamp NULL;
+
+ALTER TABLE icinga_zonestatus
+ MODIFY COLUMN status_update_time timestamp NULL;
+
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.14.2', NOW(), NOW())
+ON DUPLICATE KEY UPDATE version='1.14.2', modify_time=NOW();
diff --git a/lib/db_ido_mysql/schema/upgrade/2.8.0.sql b/lib/db_ido_mysql/schema/upgrade/2.8.0.sql
new file mode 100644
index 0000000..8d511a7
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.8.0.sql
@@ -0,0 +1,81 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.8.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+
+-- --------------------------------------------------------
+-- Helper functions and procedures for DROP INDEX IF EXISTS
+-- --------------------------------------------------------
+
+DELIMITER //
+DROP FUNCTION IF EXISTS ido_index_exists //
+CREATE FUNCTION ido_index_exists(
+ f_table_name varchar(64),
+ f_index_name varchar(64)
+)
+ RETURNS BOOL
+ DETERMINISTIC
+ READS SQL DATA
+ BEGIN
+ DECLARE index_exists BOOL DEFAULT FALSE;
+ SELECT EXISTS (
+ SELECT 1
+ FROM information_schema.statistics
+ WHERE table_schema = SCHEMA()
+ AND table_name = f_table_name
+ AND index_name = f_index_name
+ ) INTO index_exists;
+ RETURN index_exists;
+ END //
+
+DROP PROCEDURE IF EXISTS ido_drop_index_if_exists //
+CREATE PROCEDURE ido_drop_index_if_exists (
+ IN p_table_name varchar(64),
+ IN p_index_name varchar(64)
+)
+ DETERMINISTIC
+ MODIFIES SQL DATA
+ BEGIN
+ IF ido_index_exists(p_table_name, p_index_name)
+ THEN
+ SET @ido_drop_index_sql = CONCAT('ALTER TABLE `', SCHEMA(), '`.`', p_table_name, '` DROP INDEX `', p_index_name, '`');
+ PREPARE stmt FROM @ido_drop_index_sql;
+ EXECUTE stmt;
+ DEALLOCATE PREPARE stmt;
+ SET @ido_drop_index_sql = NULL;
+ END IF;
+ END //
+DELIMITER ;
+
+CALL ido_drop_index_if_exists('icinga_downtimehistory', 'instance_id');
+CALL ido_drop_index_if_exists('icinga_scheduleddowntime', 'instance_id');
+CALL ido_drop_index_if_exists('icinga_commenthistory', 'instance_id');
+CALL ido_drop_index_if_exists('icinga_comments', 'instance_id');
+
+DROP FUNCTION ido_index_exists;
+DROP PROCEDURE ido_drop_index_if_exists;
+
+-- -----------------------------------------
+-- #5458 IDO: Improve downtime removal/cancel
+-- -----------------------------------------
+
+CREATE INDEX idx_downtimehistory_remove ON icinga_downtimehistory (object_id, entry_time, scheduled_start_time, scheduled_end_time);
+CREATE INDEX idx_scheduleddowntime_remove ON icinga_scheduleddowntime (object_id, entry_time, scheduled_start_time, scheduled_end_time);
+
+-- -----------------------------------------
+-- #5492 IDO: Improve comment removal
+-- -----------------------------------------
+
+CREATE INDEX idx_commenthistory_remove ON icinga_commenthistory (object_id, entry_time);
+CREATE INDEX idx_comments_remove ON icinga_comments (object_id, entry_time);
+
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.14.3', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.14.3', modify_time=NOW();
diff --git a/lib/db_ido_mysql/schema/upgrade/2.8.1.sql b/lib/db_ido_mysql/schema/upgrade/2.8.1.sql
new file mode 100644
index 0000000..98f8511
--- /dev/null
+++ b/lib/db_ido_mysql/schema/upgrade/2.8.1.sql
@@ -0,0 +1,67 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.8.1 (fix for fresh 2.8.0 installation only)
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+
+-- --------------------------------------------------------
+-- Helper functions and procedures for DROP INDEX IF EXISTS
+-- --------------------------------------------------------
+
+DELIMITER //
+DROP FUNCTION IF EXISTS ido_index_exists //
+CREATE FUNCTION ido_index_exists(
+ f_table_name varchar(64),
+ f_index_name varchar(64)
+)
+ RETURNS BOOL
+ DETERMINISTIC
+ READS SQL DATA
+ BEGIN
+ DECLARE index_exists BOOL DEFAULT FALSE;
+ SELECT EXISTS (
+ SELECT 1
+ FROM information_schema.statistics
+ WHERE table_schema = SCHEMA()
+ AND table_name = f_table_name
+ AND index_name = f_index_name
+ ) INTO index_exists;
+ RETURN index_exists;
+ END //
+
+DROP PROCEDURE IF EXISTS ido_drop_index_if_exists //
+CREATE PROCEDURE ido_drop_index_if_exists (
+ IN p_table_name varchar(64),
+ IN p_index_name varchar(64)
+)
+ DETERMINISTIC
+ MODIFIES SQL DATA
+ BEGIN
+ IF ido_index_exists(p_table_name, p_index_name)
+ THEN
+ SET @ido_drop_index_sql = CONCAT('ALTER TABLE `', SCHEMA(), '`.`', p_table_name, '` DROP INDEX `', p_index_name, '`');
+ PREPARE stmt FROM @ido_drop_index_sql;
+ EXECUTE stmt;
+ DEALLOCATE PREPARE stmt;
+ SET @ido_drop_index_sql = NULL;
+ END IF;
+ END //
+DELIMITER ;
+
+CALL ido_drop_index_if_exists('icinga_downtimehistory', 'instance_id');
+CALL ido_drop_index_if_exists('icinga_scheduleddowntime', 'instance_id');
+CALL ido_drop_index_if_exists('icinga_commenthistory', 'instance_id');
+CALL ido_drop_index_if_exists('icinga_comments', 'instance_id');
+
+DROP FUNCTION ido_index_exists;
+DROP PROCEDURE ido_drop_index_if_exists;
+
+-- -----------------------------------------
+-- set dbversion (same as 2.8.0)
+-- -----------------------------------------
+INSERT INTO icinga_dbversion (name, version, create_time, modify_time) VALUES ('idoutils', '1.14.3', NOW(), NOW()) ON DUPLICATE KEY UPDATE version='1.14.3', modify_time=NOW();
diff --git a/lib/db_ido_pgsql/CMakeLists.txt b/lib/db_ido_pgsql/CMakeLists.txt
new file mode 100644
index 0000000..e081a62
--- /dev/null
+++ b/lib/db_ido_pgsql/CMakeLists.txt
@@ -0,0 +1,41 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(idopgsqlconnection.ti idopgsqlconnection-ti.cpp idopgsqlconnection-ti.hpp)
+
+set(db_ido_pgsql_SOURCES
+ idopgsqlconnection.cpp idopgsqlconnection.hpp idopgsqlconnection-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(db_ido_pgsql db_ido_pgsql db_ido_pgsql_SOURCES)
+endif()
+
+add_library(db_ido_pgsql OBJECT ${db_ido_pgsql_SOURCES})
+
+include_directories(${PostgreSQL_INCLUDE_DIRS})
+
+add_dependencies(db_ido_pgsql base config icinga db_ido)
+
+set_target_properties (
+ db_ido_pgsql PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/ido-pgsql.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install(
+ DIRECTORY schema
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/icinga2-ido-pgsql
+ FILES_MATCHING PATTERN "*.sql"
+)
+
+install(
+ DIRECTORY schema/upgrade
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/icinga2-ido-pgsql/schema
+ FILES_MATCHING PATTERN "*.sql"
+)
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/db_ido_pgsql/idopgsqlconnection.cpp b/lib/db_ido_pgsql/idopgsqlconnection.cpp
new file mode 100644
index 0000000..07e88e6
--- /dev/null
+++ b/lib/db_ido_pgsql/idopgsqlconnection.cpp
@@ -0,0 +1,1029 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido_pgsql/idopgsqlconnection.hpp"
+#include "db_ido_pgsql/idopgsqlconnection-ti.cpp"
+#include "db_ido/dbtype.hpp"
+#include "db_ido/dbvalue.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/application.hpp"
+#include "base/configtype.hpp"
+#include "base/exception.hpp"
+#include "base/context.hpp"
+#include "base/statsfunction.hpp"
+#include "base/defer.hpp"
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(IdoPgsqlConnection);
+
+REGISTER_STATSFUNCTION(IdoPgsqlConnection, &IdoPgsqlConnection::StatsFunc);
+
+const char * IdoPgsqlConnection::GetLatestSchemaVersion() const noexcept
+{
+ return "1.14.3";
+}
+
+const char * IdoPgsqlConnection::GetCompatSchemaVersion() const noexcept
+{
+ return "1.14.3";
+}
+
+IdoPgsqlConnection::IdoPgsqlConnection()
+{
+ m_QueryQueue.SetName("IdoPgsqlConnection, " + GetName());
+}
+
+void IdoPgsqlConnection::OnConfigLoaded()
+{
+ ObjectImpl<IdoPgsqlConnection>::OnConfigLoaded();
+
+ m_QueryQueue.SetName("IdoPgsqlConnection, " + GetName());
+
+ Library shimLibrary{"pgsql_shim"};
+
+ auto create_pgsql_shim = shimLibrary.GetSymbolAddress<create_pgsql_shim_ptr>("create_pgsql_shim");
+
+ m_Pgsql.reset(create_pgsql_shim());
+
+ std::swap(m_Library, shimLibrary);
+}
+
+void IdoPgsqlConnection::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const IdoPgsqlConnection::Ptr& idopgsqlconnection : ConfigType::GetObjectsByType<IdoPgsqlConnection>()) {
+ size_t queryQueueItems = idopgsqlconnection->m_QueryQueue.GetLength();
+ double queryQueueItemRate = idopgsqlconnection->m_QueryQueue.GetTaskCount(60) / 60.0;
+
+ nodes.emplace_back(idopgsqlconnection->GetName(), new Dictionary({
+ { "version", idopgsqlconnection->GetSchemaVersion() },
+ { "instance_name", idopgsqlconnection->GetInstanceName() },
+ { "connected", idopgsqlconnection->GetConnected() },
+ { "query_queue_items", queryQueueItems },
+ { "query_queue_item_rate", queryQueueItemRate }
+ }));
+
+ perfdata->Add(new PerfdataValue("idopgsqlconnection_" + idopgsqlconnection->GetName() + "_queries_rate", idopgsqlconnection->GetQueryCount(60) / 60.0));
+ perfdata->Add(new PerfdataValue("idopgsqlconnection_" + idopgsqlconnection->GetName() + "_queries_1min", idopgsqlconnection->GetQueryCount(60)));
+ perfdata->Add(new PerfdataValue("idopgsqlconnection_" + idopgsqlconnection->GetName() + "_queries_5mins", idopgsqlconnection->GetQueryCount(5 * 60)));
+ perfdata->Add(new PerfdataValue("idopgsqlconnection_" + idopgsqlconnection->GetName() + "_queries_15mins", idopgsqlconnection->GetQueryCount(15 * 60)));
+ perfdata->Add(new PerfdataValue("idopgsqlconnection_" + idopgsqlconnection->GetName() + "_query_queue_items", queryQueueItems));
+ perfdata->Add(new PerfdataValue("idopgsqlconnection_" + idopgsqlconnection->GetName() + "_query_queue_item_rate", queryQueueItemRate));
+ }
+
+ status->Set("idopgsqlconnection", new Dictionary(std::move(nodes)));
+}
+
+void IdoPgsqlConnection::Resume()
+{
+ Log(LogInformation, "IdoPgsqlConnection")
+ << "'" << GetName() << "' resumed.";
+
+ SetConnected(false);
+
+ m_QueryQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); });
+
+ /* Immediately try to connect on Resume() without timer. */
+ m_QueryQueue.Enqueue([this]() { Reconnect(); }, PriorityImmediate);
+
+ m_TxTimer = Timer::Create();
+ m_TxTimer->SetInterval(1);
+ m_TxTimer->OnTimerExpired.connect([this](const Timer * const&) { NewTransaction(); });
+ m_TxTimer->Start();
+
+ m_ReconnectTimer = Timer::Create();
+ m_ReconnectTimer->SetInterval(10);
+ m_ReconnectTimer->OnTimerExpired.connect([this](const Timer * const&) { ReconnectTimerHandler(); });
+ m_ReconnectTimer->Start();
+
+ /* Start with queries after connect. */
+ DbConnection::Resume();
+
+ ASSERT(m_Pgsql->isthreadsafe());
+}
+
+void IdoPgsqlConnection::Pause()
+{
+ DbConnection::Pause();
+
+ m_ReconnectTimer->Stop(true);
+ m_TxTimer->Stop(true);
+
+ Log(LogInformation, "IdoPgsqlConnection")
+ << "'" << GetName() << "' paused.";
+}
+
+void IdoPgsqlConnection::ExceptionHandler(boost::exception_ptr exp)
+{
+ Log(LogWarning, "IdoPgsqlConnection", "Exception during database operation: Verify that your database is operational!");
+
+ Log(LogDebug, "IdoPgsqlConnection")
+ << "Exception during database operation: " << DiagnosticInformation(std::move(exp));
+
+ if (GetConnected()) {
+ m_Pgsql->finish(m_Connection);
+ SetConnected(false);
+ }
+}
+
+void IdoPgsqlConnection::AssertOnWorkQueue()
+{
+ ASSERT(m_QueryQueue.IsWorkerThread());
+}
+
+void IdoPgsqlConnection::Disconnect()
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected())
+ return;
+
+ IncreasePendingQueries(1);
+ Query("COMMIT");
+
+ m_Pgsql->finish(m_Connection);
+ SetConnected(false);
+
+ Log(LogInformation, "IdoPgsqlConnection")
+ << "Disconnected from '" << GetName() << "' database '" << GetDatabase() << "'.";
+}
+
+void IdoPgsqlConnection::NewTransaction()
+{
+ if (IsPaused())
+ return;
+
+ m_QueryQueue.Enqueue([this]() { InternalNewTransaction(); }, PriorityNormal, true);
+}
+
+void IdoPgsqlConnection::InternalNewTransaction()
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected())
+ return;
+
+ IncreasePendingQueries(2);
+ Query("COMMIT");
+ Query("BEGIN");
+}
+
+void IdoPgsqlConnection::ReconnectTimerHandler()
+{
+ /* Only allow Reconnect events with high priority. */
+ m_QueryQueue.Enqueue([this]() { Reconnect(); }, PriorityHigh);
+}
+
+void IdoPgsqlConnection::Reconnect()
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("Reconnecting to PostgreSQL IDO database '" << GetName() << "'");
+
+ double startTime = Utility::GetTime();
+
+ SetShouldConnect(true);
+
+ bool reconnect = false;
+
+ if (GetConnected()) {
+ /* Check if we're really still connected */
+ try {
+ IncreasePendingQueries(1);
+ Query("SELECT 1");
+ return;
+ } catch (const std::exception&) {
+ m_Pgsql->finish(m_Connection);
+ SetConnected(false);
+ reconnect = true;
+ }
+ }
+
+ ClearIDCache();
+
+ String host = GetHost();
+ String port = GetPort();
+ String user = GetUser();
+ String password = GetPassword();
+ String database = GetDatabase();
+
+ String sslMode = GetSslMode();
+ String sslKey = GetSslKey();
+ String sslCert = GetSslCert();
+ String sslCa = GetSslCa();
+
+ String conninfo;
+
+ if (!host.IsEmpty())
+ conninfo += " host=" + host;
+ if (!port.IsEmpty())
+ conninfo += " port=" + port;
+ if (!user.IsEmpty())
+ conninfo += " user=" + user;
+ if (!password.IsEmpty())
+ conninfo += " password=" + password;
+ if (!database.IsEmpty())
+ conninfo += " dbname=" + database;
+
+ if (!sslMode.IsEmpty())
+ conninfo += " sslmode=" + sslMode;
+ if (!sslKey.IsEmpty())
+ conninfo += " sslkey=" + sslKey;
+ if (!sslCert.IsEmpty())
+ conninfo += " sslcert=" + sslCert;
+ if (!sslCa.IsEmpty())
+ conninfo += " sslrootcert=" + sslCa;
+
+ /* connection */
+ m_Connection = m_Pgsql->connectdb(conninfo.CStr());
+
+ if (!m_Connection)
+ return;
+
+ if (m_Pgsql->status(m_Connection) != CONNECTION_OK) {
+ String message = m_Pgsql->errorMessage(m_Connection);
+ m_Pgsql->finish(m_Connection);
+ SetConnected(false);
+
+ Log(LogCritical, "IdoPgsqlConnection")
+ << "Connection to database '" << database << "' with user '" << user << "' on '" << host << ":" << port
+ << "' failed: \"" << message << "\"";
+
+ BOOST_THROW_EXCEPTION(std::runtime_error(message));
+ }
+
+ SetConnected(true);
+
+ IdoPgsqlResult result;
+
+ String dbVersionName = "idoutils";
+ IncreasePendingQueries(1);
+ result = Query("SELECT version FROM " + GetTablePrefix() + "dbversion WHERE name='" + Escape(dbVersionName) + "'");
+
+ Dictionary::Ptr row = FetchRow(result, 0);
+
+ if (!row) {
+ m_Pgsql->finish(m_Connection);
+ SetConnected(false);
+
+ Log(LogCritical, "IdoPgsqlConnection", "Schema does not provide any valid version! Verify your schema installation.");
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid schema."));
+ }
+
+ String version = row->Get("version");
+
+ SetSchemaVersion(version);
+
+ if (Utility::CompareVersion(GetCompatSchemaVersion(), version) < 0) {
+ m_Pgsql->finish(m_Connection);
+ SetConnected(false);
+
+ Log(LogCritical, "IdoPgsqlConnection")
+ << "Schema version '" << version << "' does not match the required version '"
+ << GetCompatSchemaVersion() << "' (or newer)! Please check the upgrade documentation at "
+ << "https://icinga.com/docs/icinga2/latest/doc/16-upgrading-icinga-2/#upgrading-postgresql-db";
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Schema version mismatch."));
+ }
+
+ String instanceName = GetInstanceName();
+
+ IncreasePendingQueries(1);
+ result = Query("SELECT instance_id FROM " + GetTablePrefix() + "instances WHERE instance_name = '" + Escape(instanceName) + "'");
+ row = FetchRow(result, 0);
+
+ if (!row) {
+ IncreasePendingQueries(1);
+ Query("INSERT INTO " + GetTablePrefix() + "instances (instance_name, instance_description) VALUES ('" + Escape(instanceName) + "', '" + Escape(GetInstanceDescription()) + "')");
+ m_InstanceID = GetSequenceValue(GetTablePrefix() + "instances", "instance_id");
+ } else {
+ m_InstanceID = DbReference(row->Get("instance_id"));
+ }
+
+ Endpoint::Ptr my_endpoint = Endpoint::GetLocalEndpoint();
+
+ /* we have an endpoint in a cluster setup, so decide if we can proceed here */
+ if (my_endpoint && GetHAMode() == HARunOnce) {
+ /* get the current endpoint writing to programstatus table */
+ IncreasePendingQueries(1);
+ result = Query("SELECT UNIX_TIMESTAMP(status_update_time) AS status_update_time, endpoint_name FROM " +
+ GetTablePrefix() + "programstatus WHERE instance_id = " + Convert::ToString(m_InstanceID));
+ row = FetchRow(result, 0);
+
+ String endpoint_name;
+
+ if (row)
+ endpoint_name = row->Get("endpoint_name");
+ else
+ Log(LogNotice, "IdoPgsqlConnection", "Empty program status table");
+
+ /* if we did not write into the database earlier, another instance is active */
+ if (endpoint_name != my_endpoint->GetName()) {
+ double status_update_time;
+
+ if (row)
+ status_update_time = row->Get("status_update_time");
+ else
+ status_update_time = 0;
+
+ double now = Utility::GetTime();
+
+ double status_update_age = now - status_update_time;
+ double failoverTimeout = GetFailoverTimeout();
+
+ if (status_update_age < GetFailoverTimeout()) {
+ Log(LogInformation, "IdoPgsqlConnection")
+ << "Last update by endpoint '" << endpoint_name << "' was "
+ << status_update_age << "s ago (< failover timeout of " << failoverTimeout << "s). Retrying.";
+
+ m_Pgsql->finish(m_Connection);
+ SetConnected(false);
+ SetShouldConnect(false);
+
+ return;
+ }
+
+ /* activate the IDO only, if we're authoritative in this zone */
+ if (IsPaused()) {
+ Log(LogNotice, "IdoPgsqlConnection")
+ << "Local endpoint '" << my_endpoint->GetName() << "' is not authoritative, bailing out.";
+
+ m_Pgsql->finish(m_Connection);
+ SetConnected(false);
+
+ return;
+ }
+
+ SetLastFailover(now);
+
+ Log(LogInformation, "IdoPgsqlConnection")
+ << "Last update by endpoint '" << endpoint_name << "' was "
+ << status_update_age << "s ago. Taking over '" << GetName() << "' in HA zone '" << Zone::GetLocalZone()->GetName() << "'.";
+ }
+
+ Log(LogNotice, "IdoPgsqlConnection", "Enabling IDO connection.");
+ }
+
+ Log(LogInformation, "IdoPgsqlConnection")
+ << "PGSQL IDO instance id: " << static_cast<long>(m_InstanceID) << " (schema version: '" + version + "')"
+ << (!sslMode.IsEmpty() ? ", sslmode='" + sslMode + "'" : "");
+
+ IncreasePendingQueries(1);
+ Query("BEGIN");
+
+ /* update programstatus table */
+ UpdateProgramStatus();
+
+ /* record connection */
+ IncreasePendingQueries(1);
+ Query("INSERT INTO " + GetTablePrefix() + "conninfo " +
+ "(instance_id, connect_time, last_checkin_time, agent_name, agent_version, connect_type, data_start_time) VALUES ("
+ + Convert::ToString(static_cast<long>(m_InstanceID)) + ", NOW(), NOW(), 'icinga2 db_ido_pgsql', '" + Escape(Application::GetAppVersion())
+ + "', '" + (reconnect ? "RECONNECT" : "INITIAL") + "', NOW())");
+
+ /* clear config tables for the initial config dump */
+ PrepareDatabase();
+
+ std::ostringstream q1buf;
+ q1buf << "SELECT object_id, objecttype_id, name1, name2, is_active FROM " + GetTablePrefix() + "objects WHERE instance_id = " << static_cast<long>(m_InstanceID);
+ IncreasePendingQueries(1);
+ result = Query(q1buf.str());
+
+ std::vector<DbObject::Ptr> activeDbObjs;
+
+ int index = 0;
+ while ((row = FetchRow(result, index))) {
+ index++;
+
+ DbType::Ptr dbtype = DbType::GetByID(row->Get("objecttype_id"));
+
+ if (!dbtype)
+ continue;
+
+ DbObject::Ptr dbobj = dbtype->GetOrCreateObjectByName(row->Get("name1"), row->Get("name2"));
+ SetObjectID(dbobj, DbReference(row->Get("object_id")));
+ bool active = row->Get("is_active");
+ SetObjectActive(dbobj, active);
+
+ if (active)
+ activeDbObjs.push_back(dbobj);
+ }
+
+ SetIDCacheValid(true);
+
+ EnableActiveChangedHandler();
+
+ for (const DbObject::Ptr& dbobj : activeDbObjs) {
+ if (dbobj->GetObject())
+ continue;
+
+ Log(LogNotice, "IdoPgsqlConnection")
+ << "Deactivate deleted object name1: '" << dbobj->GetName1()
+ << "' name2: '" << dbobj->GetName2() + "'.";
+ DeactivateObject(dbobj);
+ }
+
+ UpdateAllObjects();
+
+ m_QueryQueue.Enqueue([this]() { ClearTablesBySession(); }, PriorityNormal);
+
+ m_QueryQueue.Enqueue([this, startTime]() { FinishConnect(startTime); }, PriorityNormal);
+}
+
+void IdoPgsqlConnection::FinishConnect(double startTime)
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected())
+ return;
+
+ Log(LogInformation, "IdoPgsqlConnection")
+ << "Finished reconnecting to '" << GetName() << "' database '" << GetDatabase() << "' in "
+ << std::setw(2) << Utility::GetTime() - startTime << " second(s).";
+
+ IncreasePendingQueries(2);
+ Query("COMMIT");
+ Query("BEGIN");
+}
+
+void IdoPgsqlConnection::ClearTablesBySession()
+{
+ /* delete all comments and downtimes without current session token */
+ ClearTableBySession("comments");
+ ClearTableBySession("scheduleddowntime");
+}
+
+void IdoPgsqlConnection::ClearTableBySession(const String& table)
+{
+ IncreasePendingQueries(1);
+ Query("DELETE FROM " + GetTablePrefix() + table + " WHERE instance_id = " +
+ Convert::ToString(static_cast<long>(m_InstanceID)) + " AND session_token <> " +
+ Convert::ToString(GetSessionToken()));
+}
+
+IdoPgsqlResult IdoPgsqlConnection::Query(const String& query)
+{
+ AssertOnWorkQueue();
+
+ Defer decreaseQueries ([this]() { DecreasePendingQueries(1); });
+
+ Log(LogDebug, "IdoPgsqlConnection")
+ << "Query: " << query;
+
+ IncreaseQueryCount();
+
+ PGresult *result = m_Pgsql->exec(m_Connection, query.CStr());
+
+ if (!result) {
+ String message = m_Pgsql->errorMessage(m_Connection);
+ Log(LogCritical, "IdoPgsqlConnection")
+ << "Error \"" << message << "\" when executing query \"" << query << "\"";
+
+ BOOST_THROW_EXCEPTION(
+ database_error()
+ << errinfo_message(message)
+ << errinfo_database_query(query)
+ );
+ }
+
+ char *rowCount = m_Pgsql->cmdTuples(result);
+ m_AffectedRows = atoi(rowCount);
+
+ if (m_Pgsql->resultStatus(result) == PGRES_COMMAND_OK) {
+ m_Pgsql->clear(result);
+ return IdoPgsqlResult();
+ }
+
+ if (m_Pgsql->resultStatus(result) != PGRES_TUPLES_OK) {
+ String message = m_Pgsql->resultErrorMessage(result);
+ m_Pgsql->clear(result);
+
+ Log(LogCritical, "IdoPgsqlConnection")
+ << "Error \"" << message << "\" when executing query \"" << query << "\"";
+
+ BOOST_THROW_EXCEPTION(
+ database_error()
+ << errinfo_message(message)
+ << errinfo_database_query(query)
+ );
+ }
+
+ return IdoPgsqlResult(result, [this](PGresult* result) { m_Pgsql->clear(result); });
+}
+
+DbReference IdoPgsqlConnection::GetSequenceValue(const String& table, const String& column)
+{
+ AssertOnWorkQueue();
+
+ IncreasePendingQueries(1);
+ IdoPgsqlResult result = Query("SELECT CURRVAL(pg_get_serial_sequence('" + Escape(table) + "', '" + Escape(column) + "')) AS id");
+
+ Dictionary::Ptr row = FetchRow(result, 0);
+
+ ASSERT(row);
+
+ Log(LogDebug, "IdoPgsqlConnection")
+ << "Sequence Value: " << row->Get("id");
+
+ return {Convert::ToLong(row->Get("id"))};
+}
+
+int IdoPgsqlConnection::GetAffectedRows()
+{
+ AssertOnWorkQueue();
+
+ return m_AffectedRows;
+}
+
+String IdoPgsqlConnection::Escape(const String& s)
+{
+ AssertOnWorkQueue();
+
+ String utf8s = Utility::ValidateUTF8(s);
+
+ size_t length = utf8s.GetLength();
+ auto *to = new char[utf8s.GetLength() * 2 + 1];
+
+ m_Pgsql->escapeStringConn(m_Connection, to, utf8s.CStr(), length, nullptr);
+
+ String result = String(to);
+
+ delete [] to;
+
+ return result;
+}
+
+Dictionary::Ptr IdoPgsqlConnection::FetchRow(const IdoPgsqlResult& result, int row)
+{
+ AssertOnWorkQueue();
+
+ if (row >= m_Pgsql->ntuples(result.get()))
+ return nullptr;
+
+ int columns = m_Pgsql->nfields(result.get());
+
+ DictionaryData dict;
+
+ for (int column = 0; column < columns; column++) {
+ Value value;
+
+ if (!m_Pgsql->getisnull(result.get(), row, column))
+ value = m_Pgsql->getvalue(result.get(), row, column);
+
+ dict.emplace_back(m_Pgsql->fname(result.get(), column), value);
+ }
+
+ return new Dictionary(std::move(dict));
+}
+
+void IdoPgsqlConnection::ActivateObject(const DbObject::Ptr& dbobj)
+{
+ if (IsPaused())
+ return;
+
+ m_QueryQueue.Enqueue([this, dbobj]() { InternalActivateObject(dbobj); }, PriorityNormal);
+}
+
+void IdoPgsqlConnection::InternalActivateObject(const DbObject::Ptr& dbobj)
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected())
+ return;
+
+ DbReference dbref = GetObjectID(dbobj);
+ std::ostringstream qbuf;
+
+ if (!dbref.IsValid()) {
+ if (!dbobj->GetName2().IsEmpty()) {
+ qbuf << "INSERT INTO " + GetTablePrefix() + "objects (instance_id, objecttype_id, name1, name2, is_active) VALUES ("
+ << static_cast<long>(m_InstanceID) << ", " << dbobj->GetType()->GetTypeID() << ", "
+ << "'" << Escape(dbobj->GetName1()) << "', '" << Escape(dbobj->GetName2()) << "', 1)";
+ } else {
+ qbuf << "INSERT INTO " + GetTablePrefix() + "objects (instance_id, objecttype_id, name1, is_active) VALUES ("
+ << static_cast<long>(m_InstanceID) << ", " << dbobj->GetType()->GetTypeID() << ", "
+ << "'" << Escape(dbobj->GetName1()) << "', 1)";
+ }
+
+ IncreasePendingQueries(1);
+ Query(qbuf.str());
+ SetObjectID(dbobj, GetSequenceValue(GetTablePrefix() + "objects", "object_id"));
+ } else {
+ qbuf << "UPDATE " + GetTablePrefix() + "objects SET is_active = 1 WHERE object_id = " << static_cast<long>(dbref);
+ IncreasePendingQueries(1);
+ Query(qbuf.str());
+ }
+}
+
+void IdoPgsqlConnection::DeactivateObject(const DbObject::Ptr& dbobj)
+{
+ if (IsPaused())
+ return;
+
+ m_QueryQueue.Enqueue([this, dbobj]() { InternalDeactivateObject(dbobj); }, PriorityNormal);
+}
+
+void IdoPgsqlConnection::InternalDeactivateObject(const DbObject::Ptr& dbobj)
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected())
+ return;
+
+ DbReference dbref = GetObjectID(dbobj);
+
+ if (!dbref.IsValid())
+ return;
+
+ std::ostringstream qbuf;
+ qbuf << "UPDATE " + GetTablePrefix() + "objects SET is_active = 0 WHERE object_id = " << static_cast<long>(dbref);
+ IncreasePendingQueries(1);
+ Query(qbuf.str());
+
+ /* Note that we're _NOT_ clearing the db refs via SetReference/SetConfigUpdate/SetStatusUpdate
+ * because the object is still in the database. */
+
+ SetObjectActive(dbobj, false);
+}
+
+bool IdoPgsqlConnection::FieldToEscapedString(const String& key, const Value& value, Value *result)
+{
+ if (key == "instance_id") {
+ *result = static_cast<long>(m_InstanceID);
+ return true;
+ } else if (key == "session_token") {
+ *result = GetSessionToken();
+ return true;
+ }
+
+ Value rawvalue = DbValue::ExtractValue(value);
+
+ if (rawvalue.GetType() == ValueEmpty) {
+ *result = "NULL";
+ } else if (rawvalue.IsObjectType<ConfigObject>()) {
+ DbObject::Ptr dbobjcol = DbObject::GetOrCreateByObject(rawvalue);
+
+ if (!dbobjcol) {
+ *result = 0;
+ return true;
+ }
+
+ if (!IsIDCacheValid())
+ return false;
+
+ DbReference dbrefcol;
+
+ if (DbValue::IsObjectInsertID(value)) {
+ dbrefcol = GetInsertID(dbobjcol);
+
+ if (!dbrefcol.IsValid())
+ return false;
+ } else {
+ dbrefcol = GetObjectID(dbobjcol);
+
+ if (!dbrefcol.IsValid()) {
+ InternalActivateObject(dbobjcol);
+
+ dbrefcol = GetObjectID(dbobjcol);
+
+ if (!dbrefcol.IsValid())
+ return false;
+ }
+ }
+
+ *result = static_cast<long>(dbrefcol);
+ } else if (DbValue::IsTimestamp(value)) {
+ long ts = rawvalue;
+ std::ostringstream msgbuf;
+ msgbuf << "TO_TIMESTAMP(" << ts << ") AT TIME ZONE 'UTC'";
+ *result = Value(msgbuf.str());
+ } else if (DbValue::IsObjectInsertID(value)) {
+ auto id = static_cast<long>(rawvalue);
+
+ if (id <= 0)
+ return false;
+
+ *result = id;
+ return true;
+ } else {
+ Value fvalue;
+
+ if (rawvalue.IsBoolean())
+ fvalue = Convert::ToLong(rawvalue);
+ else
+ fvalue = rawvalue;
+
+ *result = "'" + Escape(fvalue) + "'";
+ }
+
+ return true;
+}
+
+void IdoPgsqlConnection::ExecuteQuery(const DbQuery& query)
+{
+ if (IsPaused() && GetPauseCalled())
+ return;
+
+ ASSERT(query.Category != DbCatInvalid);
+
+ IncreasePendingQueries(1);
+ m_QueryQueue.Enqueue([this, query]() { InternalExecuteQuery(query, -1); }, query.Priority, true);
+}
+
+void IdoPgsqlConnection::ExecuteMultipleQueries(const std::vector<DbQuery>& queries)
+{
+ if (IsPaused())
+ return;
+
+ if (queries.empty())
+ return;
+
+ IncreasePendingQueries(queries.size());
+ m_QueryQueue.Enqueue([this, queries]() { InternalExecuteMultipleQueries(queries); }, queries[0].Priority, true);
+}
+
+bool IdoPgsqlConnection::CanExecuteQuery(const DbQuery& query)
+{
+ if (query.Object && !IsIDCacheValid())
+ return false;
+
+ if (query.WhereCriteria) {
+ ObjectLock olock(query.WhereCriteria);
+ Value value;
+
+ for (const Dictionary::Pair& kv : query.WhereCriteria) {
+ if (!FieldToEscapedString(kv.first, kv.second, &value))
+ return false;
+ }
+ }
+
+ if (query.Fields) {
+ ObjectLock olock(query.Fields);
+
+ for (const Dictionary::Pair& kv : query.Fields) {
+ Value value;
+
+ if (!FieldToEscapedString(kv.first, kv.second, &value))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void IdoPgsqlConnection::InternalExecuteMultipleQueries(const std::vector<DbQuery>& queries)
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused()) {
+ DecreasePendingQueries(queries.size());
+ return;
+ }
+
+ if (!GetConnected()) {
+ DecreasePendingQueries(queries.size());
+ return;
+ }
+
+ for (const DbQuery& query : queries) {
+ ASSERT(query.Type == DbQueryNewTransaction || query.Category != DbCatInvalid);
+
+ if (!CanExecuteQuery(query)) {
+ m_QueryQueue.Enqueue([this, queries]() { InternalExecuteMultipleQueries(queries); }, query.Priority);
+ return;
+ }
+ }
+
+ for (const DbQuery& query : queries) {
+ InternalExecuteQuery(query);
+ }
+}
+
+void IdoPgsqlConnection::InternalExecuteQuery(const DbQuery& query, int typeOverride)
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused() && GetPauseCalled()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ if (!GetConnected()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ if (query.Type == DbQueryNewTransaction) {
+ DecreasePendingQueries(1);
+ InternalNewTransaction();
+ return;
+ }
+
+ /* check whether we're allowed to execute the query first */
+ if (GetCategoryFilter() != DbCatEverything && (query.Category & GetCategoryFilter()) == 0) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ if (query.Object && query.Object->GetObject()->GetExtension("agent_check").ToBool()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ /* check if there are missing object/insert ids and re-enqueue the query */
+ if (!CanExecuteQuery(query)) {
+ m_QueryQueue.Enqueue([this, query, typeOverride]() { InternalExecuteQuery(query, typeOverride); }, query.Priority);
+ return;
+ }
+
+ std::ostringstream qbuf, where;
+ int type;
+
+ if (query.WhereCriteria) {
+ where << " WHERE ";
+
+ ObjectLock olock(query.WhereCriteria);
+ Value value;
+ bool first = true;
+
+ for (const Dictionary::Pair& kv : query.WhereCriteria) {
+ if (!FieldToEscapedString(kv.first, kv.second, &value)) {
+ m_QueryQueue.Enqueue([this, query]() { InternalExecuteQuery(query, -1); }, query.Priority);
+ return;
+ }
+
+ if (!first)
+ where << " AND ";
+
+ where << kv.first << " = " << value;
+
+ if (first)
+ first = false;
+ }
+ }
+
+ type = (typeOverride != -1) ? typeOverride : query.Type;
+
+ bool upsert = false;
+
+ if ((type & DbQueryInsert) && (type & DbQueryUpdate)) {
+ bool hasid = false;
+
+ if (query.Object) {
+ if (query.ConfigUpdate)
+ hasid = GetConfigUpdate(query.Object);
+ else if (query.StatusUpdate)
+ hasid = GetStatusUpdate(query.Object);
+ }
+
+ if (!hasid)
+ upsert = true;
+
+ type = DbQueryUpdate;
+ }
+
+ if ((type & DbQueryInsert) && (type & DbQueryDelete)) {
+ std::ostringstream qdel;
+ qdel << "DELETE FROM " << GetTablePrefix() << query.Table << where.str();
+ IncreasePendingQueries(1);
+ Query(qdel.str());
+
+ type = DbQueryInsert;
+ }
+
+ switch (type) {
+ case DbQueryInsert:
+ qbuf << "INSERT INTO " << GetTablePrefix() << query.Table;
+ break;
+ case DbQueryUpdate:
+ qbuf << "UPDATE " << GetTablePrefix() << query.Table << " SET";
+ break;
+ case DbQueryDelete:
+ qbuf << "DELETE FROM " << GetTablePrefix() << query.Table;
+ break;
+ default:
+ VERIFY(!"Invalid query type.");
+ }
+
+ if (type == DbQueryInsert || type == DbQueryUpdate) {
+ std::ostringstream colbuf, valbuf;
+
+ if (type == DbQueryUpdate && query.Fields->GetLength() == 0)
+ return;
+
+ ObjectLock olock(query.Fields);
+
+ Value value;
+ bool first = true;
+ for (const Dictionary::Pair& kv : query.Fields) {
+ if (!FieldToEscapedString(kv.first, kv.second, &value)) {
+ m_QueryQueue.Enqueue([this, query]() { InternalExecuteQuery(query, -1); }, query.Priority);
+ return;
+ }
+
+ if (type == DbQueryInsert) {
+ if (!first) {
+ colbuf << ", ";
+ valbuf << ", ";
+ }
+
+ colbuf << kv.first;
+ valbuf << value;
+ } else {
+ if (!first)
+ qbuf << ", ";
+
+ qbuf << " " << kv.first << " = " << value;
+ }
+
+ if (first)
+ first = false;
+ }
+
+ if (type == DbQueryInsert)
+ qbuf << " (" << colbuf.str() << ") VALUES (" << valbuf.str() << ")";
+ }
+
+ if (type != DbQueryInsert)
+ qbuf << where.str();
+
+ Query(qbuf.str());
+
+ if (upsert && GetAffectedRows() == 0) {
+ IncreasePendingQueries(1);
+ InternalExecuteQuery(query, DbQueryDelete | DbQueryInsert);
+
+ return;
+ }
+
+ if (type == DbQueryInsert && query.Object) {
+ if (query.ConfigUpdate) {
+ String idField = query.IdColumn;
+
+ if (idField.IsEmpty())
+ idField = query.Table.SubStr(0, query.Table.GetLength() - 1) + "_id";
+
+ SetInsertID(query.Object, GetSequenceValue(GetTablePrefix() + query.Table, idField));
+
+ SetConfigUpdate(query.Object, true);
+ } else if (query.StatusUpdate)
+ SetStatusUpdate(query.Object, true);
+ }
+
+ if (type == DbQueryInsert && query.Table == "notifications" && query.NotificationInsertID) {
+ DbReference seqval = GetSequenceValue(GetTablePrefix() + query.Table, "notification_id");
+ query.NotificationInsertID->SetValue(static_cast<long>(seqval));
+ }
+}
+
+void IdoPgsqlConnection::CleanUpExecuteQuery(const String& table, const String& time_column, double max_age)
+{
+ if (IsPaused())
+ return;
+
+ IncreasePendingQueries(1);
+ m_QueryQueue.Enqueue([this, table, time_column, max_age]() { InternalCleanUpExecuteQuery(table, time_column, max_age); }, PriorityLow, true);
+}
+
+void IdoPgsqlConnection::InternalCleanUpExecuteQuery(const String& table, const String& time_column, double max_age)
+{
+ AssertOnWorkQueue();
+
+ if (!GetConnected()) {
+ DecreasePendingQueries(1);
+ return;
+ }
+
+ Query("DELETE FROM " + GetTablePrefix() + table + " WHERE instance_id = " +
+ Convert::ToString(static_cast<long>(m_InstanceID)) + " AND " + time_column +
+ " < TO_TIMESTAMP(" + Convert::ToString(static_cast<long>(max_age)) + ") AT TIME ZONE 'UTC'");
+}
+
+void IdoPgsqlConnection::FillIDCache(const DbType::Ptr& type)
+{
+ String query = "SELECT " + type->GetIDColumn() + " AS object_id, " + type->GetTable() + "_id, config_hash FROM " + GetTablePrefix() + type->GetTable() + "s";
+ IncreasePendingQueries(1);
+ IdoPgsqlResult result = Query(query);
+
+ Dictionary::Ptr row;
+
+ int index = 0;
+ while ((row = FetchRow(result, index))) {
+ index++;
+ DbReference dbref(row->Get("object_id"));
+ SetInsertID(type, dbref, DbReference(row->Get(type->GetTable() + "_id")));
+ SetConfigHash(type, dbref, row->Get("config_hash"));
+ }
+}
+
+int IdoPgsqlConnection::GetPendingQueryCount() const
+{
+ return m_QueryQueue.GetLength();
+}
diff --git a/lib/db_ido_pgsql/idopgsqlconnection.hpp b/lib/db_ido_pgsql/idopgsqlconnection.hpp
new file mode 100644
index 0000000..dc06a93
--- /dev/null
+++ b/lib/db_ido_pgsql/idopgsqlconnection.hpp
@@ -0,0 +1,99 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef IDOPGSQLCONNECTION_H
+#define IDOPGSQLCONNECTION_H
+
+#include "db_ido_pgsql/idopgsqlconnection-ti.hpp"
+#include "pgsql_shim/pgsqlinterface.hpp"
+#include "base/array.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include "base/library.hpp"
+
+namespace icinga
+{
+
+typedef std::shared_ptr<PGresult> IdoPgsqlResult;
+
+/**
+ * An IDO pgSQL database connection.
+ *
+ * @ingroup ido
+ */
+class IdoPgsqlConnection final : public ObjectImpl<IdoPgsqlConnection>
+{
+public:
+ DECLARE_OBJECT(IdoPgsqlConnection);
+ DECLARE_OBJECTNAME(IdoPgsqlConnection);
+
+ IdoPgsqlConnection();
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ const char * GetLatestSchemaVersion() const noexcept override;
+ const char * GetCompatSchemaVersion() const noexcept override;
+
+ int GetPendingQueryCount() const override;
+
+protected:
+ void OnConfigLoaded() override;
+ void Resume() override;
+ void Pause() override;
+
+ void ActivateObject(const DbObject::Ptr& dbobj) override;
+ void DeactivateObject(const DbObject::Ptr& dbobj) override;
+ void ExecuteQuery(const DbQuery& query) override;
+ void ExecuteMultipleQueries(const std::vector<DbQuery>& queries) override;
+ void CleanUpExecuteQuery(const String& table, const String& time_key, double time_value) override;
+ void FillIDCache(const DbType::Ptr& type) override;
+ void NewTransaction() override;
+ void Disconnect() override;
+
+private:
+ DbReference m_InstanceID;
+
+ Library m_Library;
+ std::unique_ptr<PgsqlInterface, PgsqlInterfaceDeleter> m_Pgsql;
+
+ PGconn *m_Connection;
+ int m_AffectedRows;
+
+ Timer::Ptr m_ReconnectTimer;
+ Timer::Ptr m_TxTimer;
+
+ IdoPgsqlResult Query(const String& query);
+ DbReference GetSequenceValue(const String& table, const String& column);
+ int GetAffectedRows();
+ String Escape(const String& s);
+ Dictionary::Ptr FetchRow(const IdoPgsqlResult& result, int row);
+
+ bool FieldToEscapedString(const String& key, const Value& value, Value *result);
+ void InternalActivateObject(const DbObject::Ptr& dbobj);
+ void InternalDeactivateObject(const DbObject::Ptr& dbobj);
+
+ void InternalNewTransaction();
+ void Reconnect();
+
+ void AssertOnWorkQueue();
+
+ void ReconnectTimerHandler();
+
+ void StatsLoggerTimerHandler();
+
+ bool CanExecuteQuery(const DbQuery& query);
+
+ void InternalExecuteQuery(const DbQuery& query, int typeOverride = -1);
+ void InternalExecuteMultipleQueries(const std::vector<DbQuery>& queries);
+ void InternalCleanUpExecuteQuery(const String& table, const String& time_key, double time_value);
+
+ void ClearTableBySession(const String& table);
+ void ClearTablesBySession();
+
+ void ExceptionHandler(boost::exception_ptr exp);
+
+ void FinishConnect(double startTime);
+};
+
+}
+
+#endif /* IDOPGSQLCONNECTION_H */
diff --git a/lib/db_ido_pgsql/idopgsqlconnection.ti b/lib/db_ido_pgsql/idopgsqlconnection.ti
new file mode 100644
index 0000000..bc4deff
--- /dev/null
+++ b/lib/db_ido_pgsql/idopgsqlconnection.ti
@@ -0,0 +1,39 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "db_ido/dbconnection.hpp"
+
+library db_ido_pgsql;
+
+namespace icinga
+{
+
+class IdoPgsqlConnection : DbConnection
+{
+ activation_priority 100;
+
+ [config] String host {
+ default {{{ return "localhost"; }}}
+ };
+ [config] String port {
+ default {{{ return "5432"; }}}
+ };
+ [config] String user {
+ default {{{ return "icinga"; }}}
+ };
+ [config, no_user_view, no_user_modify] String password {
+ default {{{ return "icinga"; }}}
+ };
+ [config] String database {
+ default {{{ return "icinga"; }}}
+ };
+ [config] String instance_name {
+ default {{{ return "default"; }}}
+ };
+ [config] String instance_description;
+ [config] String ssl_mode;
+ [config] String ssl_key;
+ [config] String ssl_cert;
+ [config] String ssl_ca;
+};
+
+}
diff --git a/lib/db_ido_pgsql/schema/pgsql.sql b/lib/db_ido_pgsql/schema/pgsql.sql
new file mode 100644
index 0000000..242b6db
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/pgsql.sql
@@ -0,0 +1,1733 @@
+-- --------------------------------------------------------
+-- pgsql.sql
+-- DB definition for IDO Postgresql
+--
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- --------------------------------------------------------
+
+--
+-- Functions
+--
+
+DROP FUNCTION IF EXISTS from_unixtime(bigint);
+CREATE FUNCTION from_unixtime(bigint) RETURNS timestamp AS $$
+ SELECT to_timestamp($1) AT TIME ZONE 'UTC' AS result
+$$ LANGUAGE sql;
+
+DROP FUNCTION IF EXISTS unix_timestamp(timestamp WITH TIME ZONE);
+CREATE OR REPLACE FUNCTION unix_timestamp(timestamp) RETURNS bigint AS '
+ SELECT CAST(EXTRACT(EPOCH FROM $1) AS bigint) AS result;
+' LANGUAGE sql;
+
+
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+
+CREATE OR REPLACE FUNCTION updatedbversion(version_i TEXT) RETURNS void AS $$
+BEGIN
+ IF EXISTS( SELECT * FROM icinga_dbversion WHERE name='idoutils')
+ THEN
+ UPDATE icinga_dbversion
+ SET version=version_i, modify_time=NOW()
+ WHERE name='idoutils';
+ ELSE
+ INSERT INTO icinga_dbversion (dbversion_id, name, version, create_time, modify_time) VALUES ('1', 'idoutils', version_i, NOW(), NOW());
+ END IF;
+
+ RETURN;
+END;
+$$ LANGUAGE plpgsql;
+-- HINT: su - postgres; createlang plpgsql icinga;
+
+
+
+--
+-- Database: icinga
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_acknowledgements
+--
+
+CREATE TABLE icinga_acknowledgements (
+ acknowledgement_id bigserial,
+ instance_id bigint default 0,
+ entry_time timestamp,
+ entry_time_usec INTEGER default 0,
+ acknowledgement_type INTEGER default 0,
+ object_id bigint default 0,
+ state INTEGER default 0,
+ author_name TEXT default '',
+ comment_data TEXT default '',
+ is_sticky INTEGER default 0,
+ persistent_comment INTEGER default 0,
+ notify_contacts INTEGER default 0,
+ end_time timestamp,
+ CONSTRAINT PK_acknowledgement_id PRIMARY KEY (acknowledgement_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_commands
+--
+
+CREATE TABLE icinga_commands (
+ command_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ object_id bigint default 0,
+ command_line TEXT default '',
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_command_id PRIMARY KEY (command_id) ,
+ CONSTRAINT UQ_commands UNIQUE (instance_id,object_id,config_type)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_commenthistory
+--
+
+CREATE TABLE icinga_commenthistory (
+ commenthistory_id bigserial,
+ instance_id bigint default 0,
+ entry_time timestamp,
+ entry_time_usec INTEGER default 0,
+ comment_type INTEGER default 0,
+ entry_type INTEGER default 0,
+ object_id bigint default 0,
+ comment_time timestamp,
+ internal_comment_id bigint default 0,
+ author_name TEXT default '',
+ comment_data TEXT default '',
+ is_persistent INTEGER default 0,
+ comment_source INTEGER default 0,
+ expires INTEGER default 0,
+ expiration_time timestamp,
+ deletion_time timestamp,
+ deletion_time_usec INTEGER default 0,
+ name TEXT default NULL,
+ CONSTRAINT PK_commenthistory_id PRIMARY KEY (commenthistory_id)
+);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_comments
+--
+
+CREATE TABLE icinga_comments (
+ comment_id bigserial,
+ instance_id bigint default 0,
+ entry_time timestamp,
+ entry_time_usec INTEGER default 0,
+ comment_type INTEGER default 0,
+ entry_type INTEGER default 0,
+ object_id bigint default 0,
+ comment_time timestamp,
+ internal_comment_id bigint default 0,
+ author_name TEXT default '',
+ comment_data TEXT default '',
+ is_persistent INTEGER default 0,
+ comment_source INTEGER default 0,
+ expires INTEGER default 0,
+ expiration_time timestamp,
+ name TEXT default NULL,
+ session_token INTEGER default NULL,
+ CONSTRAINT PK_comment_id PRIMARY KEY (comment_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_configfiles
+--
+
+CREATE TABLE icinga_configfiles (
+ configfile_id bigserial,
+ instance_id bigint default 0,
+ configfile_type INTEGER default 0,
+ configfile_path TEXT default '',
+ CONSTRAINT PK_configfile_id PRIMARY KEY (configfile_id) ,
+ CONSTRAINT UQ_configfiles UNIQUE (instance_id,configfile_type,configfile_path)
+);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_configfilevariables
+--
+
+CREATE TABLE icinga_configfilevariables (
+ configfilevariable_id bigserial,
+ instance_id bigint default 0,
+ configfile_id bigint default 0,
+ varname TEXT default '',
+ varvalue TEXT default '',
+ CONSTRAINT PK_configfilevariable_id PRIMARY KEY (configfilevariable_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_conninfo
+--
+
+CREATE TABLE icinga_conninfo (
+ conninfo_id bigserial,
+ instance_id bigint default 0,
+ agent_name TEXT default '',
+ agent_version TEXT default '',
+ disposition TEXT default '',
+ connect_source TEXT default '',
+ connect_type TEXT default '',
+ connect_time timestamp,
+ disconnect_time timestamp,
+ last_checkin_time timestamp,
+ data_start_time timestamp,
+ data_end_time timestamp,
+ bytes_processed bigint default 0,
+ lines_processed bigint default 0,
+ entries_processed bigint default 0,
+ CONSTRAINT PK_conninfo_id PRIMARY KEY (conninfo_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactgroups
+--
+
+CREATE TABLE icinga_contactgroups (
+ contactgroup_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ contactgroup_object_id bigint default 0,
+ alias TEXT default '',
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_contactgroup_id PRIMARY KEY (contactgroup_id) ,
+ CONSTRAINT UQ_contactgroups UNIQUE (instance_id,config_type,contactgroup_object_id)
+);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactgroup_members
+--
+
+CREATE TABLE icinga_contactgroup_members (
+ contactgroup_member_id bigserial,
+ instance_id bigint default 0,
+ contactgroup_id bigint default 0,
+ contact_object_id bigint default 0,
+ session_token INTEGER default NULL,
+ CONSTRAINT PK_contactgroup_member_id PRIMARY KEY (contactgroup_member_id)
+);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactnotificationmethods
+--
+
+CREATE TABLE icinga_contactnotificationmethods (
+ contactnotificationmethod_id bigserial,
+ instance_id bigint default 0,
+ contactnotification_id bigint default 0,
+ start_time timestamp,
+ start_time_usec INTEGER default 0,
+ end_time timestamp,
+ end_time_usec INTEGER default 0,
+ command_object_id bigint default 0,
+ command_args TEXT default '',
+ CONSTRAINT PK_contactnotificationmethod_id PRIMARY KEY (contactnotificationmethod_id) ,
+ CONSTRAINT UQ_contactnotificationmethods UNIQUE (instance_id,contactnotification_id,start_time,start_time_usec)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactnotifications
+--
+
+CREATE TABLE icinga_contactnotifications (
+ contactnotification_id bigserial,
+ instance_id bigint default 0,
+ notification_id bigint default 0,
+ contact_object_id bigint default 0,
+ start_time timestamp,
+ start_time_usec INTEGER default 0,
+ end_time timestamp,
+ end_time_usec INTEGER default 0,
+ CONSTRAINT PK_contactnotification_id PRIMARY KEY (contactnotification_id) ,
+ CONSTRAINT UQ_contactnotifications UNIQUE (instance_id,contact_object_id,start_time,start_time_usec)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contacts
+--
+
+CREATE TABLE icinga_contacts (
+ contact_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ contact_object_id bigint default 0,
+ alias TEXT default '',
+ email_address TEXT default '',
+ pager_address TEXT default '',
+ host_timeperiod_object_id bigint default 0,
+ service_timeperiod_object_id bigint default 0,
+ host_notifications_enabled INTEGER default 0,
+ service_notifications_enabled INTEGER default 0,
+ can_submit_commands INTEGER default 0,
+ notify_service_recovery INTEGER default 0,
+ notify_service_warning INTEGER default 0,
+ notify_service_unknown INTEGER default 0,
+ notify_service_critical INTEGER default 0,
+ notify_service_flapping INTEGER default 0,
+ notify_service_downtime INTEGER default 0,
+ notify_host_recovery INTEGER default 0,
+ notify_host_down INTEGER default 0,
+ notify_host_unreachable INTEGER default 0,
+ notify_host_flapping INTEGER default 0,
+ notify_host_downtime INTEGER default 0,
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_contact_id PRIMARY KEY (contact_id) ,
+ CONSTRAINT UQ_contacts UNIQUE (instance_id,config_type,contact_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contactstatus
+--
+
+CREATE TABLE icinga_contactstatus (
+ contactstatus_id bigserial,
+ instance_id bigint default 0,
+ contact_object_id bigint default 0,
+ status_update_time timestamp,
+ host_notifications_enabled INTEGER default 0,
+ service_notifications_enabled INTEGER default 0,
+ last_host_notification timestamp,
+ last_service_notification timestamp,
+ modified_attributes INTEGER default 0,
+ modified_host_attributes INTEGER default 0,
+ modified_service_attributes INTEGER default 0,
+ CONSTRAINT PK_contactstatus_id PRIMARY KEY (contactstatus_id) ,
+ CONSTRAINT UQ_contactstatus UNIQUE (contact_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contact_addresses
+--
+
+CREATE TABLE icinga_contact_addresses (
+ contact_address_id bigserial,
+ instance_id bigint default 0,
+ contact_id bigint default 0,
+ address_number INTEGER default 0,
+ address TEXT default '',
+ CONSTRAINT PK_contact_address_id PRIMARY KEY (contact_address_id) ,
+ CONSTRAINT UQ_contact_addresses UNIQUE (contact_id,address_number)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_contact_notificationcommands
+--
+
+CREATE TABLE icinga_contact_notificationcommands (
+ contact_notificationcommand_id bigserial,
+ instance_id bigint default 0,
+ contact_id bigint default 0,
+ notification_type INTEGER default 0,
+ command_object_id bigint default 0,
+ command_args TEXT default '',
+ CONSTRAINT PK_contact_notificationcommand_id PRIMARY KEY (contact_notificationcommand_id) ,
+ CONSTRAINT UQ_contact_notificationcommands UNIQUE (contact_id,notification_type,command_object_id,command_args)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_customvariables
+--
+
+CREATE TABLE icinga_customvariables (
+ customvariable_id bigserial,
+ instance_id bigint default 0,
+ object_id bigint default 0,
+ config_type INTEGER default 0,
+ has_been_modified INTEGER default 0,
+ varname TEXT default '',
+ varvalue TEXT default '',
+ is_json INTEGER default 0,
+ session_token INTEGER default NULL,
+ CONSTRAINT PK_customvariable_id PRIMARY KEY (customvariable_id) ,
+ CONSTRAINT UQ_customvariables UNIQUE (object_id,config_type,varname)
+) ;
+CREATE INDEX icinga_customvariables_i ON icinga_customvariables(varname);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_customvariablestatus
+--
+
+CREATE TABLE icinga_customvariablestatus (
+ customvariablestatus_id bigserial,
+ instance_id bigint default 0,
+ object_id bigint default 0,
+ status_update_time timestamp,
+ has_been_modified INTEGER default 0,
+ varname TEXT default '',
+ varvalue TEXT default '',
+ is_json INTEGER default 0,
+ session_token INTEGER default NULL,
+ CONSTRAINT PK_customvariablestatus_id PRIMARY KEY (customvariablestatus_id) ,
+ CONSTRAINT UQ_customvariablestatus UNIQUE (object_id,varname)
+) ;
+CREATE INDEX icinga_customvariablestatus_i ON icinga_customvariablestatus(varname);
+
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_dbversion
+--
+
+CREATE TABLE icinga_dbversion (
+ dbversion_id bigserial,
+ name TEXT default '',
+ version TEXT default '',
+ create_time timestamp,
+ modify_time timestamp,
+ CONSTRAINT PK_dbversion_id PRIMARY KEY (dbversion_id) ,
+ CONSTRAINT UQ_dbversion UNIQUE (name)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_downtimehistory
+--
+
+CREATE TABLE icinga_downtimehistory (
+ downtimehistory_id bigserial,
+ instance_id bigint default 0,
+ downtime_type INTEGER default 0,
+ object_id bigint default 0,
+ entry_time timestamp,
+ author_name TEXT default '',
+ comment_data TEXT default '',
+ internal_downtime_id bigint default 0,
+ triggered_by_id bigint default 0,
+ is_fixed INTEGER default 0,
+ duration BIGINT default 0,
+ scheduled_start_time timestamp,
+ scheduled_end_time timestamp,
+ was_started INTEGER default 0,
+ actual_start_time timestamp,
+ actual_start_time_usec INTEGER default 0,
+ actual_end_time timestamp,
+ actual_end_time_usec INTEGER default 0,
+ was_cancelled INTEGER default 0,
+ is_in_effect INTEGER default 0,
+ trigger_time timestamp,
+ name TEXT default NULL,
+ CONSTRAINT PK_downtimehistory_id PRIMARY KEY (downtimehistory_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_eventhandlers
+--
+
+CREATE TABLE icinga_eventhandlers (
+ eventhandler_id bigserial,
+ instance_id bigint default 0,
+ eventhandler_type INTEGER default 0,
+ object_id bigint default 0,
+ state INTEGER default 0,
+ state_type INTEGER default 0,
+ start_time timestamp,
+ start_time_usec INTEGER default 0,
+ end_time timestamp,
+ end_time_usec INTEGER default 0,
+ command_object_id bigint default 0,
+ command_args TEXT default '',
+ command_line TEXT default '',
+ timeout INTEGER default 0,
+ early_timeout INTEGER default 0,
+ execution_time double precision default 0,
+ return_code INTEGER default 0,
+ output TEXT default '',
+ long_output TEXT default '',
+ CONSTRAINT PK_eventhandler_id PRIMARY KEY (eventhandler_id) ,
+ CONSTRAINT UQ_eventhandlers UNIQUE (instance_id,object_id,start_time,start_time_usec)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_externalcommands
+--
+
+CREATE TABLE icinga_externalcommands (
+ externalcommand_id bigserial,
+ instance_id bigint default 0,
+ entry_time timestamp,
+ command_type INTEGER default 0,
+ command_name TEXT default '',
+ command_args TEXT default '',
+ CONSTRAINT PK_externalcommand_id PRIMARY KEY (externalcommand_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_flappinghistory
+--
+
+CREATE TABLE icinga_flappinghistory (
+ flappinghistory_id bigserial,
+ instance_id bigint default 0,
+ event_time timestamp,
+ event_time_usec INTEGER default 0,
+ event_type INTEGER default 0,
+ reason_type INTEGER default 0,
+ flapping_type INTEGER default 0,
+ object_id bigint default 0,
+ percent_state_change double precision default 0,
+ low_threshold double precision default 0,
+ high_threshold double precision default 0,
+ comment_time timestamp,
+ internal_comment_id bigint default 0,
+ CONSTRAINT PK_flappinghistory_id PRIMARY KEY (flappinghistory_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostchecks
+--
+
+CREATE TABLE icinga_hostchecks (
+ hostcheck_id bigserial,
+ instance_id bigint default 0,
+ host_object_id bigint default 0,
+ check_type INTEGER default 0,
+ is_raw_check INTEGER default 0,
+ current_check_attempt INTEGER default 0,
+ max_check_attempts INTEGER default 0,
+ state INTEGER default 0,
+ state_type INTEGER default 0,
+ start_time timestamp,
+ start_time_usec INTEGER default 0,
+ end_time timestamp,
+ end_time_usec INTEGER default 0,
+ command_object_id bigint default 0,
+ command_args TEXT default '',
+ command_line TEXT default '',
+ timeout INTEGER default 0,
+ early_timeout INTEGER default 0,
+ execution_time double precision default 0,
+ latency double precision default 0,
+ return_code INTEGER default 0,
+ output TEXT default '',
+ long_output TEXT default '',
+ perfdata TEXT default '',
+ CONSTRAINT PK_hostcheck_id PRIMARY KEY (hostcheck_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostdependencies
+--
+
+CREATE TABLE icinga_hostdependencies (
+ hostdependency_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ host_object_id bigint default 0,
+ dependent_host_object_id bigint default 0,
+ dependency_type INTEGER default 0,
+ inherits_parent INTEGER default 0,
+ timeperiod_object_id bigint default 0,
+ fail_on_up INTEGER default 0,
+ fail_on_down INTEGER default 0,
+ fail_on_unreachable INTEGER default 0,
+ CONSTRAINT PK_hostdependency_id PRIMARY KEY (hostdependency_id)
+) ;
+CREATE INDEX idx_hostdependencies ON icinga_hostdependencies(instance_id,config_type,host_object_id,dependent_host_object_id,dependency_type,inherits_parent,fail_on_up,fail_on_down,fail_on_unreachable);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostescalations
+--
+
+CREATE TABLE icinga_hostescalations (
+ hostescalation_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ host_object_id bigint default 0,
+ timeperiod_object_id bigint default 0,
+ first_notification INTEGER default 0,
+ last_notification INTEGER default 0,
+ notification_interval double precision default 0,
+ escalate_on_recovery INTEGER default 0,
+ escalate_on_down INTEGER default 0,
+ escalate_on_unreachable INTEGER default 0,
+ CONSTRAINT PK_hostescalation_id PRIMARY KEY (hostescalation_id) ,
+ CONSTRAINT UQ_hostescalations UNIQUE (instance_id,config_type,host_object_id,timeperiod_object_id,first_notification,last_notification)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostescalation_contactgroups
+--
+
+CREATE TABLE icinga_hostescalation_contactgroups (
+ hostescalation_contactgroup_id bigserial,
+ instance_id bigint default 0,
+ hostescalation_id bigint default 0,
+ contactgroup_object_id bigint default 0,
+ CONSTRAINT PK_hostescalation_contactgroup_id PRIMARY KEY (hostescalation_contactgroup_id) ,
+ CONSTRAINT UQ_hostescalation_contactgroups UNIQUE (hostescalation_id,contactgroup_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostescalation_contacts
+--
+
+CREATE TABLE icinga_hostescalation_contacts (
+ hostescalation_contact_id bigserial,
+ instance_id bigint default 0,
+ hostescalation_id bigint default 0,
+ contact_object_id bigint default 0,
+ CONSTRAINT PK_hostescalation_contact_id PRIMARY KEY (hostescalation_contact_id) ,
+ CONSTRAINT UQ_hostescalation_contacts UNIQUE (instance_id,hostescalation_id,contact_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostgroups
+--
+
+CREATE TABLE icinga_hostgroups (
+ hostgroup_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ hostgroup_object_id bigint default 0,
+ alias TEXT default '',
+ notes TEXT default NULL,
+ notes_url TEXT default NULL,
+ action_url TEXT default NULL,
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_hostgroup_id PRIMARY KEY (hostgroup_id) ,
+ CONSTRAINT UQ_hostgroups UNIQUE (instance_id,hostgroup_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hostgroup_members
+--
+
+CREATE TABLE icinga_hostgroup_members (
+ hostgroup_member_id bigserial,
+ instance_id bigint default 0,
+ hostgroup_id bigint default 0,
+ host_object_id bigint default 0,
+ session_token INTEGER default NULL,
+ CONSTRAINT PK_hostgroup_member_id PRIMARY KEY (hostgroup_member_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hosts
+--
+
+CREATE TABLE icinga_hosts (
+ host_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ host_object_id bigint default 0,
+ alias TEXT default '',
+ display_name TEXT default '',
+ address TEXT default '',
+ address6 TEXT default '',
+ check_command_object_id bigint default 0,
+ check_command_args TEXT default '',
+ eventhandler_command_object_id bigint default 0,
+ eventhandler_command_args TEXT default '',
+ notification_timeperiod_object_id bigint default 0,
+ check_timeperiod_object_id bigint default 0,
+ failure_prediction_options TEXT default '',
+ check_interval double precision default 0,
+ retry_interval double precision default 0,
+ max_check_attempts INTEGER default 0,
+ first_notification_delay double precision default 0,
+ notification_interval double precision default 0,
+ notify_on_down INTEGER default 0,
+ notify_on_unreachable INTEGER default 0,
+ notify_on_recovery INTEGER default 0,
+ notify_on_flapping INTEGER default 0,
+ notify_on_downtime INTEGER default 0,
+ stalk_on_up INTEGER default 0,
+ stalk_on_down INTEGER default 0,
+ stalk_on_unreachable INTEGER default 0,
+ flap_detection_enabled INTEGER default 0,
+ flap_detection_on_up INTEGER default 0,
+ flap_detection_on_down INTEGER default 0,
+ flap_detection_on_unreachable INTEGER default 0,
+ low_flap_threshold double precision default 0,
+ high_flap_threshold double precision default 0,
+ process_performance_data INTEGER default 0,
+ freshness_checks_enabled INTEGER default 0,
+ freshness_threshold INTEGER default 0,
+ passive_checks_enabled INTEGER default 0,
+ event_handler_enabled INTEGER default 0,
+ active_checks_enabled INTEGER default 0,
+ retain_status_information INTEGER default 0,
+ retain_nonstatus_information INTEGER default 0,
+ notifications_enabled INTEGER default 0,
+ obsess_over_host INTEGER default 0,
+ failure_prediction_enabled INTEGER default 0,
+ notes TEXT default '',
+ notes_url TEXT default '',
+ action_url TEXT default '',
+ icon_image TEXT default '',
+ icon_image_alt TEXT default '',
+ vrml_image TEXT default '',
+ statusmap_image TEXT default '',
+ have_2d_coords INTEGER default 0,
+ x_2d INTEGER default 0,
+ y_2d INTEGER default 0,
+ have_3d_coords INTEGER default 0,
+ x_3d double precision default 0,
+ y_3d double precision default 0,
+ z_3d double precision default 0,
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_host_id PRIMARY KEY (host_id) ,
+ CONSTRAINT UQ_hosts UNIQUE (instance_id,config_type,host_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_hoststatus
+--
+
+CREATE TABLE icinga_hoststatus (
+ hoststatus_id bigserial,
+ instance_id bigint default 0,
+ host_object_id bigint default 0,
+ status_update_time timestamp,
+ output TEXT default '',
+ long_output TEXT default '',
+ perfdata TEXT default '',
+ check_source varchar(255) default '',
+ current_state INTEGER default 0,
+ has_been_checked INTEGER default 0,
+ should_be_scheduled INTEGER default 0,
+ current_check_attempt INTEGER default 0,
+ max_check_attempts INTEGER default 0,
+ last_check timestamp,
+ next_check timestamp,
+ check_type INTEGER default 0,
+ last_state_change timestamp,
+ last_hard_state_change timestamp,
+ last_hard_state INTEGER default 0,
+ last_time_up timestamp,
+ last_time_down timestamp,
+ last_time_unreachable timestamp,
+ state_type INTEGER default 0,
+ last_notification timestamp,
+ next_notification timestamp,
+ no_more_notifications INTEGER default 0,
+ notifications_enabled INTEGER default 0,
+ problem_has_been_acknowledged INTEGER default 0,
+ acknowledgement_type INTEGER default 0,
+ current_notification_number INTEGER default 0,
+ passive_checks_enabled INTEGER default 0,
+ active_checks_enabled INTEGER default 0,
+ event_handler_enabled INTEGER default 0,
+ flap_detection_enabled INTEGER default 0,
+ is_flapping INTEGER default 0,
+ percent_state_change double precision default 0,
+ latency double precision default 0,
+ execution_time double precision default 0,
+ scheduled_downtime_depth INTEGER default 0,
+ failure_prediction_enabled INTEGER default 0,
+ process_performance_data INTEGER default 0,
+ obsess_over_host INTEGER default 0,
+ modified_host_attributes INTEGER default 0,
+ original_attributes TEXT default NULL,
+ event_handler TEXT default '',
+ check_command TEXT default '',
+ normal_check_interval double precision default 0,
+ retry_check_interval double precision default 0,
+ check_timeperiod_object_id bigint default 0,
+ is_reachable INTEGER default 0,
+ CONSTRAINT PK_hoststatus_id PRIMARY KEY (hoststatus_id) ,
+ CONSTRAINT UQ_hoststatus UNIQUE (host_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_host_contactgroups
+--
+
+CREATE TABLE icinga_host_contactgroups (
+ host_contactgroup_id bigserial,
+ instance_id bigint default 0,
+ host_id bigint default 0,
+ contactgroup_object_id bigint default 0,
+ CONSTRAINT PK_host_contactgroup_id PRIMARY KEY (host_contactgroup_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_host_contacts
+--
+
+CREATE TABLE icinga_host_contacts (
+ host_contact_id bigserial,
+ instance_id bigint default 0,
+ host_id bigint default 0,
+ contact_object_id bigint default 0,
+ CONSTRAINT PK_host_contact_id PRIMARY KEY (host_contact_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_host_parenthosts
+--
+
+CREATE TABLE icinga_host_parenthosts (
+ host_parenthost_id bigserial,
+ instance_id bigint default 0,
+ host_id bigint default 0,
+ parent_host_object_id bigint default 0,
+ CONSTRAINT PK_host_parenthost_id PRIMARY KEY (host_parenthost_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_instances
+--
+
+CREATE TABLE icinga_instances (
+ instance_id bigserial,
+ instance_name TEXT default '',
+ instance_description TEXT default '',
+ CONSTRAINT PK_instance_id PRIMARY KEY (instance_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_logentries
+--
+
+CREATE TABLE icinga_logentries (
+ logentry_id bigserial,
+ instance_id bigint default 0,
+ logentry_time timestamp,
+ entry_time timestamp,
+ entry_time_usec INTEGER default 0,
+ logentry_type INTEGER default 0,
+ logentry_data TEXT default '',
+ realtime_data INTEGER default 0,
+ inferred_data_extracted INTEGER default 0,
+ object_id bigint default NULL,
+ CONSTRAINT PK_logentry_id PRIMARY KEY (logentry_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_notifications
+--
+
+CREATE TABLE icinga_notifications (
+ notification_id bigserial,
+ instance_id bigint default 0,
+ notification_type INTEGER default 0,
+ notification_reason INTEGER default 0,
+ object_id bigint default 0,
+ start_time timestamp,
+ start_time_usec INTEGER default 0,
+ end_time timestamp,
+ end_time_usec INTEGER default 0,
+ state INTEGER default 0,
+ output TEXT default '',
+ long_output TEXT default '',
+ escalated INTEGER default 0,
+ contacts_notified INTEGER default 0,
+ CONSTRAINT PK_notification_id PRIMARY KEY (notification_id) ,
+ CONSTRAINT UQ_notifications UNIQUE (instance_id,object_id,start_time,start_time_usec)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_objects
+--
+
+CREATE TABLE icinga_objects (
+ object_id bigserial,
+ instance_id bigint default 0,
+ objecttype_id bigint default 0,
+ name1 TEXT,
+ name2 TEXT,
+ is_active INTEGER default 0,
+ CONSTRAINT PK_object_id PRIMARY KEY (object_id)
+-- UNIQUE (objecttype_id,name1,name2)
+) ;
+CREATE INDEX icinga_objects_i ON icinga_objects(objecttype_id,name1,name2);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_processevents
+--
+
+CREATE TABLE icinga_processevents (
+ processevent_id bigserial,
+ instance_id bigint default 0,
+ event_type INTEGER default 0,
+ event_time timestamp,
+ event_time_usec INTEGER default 0,
+ process_id bigint default 0,
+ program_name TEXT default '',
+ program_version TEXT default '',
+ program_date TEXT default '',
+ CONSTRAINT PK_processevent_id PRIMARY KEY (processevent_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_programstatus
+--
+
+CREATE TABLE icinga_programstatus (
+ programstatus_id bigserial,
+ instance_id bigint default 0,
+ program_version TEXT default NULL,
+ status_update_time timestamp,
+ program_start_time timestamp,
+ program_end_time timestamp,
+ is_currently_running INTEGER default 0,
+ endpoint_name TEXT default '',
+ process_id bigint default 0,
+ daemon_mode INTEGER default 0,
+ last_command_check timestamp,
+ last_log_rotation timestamp,
+ notifications_enabled INTEGER default 0,
+ disable_notif_expire_time timestamp,
+ active_service_checks_enabled INTEGER default 0,
+ passive_service_checks_enabled INTEGER default 0,
+ active_host_checks_enabled INTEGER default 0,
+ passive_host_checks_enabled INTEGER default 0,
+ event_handlers_enabled INTEGER default 0,
+ flap_detection_enabled INTEGER default 0,
+ failure_prediction_enabled INTEGER default 0,
+ process_performance_data INTEGER default 0,
+ obsess_over_hosts INTEGER default 0,
+ obsess_over_services INTEGER default 0,
+ modified_host_attributes INTEGER default 0,
+ modified_service_attributes INTEGER default 0,
+ global_host_event_handler TEXT default '',
+ global_service_event_handler TEXT default '',
+ config_dump_in_progress INTEGER default 0,
+ CONSTRAINT PK_programstatus_id PRIMARY KEY (programstatus_id) ,
+ CONSTRAINT UQ_programstatus UNIQUE (instance_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_runtimevariables
+--
+
+CREATE TABLE icinga_runtimevariables (
+ runtimevariable_id bigserial,
+ instance_id bigint default 0,
+ varname TEXT default '',
+ varvalue TEXT default '',
+ CONSTRAINT PK_runtimevariable_id PRIMARY KEY (runtimevariable_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_scheduleddowntime
+--
+
+CREATE TABLE icinga_scheduleddowntime (
+ scheduleddowntime_id bigserial,
+ instance_id bigint default 0,
+ downtime_type INTEGER default 0,
+ object_id bigint default 0,
+ entry_time timestamp,
+ author_name TEXT default '',
+ comment_data TEXT default '',
+ internal_downtime_id bigint default 0,
+ triggered_by_id bigint default 0,
+ is_fixed INTEGER default 0,
+ duration BIGINT default 0,
+ scheduled_start_time timestamp,
+ scheduled_end_time timestamp,
+ was_started INTEGER default 0,
+ actual_start_time timestamp,
+ actual_start_time_usec INTEGER default 0,
+ is_in_effect INTEGER default 0,
+ trigger_time timestamp,
+ name TEXT default NULL,
+ session_token INTEGER default NULL,
+ CONSTRAINT PK_scheduleddowntime_id PRIMARY KEY (scheduleddowntime_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicechecks
+--
+
+CREATE TABLE icinga_servicechecks (
+ servicecheck_id bigserial,
+ instance_id bigint default 0,
+ service_object_id bigint default 0,
+ check_type INTEGER default 0,
+ current_check_attempt INTEGER default 0,
+ max_check_attempts INTEGER default 0,
+ state INTEGER default 0,
+ state_type INTEGER default 0,
+ start_time timestamp,
+ start_time_usec INTEGER default 0,
+ end_time timestamp,
+ end_time_usec INTEGER default 0,
+ command_object_id bigint default 0,
+ command_args TEXT default '',
+ command_line TEXT default '',
+ timeout INTEGER default 0,
+ early_timeout INTEGER default 0,
+ execution_time double precision default 0,
+ latency double precision default 0,
+ return_code INTEGER default 0,
+ output TEXT default '',
+ long_output TEXT default '',
+ perfdata TEXT default '',
+ CONSTRAINT PK_servicecheck_id PRIMARY KEY (servicecheck_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicedependencies
+--
+
+CREATE TABLE icinga_servicedependencies (
+ servicedependency_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ service_object_id bigint default 0,
+ dependent_service_object_id bigint default 0,
+ dependency_type INTEGER default 0,
+ inherits_parent INTEGER default 0,
+ timeperiod_object_id bigint default 0,
+ fail_on_ok INTEGER default 0,
+ fail_on_warning INTEGER default 0,
+ fail_on_unknown INTEGER default 0,
+ fail_on_critical INTEGER default 0,
+ CONSTRAINT PK_servicedependency_id PRIMARY KEY (servicedependency_id)
+) ;
+CREATE INDEX idx_servicedependencies ON icinga_servicedependencies(instance_id,config_type,service_object_id,dependent_service_object_id,dependency_type,inherits_parent,fail_on_ok,fail_on_warning,fail_on_unknown,fail_on_critical);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_serviceescalations
+--
+
+CREATE TABLE icinga_serviceescalations (
+ serviceescalation_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ service_object_id bigint default 0,
+ timeperiod_object_id bigint default 0,
+ first_notification INTEGER default 0,
+ last_notification INTEGER default 0,
+ notification_interval double precision default 0,
+ escalate_on_recovery INTEGER default 0,
+ escalate_on_warning INTEGER default 0,
+ escalate_on_unknown INTEGER default 0,
+ escalate_on_critical INTEGER default 0,
+ CONSTRAINT PK_serviceescalation_id PRIMARY KEY (serviceescalation_id) ,
+ CONSTRAINT UQ_serviceescalations UNIQUE (instance_id,config_type,service_object_id,timeperiod_object_id,first_notification,last_notification)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_serviceescalation_contactgroups
+--
+
+CREATE TABLE icinga_serviceescalation_contactgroups (
+ serviceescalation_contactgroup_id bigserial,
+ instance_id bigint default 0,
+ serviceescalation_id bigint default 0,
+ contactgroup_object_id bigint default 0,
+ CONSTRAINT PK_serviceescalation_contactgroup_id PRIMARY KEY (serviceescalation_contactgroup_id) ,
+ CONSTRAINT UQ_serviceescalation_contactgro UNIQUE (serviceescalation_id,contactgroup_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_serviceescalation_contacts
+--
+
+CREATE TABLE icinga_serviceescalation_contacts (
+ serviceescalation_contact_id bigserial,
+ instance_id bigint default 0,
+ serviceescalation_id bigint default 0,
+ contact_object_id bigint default 0,
+ CONSTRAINT PK_serviceescalation_contact_id PRIMARY KEY (serviceescalation_contact_id) ,
+ CONSTRAINT UQ_serviceescalation_contacts UNIQUE (instance_id,serviceescalation_id,contact_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicegroups
+--
+
+CREATE TABLE icinga_servicegroups (
+ servicegroup_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ servicegroup_object_id bigint default 0,
+ alias TEXT default '',
+ notes TEXT default NULL,
+ notes_url TEXT default NULL,
+ action_url TEXT default NULL,
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_servicegroup_id PRIMARY KEY (servicegroup_id) ,
+ CONSTRAINT UQ_servicegroups UNIQUE (instance_id,config_type,servicegroup_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicegroup_members
+--
+
+CREATE TABLE icinga_servicegroup_members (
+ servicegroup_member_id bigserial,
+ instance_id bigint default 0,
+ servicegroup_id bigint default 0,
+ service_object_id bigint default 0,
+ session_token INTEGER default NULL,
+ CONSTRAINT PK_servicegroup_member_id PRIMARY KEY (servicegroup_member_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_services
+--
+
+CREATE TABLE icinga_services (
+ service_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ host_object_id bigint default 0,
+ service_object_id bigint default 0,
+ display_name TEXT default '',
+ check_command_object_id bigint default 0,
+ check_command_args TEXT default '',
+ eventhandler_command_object_id bigint default 0,
+ eventhandler_command_args TEXT default '',
+ notification_timeperiod_object_id bigint default 0,
+ check_timeperiod_object_id bigint default 0,
+ failure_prediction_options TEXT default '',
+ check_interval double precision default 0,
+ retry_interval double precision default 0,
+ max_check_attempts INTEGER default 0,
+ first_notification_delay double precision default 0,
+ notification_interval double precision default 0,
+ notify_on_warning INTEGER default 0,
+ notify_on_unknown INTEGER default 0,
+ notify_on_critical INTEGER default 0,
+ notify_on_recovery INTEGER default 0,
+ notify_on_flapping INTEGER default 0,
+ notify_on_downtime INTEGER default 0,
+ stalk_on_ok INTEGER default 0,
+ stalk_on_warning INTEGER default 0,
+ stalk_on_unknown INTEGER default 0,
+ stalk_on_critical INTEGER default 0,
+ is_volatile INTEGER default 0,
+ flap_detection_enabled INTEGER default 0,
+ flap_detection_on_ok INTEGER default 0,
+ flap_detection_on_warning INTEGER default 0,
+ flap_detection_on_unknown INTEGER default 0,
+ flap_detection_on_critical INTEGER default 0,
+ low_flap_threshold double precision default 0,
+ high_flap_threshold double precision default 0,
+ process_performance_data INTEGER default 0,
+ freshness_checks_enabled INTEGER default 0,
+ freshness_threshold INTEGER default 0,
+ passive_checks_enabled INTEGER default 0,
+ event_handler_enabled INTEGER default 0,
+ active_checks_enabled INTEGER default 0,
+ retain_status_information INTEGER default 0,
+ retain_nonstatus_information INTEGER default 0,
+ notifications_enabled INTEGER default 0,
+ obsess_over_service INTEGER default 0,
+ failure_prediction_enabled INTEGER default 0,
+ notes TEXT default '',
+ notes_url TEXT default '',
+ action_url TEXT default '',
+ icon_image TEXT default '',
+ icon_image_alt TEXT default '',
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_service_id PRIMARY KEY (service_id) ,
+ CONSTRAINT UQ_services UNIQUE (instance_id,config_type,service_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_servicestatus
+--
+
+CREATE TABLE icinga_servicestatus (
+ servicestatus_id bigserial,
+ instance_id bigint default 0,
+ service_object_id bigint default 0,
+ status_update_time timestamp,
+ output TEXT default '',
+ long_output TEXT default '',
+ perfdata TEXT default '',
+ check_source varchar(255) default '',
+ current_state INTEGER default 0,
+ has_been_checked INTEGER default 0,
+ should_be_scheduled INTEGER default 0,
+ current_check_attempt INTEGER default 0,
+ max_check_attempts INTEGER default 0,
+ last_check timestamp,
+ next_check timestamp,
+ check_type INTEGER default 0,
+ last_state_change timestamp,
+ last_hard_state_change timestamp,
+ last_hard_state INTEGER default 0,
+ last_time_ok timestamp,
+ last_time_warning timestamp,
+ last_time_unknown timestamp,
+ last_time_critical timestamp,
+ state_type INTEGER default 0,
+ last_notification timestamp,
+ next_notification timestamp,
+ no_more_notifications INTEGER default 0,
+ notifications_enabled INTEGER default 0,
+ problem_has_been_acknowledged INTEGER default 0,
+ acknowledgement_type INTEGER default 0,
+ current_notification_number INTEGER default 0,
+ passive_checks_enabled INTEGER default 0,
+ active_checks_enabled INTEGER default 0,
+ event_handler_enabled INTEGER default 0,
+ flap_detection_enabled INTEGER default 0,
+ is_flapping INTEGER default 0,
+ percent_state_change double precision default 0,
+ latency double precision default 0,
+ execution_time double precision default 0,
+ scheduled_downtime_depth INTEGER default 0,
+ failure_prediction_enabled INTEGER default 0,
+ process_performance_data INTEGER default 0,
+ obsess_over_service INTEGER default 0,
+ modified_service_attributes INTEGER default 0,
+ original_attributes TEXT default NULL,
+ event_handler TEXT default '',
+ check_command TEXT default '',
+ normal_check_interval double precision default 0,
+ retry_check_interval double precision default 0,
+ check_timeperiod_object_id bigint default 0,
+ is_reachable INTEGER default 0,
+ CONSTRAINT PK_servicestatus_id PRIMARY KEY (servicestatus_id) ,
+ CONSTRAINT UQ_servicestatus UNIQUE (service_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_service_contactgroups
+--
+
+CREATE TABLE icinga_service_contactgroups (
+ service_contactgroup_id bigserial,
+ instance_id bigint default 0,
+ service_id bigint default 0,
+ contactgroup_object_id bigint default 0,
+ CONSTRAINT PK_service_contactgroup_id PRIMARY KEY (service_contactgroup_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_service_contacts
+--
+
+CREATE TABLE icinga_service_contacts (
+ service_contact_id bigserial,
+ instance_id bigint default 0,
+ service_id bigint default 0,
+ contact_object_id bigint default 0,
+ CONSTRAINT PK_service_contact_id PRIMARY KEY (service_contact_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_statehistory
+--
+
+CREATE TABLE icinga_statehistory (
+ statehistory_id bigserial,
+ instance_id bigint default 0,
+ state_time timestamp,
+ state_time_usec INTEGER default 0,
+ object_id bigint default 0,
+ state_change INTEGER default 0,
+ state INTEGER default 0,
+ state_type INTEGER default 0,
+ current_check_attempt INTEGER default 0,
+ max_check_attempts INTEGER default 0,
+ last_state INTEGER default '-1',
+ last_hard_state INTEGER default '-1',
+ output TEXT default '',
+ long_output TEXT default '',
+ check_source varchar(255) default '',
+ CONSTRAINT PK_statehistory_id PRIMARY KEY (statehistory_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_systemcommands
+--
+
+CREATE TABLE icinga_systemcommands (
+ systemcommand_id bigserial,
+ instance_id bigint default 0,
+ start_time timestamp,
+ start_time_usec INTEGER default 0,
+ end_time timestamp,
+ end_time_usec INTEGER default 0,
+ command_line TEXT default '',
+ timeout INTEGER default 0,
+ early_timeout INTEGER default 0,
+ execution_time double precision default 0,
+ return_code INTEGER default 0,
+ output TEXT default '',
+ long_output TEXT default '',
+ CONSTRAINT PK_systemcommand_id PRIMARY KEY (systemcommand_id) ,
+ CONSTRAINT UQ_systemcommands UNIQUE (instance_id,start_time,start_time_usec)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_timeperiods
+--
+
+CREATE TABLE icinga_timeperiods (
+ timeperiod_id bigserial,
+ instance_id bigint default 0,
+ config_type INTEGER default 0,
+ timeperiod_object_id bigint default 0,
+ alias TEXT default '',
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_timeperiod_id PRIMARY KEY (timeperiod_id) ,
+ CONSTRAINT UQ_timeperiods UNIQUE (instance_id,config_type,timeperiod_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_timeperiod_timeranges
+--
+
+CREATE TABLE icinga_timeperiod_timeranges (
+ timeperiod_timerange_id bigserial,
+ instance_id bigint default 0,
+ timeperiod_id bigint default 0,
+ day INTEGER default 0,
+ start_sec INTEGER default 0,
+ end_sec INTEGER default 0,
+ CONSTRAINT PK_timeperiod_timerange_id PRIMARY KEY (timeperiod_timerange_id)
+) ;
+
+
+-- --------------------------------------------------------
+-- Icinga 2 specific schema extensions
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_endpoints
+--
+
+CREATE TABLE icinga_endpoints (
+ endpoint_id bigserial,
+ instance_id bigint default 0,
+ endpoint_object_id bigint default 0,
+ zone_object_id bigint default 0,
+ config_type integer default 0,
+ identity text DEFAULT NULL,
+ node text DEFAULT NULL,
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_endpoint_id PRIMARY KEY (endpoint_id) ,
+ CONSTRAINT UQ_endpoints UNIQUE (instance_id,config_type,endpoint_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_endpointstatus
+--
+
+CREATE TABLE icinga_endpointstatus (
+ endpointstatus_id bigserial,
+ instance_id bigint default 0,
+ endpoint_object_id bigint default 0,
+ zone_object_id bigint default 0,
+ status_update_time timestamp,
+ identity text DEFAULT NULL,
+ node text DEFAULT NULL,
+ is_connected integer default 0,
+ CONSTRAINT PK_endpointstatus_id PRIMARY KEY (endpointstatus_id) ,
+ CONSTRAINT UQ_endpointstatus UNIQUE (endpoint_object_id)
+) ;
+
+--
+-- Table structure for table icinga_zones
+--
+
+CREATE TABLE icinga_zones (
+ zone_id bigserial,
+ instance_id bigint default 0,
+ zone_object_id bigint default 0,
+ parent_zone_object_id bigint default 0,
+ config_type integer default 0,
+ is_global integer default 0,
+ config_hash varchar(64) DEFAULT NULL,
+ CONSTRAINT PK_zone_id PRIMARY KEY (zone_id) ,
+ CONSTRAINT UQ_zones UNIQUE (instance_id,config_type,zone_object_id)
+) ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table icinga_zonestatus
+--
+
+CREATE TABLE icinga_zonestatus (
+ zonestatus_id bigserial,
+ instance_id bigint default 0,
+ zone_object_id bigint default 0,
+ parent_zone_object_id bigint default 0,
+ status_update_time timestamp,
+ CONSTRAINT PK_zonestatus_id PRIMARY KEY (zonestatus_id) ,
+ CONSTRAINT UQ_zonestatus UNIQUE (zone_object_id)
+) ;
+
+
+ALTER TABLE icinga_servicestatus ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_hoststatus ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_contactstatus ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_programstatus ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_comments ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_scheduleddowntime ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_runtimevariables ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_customvariablestatus ADD COLUMN endpoint_object_id bigint default NULL;
+
+ALTER TABLE icinga_acknowledgements ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_commenthistory ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_contactnotifications ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_downtimehistory ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_eventhandlers ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_externalcommands ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_flappinghistory ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_hostchecks ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_logentries ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_notifications ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_processevents ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_servicechecks ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_statehistory ADD COLUMN endpoint_object_id bigint default NULL;
+ALTER TABLE icinga_systemcommands ADD COLUMN endpoint_object_id bigint default NULL;
+
+
+-- -----------------------------------------
+-- add index (delete)
+-- -----------------------------------------
+
+-- for periodic delete
+-- instance_id and
+-- TIMEDEVENTS => scheduled_time
+-- SYSTEMCOMMANDS, SERVICECHECKS, HOSTCHECKS, EVENTHANDLERS => start_time
+-- EXTERNALCOMMANDS => entry_time
+
+-- instance_id
+CREATE INDEX systemcommands_i_id_idx on icinga_systemcommands(instance_id);
+CREATE INDEX servicechecks_i_id_idx on icinga_servicechecks(instance_id);
+CREATE INDEX hostchecks_i_id_idx on icinga_hostchecks(instance_id);
+CREATE INDEX eventhandlers_i_id_idx on icinga_eventhandlers(instance_id);
+CREATE INDEX externalcommands_i_id_idx on icinga_externalcommands(instance_id);
+
+-- time
+CREATE INDEX systemcommands_time_id_idx on icinga_systemcommands(start_time);
+CREATE INDEX servicechecks_time_id_idx on icinga_servicechecks(start_time);
+CREATE INDEX hostchecks_time_id_idx on icinga_hostchecks(start_time);
+CREATE INDEX eventhandlers_time_id_idx on icinga_eventhandlers(start_time);
+CREATE INDEX externalcommands_time_id_idx on icinga_externalcommands(entry_time);
+
+
+-- for starting cleanup - referenced in dbhandler.c:882
+-- instance_id only
+
+-- realtime data
+CREATE INDEX programstatus_i_id_idx on icinga_programstatus(instance_id);
+CREATE INDEX hoststatus_i_id_idx on icinga_hoststatus(instance_id);
+CREATE INDEX servicestatus_i_id_idx on icinga_servicestatus(instance_id);
+CREATE INDEX contactstatus_i_id_idx on icinga_contactstatus(instance_id);
+CREATE INDEX comments_i_id_idx on icinga_comments(instance_id);
+CREATE INDEX scheduleddowntime_i_id_idx on icinga_scheduleddowntime(instance_id);
+CREATE INDEX runtimevariables_i_id_idx on icinga_runtimevariables(instance_id);
+CREATE INDEX customvariablestatus_i_id_idx on icinga_customvariablestatus(instance_id);
+
+-- config data
+CREATE INDEX configfiles_i_id_idx on icinga_configfiles(instance_id);
+CREATE INDEX configfilevariables_i_id_idx on icinga_configfilevariables(instance_id);
+CREATE INDEX customvariables_i_id_idx on icinga_customvariables(instance_id);
+CREATE INDEX commands_i_id_idx on icinga_commands(instance_id);
+CREATE INDEX timeperiods_i_id_idx on icinga_timeperiods(instance_id);
+CREATE INDEX timeperiod_timeranges_i_id_idx on icinga_timeperiod_timeranges(instance_id);
+CREATE INDEX contactgroups_i_id_idx on icinga_contactgroups(instance_id);
+CREATE INDEX contactgroup_members_i_id_idx on icinga_contactgroup_members(instance_id);
+CREATE INDEX hostgroups_i_id_idx on icinga_hostgroups(instance_id);
+CREATE INDEX hostgroup_members_i_id_idx on icinga_hostgroup_members(instance_id);
+CREATE INDEX servicegroups_i_id_idx on icinga_servicegroups(instance_id);
+CREATE INDEX servicegroup_members_i_id_idx on icinga_servicegroup_members(instance_id);
+CREATE INDEX hostesc_i_id_idx on icinga_hostescalations(instance_id);
+CREATE INDEX hostesc_contacts_i_id_idx on icinga_hostescalation_contacts(instance_id);
+CREATE INDEX serviceesc_i_id_idx on icinga_serviceescalations(instance_id);
+CREATE INDEX serviceesc_contacts_i_id_idx on icinga_serviceescalation_contacts(instance_id);
+CREATE INDEX hostdependencies_i_id_idx on icinga_hostdependencies(instance_id);
+CREATE INDEX contacts_i_id_idx on icinga_contacts(instance_id);
+CREATE INDEX contact_addresses_i_id_idx on icinga_contact_addresses(instance_id);
+CREATE INDEX contact_notifcommands_i_id_idx on icinga_contact_notificationcommands(instance_id);
+CREATE INDEX hosts_i_id_idx on icinga_hosts(instance_id);
+CREATE INDEX host_parenthosts_i_id_idx on icinga_host_parenthosts(instance_id);
+CREATE INDEX host_contacts_i_id_idx on icinga_host_contacts(instance_id);
+CREATE INDEX services_i_id_idx on icinga_services(instance_id);
+CREATE INDEX service_contacts_i_id_idx on icinga_service_contacts(instance_id);
+CREATE INDEX service_contactgroups_i_id_idx on icinga_service_contactgroups(instance_id);
+CREATE INDEX host_contactgroups_i_id_idx on icinga_host_contactgroups(instance_id);
+CREATE INDEX hostesc_cgroups_i_id_idx on icinga_hostescalation_contactgroups(instance_id);
+CREATE INDEX serviceesc_cgroups_i_id_idx on icinga_serviceescalation_contactgroups(instance_id);
+
+-- -----------------------------------------
+-- more index stuff (WHERE clauses)
+-- -----------------------------------------
+
+-- hosts
+CREATE INDEX hosts_host_object_id_idx on icinga_hosts(host_object_id);
+
+-- hoststatus
+CREATE INDEX hoststatus_stat_upd_time_idx on icinga_hoststatus(status_update_time);
+CREATE INDEX hoststatus_current_state_idx on icinga_hoststatus(current_state);
+CREATE INDEX hoststatus_check_type_idx on icinga_hoststatus(check_type);
+CREATE INDEX hoststatus_state_type_idx on icinga_hoststatus(state_type);
+CREATE INDEX hoststatus_last_state_chg_idx on icinga_hoststatus(last_state_change);
+CREATE INDEX hoststatus_notif_enabled_idx on icinga_hoststatus(notifications_enabled);
+CREATE INDEX hoststatus_problem_ack_idx on icinga_hoststatus(problem_has_been_acknowledged);
+CREATE INDEX hoststatus_act_chks_en_idx on icinga_hoststatus(active_checks_enabled);
+CREATE INDEX hoststatus_pas_chks_en_idx on icinga_hoststatus(passive_checks_enabled);
+CREATE INDEX hoststatus_event_hdl_en_idx on icinga_hoststatus(event_handler_enabled);
+CREATE INDEX hoststatus_flap_det_en_idx on icinga_hoststatus(flap_detection_enabled);
+CREATE INDEX hoststatus_is_flapping_idx on icinga_hoststatus(is_flapping);
+CREATE INDEX hoststatus_p_state_chg_idx on icinga_hoststatus(percent_state_change);
+CREATE INDEX hoststatus_latency_idx on icinga_hoststatus(latency);
+CREATE INDEX hoststatus_ex_time_idx on icinga_hoststatus(execution_time);
+CREATE INDEX hoststatus_sch_downt_d_idx on icinga_hoststatus(scheduled_downtime_depth);
+
+-- services
+CREATE INDEX services_host_object_id_idx on icinga_services(host_object_id);
+
+--servicestatus
+CREATE INDEX srvcstatus_stat_upd_time_idx on icinga_servicestatus(status_update_time);
+CREATE INDEX srvcstatus_current_state_idx on icinga_servicestatus(current_state);
+CREATE INDEX srvcstatus_check_type_idx on icinga_servicestatus(check_type);
+CREATE INDEX srvcstatus_state_type_idx on icinga_servicestatus(state_type);
+CREATE INDEX srvcstatus_last_state_chg_idx on icinga_servicestatus(last_state_change);
+CREATE INDEX srvcstatus_notif_enabled_idx on icinga_servicestatus(notifications_enabled);
+CREATE INDEX srvcstatus_problem_ack_idx on icinga_servicestatus(problem_has_been_acknowledged);
+CREATE INDEX srvcstatus_act_chks_en_idx on icinga_servicestatus(active_checks_enabled);
+CREATE INDEX srvcstatus_pas_chks_en_idx on icinga_servicestatus(passive_checks_enabled);
+CREATE INDEX srvcstatus_event_hdl_en_idx on icinga_servicestatus(event_handler_enabled);
+CREATE INDEX srvcstatus_flap_det_en_idx on icinga_servicestatus(flap_detection_enabled);
+CREATE INDEX srvcstatus_is_flapping_idx on icinga_servicestatus(is_flapping);
+CREATE INDEX srvcstatus_p_state_chg_idx on icinga_servicestatus(percent_state_change);
+CREATE INDEX srvcstatus_latency_idx on icinga_servicestatus(latency);
+CREATE INDEX srvcstatus_ex_time_idx on icinga_servicestatus(execution_time);
+CREATE INDEX srvcstatus_sch_downt_d_idx on icinga_servicestatus(scheduled_downtime_depth);
+
+-- hostchecks
+CREATE INDEX hostchks_h_obj_id_idx on icinga_hostchecks(host_object_id);
+
+-- servicechecks
+CREATE INDEX servicechks_s_obj_id_idx on icinga_servicechecks(service_object_id);
+
+-- objects
+CREATE INDEX objects_objtype_id_idx ON icinga_objects(objecttype_id);
+CREATE INDEX objects_name1_idx ON icinga_objects(name1);
+CREATE INDEX objects_name2_idx ON icinga_objects(name2);
+CREATE INDEX objects_inst_id_idx ON icinga_objects(instance_id);
+
+-- instances
+-- CREATE INDEX instances_name_idx on icinga_instances(instance_name);
+
+-- logentries
+-- CREATE INDEX loge_instance_id_idx on icinga_logentries(instance_id);
+-- #236
+CREATE INDEX loge_time_idx on icinga_logentries(logentry_time);
+-- CREATE INDEX loge_data_idx on icinga_logentries(logentry_data);
+CREATE INDEX loge_inst_id_time_idx on icinga_logentries (instance_id, logentry_time);
+
+
+-- commenthistory
+-- CREATE INDEX c_hist_instance_id_idx on icinga_logentries(instance_id);
+-- CREATE INDEX c_hist_c_time_idx on icinga_logentries(comment_time);
+-- CREATE INDEX c_hist_i_c_id_idx on icinga_logentries(internal_comment_id);
+
+-- downtimehistory
+-- CREATE INDEX d_t_hist_nstance_id_idx on icinga_downtimehistory(instance_id);
+-- CREATE INDEX d_t_hist_type_idx on icinga_downtimehistory(downtime_type);
+-- CREATE INDEX d_t_hist_object_id_idx on icinga_downtimehistory(object_id);
+-- CREATE INDEX d_t_hist_entry_time_idx on icinga_downtimehistory(entry_time);
+-- CREATE INDEX d_t_hist_sched_start_idx on icinga_downtimehistory(scheduled_start_time);
+-- CREATE INDEX d_t_hist_sched_end_idx on icinga_downtimehistory(scheduled_end_time);
+
+-- scheduleddowntime
+-- CREATE INDEX sched_d_t_downtime_type_idx on icinga_scheduleddowntime(downtime_type);
+-- CREATE INDEX sched_d_t_object_id_idx on icinga_scheduleddowntime(object_id);
+-- CREATE INDEX sched_d_t_entry_time_idx on icinga_scheduleddowntime(entry_time);
+-- CREATE INDEX sched_d_t_start_time_idx on icinga_scheduleddowntime(scheduled_start_time);
+-- CREATE INDEX sched_d_t_end_time_idx on icinga_scheduleddowntime(scheduled_end_time);
+
+-- Icinga Web Notifications
+CREATE INDEX notification_idx ON icinga_notifications(notification_type, object_id, start_time);
+CREATE INDEX notification_object_id_idx ON icinga_notifications(object_id);
+CREATE INDEX contact_notification_idx ON icinga_contactnotifications(notification_id, contact_object_id);
+CREATE INDEX contacts_object_id_idx ON icinga_contacts(contact_object_id);
+CREATE INDEX contact_notif_meth_notif_idx ON icinga_contactnotificationmethods(contactnotification_id, command_object_id);
+CREATE INDEX command_object_idx ON icinga_commands(object_id);
+CREATE INDEX services_combined_object_idx ON icinga_services(service_object_id, host_object_id);
+
+-- statehistory
+CREATE INDEX statehist_i_id_o_id_s_ty_s_ti on icinga_statehistory(instance_id, object_id, state_type, state_time);
+--#2274
+create index statehist_state_idx on icinga_statehistory(object_id,state);
+
+-- #2618
+CREATE INDEX cntgrpmbrs_cgid_coid ON icinga_contactgroup_members (contactgroup_id,contact_object_id);
+CREATE INDEX hstgrpmbrs_hgid_hoid ON icinga_hostgroup_members (hostgroup_id,host_object_id);
+CREATE INDEX hstcntgrps_hid_cgoid ON icinga_host_contactgroups (host_id,contactgroup_object_id);
+CREATE INDEX hstprnthsts_hid_phoid ON icinga_host_parenthosts (host_id,parent_host_object_id);
+CREATE INDEX runtimevars_iid_varn ON icinga_runtimevariables (instance_id,varname);
+CREATE INDEX sgmbrs_sgid_soid ON icinga_servicegroup_members (servicegroup_id,service_object_id);
+CREATE INDEX scgrps_sid_cgoid ON icinga_service_contactgroups (service_id,contactgroup_object_id);
+CREATE INDEX tperiod_tid_d_ss_es ON icinga_timeperiod_timeranges (timeperiod_id,day,start_sec,end_sec);
+
+-- #3649
+CREATE INDEX sla_idx_sthist ON icinga_statehistory (object_id, state_time DESC);
+CREATE INDEX sla_idx_dohist ON icinga_downtimehistory (object_id, actual_start_time, actual_end_time);
+CREATE INDEX sla_idx_obj ON icinga_objects (objecttype_id, is_active, name1);
+
+-- #4985
+CREATE INDEX commenthistory_delete_idx ON icinga_commenthistory (instance_id, comment_time, internal_comment_id);
+
+-- #10070
+CREATE INDEX idx_comments_object_id on icinga_comments(object_id);
+CREATE INDEX idx_scheduleddowntime_object_id on icinga_scheduleddowntime(object_id);
+
+-- #10066
+CREATE INDEX idx_endpoints_object_id on icinga_endpoints(endpoint_object_id);
+CREATE INDEX idx_endpointstatus_object_id on icinga_endpointstatus(endpoint_object_id);
+
+CREATE INDEX idx_endpoints_zone_object_id on icinga_endpoints(zone_object_id);
+CREATE INDEX idx_endpointstatus_zone_object_id on icinga_endpointstatus(zone_object_id);
+
+CREATE INDEX idx_zones_object_id on icinga_zones(zone_object_id);
+CREATE INDEX idx_zonestatus_object_id on icinga_zonestatus(zone_object_id);
+
+CREATE INDEX idx_zones_parent_object_id on icinga_zones(parent_zone_object_id);
+CREATE INDEX idx_zonestatus_parent_object_id on icinga_zonestatus(parent_zone_object_id);
+
+-- #12210
+CREATE INDEX idx_comments_session_del ON icinga_comments (instance_id, session_token);
+CREATE INDEX idx_downtimes_session_del ON icinga_scheduleddowntime (instance_id, session_token);
+
+-- #12107
+CREATE INDEX idx_statehistory_cleanup on icinga_statehistory(instance_id, state_time);
+
+-- #12435
+CREATE INDEX idx_customvariables_object_id on icinga_customvariables(object_id);
+CREATE INDEX idx_contactgroup_members_object_id on icinga_contactgroup_members(contact_object_id);
+CREATE INDEX idx_hostgroup_members_object_id on icinga_hostgroup_members(host_object_id);
+CREATE INDEX idx_servicegroup_members_object_id on icinga_servicegroup_members(service_object_id);
+CREATE INDEX idx_servicedependencies_dependent_service_object_id on icinga_servicedependencies(dependent_service_object_id);
+CREATE INDEX idx_hostdependencies_dependent_host_object_id on icinga_hostdependencies(dependent_host_object_id);
+CREATE INDEX idx_service_contacts_service_id on icinga_service_contacts(service_id);
+CREATE INDEX idx_host_contacts_host_id on icinga_host_contacts(host_id);
+
+-- #5458
+CREATE INDEX idx_downtimehistory_remove ON icinga_downtimehistory (object_id, entry_time, scheduled_start_time, scheduled_end_time);
+CREATE INDEX idx_scheduleddowntime_remove ON icinga_scheduleddowntime (object_id, entry_time, scheduled_start_time, scheduled_end_time);
+
+-- #5492
+CREATE INDEX idx_commenthistory_remove ON icinga_commenthistory (object_id, entry_time);
+CREATE INDEX idx_comments_remove ON icinga_comments (object_id, entry_time);
+
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.14.3');
+
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.0.2.sql b/lib/db_ido_pgsql/schema/upgrade/2.0.2.sql
new file mode 100644
index 0000000..60710ef
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.0.2.sql
@@ -0,0 +1,17 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.0.2
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+UPDATE icinga_objects SET name2 = NULL WHERE name2 = '';
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.11.6');
+
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.1.0.sql b/lib/db_ido_pgsql/schema/upgrade/2.1.0.sql
new file mode 100644
index 0000000..a32ecea
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.1.0.sql
@@ -0,0 +1,17 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.1.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+ALTER TABLE icinga_programstatus ADD COLUMN endpoint_name TEXT default NULL;
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.11.7');
+
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.2.0.sql b/lib/db_ido_pgsql/schema/upgrade/2.2.0.sql
new file mode 100644
index 0000000..d105a34
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.2.0.sql
@@ -0,0 +1,21 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.2.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+ALTER TABLE icinga_programstatus ADD COLUMN program_version TEXT default NULL;
+
+ALTER TABLE icinga_customvariables ADD COLUMN is_json INTEGER default 0;
+ALTER TABLE icinga_customvariablestatus ADD COLUMN is_json INTEGER default 0;
+
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.12.0');
+
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.3.0.sql b/lib/db_ido_pgsql/schema/upgrade/2.3.0.sql
new file mode 100644
index 0000000..91764de
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.3.0.sql
@@ -0,0 +1,26 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.3.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+-- -----------------------------------------
+-- #7765 drop unique constraint
+-- -----------------------------------------
+
+ALTER TABLE icinga_servicedependencies DROP CONSTRAINT uq_servicedependencies;
+ALTER TABLE icinga_hostdependencies DROP CONSTRAINT uq_hostdependencies;
+
+CREATE INDEX idx_servicedependencies ON icinga_servicedependencies(instance_id,config_type,service_object_id,dependent_service_object_id,dependency_type,inherits_parent,fail_on_ok,fail_on_warning,fail_on_unknown,fail_on_critical);
+CREATE INDEX idx_hostdependencies ON icinga_hostdependencies(instance_id,config_type,host_object_id,dependent_host_object_id,dependency_type,inherits_parent,fail_on_up,fail_on_down,fail_on_unreachable);
+
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.13.0');
+
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.4.0.sql b/lib/db_ido_pgsql/schema/upgrade/2.4.0.sql
new file mode 100644
index 0000000..4a6e45e
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.4.0.sql
@@ -0,0 +1,185 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.4.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+-- -----------------------------------------
+-- #9027 Default timestamps lack time zone
+-- -----------------------------------------
+
+ALTER TABLE icinga_acknowledgements ALTER COLUMN entry_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_acknowledgements ALTER COLUMN end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_commenthistory ALTER COLUMN entry_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_commenthistory ALTER COLUMN comment_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_commenthistory ALTER COLUMN expiration_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_commenthistory ALTER COLUMN deletion_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_comments ALTER COLUMN entry_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_comments ALTER COLUMN comment_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_comments ALTER COLUMN expiration_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_conninfo ALTER COLUMN connect_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_conninfo ALTER COLUMN disconnect_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_conninfo ALTER COLUMN last_checkin_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_conninfo ALTER COLUMN data_start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_conninfo ALTER COLUMN data_end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_contactnotificationmethods ALTER COLUMN start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_contactnotificationmethods ALTER COLUMN end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_contactnotifications ALTER COLUMN start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_contactnotifications ALTER COLUMN end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_contactstatus ALTER COLUMN status_update_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_contactstatus ALTER COLUMN last_host_notification SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_contactstatus ALTER COLUMN last_service_notification SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_customvariablestatus ALTER COLUMN status_update_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_dbversion ALTER COLUMN create_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_dbversion ALTER COLUMN modify_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_downtimehistory ALTER COLUMN entry_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_downtimehistory ALTER COLUMN scheduled_start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_downtimehistory ALTER COLUMN scheduled_end_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_downtimehistory ALTER COLUMN actual_start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_downtimehistory ALTER COLUMN actual_end_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_downtimehistory ALTER COLUMN trigger_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_eventhandlers ALTER COLUMN start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_eventhandlers ALTER COLUMN end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_externalcommands ALTER COLUMN entry_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_flappinghistory ALTER COLUMN event_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_flappinghistory ALTER COLUMN comment_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_hostchecks ALTER COLUMN start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hostchecks ALTER COLUMN end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_hoststatus ALTER COLUMN status_update_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN last_check SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN next_check SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN last_state_change SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN last_hard_state_change SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN last_time_up SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN last_time_down SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN last_time_unreachable SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN last_notification SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_hoststatus ALTER COLUMN next_notification SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_logentries ALTER COLUMN logentry_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_logentries ALTER COLUMN entry_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_notifications ALTER COLUMN start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_notifications ALTER COLUMN end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_processevents ALTER COLUMN event_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_programstatus ALTER COLUMN status_update_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_programstatus ALTER COLUMN program_start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_programstatus ALTER COLUMN program_end_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_programstatus ALTER COLUMN last_command_check SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_programstatus ALTER COLUMN last_log_rotation SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_programstatus ALTER COLUMN disable_notif_expire_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_scheduleddowntime ALTER COLUMN entry_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_scheduleddowntime ALTER COLUMN scheduled_start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_scheduleddowntime ALTER COLUMN scheduled_end_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_scheduleddowntime ALTER COLUMN actual_start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_scheduleddowntime ALTER COLUMN trigger_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_servicechecks ALTER COLUMN start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicechecks ALTER COLUMN end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_servicestatus ALTER COLUMN status_update_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN last_check SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN next_check SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN last_state_change SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN last_hard_state_change SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN last_time_ok SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN last_time_warning SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN last_time_unknown SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN last_time_critical SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN last_notification SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_servicestatus ALTER COLUMN next_notification SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_statehistory ALTER COLUMN state_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_systemcommands ALTER COLUMN start_time SET DEFAULT '1970-01-01 00:00:00+00';
+ALTER TABLE icinga_systemcommands ALTER COLUMN end_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+ALTER TABLE icinga_endpointstatus ALTER COLUMN status_update_time SET DEFAULT '1970-01-01 00:00:00+00';
+
+-- -----------------------------------------
+-- #9455 check_source data type
+-- -----------------------------------------
+
+ALTER TABLE icinga_statehistory ALTER COLUMN check_source TYPE TEXT;
+ALTER TABLE icinga_statehistory ALTER COLUMN check_source SET default '';
+
+-- -----------------------------------------
+-- #9286 zones table
+-- -----------------------------------------
+
+ALTER TABLE icinga_endpoints ADD COLUMN zone_object_id bigint default 0;
+ALTER TABLE icinga_endpointstatus ADD COLUMN zone_object_id bigint default 0;
+
+CREATE TABLE icinga_zones (
+ zone_id bigserial,
+ instance_id bigint default 0,
+ zone_object_id bigint default 0,
+ parent_zone_object_id bigint default 0,
+ config_type integer default 0,
+ is_global integer default 0,
+ CONSTRAINT PK_zone_id PRIMARY KEY (zone_id) ,
+ CONSTRAINT UQ_zones UNIQUE (instance_id,config_type,zone_object_id)
+) ;
+
+CREATE TABLE icinga_zonestatus (
+ zonestatus_id bigserial,
+ instance_id bigint default 0,
+ zone_object_id bigint default 0,
+ parent_zone_object_id bigint default 0,
+ status_update_time timestamp with time zone default '1970-01-01 00:00:00+00',
+ CONSTRAINT PK_zonestatus_id PRIMARY KEY (zonestatus_id) ,
+ CONSTRAINT UQ_zonestatus UNIQUE (zone_object_id)
+) ;
+
+-- -----------------------------------------
+-- #10392 original attributes
+-- -----------------------------------------
+
+ALTER TABLE icinga_servicestatus ADD COLUMN original_attributes TEXT default NULL;
+ALTER TABLE icinga_hoststatus ADD COLUMN original_attributes TEXT default NULL;
+
+-- -----------------------------------------
+-- #10436 deleted custom vars
+-- -----------------------------------------
+
+ALTER TABLE icinga_customvariables ADD COLUMN session_token INTEGER default NULL;
+ALTER TABLE icinga_customvariablestatus ADD COLUMN session_token INTEGER default NULL;
+
+CREATE INDEX cv_session_del_idx ON icinga_customvariables (session_token);
+CREATE INDEX cvs_session_del_idx ON icinga_customvariablestatus (session_token);
+
+-- -----------------------------------------
+-- #10431 comment/downtime name
+-- -----------------------------------------
+
+ALTER TABLE icinga_comments ADD COLUMN name TEXT default NULL;
+ALTER TABLE icinga_commenthistory ADD COLUMN name TEXT default NULL;
+
+ALTER TABLE icinga_scheduleddowntime ADD COLUMN name TEXT default NULL;
+ALTER TABLE icinga_downtimehistory ADD COLUMN name TEXT default NULL;
+
+-- -----------------------------------------
+-- update dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.14.0');
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.5.0.sql b/lib/db_ido_pgsql/schema/upgrade/2.5.0.sql
new file mode 100644
index 0000000..063a812
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.5.0.sql
@@ -0,0 +1,85 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.5.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+-- -----------------------------------------
+-- #10069 IDO: check_source should not be a TEXT field
+-- -----------------------------------------
+
+ALTER TABLE icinga_hoststatus ALTER COLUMN check_source TYPE varchar(255);
+ALTER TABLE icinga_servicestatus ALTER COLUMN check_source TYPE varchar(255);
+ALTER TABLE icinga_statehistory ALTER COLUMN check_source TYPE varchar(255);
+
+-- -----------------------------------------
+-- #10070
+-- -----------------------------------------
+
+CREATE INDEX idx_comments_object_id on icinga_comments(object_id);
+CREATE INDEX idx_scheduleddowntime_object_id on icinga_scheduleddowntime(object_id);
+
+-- -----------------------------------------
+-- #10066
+-- -----------------------------------------
+
+CREATE INDEX idx_endpoints_object_id on icinga_endpoints(endpoint_object_id);
+CREATE INDEX idx_endpointstatus_object_id on icinga_endpointstatus(endpoint_object_id);
+
+CREATE INDEX idx_endpoints_zone_object_id on icinga_endpoints(zone_object_id);
+CREATE INDEX idx_endpointstatus_zone_object_id on icinga_endpointstatus(zone_object_id);
+
+CREATE INDEX idx_zones_object_id on icinga_zones(zone_object_id);
+CREATE INDEX idx_zonestatus_object_id on icinga_zonestatus(zone_object_id);
+
+CREATE INDEX idx_zones_parent_object_id on icinga_zones(parent_zone_object_id);
+CREATE INDEX idx_zonestatus_parent_object_id on icinga_zonestatus(parent_zone_object_id);
+
+-- -----------------------------------------
+-- #12258
+-- -----------------------------------------
+ALTER TABLE icinga_comments ADD COLUMN session_token INTEGER default NULL;
+ALTER TABLE icinga_scheduleddowntime ADD COLUMN session_token INTEGER default NULL;
+
+CREATE INDEX idx_comments_session_del ON icinga_comments (instance_id, session_token);
+CREATE INDEX idx_downtimes_session_del ON icinga_scheduleddowntime (instance_id, session_token);
+
+-- -----------------------------------------
+-- #12107
+-- -----------------------------------------
+CREATE INDEX idx_statehistory_cleanup on icinga_statehistory(instance_id, state_time);
+
+-- -----------------------------------------
+-- #12435
+-- -----------------------------------------
+ALTER TABLE icinga_commands ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_contactgroups ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_contacts ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_hostgroups ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_hosts ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_servicegroups ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_services ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_timeperiods ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_endpoints ADD config_hash VARCHAR(64) DEFAULT NULL;
+ALTER TABLE icinga_zones ADD config_hash VARCHAR(64) DEFAULT NULL;
+
+ALTER TABLE icinga_customvariables DROP session_token;
+ALTER TABLE icinga_customvariablestatus DROP session_token;
+
+CREATE INDEX idx_customvariables_object_id on icinga_customvariables(object_id);
+CREATE INDEX idx_contactgroup_members_object_id on icinga_contactgroup_members(contact_object_id);
+CREATE INDEX idx_hostgroup_members_object_id on icinga_hostgroup_members(host_object_id);
+CREATE INDEX idx_servicegroup_members_object_id on icinga_servicegroup_members(service_object_id);
+CREATE INDEX idx_servicedependencies_dependent_service_object_id on icinga_servicedependencies(dependent_service_object_id);
+CREATE INDEX idx_hostdependencies_dependent_host_object_id on icinga_hostdependencies(dependent_host_object_id);
+CREATE INDEX idx_service_contacts_service_id on icinga_service_contacts(service_id);
+CREATE INDEX idx_host_contacts_host_id on icinga_host_contacts(host_id);
+
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.14.1');
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.6.0.sql b/lib/db_ido_pgsql/schema/upgrade/2.6.0.sql
new file mode 100644
index 0000000..aa538a6
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.6.0.sql
@@ -0,0 +1,161 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.6.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+-- -----------------------------------------
+-- #13221 IDO: PostgreSQL: Don't use timestamp with timezone for unix timestamp columns
+-- -----------------------------------------
+
+DROP FUNCTION IF EXISTS from_unixtime(bigint);
+CREATE FUNCTION from_unixtime(bigint) RETURNS timestamp AS $$
+ SELECT to_timestamp($1) AT TIME ZONE 'UTC' AS result
+$$ LANGUAGE sql;
+
+DROP FUNCTION IF EXISTS unix_timestamp(timestamp WITH TIME ZONE);
+CREATE OR REPLACE FUNCTION unix_timestamp(timestamp) RETURNS bigint AS '
+ SELECT CAST(EXTRACT(EPOCH FROM $1) AS bigint) AS result;
+' LANGUAGE sql;
+
+ALTER TABLE icinga_acknowledgements
+ ALTER COLUMN entry_time DROP DEFAULT, ALTER COLUMN entry_time TYPE timestamp,
+ ALTER COLUMN end_time DROP DEFAULT, ALTER COLUMN end_time TYPE timestamp;
+
+ALTER TABLE icinga_commenthistory
+ ALTER COLUMN entry_time DROP DEFAULT, ALTER COLUMN entry_time TYPE timestamp,
+ ALTER COLUMN comment_time DROP DEFAULT, ALTER COLUMN comment_time TYPE timestamp,
+ ALTER COLUMN expiration_time DROP DEFAULT, ALTER COLUMN expiration_time TYPE timestamp,
+ ALTER COLUMN deletion_time DROP DEFAULT, ALTER COLUMN deletion_time TYPE timestamp;
+
+ALTER TABLE icinga_comments
+ ALTER COLUMN entry_time DROP DEFAULT, ALTER COLUMN entry_time TYPE timestamp,
+ ALTER COLUMN comment_time DROP DEFAULT, ALTER COLUMN comment_time TYPE timestamp,
+ ALTER COLUMN expiration_time DROP DEFAULT, ALTER COLUMN expiration_time TYPE timestamp;
+
+ALTER TABLE icinga_conninfo
+ ALTER COLUMN connect_time DROP DEFAULT, ALTER COLUMN connect_time TYPE timestamp,
+ ALTER COLUMN disconnect_time DROP DEFAULT, ALTER COLUMN disconnect_time TYPE timestamp,
+ ALTER COLUMN last_checkin_time DROP DEFAULT, ALTER COLUMN last_checkin_time TYPE timestamp,
+ ALTER COLUMN data_start_time DROP DEFAULT, ALTER COLUMN data_start_time TYPE timestamp,
+ ALTER COLUMN data_end_time DROP DEFAULT, ALTER COLUMN data_end_time TYPE timestamp;
+
+ALTER TABLE icinga_contactnotificationmethods
+ ALTER COLUMN start_time DROP DEFAULT, ALTER COLUMN start_time TYPE timestamp,
+ ALTER COLUMN end_time DROP DEFAULT, ALTER COLUMN end_time TYPE timestamp;
+
+ALTER TABLE icinga_contactnotifications
+ ALTER COLUMN start_time DROP DEFAULT, ALTER COLUMN start_time TYPE timestamp,
+ ALTER COLUMN end_time DROP DEFAULT, ALTER COLUMN end_time TYPE timestamp;
+
+ALTER TABLE icinga_contactstatus
+ ALTER COLUMN status_update_time DROP DEFAULT, ALTER COLUMN status_update_time TYPE timestamp,
+ ALTER COLUMN last_host_notification DROP DEFAULT, ALTER COLUMN last_host_notification TYPE timestamp,
+ ALTER COLUMN last_service_notification DROP DEFAULT, ALTER COLUMN last_service_notification TYPE timestamp;
+
+ALTER TABLE icinga_customvariablestatus
+ ALTER COLUMN status_update_time DROP DEFAULT, ALTER COLUMN status_update_time TYPE timestamp;
+
+ALTER TABLE icinga_dbversion
+ ALTER COLUMN create_time DROP DEFAULT, ALTER COLUMN create_time TYPE timestamp,
+ ALTER COLUMN modify_time DROP DEFAULT, ALTER COLUMN modify_time TYPE timestamp;
+
+ALTER TABLE icinga_downtimehistory
+ ALTER COLUMN entry_time DROP DEFAULT, ALTER COLUMN entry_time TYPE timestamp,
+ ALTER COLUMN scheduled_start_time DROP DEFAULT, ALTER COLUMN scheduled_start_time TYPE timestamp,
+ ALTER COLUMN scheduled_end_time DROP DEFAULT, ALTER COLUMN scheduled_end_time TYPE timestamp,
+ ALTER COLUMN actual_start_time DROP DEFAULT, ALTER COLUMN actual_start_time TYPE timestamp,
+ ALTER COLUMN actual_end_time DROP DEFAULT, ALTER COLUMN actual_end_time TYPE timestamp,
+ ALTER COLUMN trigger_time DROP DEFAULT, ALTER COLUMN trigger_time TYPE timestamp;
+
+ALTER TABLE icinga_eventhandlers
+ ALTER COLUMN start_time DROP DEFAULT, ALTER COLUMN start_time TYPE timestamp,
+ ALTER COLUMN end_time DROP DEFAULT, ALTER COLUMN end_time TYPE timestamp;
+
+ALTER TABLE icinga_externalcommands
+ ALTER COLUMN entry_time DROP DEFAULT, ALTER COLUMN entry_time TYPE timestamp;
+
+ALTER TABLE icinga_flappinghistory
+ ALTER COLUMN event_time DROP DEFAULT, ALTER COLUMN event_time TYPE timestamp,
+ ALTER COLUMN comment_time DROP DEFAULT, ALTER COLUMN comment_time TYPE timestamp;
+
+ALTER TABLE icinga_hostchecks
+ ALTER COLUMN start_time DROP DEFAULT, ALTER COLUMN start_time TYPE timestamp,
+ ALTER COLUMN end_time DROP DEFAULT, ALTER COLUMN end_time TYPE timestamp;
+
+ALTER TABLE icinga_hoststatus
+ ALTER COLUMN status_update_time DROP DEFAULT, ALTER COLUMN status_update_time TYPE timestamp,
+ ALTER COLUMN last_check DROP DEFAULT, ALTER COLUMN last_check TYPE timestamp,
+ ALTER COLUMN next_check DROP DEFAULT, ALTER COLUMN next_check TYPE timestamp,
+ ALTER COLUMN last_state_change DROP DEFAULT, ALTER COLUMN last_state_change TYPE timestamp,
+ ALTER COLUMN last_hard_state_change DROP DEFAULT, ALTER COLUMN last_hard_state_change TYPE timestamp,
+ ALTER COLUMN last_time_up DROP DEFAULT, ALTER COLUMN last_time_up TYPE timestamp,
+ ALTER COLUMN last_time_down DROP DEFAULT, ALTER COLUMN last_time_down TYPE timestamp,
+ ALTER COLUMN last_time_unreachable DROP DEFAULT, ALTER COLUMN last_time_unreachable TYPE timestamp,
+ ALTER COLUMN last_notification DROP DEFAULT, ALTER COLUMN last_notification TYPE timestamp,
+ ALTER COLUMN next_notification DROP DEFAULT, ALTER COLUMN next_notification TYPE timestamp;
+
+ALTER TABLE icinga_logentries
+ ALTER COLUMN logentry_time DROP DEFAULT, ALTER COLUMN logentry_time TYPE timestamp,
+ ALTER COLUMN entry_time DROP DEFAULT, ALTER COLUMN entry_time TYPE timestamp;
+
+ALTER TABLE icinga_notifications
+ ALTER COLUMN start_time DROP DEFAULT, ALTER COLUMN start_time TYPE timestamp,
+ ALTER COLUMN end_time DROP DEFAULT, ALTER COLUMN end_time TYPE timestamp;
+
+ALTER TABLE icinga_processevents
+ ALTER COLUMN event_time DROP DEFAULT, ALTER COLUMN event_time TYPE timestamp;
+
+ALTER TABLE icinga_programstatus
+ ALTER COLUMN status_update_time DROP DEFAULT, ALTER COLUMN status_update_time TYPE timestamp,
+ ALTER COLUMN program_start_time DROP DEFAULT, ALTER COLUMN program_start_time TYPE timestamp,
+ ALTER COLUMN program_end_time DROP DEFAULT, ALTER COLUMN program_end_time TYPE timestamp,
+ ALTER COLUMN last_command_check DROP DEFAULT, ALTER COLUMN last_command_check TYPE timestamp,
+ ALTER COLUMN last_log_rotation DROP DEFAULT, ALTER COLUMN last_log_rotation TYPE timestamp,
+ ALTER COLUMN disable_notif_expire_time DROP DEFAULT, ALTER COLUMN disable_notif_expire_time TYPE timestamp;
+
+ALTER TABLE icinga_scheduleddowntime
+ ALTER COLUMN entry_time DROP DEFAULT, ALTER COLUMN entry_time TYPE timestamp,
+ ALTER COLUMN scheduled_start_time DROP DEFAULT, ALTER COLUMN scheduled_start_time TYPE timestamp,
+ ALTER COLUMN scheduled_end_time DROP DEFAULT, ALTER COLUMN scheduled_end_time TYPE timestamp,
+ ALTER COLUMN actual_start_time DROP DEFAULT, ALTER COLUMN actual_start_time TYPE timestamp,
+ ALTER COLUMN trigger_time DROP DEFAULT, ALTER COLUMN trigger_time TYPE timestamp;
+
+ALTER TABLE icinga_servicechecks
+ ALTER COLUMN start_time DROP DEFAULT, ALTER COLUMN start_time TYPE timestamp,
+ ALTER COLUMN end_time DROP DEFAULT, ALTER COLUMN end_time TYPE timestamp;
+
+ALTER TABLE icinga_servicestatus
+ ALTER COLUMN status_update_time DROP DEFAULT, ALTER COLUMN status_update_time TYPE timestamp,
+ ALTER COLUMN last_check DROP DEFAULT, ALTER COLUMN last_check TYPE timestamp,
+ ALTER COLUMN next_check DROP DEFAULT, ALTER COLUMN next_check TYPE timestamp,
+ ALTER COLUMN last_state_change DROP DEFAULT, ALTER COLUMN last_state_change TYPE timestamp,
+ ALTER COLUMN last_hard_state_change DROP DEFAULT, ALTER COLUMN last_hard_state_change TYPE timestamp,
+ ALTER COLUMN last_time_ok DROP DEFAULT, ALTER COLUMN last_time_ok TYPE timestamp,
+ ALTER COLUMN last_time_warning DROP DEFAULT, ALTER COLUMN last_time_warning TYPE timestamp,
+ ALTER COLUMN last_time_unknown DROP DEFAULT, ALTER COLUMN last_time_unknown TYPE timestamp,
+ ALTER COLUMN last_time_critical DROP DEFAULT, ALTER COLUMN last_time_critical TYPE timestamp,
+ ALTER COLUMN last_notification DROP DEFAULT, ALTER COLUMN last_notification TYPE timestamp,
+ ALTER COLUMN next_notification DROP DEFAULT, ALTER COLUMN next_notification TYPE timestamp;
+
+ALTER TABLE icinga_statehistory
+ ALTER COLUMN state_time DROP DEFAULT, ALTER COLUMN state_time TYPE timestamp;
+
+ALTER TABLE icinga_systemcommands
+ ALTER COLUMN start_time DROP DEFAULT, ALTER COLUMN start_time TYPE timestamp,
+ ALTER COLUMN end_time DROP DEFAULT, ALTER COLUMN end_time TYPE timestamp;
+
+ALTER TABLE icinga_endpointstatus
+ ALTER COLUMN status_update_time DROP DEFAULT, ALTER COLUMN status_update_time TYPE timestamp;
+
+ALTER TABLE icinga_zonestatus
+ ALTER COLUMN status_update_time DROP DEFAULT, ALTER COLUMN status_update_time TYPE timestamp;
+
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.14.2');
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.8.0.sql b/lib/db_ido_pgsql/schema/upgrade/2.8.0.sql
new file mode 100644
index 0000000..31ab324
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.8.0.sql
@@ -0,0 +1,32 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.8.0
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+ALTER TABLE icinga_downtimehistory DROP CONSTRAINT IF EXISTS UQ_downtimehistory;
+ALTER TABLE icinga_scheduleddowntime DROP CONSTRAINT IF EXISTS UQ_scheduleddowntime;
+ALTER TABLE icinga_commenthistory DROP CONSTRAINT IF EXISTS UQ_commenthistory;
+ALTER TABLE icinga_comments DROP CONSTRAINT IF EXISTS UQ_comments;
+
+-- -----------------------------------------
+-- #5458 IDO: Improve downtime removal/cancel
+-- -----------------------------------------
+
+CREATE INDEX idx_downtimehistory_remove ON icinga_downtimehistory (object_id, entry_time, scheduled_start_time, scheduled_end_time);
+CREATE INDEX idx_scheduleddowntime_remove ON icinga_scheduleddowntime (object_id, entry_time, scheduled_start_time, scheduled_end_time);
+
+-- -----------------------------------------
+-- #5492
+-- -----------------------------------------
+
+CREATE INDEX idx_commenthistory_remove ON icinga_commenthistory (object_id, entry_time);
+CREATE INDEX idx_comments_remove ON icinga_comments (object_id, entry_time);
+-- -----------------------------------------
+-- set dbversion
+-- -----------------------------------------
+
+SELECT updatedbversion('1.14.3');
diff --git a/lib/db_ido_pgsql/schema/upgrade/2.8.1.sql b/lib/db_ido_pgsql/schema/upgrade/2.8.1.sql
new file mode 100644
index 0000000..05202c0
--- /dev/null
+++ b/lib/db_ido_pgsql/schema/upgrade/2.8.1.sql
@@ -0,0 +1,19 @@
+-- -----------------------------------------
+-- upgrade path for Icinga 2.8.1 (fix for fresh 2.8.0 installation only)
+--
+-- -----------------------------------------
+-- Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+--
+-- Please check https://docs.icinga.com for upgrading information!
+-- -----------------------------------------
+
+ALTER TABLE icinga_downtimehistory DROP CONSTRAINT IF EXISTS UQ_downtimehistory;
+ALTER TABLE icinga_scheduleddowntime DROP CONSTRAINT IF EXISTS UQ_scheduleddowntime;
+ALTER TABLE icinga_commenthistory DROP CONSTRAINT IF EXISTS UQ_commenthistory;
+ALTER TABLE icinga_comments DROP CONSTRAINT IF EXISTS UQ_comments;
+
+-- -----------------------------------------
+-- set dbversion (same as 2.8.0)
+-- -----------------------------------------
+
+SELECT updatedbversion('1.14.3');
diff --git a/lib/icinga/CMakeLists.txt b/lib/icinga/CMakeLists.txt
new file mode 100644
index 0000000..62077bc
--- /dev/null
+++ b/lib/icinga/CMakeLists.txt
@@ -0,0 +1,76 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(checkable.ti checkable-ti.cpp checkable-ti.hpp)
+mkclass_target(checkcommand.ti checkcommand-ti.cpp checkcommand-ti.hpp)
+mkclass_target(checkresult.ti checkresult-ti.cpp checkresult-ti.hpp)
+mkclass_target(command.ti command-ti.cpp command-ti.hpp)
+mkclass_target(comment.ti comment-ti.cpp comment-ti.hpp)
+mkclass_target(dependency.ti dependency-ti.cpp dependency-ti.hpp)
+mkclass_target(downtime.ti downtime-ti.cpp downtime-ti.hpp)
+mkclass_target(eventcommand.ti eventcommand-ti.cpp eventcommand-ti.hpp)
+mkclass_target(hostgroup.ti hostgroup-ti.cpp hostgroup-ti.hpp)
+mkclass_target(host.ti host-ti.cpp host-ti.hpp)
+mkclass_target(icingaapplication.ti icingaapplication-ti.cpp icingaapplication-ti.hpp)
+mkclass_target(customvarobject.ti customvarobject-ti.cpp customvarobject-ti.hpp)
+mkclass_target(notificationcommand.ti notificationcommand-ti.cpp notificationcommand-ti.hpp)
+mkclass_target(notification.ti notification-ti.cpp notification-ti.hpp)
+mkclass_target(scheduleddowntime.ti scheduleddowntime-ti.cpp scheduleddowntime-ti.hpp)
+mkclass_target(servicegroup.ti servicegroup-ti.cpp servicegroup-ti.hpp)
+mkclass_target(service.ti service-ti.cpp service-ti.hpp)
+mkclass_target(timeperiod.ti timeperiod-ti.cpp timeperiod-ti.hpp)
+mkclass_target(usergroup.ti usergroup-ti.cpp usergroup-ti.hpp)
+mkclass_target(user.ti user-ti.cpp user-ti.hpp)
+
+mkembedconfig_target(icinga-itl.conf icinga-itl.cpp)
+
+set(icinga_SOURCES
+ i2-icinga.hpp icinga-itl.cpp
+ apiactions.cpp apiactions.hpp
+ apievents.cpp apievents.hpp
+ checkable.cpp checkable.hpp checkable-ti.hpp
+ checkable-check.cpp checkable-comment.cpp checkable-dependency.cpp
+ checkable-downtime.cpp checkable-event.cpp checkable-flapping.cpp
+ checkable-notification.cpp checkable-script.cpp
+ checkcommand.cpp checkcommand.hpp checkcommand-ti.hpp
+ checkresult.cpp checkresult.hpp checkresult-ti.hpp
+ cib.cpp cib.hpp
+ clusterevents.cpp clusterevents.hpp clusterevents-check.cpp
+ command.cpp command.hpp command-ti.hpp
+ comment.cpp comment.hpp comment-ti.hpp
+ compatutility.cpp compatutility.hpp
+ customvarobject.cpp customvarobject.hpp customvarobject-ti.hpp
+ dependency.cpp dependency.hpp dependency-ti.hpp dependency-apply.cpp
+ downtime.cpp downtime.hpp downtime-ti.hpp
+ envresolver.cpp envresolver.hpp
+ eventcommand.cpp eventcommand.hpp eventcommand-ti.hpp
+ externalcommandprocessor.cpp externalcommandprocessor.hpp
+ host.cpp host.hpp host-ti.hpp
+ hostgroup.cpp hostgroup.hpp hostgroup-ti.hpp
+ icingaapplication.cpp icingaapplication.hpp icingaapplication-ti.hpp
+ legacytimeperiod.cpp legacytimeperiod.hpp
+ macroprocessor.cpp macroprocessor.hpp
+ macroresolver.hpp
+ notification.cpp notification.hpp notification-ti.hpp notification-apply.cpp
+ notificationcommand.cpp notificationcommand.hpp notificationcommand-ti.hpp
+ objectutils.cpp objectutils.hpp
+ pluginutility.cpp pluginutility.hpp
+ scheduleddowntime.cpp scheduleddowntime.hpp scheduleddowntime-ti.hpp scheduleddowntime-apply.cpp
+ service.cpp service.hpp service-ti.hpp service-apply.cpp
+ servicegroup.cpp servicegroup.hpp servicegroup-ti.hpp
+ timeperiod.cpp timeperiod.hpp timeperiod-ti.hpp
+ user.cpp user.hpp user-ti.hpp
+ usergroup.cpp usergroup.hpp usergroup-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(icinga icinga icinga_SOURCES)
+endif()
+
+add_library(icinga OBJECT ${icinga_SOURCES})
+
+add_dependencies(icinga base config remote)
+
+set_target_properties (
+ icinga PROPERTIES
+ FOLDER Lib
+)
diff --git a/lib/icinga/apiactions.cpp b/lib/icinga/apiactions.cpp
new file mode 100644
index 0000000..885834e
--- /dev/null
+++ b/lib/icinga/apiactions.cpp
@@ -0,0 +1,962 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/apiactions.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/service.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/clusterevents.hpp"
+#include "remote/apiaction.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/pkiutility.hpp"
+#include "remote/httputility.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/defer.hpp"
+#include "remote/actionshandler.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+REGISTER_APIACTION(process_check_result, "Service;Host", &ApiActions::ProcessCheckResult);
+REGISTER_APIACTION(reschedule_check, "Service;Host", &ApiActions::RescheduleCheck);
+REGISTER_APIACTION(send_custom_notification, "Service;Host", &ApiActions::SendCustomNotification);
+REGISTER_APIACTION(delay_notification, "Service;Host", &ApiActions::DelayNotification);
+REGISTER_APIACTION(acknowledge_problem, "Service;Host", &ApiActions::AcknowledgeProblem);
+REGISTER_APIACTION(remove_acknowledgement, "Service;Host", &ApiActions::RemoveAcknowledgement);
+REGISTER_APIACTION(add_comment, "Service;Host", &ApiActions::AddComment);
+REGISTER_APIACTION(remove_comment, "Service;Host;Comment", &ApiActions::RemoveComment);
+REGISTER_APIACTION(schedule_downtime, "Service;Host", &ApiActions::ScheduleDowntime);
+REGISTER_APIACTION(remove_downtime, "Service;Host;Downtime", &ApiActions::RemoveDowntime);
+REGISTER_APIACTION(shutdown_process, "", &ApiActions::ShutdownProcess);
+REGISTER_APIACTION(restart_process, "", &ApiActions::RestartProcess);
+REGISTER_APIACTION(generate_ticket, "", &ApiActions::GenerateTicket);
+REGISTER_APIACTION(execute_command, "Service;Host", &ApiActions::ExecuteCommand);
+
+Dictionary::Ptr ApiActions::CreateResult(int code, const String& status,
+ const Dictionary::Ptr& additional)
+{
+ Dictionary::Ptr result = new Dictionary({
+ { "code", code },
+ { "status", status }
+ });
+
+ if (additional)
+ additional->CopyTo(result);
+
+ return result;
+}
+
+Dictionary::Ptr ApiActions::ProcessCheckResult(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ using Result = Checkable::ProcessingResult;
+
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404,
+ "Cannot process passive check result for non-existent object.");
+
+ if (!checkable->GetEnablePassiveChecks())
+ return ApiActions::CreateResult(403, "Passive checks are disabled for object '" + checkable->GetName() + "'.");
+
+ if (!checkable->IsReachable(DependencyCheckExecution))
+ return ApiActions::CreateResult(200, "Ignoring passive check result for unreachable object '" + checkable->GetName() + "'.");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ if (!params->Contains("exit_status"))
+ return ApiActions::CreateResult(400, "Parameter 'exit_status' is required.");
+
+ int exitStatus = HttpUtility::GetLastParameter(params, "exit_status");
+
+ ServiceState state;
+
+ if (!service) {
+ if (exitStatus == 0)
+ state = ServiceOK;
+ else if (exitStatus == 1)
+ state = ServiceCritical;
+ else
+ return ApiActions::CreateResult(400, "Invalid 'exit_status' for Host "
+ + checkable->GetName() + ".");
+ } else {
+ state = PluginUtility::ExitStatusToState(exitStatus);
+ }
+
+ if (!params->Contains("plugin_output"))
+ return ApiActions::CreateResult(400, "Parameter 'plugin_output' is required");
+
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetOutput(HttpUtility::GetLastParameter(params, "plugin_output"));
+ cr->SetState(state);
+
+ if (params->Contains("execution_start"))
+ cr->SetExecutionStart(HttpUtility::GetLastParameter(params, "execution_start"));
+
+ if (params->Contains("execution_end"))
+ cr->SetExecutionEnd(HttpUtility::GetLastParameter(params, "execution_end"));
+
+ cr->SetCheckSource(HttpUtility::GetLastParameter(params, "check_source"));
+ cr->SetSchedulingSource(HttpUtility::GetLastParameter(params, "scheduling_source"));
+
+ Value perfData = params->Get("performance_data");
+
+ /* Allow to pass a performance data string from Icinga Web 2 next to the new Array notation. */
+ if (perfData.IsString())
+ cr->SetPerformanceData(PluginUtility::SplitPerfdata(perfData));
+ else
+ cr->SetPerformanceData(perfData);
+
+ cr->SetCommand(params->Get("check_command"));
+
+ /* Mark this check result as passive. */
+ cr->SetActive(false);
+
+ /* Result TTL allows to overrule the next expected freshness check. */
+ if (params->Contains("ttl"))
+ cr->SetTtl(HttpUtility::GetLastParameter(params, "ttl"));
+
+ Result result = checkable->ProcessCheckResult(cr);
+ switch (result) {
+ case Result::Ok:
+ return ApiActions::CreateResult(200, "Successfully processed check result for object '" + checkable->GetName() + "'.");
+ case Result::NoCheckResult:
+ return ApiActions::CreateResult(400, "Could not process check result for object '" + checkable->GetName() + "' because no check result was passed.");
+ case Result::CheckableInactive:
+ return ApiActions::CreateResult(503, "Could not process check result for object '" + checkable->GetName() + "' because the object is inactive.");
+ case Result::NewerCheckResultPresent:
+ return ApiActions::CreateResult(409, "Newer check result already present. Check result for '" + checkable->GetName() + "' was discarded.");
+ }
+
+ return ApiActions::CreateResult(500, "Unexpected result (" + std::to_string(static_cast<int>(result)) + ") for object '" + checkable->GetName() + "'. Please submit a bug report at https://github.com/Icinga/icinga2");
+}
+
+Dictionary::Ptr ApiActions::RescheduleCheck(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot reschedule check for non-existent object.");
+
+ if (Convert::ToBool(HttpUtility::GetLastParameter(params, "force")))
+ checkable->SetForceNextCheck(true);
+
+ double nextCheck;
+ if (params->Contains("next_check"))
+ nextCheck = HttpUtility::GetLastParameter(params, "next_check");
+ else
+ nextCheck = Utility::GetTime();
+
+ checkable->SetNextCheck(nextCheck);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(checkable);
+
+ return ApiActions::CreateResult(200, "Successfully rescheduled check for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::SendCustomNotification(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot send notification for non-existent object.");
+
+ if (!params->Contains("author"))
+ return ApiActions::CreateResult(400, "Parameter 'author' is required.");
+
+ if (!params->Contains("comment"))
+ return ApiActions::CreateResult(400, "Parameter 'comment' is required.");
+
+ if (Convert::ToBool(HttpUtility::GetLastParameter(params, "force")))
+ checkable->SetForceNextNotification(true);
+
+ Checkable::OnNotificationsRequested(checkable, NotificationCustom, checkable->GetLastCheckResult(),
+ HttpUtility::GetLastParameter(params, "author"), HttpUtility::GetLastParameter(params, "comment"), nullptr);
+
+ return ApiActions::CreateResult(200, "Successfully sent custom notification for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::DelayNotification(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot delay notifications for non-existent object");
+
+ if (!params->Contains("timestamp"))
+ return ApiActions::CreateResult(400, "A timestamp is required to delay notifications");
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ notification->SetNextNotification(HttpUtility::GetLastParameter(params, "timestamp"));
+ }
+
+ return ApiActions::CreateResult(200, "Successfully delayed notifications for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::AcknowledgeProblem(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot acknowledge problem for non-existent object.");
+
+ if (!params->Contains("author") || !params->Contains("comment"))
+ return ApiActions::CreateResult(400, "Acknowledgements require author and comment.");
+
+ AcknowledgementType sticky = AcknowledgementNormal;
+ bool notify = false;
+ bool persistent = false;
+ double timestamp = 0.0;
+
+ if (params->Contains("sticky") && HttpUtility::GetLastParameter(params, "sticky"))
+ sticky = AcknowledgementSticky;
+ if (params->Contains("notify"))
+ notify = HttpUtility::GetLastParameter(params, "notify");
+ if (params->Contains("persistent"))
+ persistent = HttpUtility::GetLastParameter(params, "persistent");
+ if (params->Contains("expiry")) {
+ timestamp = HttpUtility::GetLastParameter(params, "expiry");
+
+ if (timestamp <= Utility::GetTime())
+ return ApiActions::CreateResult(409, "Acknowledgement 'expiry' timestamp must be in the future for object " + checkable->GetName());
+ } else
+ timestamp = 0;
+
+ ObjectLock oLock (checkable);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ if (!service) {
+ if (host->GetState() == HostUp)
+ return ApiActions::CreateResult(409, "Host " + checkable->GetName() + " is UP.");
+ } else {
+ if (service->GetState() == ServiceOK)
+ return ApiActions::CreateResult(409, "Service " + checkable->GetName() + " is OK.");
+ }
+
+ if (checkable->IsAcknowledged()) {
+ return ApiActions::CreateResult(409, (service ? "Service " : "Host ") + checkable->GetName() + " is already acknowledged.");
+ }
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ Comment::AddComment(checkable, CommentAcknowledgement, HttpUtility::GetLastParameter(params, "author"),
+ HttpUtility::GetLastParameter(params, "comment"), persistent, timestamp, sticky == AcknowledgementSticky);
+ checkable->AcknowledgeProblem(HttpUtility::GetLastParameter(params, "author"),
+ HttpUtility::GetLastParameter(params, "comment"), sticky, notify, persistent, Utility::GetTime(), timestamp);
+
+ return ApiActions::CreateResult(200, "Successfully acknowledged problem for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::RemoveAcknowledgement(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404,
+ "Cannot remove acknowledgement for non-existent checkable object "
+ + object->GetName() + ".");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ String removedBy (HttpUtility::GetLastParameter(params, "author"));
+
+ checkable->ClearAcknowledgement(removedBy);
+ checkable->RemoveAckComments(removedBy);
+
+ return ApiActions::CreateResult(200, "Successfully removed acknowledgement for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::AddComment(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot add comment for non-existent object");
+
+ if (!params->Contains("author") || !params->Contains("comment"))
+ return ApiActions::CreateResult(400, "Comments require author and comment.");
+
+ double timestamp = 0.0;
+
+ if (params->Contains("expiry")) {
+ timestamp = HttpUtility::GetLastParameter(params, "expiry");
+ }
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ String commentName = Comment::AddComment(checkable, CommentUser,
+ HttpUtility::GetLastParameter(params, "author"),
+ HttpUtility::GetLastParameter(params, "comment"), false, timestamp);
+
+ Comment::Ptr comment = Comment::GetByName(commentName);
+
+ Dictionary::Ptr additional = new Dictionary({
+ { "name", commentName },
+ { "legacy_id", comment->GetLegacyId() }
+ });
+
+ return ApiActions::CreateResult(200, "Successfully added comment '"
+ + commentName + "' for object '" + checkable->GetName()
+ + "'.", additional);
+}
+
+Dictionary::Ptr ApiActions::RemoveComment(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ auto author (HttpUtility::GetLastParameter(params, "author"));
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+
+ if (checkable) {
+ std::set<Comment::Ptr> comments = checkable->GetComments();
+
+ for (const Comment::Ptr& comment : comments) {
+ Comment::RemoveComment(comment->GetName(), true, author);
+ }
+
+ return ApiActions::CreateResult(200, "Successfully removed all comments for object '" + checkable->GetName() + "'.");
+ }
+
+ Comment::Ptr comment = static_pointer_cast<Comment>(object);
+
+ if (!comment)
+ return ApiActions::CreateResult(404, "Cannot remove non-existent comment object.");
+
+ String commentName = comment->GetName();
+
+ Comment::RemoveComment(commentName, true, author);
+
+ return ApiActions::CreateResult(200, "Successfully removed comment '" + commentName + "'.");
+}
+
+Dictionary::Ptr ApiActions::ScheduleDowntime(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Can't schedule downtime for non-existent object.");
+
+ if (!params->Contains("start_time") || !params->Contains("end_time") ||
+ !params->Contains("author") || !params->Contains("comment")) {
+
+ return ApiActions::CreateResult(400, "Options 'start_time', 'end_time', 'author' and 'comment' are required");
+ }
+
+ bool fixed = true;
+ if (params->Contains("fixed"))
+ fixed = HttpUtility::GetLastParameter(params, "fixed");
+
+ if (!fixed && !params->Contains("duration"))
+ return ApiActions::CreateResult(400, "Option 'duration' is required for flexible downtime");
+
+ double duration = 0.0;
+ if (params->Contains("duration"))
+ duration = HttpUtility::GetLastParameter(params, "duration");
+
+ String triggerName;
+ if (params->Contains("trigger_name"))
+ triggerName = HttpUtility::GetLastParameter(params, "trigger_name");
+
+ String author = HttpUtility::GetLastParameter(params, "author");
+ String comment = HttpUtility::GetLastParameter(params, "comment");
+ double startTime = HttpUtility::GetLastParameter(params, "start_time");
+ double endTime = HttpUtility::GetLastParameter(params, "end_time");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DowntimeChildOptions childOptions = DowntimeNoChildren;
+ if (params->Contains("child_options")) {
+ try {
+ childOptions = Downtime::ChildOptionsFromValue(HttpUtility::GetLastParameter(params, "child_options"));
+ } catch (const std::exception&) {
+ return ApiActions::CreateResult(400, "Option 'child_options' provided an invalid value.");
+ }
+ }
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ Downtime::Ptr downtime = Downtime::AddDowntime(checkable, author, comment, startTime, endTime,
+ fixed, triggerName, duration);
+ String downtimeName = downtime->GetName();
+
+ Dictionary::Ptr additional = new Dictionary({
+ { "name", downtimeName },
+ { "legacy_id", downtime->GetLegacyId() }
+ });
+
+ /* Schedule downtime for all services for the host type. */
+ bool allServices = false;
+
+ if (params->Contains("all_services"))
+ allServices = HttpUtility::GetLastParameter(params, "all_services");
+
+ if (allServices && !service) {
+ ArrayData serviceDowntimes;
+
+ for (const Service::Ptr& hostService : host->GetServices()) {
+ Log(LogNotice, "ApiActions")
+ << "Creating downtime for service " << hostService->GetName() << " on host " << host->GetName();
+
+ Downtime::Ptr serviceDowntime = Downtime::AddDowntime(hostService, author, comment, startTime, endTime,
+ fixed, triggerName, duration, String(), String(), downtimeName);
+ String serviceDowntimeName = serviceDowntime->GetName();
+
+ serviceDowntimes.push_back(new Dictionary({
+ { "name", serviceDowntimeName },
+ { "legacy_id", serviceDowntime->GetLegacyId() }
+ }));
+ }
+
+ additional->Set("service_downtimes", new Array(std::move(serviceDowntimes)));
+ }
+
+ /* Schedule downtime for all child objects. */
+ if (childOptions != DowntimeNoChildren) {
+ /* 'DowntimeTriggeredChildren' schedules child downtimes triggered by the parent downtime.
+ * 'DowntimeNonTriggeredChildren' schedules non-triggered downtimes for all children.
+ */
+ if (childOptions == DowntimeTriggeredChildren)
+ triggerName = downtimeName;
+
+ Log(LogNotice, "ApiActions")
+ << "Processing child options " << childOptions << " for downtime " << downtimeName;
+
+ ArrayData childDowntimes;
+
+ std::set<Checkable::Ptr> allChildren = checkable->GetAllChildren();
+ for (const Checkable::Ptr& child : allChildren) {
+ Host::Ptr childHost;
+ Service::Ptr childService;
+ tie(childHost, childService) = GetHostService(child);
+
+ if (allServices && childService &&
+ allChildren.find(static_pointer_cast<Checkable>(childHost)) != allChildren.end()) {
+ /* When scheduling downtimes for all service and all children, the current child is a service, and its
+ * host is also a child, skip it here. The downtime for this service will be scheduled below together
+ * with the downtimes of all services for that host. Scheduling it below ensures that the relation
+ * from the child service downtime to the child host downtime is set properly. */
+ continue;
+ }
+
+ Log(LogNotice, "ApiActions")
+ << "Scheduling downtime for child object " << child->GetName();
+
+ Downtime::Ptr childDowntime = Downtime::AddDowntime(child, author, comment, startTime, endTime,
+ fixed, triggerName, duration);
+ String childDowntimeName = childDowntime->GetName();
+
+ Log(LogNotice, "ApiActions")
+ << "Add child downtime '" << childDowntimeName << "'.";
+
+ Dictionary::Ptr childAdditional = new Dictionary({
+ { "name", childDowntimeName },
+ { "legacy_id", childDowntime->GetLegacyId() }
+ });
+
+ /* For a host, also schedule all service downtimes if requested. */
+ if (allServices && !childService) {
+ ArrayData childServiceDowntimes;
+
+ for (const Service::Ptr& childService : childHost->GetServices()) {
+ Log(LogNotice, "ApiActions")
+ << "Creating downtime for service " << childService->GetName() << " on child host " << childHost->GetName();
+
+ Downtime::Ptr serviceDowntime = Downtime::AddDowntime(childService, author, comment, startTime, endTime,
+ fixed, triggerName, duration, String(), String(), childDowntimeName);
+ String serviceDowntimeName = serviceDowntime->GetName();
+
+ childServiceDowntimes.push_back(new Dictionary({
+ { "name", serviceDowntimeName },
+ { "legacy_id", serviceDowntime->GetLegacyId() }
+ }));
+ }
+
+ childAdditional->Set("service_downtimes", new Array(std::move(childServiceDowntimes)));
+ }
+
+ childDowntimes.push_back(childAdditional);
+ }
+
+ additional->Set("child_downtimes", new Array(std::move(childDowntimes)));
+ }
+
+ return ApiActions::CreateResult(200, "Successfully scheduled downtime '" +
+ downtimeName + "' for object '" + checkable->GetName() + "'.", additional);
+}
+
+Dictionary::Ptr ApiActions::RemoveDowntime(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ auto author (HttpUtility::GetLastParameter(params, "author"));
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+
+ size_t childCount = 0;
+
+ if (checkable) {
+ std::set<Downtime::Ptr> downtimes = checkable->GetDowntimes();
+
+ for (const Downtime::Ptr& downtime : downtimes) {
+ childCount += downtime->GetChildren().size();
+
+ try {
+ Downtime::RemoveDowntime(downtime->GetName(), true, true, false, author);
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ApiActions") << error.what();
+
+ return ApiActions::CreateResult(400, error.what());
+ }
+ }
+
+ return ApiActions::CreateResult(200, "Successfully removed all downtimes for object '" +
+ checkable->GetName() + "' and " + std::to_string(childCount) + " child downtimes.");
+ }
+
+ Downtime::Ptr downtime = static_pointer_cast<Downtime>(object);
+
+ if (!downtime)
+ return ApiActions::CreateResult(404, "Cannot remove non-existent downtime object.");
+
+ childCount += downtime->GetChildren().size();
+
+ try {
+ String downtimeName = downtime->GetName();
+ Downtime::RemoveDowntime(downtimeName, true, true, false, author);
+
+ return ApiActions::CreateResult(200, "Successfully removed downtime '" + downtimeName +
+ "' and " + std::to_string(childCount) + " child downtimes.");
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ApiActions") << error.what();
+
+ return ApiActions::CreateResult(400, error.what());
+ }
+}
+
+Dictionary::Ptr ApiActions::ShutdownProcess(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Application::RequestShutdown();
+
+ return ApiActions::CreateResult(200, "Shutting down Icinga 2.");
+}
+
+Dictionary::Ptr ApiActions::RestartProcess(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Application::RequestRestart();
+
+ return ApiActions::CreateResult(200, "Restarting Icinga 2.");
+}
+
+Dictionary::Ptr ApiActions::GenerateTicket(const ConfigObject::Ptr&,
+ const Dictionary::Ptr& params)
+{
+ if (!params->Contains("cn"))
+ return ApiActions::CreateResult(400, "Option 'cn' is required");
+
+ String cn = HttpUtility::GetLastParameter(params, "cn");
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+ String salt = listener->GetTicketSalt();
+
+ if (salt.IsEmpty())
+ return ApiActions::CreateResult(500, "Ticket salt is not configured in ApiListener object");
+
+ String ticket = PBKDF2_SHA1(cn, salt, 50000);
+
+ Dictionary::Ptr additional = new Dictionary({
+ { "ticket", ticket }
+ });
+
+ return ApiActions::CreateResult(200, "Generated PKI ticket '" + ticket + "' for common name '"
+ + cn + "'.", additional);
+}
+
+Value ApiActions::GetSingleObjectByNameUsingPermissions(const String& type, const String& objectName, const ApiUser::Ptr& user)
+{
+ Dictionary::Ptr queryParams = new Dictionary();
+ queryParams->Set("type", type);
+ queryParams->Set(type.ToLower(), objectName);
+
+ QueryDescription qd;
+ qd.Types.insert(type);
+ qd.Permission = "objects/query/" + type;
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, queryParams, user);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ApiActions") << DiagnosticInformation(ex);
+ return nullptr;
+ }
+
+ if (objs.empty())
+ return nullptr;
+
+ return objs.at(0);
+};
+
+Dictionary::Ptr ApiActions::ExecuteCommand(const ConfigObject::Ptr& object, const Dictionary::Ptr& params)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("No ApiListener instance configured."));
+
+ /* Get command_type */
+ String command_type = "EventCommand";
+
+ if (params->Contains("command_type"))
+ command_type = HttpUtility::GetLastParameter(params, "command_type");
+
+ /* Validate command_type */
+ if (command_type != "EventCommand" && command_type != "CheckCommand" && command_type != "NotificationCommand")
+ return ApiActions::CreateResult(400, "Invalid command_type '" + command_type + "'.");
+
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Can't start a command execution for a non-existent object.");
+
+ /* Get TTL param */
+ if (!params->Contains("ttl"))
+ return ApiActions::CreateResult(400, "Parameter ttl is required.");
+
+ double ttl = HttpUtility::GetLastParameter(params, "ttl");
+
+ if (ttl <= 0)
+ return ApiActions::CreateResult(400, "Parameter ttl must be greater than 0.");
+
+ ObjectLock oLock (checkable);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ String endpoint = "$command_endpoint$";
+
+ if (params->Contains("endpoint"))
+ endpoint = HttpUtility::GetLastParameter(params, "endpoint");
+
+ MacroProcessor::ResolverList resolvers;
+ Value macros;
+
+ if (params->Contains("macros")) {
+ macros = HttpUtility::GetLastParameter(params, "macros");
+ if (macros.IsObjectType<Dictionary>()) {
+ resolvers.emplace_back("override", macros);
+ } else {
+ return ApiActions::CreateResult(400, "Parameter macros must be a dictionary.");
+ }
+ }
+
+ if (service)
+ resolvers.emplace_back("service", service);
+
+ resolvers.emplace_back("host", host);
+
+ String resolved_endpoint = MacroProcessor::ResolveMacros(
+ endpoint, resolvers, checkable->GetLastCheckResult(),
+ nullptr, MacroProcessor::EscapeCallback(), nullptr, false
+ );
+
+ if (!ActionsHandler::AuthenticatedApiUser)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Can't find API user."));
+
+ /* Get endpoint */
+ Endpoint::Ptr endpointPtr = GetSingleObjectByNameUsingPermissions(Endpoint::GetTypeName(), resolved_endpoint, ActionsHandler::AuthenticatedApiUser);
+
+ if (!endpointPtr)
+ return ApiActions::CreateResult(404, "Can't find a valid endpoint for '" + resolved_endpoint + "'.");
+
+ /* Return an error when
+ * the endpoint is different from the command endpoint of the checkable
+ * and the endpoint zone can't access the checkable.
+ * The endpoints are checked to allow for the case where command_endpoint is specified in the checkable
+ * but checkable is not actually present in the agent.
+ */
+ Zone::Ptr endpointZone = endpointPtr->GetZone();
+ Endpoint::Ptr commandEndpoint = checkable->GetCommandEndpoint();
+ if (endpointPtr != commandEndpoint && !endpointZone->CanAccessObject(checkable)) {
+ return ApiActions::CreateResult(
+ 409,
+ "Zone '" + endpointZone->GetName() + "' cannot access checkable '" + checkable->GetName() + "'."
+ );
+ }
+
+ /* Get command */
+ String command;
+
+ if (!params->Contains("command")) {
+ if (command_type == "CheckCommand" ) {
+ command = "$check_command$";
+ } else if (command_type == "EventCommand") {
+ command = "$event_command$";
+ } else if (command_type == "NotificationCommand") {
+ command = "$notification_command$";
+ }
+ } else {
+ command = HttpUtility::GetLastParameter(params, "command");
+ }
+
+ /* Resolve command macro */
+ String resolved_command = MacroProcessor::ResolveMacros(
+ command, resolvers, checkable->GetLastCheckResult(), nullptr,
+ MacroProcessor::EscapeCallback(), nullptr, false
+ );
+
+ CheckResult::Ptr cr = checkable->GetLastCheckResult();
+
+ if (!cr)
+ cr = new CheckResult();
+
+ /* Check if resolved_command exists and it is of type command_type */
+ Dictionary::Ptr execMacros = new Dictionary();
+
+ MacroResolver::OverrideMacros = macros;
+ Defer o ([]() {
+ MacroResolver::OverrideMacros = nullptr;
+ });
+
+ /* Create execution parameters */
+ Dictionary::Ptr execParams = new Dictionary();
+
+ if (command_type == "CheckCommand") {
+ CheckCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(CheckCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser);
+
+ if (!cmd)
+ return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'.");
+ else {
+ CheckCommand::ExecuteOverride = cmd;
+ Defer resetCheckCommandOverride([]() {
+ CheckCommand::ExecuteOverride = nullptr;
+ });
+ cmd->Execute(checkable, cr, execMacros, false);
+ }
+ } else if (command_type == "EventCommand") {
+ EventCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(EventCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser);
+
+ if (!cmd)
+ return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'.");
+ else {
+ EventCommand::ExecuteOverride = cmd;
+ Defer resetEventCommandOverride ([]() {
+ EventCommand::ExecuteOverride = nullptr;
+ });
+ cmd->Execute(checkable, execMacros, false);
+ }
+ } else if (command_type == "NotificationCommand") {
+ NotificationCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(NotificationCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser);
+
+ if (!cmd)
+ return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'.");
+ else {
+ /* Get user */
+ String user_string = "";
+
+ if (params->Contains("user"))
+ user_string = HttpUtility::GetLastParameter(params, "user");
+
+ /* Resolve user macro */
+ String resolved_user = MacroProcessor::ResolveMacros(
+ user_string, resolvers, checkable->GetLastCheckResult(), nullptr,
+ MacroProcessor::EscapeCallback(), nullptr, false
+ );
+
+ User::Ptr user = GetSingleObjectByNameUsingPermissions(User::GetTypeName(), resolved_user, ActionsHandler::AuthenticatedApiUser);
+
+ if (!user)
+ return ApiActions::CreateResult(404, "Can't find a valid user for '" + resolved_user + "'.");
+
+ execParams->Set("user", user->GetName());
+
+ /* Get notification */
+ String notification_string = "";
+
+ if (params->Contains("notification"))
+ notification_string = HttpUtility::GetLastParameter(params, "notification");
+
+ /* Resolve notification macro */
+ String resolved_notification = MacroProcessor::ResolveMacros(
+ notification_string, resolvers, checkable->GetLastCheckResult(), nullptr,
+ MacroProcessor::EscapeCallback(), nullptr, false
+ );
+
+ Notification::Ptr notification = GetSingleObjectByNameUsingPermissions(Notification::GetTypeName(), resolved_notification, ActionsHandler::AuthenticatedApiUser);
+
+ if (!notification)
+ return ApiActions::CreateResult(404, "Can't find a valid notification for '" + resolved_notification + "'.");
+
+ execParams->Set("notification", notification->GetName());
+
+ NotificationCommand::ExecuteOverride = cmd;
+ Defer resetNotificationCommandOverride ([]() {
+ NotificationCommand::ExecuteOverride = nullptr;
+ });
+
+ cmd->Execute(notification, user, cr, NotificationType::NotificationCustom,
+ ActionsHandler::AuthenticatedApiUser->GetName(), "", execMacros, false);
+ }
+ }
+
+ /* This generates a UUID */
+ String uuid = Utility::NewUniqueID();
+
+ /* Create the deadline */
+ double deadline = Utility::GetTime() + ttl;
+
+ /* Update executions */
+ Dictionary::Ptr pending_execution = new Dictionary();
+ pending_execution->Set("pending", true);
+ pending_execution->Set("deadline", deadline);
+ pending_execution->Set("endpoint", resolved_endpoint);
+ Dictionary::Ptr executions = checkable->GetExecutions();
+
+ if (!executions)
+ executions = new Dictionary();
+
+ executions->Set(uuid, pending_execution);
+ checkable->SetExecutions(executions);
+
+ /* Broadcast the update */
+ Dictionary::Ptr executionsToBroadcast = new Dictionary();
+ executionsToBroadcast->Set(uuid, pending_execution);
+ Dictionary::Ptr updateParams = new Dictionary();
+ updateParams->Set("host", host->GetName());
+
+ if (service)
+ updateParams->Set("service", service->GetShortName());
+
+ updateParams->Set("executions", executionsToBroadcast);
+
+ Dictionary::Ptr updateMessage = new Dictionary();
+ updateMessage->Set("jsonrpc", "2.0");
+ updateMessage->Set("method", "event::UpdateExecutions");
+ updateMessage->Set("params", updateParams);
+
+ MessageOrigin::Ptr origin = new MessageOrigin();
+ listener->RelayMessage(origin, checkable, updateMessage, true);
+
+ /* Populate execution parameters */
+ if (command_type == "CheckCommand")
+ execParams->Set("command_type", "check_command");
+ else if (command_type == "EventCommand")
+ execParams->Set("command_type", "event_command");
+ else if (command_type == "NotificationCommand")
+ execParams->Set("command_type", "notification_command");
+
+ execParams->Set("command", resolved_command);
+ execParams->Set("host", host->GetName());
+
+ if (service)
+ execParams->Set("service", service->GetShortName());
+
+ /*
+ * If the host/service object specifies the 'check_timeout' attribute,
+ * forward this to the remote endpoint to limit the command execution time.
+ */
+ if (!checkable->GetCheckTimeout().IsEmpty())
+ execParams->Set("check_timeout", checkable->GetCheckTimeout());
+
+ execParams->Set("source", uuid);
+ execParams->Set("deadline", deadline);
+ execParams->Set("macros", execMacros);
+ execParams->Set("endpoint", resolved_endpoint);
+
+ /* Execute command */
+ bool local = endpointPtr == Endpoint::GetLocalEndpoint();
+
+ if (local) {
+ ClusterEvents::ExecuteCommandAPIHandler(origin, execParams);
+ } else {
+ /* Check if the child endpoints have Icinga version >= 2.13 */
+ Zone::Ptr localZone = Zone::GetLocalZone();
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ /* Fetch immediate child zone members */
+ if (zone->GetParent() == localZone && zone->CanAccessObject(endpointPtr->GetZone())) {
+ std::set<Endpoint::Ptr> endpoints = zone->GetEndpoints();
+
+ for (const Endpoint::Ptr& childEndpoint : endpoints) {
+ if (!(childEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand)) {
+ /* Update execution */
+ double now = Utility::GetTime();
+ pending_execution->Set("exit", 126);
+ pending_execution->Set("output", "Endpoint '" + childEndpoint->GetName() + "' doesn't support executing arbitrary commands.");
+ pending_execution->Set("start", now);
+ pending_execution->Set("end", now);
+ pending_execution->Remove("pending");
+
+ listener->RelayMessage(origin, checkable, updateMessage, true);
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("checkable", checkable->GetName());
+ result->Set("execution", uuid);
+ return ApiActions::CreateResult(202, "Accepted", result);
+ }
+ }
+ }
+ }
+
+ Dictionary::Ptr execMessage = new Dictionary();
+ execMessage->Set("jsonrpc", "2.0");
+ execMessage->Set("method", "event::ExecuteCommand");
+ execMessage->Set("params", execParams);
+
+ listener->RelayMessage(origin, endpointPtr->GetZone(), execMessage, true);
+ }
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("checkable", checkable->GetName());
+ result->Set("execution", uuid);
+ return ApiActions::CreateResult(202, "Accepted", result);
+}
diff --git a/lib/icinga/apiactions.hpp b/lib/icinga/apiactions.hpp
new file mode 100644
index 0000000..b6ba835
--- /dev/null
+++ b/lib/icinga/apiactions.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIACTIONS_H
+#define APIACTIONS_H
+
+#include "icinga/i2-icinga.hpp"
+#include "base/configobject.hpp"
+#include "base/dictionary.hpp"
+#include "remote/apiuser.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+class ApiActions
+{
+public:
+ static Dictionary::Ptr ProcessCheckResult(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RescheduleCheck(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr SendCustomNotification(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr DelayNotification(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr AcknowledgeProblem(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RemoveAcknowledgement(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr AddComment(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RemoveComment(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr ScheduleDowntime(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RemoveDowntime(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr ShutdownProcess(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RestartProcess(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr GenerateTicket(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr ExecuteCommand(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+
+private:
+ static Dictionary::Ptr CreateResult(int code, const String& status, const Dictionary::Ptr& additional = nullptr);
+ static Value GetSingleObjectByNameUsingPermissions(const String& type, const String& value, const ApiUser::Ptr& user);
+};
+
+}
+
+#endif /* APIACTIONS_H */
diff --git a/lib/icinga/apievents.cpp b/lib/icinga/apievents.cpp
new file mode 100644
index 0000000..53008fd
--- /dev/null
+++ b/lib/icinga/apievents.cpp
@@ -0,0 +1,438 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/apievents.hpp"
+#include "icinga/service.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "base/initialize.hpp"
+#include "base/serializer.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE(&ApiEvents::StaticInitialize);
+
+void ApiEvents::StaticInitialize()
+{
+ Checkable::OnNewCheckResult.connect(&ApiEvents::CheckResultHandler);
+ Checkable::OnStateChange.connect(&ApiEvents::StateChangeHandler);
+ Checkable::OnNotificationSentToAllUsers.connect(&ApiEvents::NotificationSentToAllUsersHandler);
+
+ Checkable::OnFlappingChanged.connect(&ApiEvents::FlappingChangedHandler);
+
+ Checkable::OnAcknowledgementSet.connect(&ApiEvents::AcknowledgementSetHandler);
+ Checkable::OnAcknowledgementCleared.connect(&ApiEvents::AcknowledgementClearedHandler);
+
+ Comment::OnCommentAdded.connect(&ApiEvents::CommentAddedHandler);
+ Comment::OnCommentRemoved.connect(&ApiEvents::CommentRemovedHandler);
+
+ Downtime::OnDowntimeAdded.connect(&ApiEvents::DowntimeAddedHandler);
+ Downtime::OnDowntimeRemoved.connect(&ApiEvents::DowntimeRemovedHandler);
+ Downtime::OnDowntimeStarted.connect(&ApiEvents::DowntimeStartedHandler);
+ Downtime::OnDowntimeTriggered.connect(&ApiEvents::DowntimeTriggeredHandler);
+
+ ConfigObject::OnActiveChanged.connect(&ApiEvents::OnActiveChangedHandler);
+ ConfigObject::OnVersionChanged.connect(&ApiEvents::OnVersionChangedHandler);
+}
+
+void ApiEvents::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("CheckResult");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::CheckResult));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'CheckResult'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "CheckResult");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("check_result", Serialize(cr));
+
+ result->Set("downtime_depth", checkable->GetDowntimeDepth());
+ result->Set("acknowledgement", checkable->IsAcknowledged());
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("StateChange");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::StateChange));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'StateChange'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "StateChange");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("state", service ? static_cast<int>(service->GetState()) : static_cast<int>(host->GetState()));
+ result->Set("state_type", checkable->GetStateType());
+ result->Set("check_result", Serialize(cr));
+
+ result->Set("downtime_depth", checkable->GetDowntimeDepth());
+ result->Set("acknowledgement", checkable->IsAcknowledged());
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::NotificationSentToAllUsersHandler(const Notification::Ptr& notification,
+ const Checkable::Ptr& checkable, const std::set<User::Ptr>& users, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("Notification");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::Notification));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'Notification'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "Notification");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ NotificationCommand::Ptr command = notification->GetCommand();
+
+ if (command)
+ result->Set("command", command->GetName());
+
+ ArrayData userNames;
+
+ for (const User::Ptr& user : users) {
+ userNames.push_back(user->GetName());
+ }
+
+ result->Set("users", new Array(std::move(userNames)));
+ result->Set("notification_type", Notification::NotificationTypeToStringCompat(type)); //TODO: Change this to our own types.
+ result->Set("author", author);
+ result->Set("text", text);
+ result->Set("check_result", Serialize(cr));
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::FlappingChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("Flapping");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::Flapping));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'Flapping'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "Flapping");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("state", service ? static_cast<int>(service->GetState()) : static_cast<int>(host->GetState()));
+ result->Set("state_type", checkable->GetStateType());
+ result->Set("is_flapping", checkable->IsFlapping());
+ result->Set("flapping_current", checkable->GetFlappingCurrent());
+ result->Set("threshold_low", checkable->GetFlappingThresholdLow());
+ result->Set("threshold_high", checkable->GetFlappingThresholdHigh());
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::AcknowledgementSetHandler(const Checkable::Ptr& checkable,
+ const String& author, const String& comment, AcknowledgementType type,
+ bool notify, bool persistent, double, double expiry, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("AcknowledgementSet");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::AcknowledgementSet));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'AcknowledgementSet'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "AcknowledgementSet");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("state", service ? static_cast<int>(service->GetState()) : static_cast<int>(host->GetState()));
+ result->Set("state_type", checkable->GetStateType());
+
+ result->Set("author", author);
+ result->Set("comment", comment);
+ result->Set("acknowledgement_type", type);
+ result->Set("notify", notify);
+ result->Set("persistent", persistent);
+ result->Set("expiry", expiry);
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("AcknowledgementCleared");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::AcknowledgementCleared));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'AcknowledgementCleared'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "AcknowledgementCleared");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("state", service ? static_cast<int>(service->GetState()) : static_cast<int>(host->GetState()));
+ result->Set("state_type", checkable->GetStateType());
+ result->Set("acknowledgement_type", AcknowledgementNone);
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::CommentAddedHandler(const Comment::Ptr& comment)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("CommentAdded");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::CommentAdded));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'CommentAdded'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "CommentAdded" },
+ { "timestamp", Utility::GetTime() },
+ { "comment", Serialize(comment, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::CommentRemovedHandler(const Comment::Ptr& comment)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("CommentRemoved");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::CommentRemoved));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'CommentRemoved'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "CommentRemoved" },
+ { "timestamp", Utility::GetTime() },
+ { "comment", Serialize(comment, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::DowntimeAddedHandler(const Downtime::Ptr& downtime)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("DowntimeAdded");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::DowntimeAdded));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'DowntimeAdded'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "DowntimeAdded" },
+ { "timestamp", Utility::GetTime() },
+ { "downtime", Serialize(downtime, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::DowntimeRemovedHandler(const Downtime::Ptr& downtime)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("DowntimeRemoved");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::DowntimeRemoved));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'DowntimeRemoved'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "DowntimeRemoved" },
+ { "timestamp", Utility::GetTime() },
+ { "downtime", Serialize(downtime, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::DowntimeStartedHandler(const Downtime::Ptr& downtime)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("DowntimeStarted");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::DowntimeStarted));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'DowntimeStarted'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "DowntimeStarted" },
+ { "timestamp", Utility::GetTime() },
+ { "downtime", Serialize(downtime, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::DowntimeTriggeredHandler(const Downtime::Ptr& downtime)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("DowntimeTriggered");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::DowntimeTriggered));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'DowntimeTriggered'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "DowntimeTriggered" },
+ { "timestamp", Utility::GetTime() },
+ { "downtime", Serialize(downtime, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::OnActiveChangedHandler(const ConfigObject::Ptr& object, const Value&)
+{
+ if (object->IsActive()) {
+ ApiEvents::SendObjectChangeEvent(object, EventType::ObjectCreated, "ObjectCreated");
+ } else if (!object->IsActive() && !object->GetExtension("ConfigObjectDeleted").IsEmpty()) {
+ ApiEvents::SendObjectChangeEvent(object, EventType::ObjectDeleted, "ObjectDeleted");
+ }
+}
+
+void ApiEvents::OnVersionChangedHandler(const ConfigObject::Ptr& object, const Value&)
+{
+ ApiEvents::SendObjectChangeEvent(object, EventType::ObjectModified, "ObjectModified");
+}
+
+void ApiEvents::SendObjectChangeEvent(const ConfigObject::Ptr& object, const EventType& eventType, const String& eventQueue) {
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType(eventQueue);
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(eventType));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents") << "Processing event type '" + eventQueue + "'.";
+
+ Dictionary::Ptr result = new Dictionary ({
+ {"type", eventQueue},
+ {"timestamp", Utility::GetTime()},
+ {"object_type", object->GetReflectionType()->GetName()},
+ {"object_name", object->GetName()},
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
diff --git a/lib/icinga/apievents.hpp b/lib/icinga/apievents.hpp
new file mode 100644
index 0000000..07d5c60
--- /dev/null
+++ b/lib/icinga/apievents.hpp
@@ -0,0 +1,51 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIEVENTS_H
+#define APIEVENTS_H
+
+#include "remote/eventqueue.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+class ApiEvents
+{
+public:
+ static void StaticInitialize();
+
+ static void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin);
+ static void StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr& origin);
+
+
+ static void NotificationSentToAllUsersHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const std::set<User::Ptr>& users, NotificationType type, const CheckResult::Ptr& cr, const String& author,
+ const String& text, const MessageOrigin::Ptr& origin);
+
+ static void FlappingChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+
+ static void AcknowledgementSetHandler(const Checkable::Ptr& checkable,
+ const String& author, const String& comment, AcknowledgementType type,
+ bool notify, bool persistent, double, double expiry, const MessageOrigin::Ptr& origin);
+ static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double, const MessageOrigin::Ptr& origin);
+
+ static void CommentAddedHandler(const Comment::Ptr& comment);
+ static void CommentRemovedHandler(const Comment::Ptr& comment);
+
+ static void DowntimeAddedHandler(const Downtime::Ptr& downtime);
+ static void DowntimeRemovedHandler(const Downtime::Ptr& downtime);
+ static void DowntimeStartedHandler(const Downtime::Ptr& downtime);
+ static void DowntimeTriggeredHandler(const Downtime::Ptr& downtime);
+
+ static void OnActiveChangedHandler(const ConfigObject::Ptr& object, const Value&);
+ static void OnVersionChangedHandler(const ConfigObject::Ptr& object, const Value&);
+ static void SendObjectChangeEvent(const ConfigObject::Ptr& object, const EventType& eventType, const String& eventQueue);
+};
+
+}
+
+#endif /* APIEVENTS_H */
diff --git a/lib/icinga/checkable-check.cpp b/lib/icinga/checkable-check.cpp
new file mode 100644
index 0000000..efa9477
--- /dev/null
+++ b/lib/icinga/checkable-check.cpp
@@ -0,0 +1,709 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/service.hpp"
+#include "icinga/host.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/cib.hpp"
+#include "icinga/clusterevents.hpp"
+#include "remote/messageorigin.hpp"
+#include "remote/apilistener.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/context.hpp"
+
+using namespace icinga;
+
+boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, const MessageOrigin::Ptr&)> Checkable::OnNewCheckResult;
+boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, StateType, const MessageOrigin::Ptr&)> Checkable::OnStateChange;
+boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, std::set<Checkable::Ptr>, const MessageOrigin::Ptr&)> Checkable::OnReachabilityChanged;
+boost::signals2::signal<void (const Checkable::Ptr&, NotificationType, const CheckResult::Ptr&, const String&, const String&, const MessageOrigin::Ptr&)> Checkable::OnNotificationsRequested;
+boost::signals2::signal<void (const Checkable::Ptr&)> Checkable::OnNextCheckUpdated;
+
+Atomic<uint_fast64_t> Checkable::CurrentConcurrentChecks (0);
+
+std::mutex Checkable::m_StatsMutex;
+int Checkable::m_PendingChecks = 0;
+std::condition_variable Checkable::m_PendingChecksCV;
+
+CheckCommand::Ptr Checkable::GetCheckCommand() const
+{
+ return dynamic_pointer_cast<CheckCommand>(NavigateCheckCommandRaw());
+}
+
+TimePeriod::Ptr Checkable::GetCheckPeriod() const
+{
+ return TimePeriod::GetByName(GetCheckPeriodRaw());
+}
+
+void Checkable::SetSchedulingOffset(long offset)
+{
+ m_SchedulingOffset = offset;
+}
+
+long Checkable::GetSchedulingOffset()
+{
+ return m_SchedulingOffset;
+}
+
+void Checkable::UpdateNextCheck(const MessageOrigin::Ptr& origin)
+{
+ double interval;
+
+ if (GetStateType() == StateTypeSoft && GetLastCheckResult() != nullptr)
+ interval = GetRetryInterval();
+ else
+ interval = GetCheckInterval();
+
+ double now = Utility::GetTime();
+ double adj = 0;
+
+ if (interval > 1)
+ adj = fmod(now * 100 + GetSchedulingOffset(), interval * 100) / 100.0;
+
+ if (adj != 0.0)
+ adj = std::min(0.5 + fmod(GetSchedulingOffset(), interval * 5) / 100.0, adj);
+
+ double nextCheck = now - adj + interval;
+ double lastCheck = GetLastCheck();
+
+ Log(LogDebug, "Checkable")
+ << "Update checkable '" << GetName() << "' with check interval '" << GetCheckInterval()
+ << "' from last check time at " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", (lastCheck < 0 ? 0 : lastCheck))
+ << " (" << GetLastCheck() << ") to next check time at " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", nextCheck) << " (" << nextCheck << ").";
+
+ SetNextCheck(nextCheck, false, origin);
+}
+
+bool Checkable::HasBeenChecked() const
+{
+ return GetLastCheckResult() != nullptr;
+}
+
+double Checkable::GetLastCheck() const
+{
+ CheckResult::Ptr cr = GetLastCheckResult();
+ double schedule_end = -1;
+
+ if (cr)
+ schedule_end = cr->GetScheduleEnd();
+
+ return schedule_end;
+}
+
+Checkable::ProcessingResult Checkable::ProcessCheckResult(const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin)
+{
+ using Result = Checkable::ProcessingResult;
+
+ {
+ ObjectLock olock(this);
+ m_CheckRunning = false;
+ }
+
+ if (!cr)
+ return Result::NoCheckResult;
+
+ double now = Utility::GetTime();
+
+ if (cr->GetScheduleStart() == 0)
+ cr->SetScheduleStart(now);
+
+ if (cr->GetScheduleEnd() == 0)
+ cr->SetScheduleEnd(now);
+
+ if (cr->GetExecutionStart() == 0)
+ cr->SetExecutionStart(now);
+
+ if (cr->GetExecutionEnd() == 0)
+ cr->SetExecutionEnd(now);
+
+ if (!origin || origin->IsLocal())
+ cr->SetSchedulingSource(IcingaApplication::GetInstance()->GetNodeName());
+
+ Endpoint::Ptr command_endpoint = GetCommandEndpoint();
+
+ if (cr->GetCheckSource().IsEmpty()) {
+ if ((!origin || origin->IsLocal()))
+ cr->SetCheckSource(IcingaApplication::GetInstance()->GetNodeName());
+
+ /* override check source if command_endpoint was defined */
+ if (command_endpoint && !GetExtension("agent_check"))
+ cr->SetCheckSource(command_endpoint->GetName());
+ }
+
+ /* agent checks go through the api */
+ if (command_endpoint && GetExtension("agent_check")) {
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener) {
+ /* send message back to its origin */
+ Dictionary::Ptr message = ClusterEvents::MakeCheckResultMessage(this, cr);
+ listener->SyncSendMessage(command_endpoint, message);
+ }
+
+ return Result::Ok;
+
+ }
+
+ if (!IsActive())
+ return Result::CheckableInactive;
+
+ bool reachable = IsReachable();
+ bool notification_reachable = IsReachable(DependencyNotification);
+
+ ObjectLock olock(this);
+
+ CheckResult::Ptr old_cr = GetLastCheckResult();
+ ServiceState old_state = GetStateRaw();
+ StateType old_stateType = GetStateType();
+ long old_attempt = GetCheckAttempt();
+ bool recovery = false;
+
+ /* When we have an check result already (not after fresh start),
+ * prevent to accept old check results and allow overrides for
+ * CRs happened in the future.
+ */
+ if (old_cr) {
+ double currentCRTimestamp = old_cr->GetExecutionStart();
+ double newCRTimestamp = cr->GetExecutionStart();
+
+ /* Our current timestamp may be from the future (wrong server time adjusted again). Allow overrides here. */
+ if (currentCRTimestamp > now) {
+ /* our current CR is from the future, let the new CR override it. */
+ Log(LogDebug, "Checkable")
+ << std::fixed << std::setprecision(6) << "Processing check result for checkable '" << GetName() << "' from "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newCRTimestamp) << " (" << newCRTimestamp
+ << "). Overriding since ours is from the future at "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", currentCRTimestamp) << " (" << currentCRTimestamp << ").";
+ } else {
+ /* Current timestamp is from the past, but the new timestamp is even more in the past. Skip it. */
+ if (newCRTimestamp < currentCRTimestamp) {
+ Log(LogDebug, "Checkable")
+ << std::fixed << std::setprecision(6) << "Skipping check result for checkable '" << GetName() << "' from "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newCRTimestamp) << " (" << newCRTimestamp
+ << "). It is in the past compared to ours at "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", currentCRTimestamp) << " (" << currentCRTimestamp << ").";
+ return Result::NewerCheckResultPresent;
+ }
+ }
+ }
+
+ /* The ExecuteCheck function already sets the old state, but we need to do it again
+ * in case this was a passive check result. */
+ SetLastStateRaw(old_state);
+ SetLastStateType(old_stateType);
+ SetLastReachable(reachable);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(this);
+
+ CheckableType checkableType = CheckableHost;
+ if (service)
+ checkableType = CheckableService;
+
+ long attempt = 1;
+
+ std::set<Checkable::Ptr> children = GetChildren();
+
+ if (IsStateOK(cr->GetState())) {
+ SetStateType(StateTypeHard); // NOT-OK -> HARD OK
+
+ if (!IsStateOK(old_state))
+ recovery = true;
+
+ ResetNotificationNumbers();
+ SaveLastState(ServiceOK, cr->GetExecutionEnd());
+ } else {
+ /* OK -> NOT-OK change, first SOFT state. Reset attempt counter. */
+ if (IsStateOK(old_state)) {
+ SetStateType(StateTypeSoft);
+ attempt = 1;
+ }
+
+ /* SOFT state change, increase attempt counter. */
+ if (old_stateType == StateTypeSoft && !IsStateOK(old_state)) {
+ SetStateType(StateTypeSoft);
+ attempt = old_attempt + 1;
+ }
+
+ /* HARD state change (e.g. previously 2/3 and this next attempt). Reset attempt counter. */
+ if (attempt >= GetMaxCheckAttempts()) {
+ SetStateType(StateTypeHard);
+ attempt = 1;
+ }
+
+ if (!IsStateOK(cr->GetState())) {
+ SaveLastState(cr->GetState(), cr->GetExecutionEnd());
+ }
+ }
+
+ if (!reachable)
+ SetLastStateUnreachable(cr->GetExecutionEnd());
+
+ SetCheckAttempt(attempt);
+
+ ServiceState new_state = cr->GetState();
+ SetStateRaw(new_state);
+
+ bool stateChange;
+
+ /* Exception on state change calculation for hosts. */
+ if (checkableType == CheckableService)
+ stateChange = (old_state != new_state);
+ else
+ stateChange = (Host::CalculateState(old_state) != Host::CalculateState(new_state));
+
+ /* Store the current last state change for the next iteration. */
+ SetPreviousStateChange(GetLastStateChange());
+
+ if (stateChange) {
+ SetLastStateChange(cr->GetExecutionEnd());
+
+ /* remove acknowledgements */
+ if (GetAcknowledgement() == AcknowledgementNormal ||
+ (GetAcknowledgement() == AcknowledgementSticky && IsStateOK(new_state))) {
+ ClearAcknowledgement("");
+ }
+ }
+
+ bool remove_acknowledgement_comments = false;
+
+ if (GetAcknowledgement() == AcknowledgementNone)
+ remove_acknowledgement_comments = true;
+
+ bool hardChange = (GetStateType() == StateTypeHard && old_stateType == StateTypeSoft);
+
+ if (stateChange && old_stateType == StateTypeHard && GetStateType() == StateTypeHard)
+ hardChange = true;
+
+ bool is_volatile = GetVolatile();
+
+ if (hardChange || is_volatile) {
+ SetLastHardStateRaw(new_state);
+ SetLastHardStateChange(cr->GetExecutionEnd());
+ SetLastHardStatesRaw(GetLastHardStatesRaw() / 100u + new_state * 100u);
+ }
+
+ if (stateChange) {
+ SetLastSoftStatesRaw(GetLastSoftStatesRaw() / 100u + new_state * 100u);
+ }
+
+ cr->SetPreviousHardState(ServiceState(GetLastHardStatesRaw() % 100u));
+
+ if (!IsStateOK(new_state))
+ TriggerDowntimes(cr->GetExecutionEnd());
+
+ /* statistics for external tools */
+ Checkable::UpdateStatistics(cr, checkableType);
+
+ bool in_downtime = IsInDowntime();
+
+ bool send_notification = false;
+ bool suppress_notification = !notification_reachable || in_downtime || IsAcknowledged();
+
+ /* Send notifications whether when a hard state change occurred. */
+ if (hardChange && !(old_stateType == StateTypeSoft && IsStateOK(new_state)))
+ send_notification = true;
+ /* Or if the checkable is volatile and in a HARD state. */
+ else if (is_volatile && GetStateType() == StateTypeHard)
+ send_notification = true;
+
+ if (IsStateOK(old_state) && old_stateType == StateTypeSoft)
+ send_notification = false; /* Don't send notifications for SOFT-OK -> HARD-OK. */
+
+ if (is_volatile && IsStateOK(old_state) && IsStateOK(new_state))
+ send_notification = false; /* Don't send notifications for volatile OK -> OK changes. */
+
+ olock.Unlock();
+
+ if (remove_acknowledgement_comments)
+ RemoveAckComments(String(), cr->GetExecutionEnd());
+
+ Dictionary::Ptr vars_after = new Dictionary({
+ { "state", new_state },
+ { "state_type", GetStateType() },
+ { "attempt", GetCheckAttempt() },
+ { "reachable", reachable }
+ });
+
+ if (old_cr)
+ cr->SetVarsBefore(old_cr->GetVarsAfter());
+
+ cr->SetVarsAfter(vars_after);
+
+ olock.Lock();
+
+ if (service) {
+ SetLastCheckResult(cr);
+ } else {
+ bool wasProblem = GetProblem();
+
+ SetLastCheckResult(cr);
+
+ if (GetProblem() != wasProblem) {
+ auto services = host->GetServices();
+ olock.Unlock();
+ for (auto& service : services) {
+ Service::OnHostProblemChanged(service, cr, origin);
+ }
+ olock.Lock();
+ }
+ }
+
+ bool was_flapping = IsFlapping();
+
+ UpdateFlappingStatus(cr->GetState());
+
+ bool is_flapping = IsFlapping();
+
+ if (cr->GetActive()) {
+ UpdateNextCheck(origin);
+ } else {
+ /* Reschedule the next check for external passive check results. The side effect of
+ * this is that for as long as we receive results for a service we
+ * won't execute any active checks. */
+ double offset;
+ double ttl = cr->GetTtl();
+
+ if (ttl > 0)
+ offset = ttl;
+ else
+ offset = GetCheckInterval();
+
+ SetNextCheck(Utility::GetTime() + offset, false, origin);
+ }
+
+ olock.Unlock();
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "Checkable")
+ << "Flapping: Checkable " << GetName()
+ << " was: " << was_flapping
+ << " is: " << is_flapping
+ << " threshold low: " << GetFlappingThresholdLow()
+ << " threshold high: " << GetFlappingThresholdHigh()
+ << "% current: " << GetFlappingCurrent() << "%.";
+#endif /* I2_DEBUG */
+
+ if (recovery) {
+ for (auto& child : children) {
+ if (child->GetProblem() && child->GetEnableActiveChecks()) {
+ auto nextCheck (now + Utility::Random() % 60);
+
+ ObjectLock oLock (child);
+
+ if (nextCheck < child->GetNextCheck()) {
+ child->SetNextCheck(nextCheck);
+ }
+ }
+ }
+ }
+
+ if (stateChange) {
+ /* reschedule direct parents */
+ for (const Checkable::Ptr& parent : GetParents()) {
+ if (parent.get() == this)
+ continue;
+
+ if (!parent->GetEnableActiveChecks())
+ continue;
+
+ if (parent->GetNextCheck() >= now + parent->GetRetryInterval()) {
+ ObjectLock olock(parent);
+ parent->SetNextCheck(now);
+ }
+ }
+ }
+
+ OnNewCheckResult(this, cr, origin);
+
+ /* signal status updates to for example db_ido */
+ OnStateChanged(this);
+
+ String old_state_str = (service ? Service::StateToString(old_state) : Host::StateToString(Host::CalculateState(old_state)));
+ String new_state_str = (service ? Service::StateToString(new_state) : Host::StateToString(Host::CalculateState(new_state)));
+
+ /* Whether a hard state change or a volatile state change except OK -> OK happened. */
+ if (hardChange || (is_volatile && !(IsStateOK(old_state) && IsStateOK(new_state)))) {
+ OnStateChange(this, cr, StateTypeHard, origin);
+ Log(LogNotice, "Checkable")
+ << "State Change: Checkable '" << GetName() << "' hard state change from " << old_state_str << " to " << new_state_str << " detected." << (is_volatile ? " Checkable is volatile." : "");
+ }
+ /* Whether a state change happened or the state type is SOFT (must be logged too). */
+ else if (stateChange || GetStateType() == StateTypeSoft) {
+ OnStateChange(this, cr, StateTypeSoft, origin);
+ Log(LogNotice, "Checkable")
+ << "State Change: Checkable '" << GetName() << "' soft state change from " << old_state_str << " to " << new_state_str << " detected.";
+ }
+
+ if (GetStateType() == StateTypeSoft || hardChange || recovery ||
+ (is_volatile && !(IsStateOK(old_state) && IsStateOK(new_state))))
+ ExecuteEventHandler();
+
+ int suppressed_types = 0;
+
+ /* Flapping start/end notifications */
+ if (!was_flapping && is_flapping) {
+ /* FlappingStart notifications happen on state changes, not in downtimes */
+ if (!IsPaused()) {
+ if (in_downtime) {
+ suppressed_types |= NotificationFlappingStart;
+ } else {
+ OnNotificationsRequested(this, NotificationFlappingStart, cr, "", "", nullptr);
+ }
+ }
+
+ Log(LogNotice, "Checkable")
+ << "Flapping Start: Checkable '" << GetName() << "' started flapping (Current flapping value "
+ << GetFlappingCurrent() << "% > high threshold " << GetFlappingThresholdHigh() << "%).";
+
+ NotifyFlapping(origin);
+ } else if (was_flapping && !is_flapping) {
+ /* FlappingEnd notifications are independent from state changes, must not happen in downtine */
+ if (!IsPaused()) {
+ if (in_downtime) {
+ suppressed_types |= NotificationFlappingEnd;
+ } else {
+ OnNotificationsRequested(this, NotificationFlappingEnd, cr, "", "", nullptr);
+ }
+ }
+
+ Log(LogNotice, "Checkable")
+ << "Flapping Stop: Checkable '" << GetName() << "' stopped flapping (Current flapping value "
+ << GetFlappingCurrent() << "% < low threshold " << GetFlappingThresholdLow() << "%).";
+
+ NotifyFlapping(origin);
+ }
+
+ if (send_notification && !is_flapping) {
+ if (!IsPaused()) {
+ /* If there are still some pending suppressed state notification, keep the suppression until these are
+ * handled by Checkable::FireSuppressedNotifications().
+ */
+ bool pending = GetSuppressedNotifications() & (NotificationRecovery|NotificationProblem);
+
+ if (suppress_notification || pending) {
+ suppressed_types |= (recovery ? NotificationRecovery : NotificationProblem);
+ } else {
+ OnNotificationsRequested(this, recovery ? NotificationRecovery : NotificationProblem, cr, "", "", nullptr);
+ }
+ }
+ }
+
+ if (suppressed_types) {
+ /* If some notifications were suppressed, but just because of e.g. a downtime,
+ * stash them into a notification types bitmask for maybe re-sending later.
+ */
+
+ ObjectLock olock (this);
+ int suppressed_types_before (GetSuppressedNotifications());
+ int suppressed_types_after (suppressed_types_before | suppressed_types);
+
+ const int conflict = NotificationFlappingStart | NotificationFlappingEnd;
+ if ((suppressed_types_after & conflict) == conflict) {
+ /* Flapping start and end cancel out each other. */
+ suppressed_types_after &= ~conflict;
+ }
+
+ const int stateNotifications = NotificationRecovery | NotificationProblem;
+ if (!(suppressed_types_before & stateNotifications) && (suppressed_types & stateNotifications)) {
+ /* A state-related notification is suppressed for the first time, store the previous state. When
+ * notifications are no longer suppressed, this can be compared with the current state to determine
+ * if a notification must be sent. This is done differently compared to flapping notifications just above
+ * as for state notifications, problem and recovery don't always cancel each other. For example,
+ * WARNING -> OK -> CRITICAL generates both types once, but there should still be a notification.
+ */
+ SetStateBeforeSuppression(old_stateType == StateTypeHard ? old_state : ServiceOK);
+ }
+
+ if (suppressed_types_after != suppressed_types_before) {
+ SetSuppressedNotifications(suppressed_types_after);
+ }
+ }
+
+ /* update reachability for child objects */
+ if ((stateChange || hardChange) && !children.empty())
+ OnReachabilityChanged(this, cr, children, origin);
+
+ return Result::Ok;
+}
+
+void Checkable::ExecuteRemoteCheck(const Dictionary::Ptr& resolvedMacros)
+{
+ CONTEXT("Executing remote check for object '" << GetName() << "'");
+
+ double scheduled_start = GetNextCheck();
+ double before_check = Utility::GetTime();
+
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetScheduleStart(scheduled_start);
+ cr->SetExecutionStart(before_check);
+
+ GetCheckCommand()->Execute(this, cr, resolvedMacros, true);
+}
+
+void Checkable::ExecuteCheck()
+{
+ CONTEXT("Executing check for object '" << GetName() << "'");
+
+ /* keep track of scheduling info in case the check type doesn't provide its own information */
+ double scheduled_start = GetNextCheck();
+ double before_check = Utility::GetTime();
+
+ SetLastCheckStarted(Utility::GetTime());
+
+ /* This calls SetNextCheck() which updates the CheckerComponent's idle/pending
+ * queues and ensures that checks are not fired multiple times. ProcessCheckResult()
+ * is called too late. See #6421.
+ */
+ UpdateNextCheck();
+
+ bool reachable = IsReachable();
+
+ {
+ ObjectLock olock(this);
+
+ /* don't run another check if there is one pending */
+ if (m_CheckRunning)
+ return;
+
+ m_CheckRunning = true;
+
+ SetLastStateRaw(GetStateRaw());
+ SetLastStateType(GetLastStateType());
+ SetLastReachable(reachable);
+ }
+
+ CheckResult::Ptr cr = new CheckResult();
+
+ cr->SetScheduleStart(scheduled_start);
+ cr->SetExecutionStart(before_check);
+
+ Endpoint::Ptr endpoint = GetCommandEndpoint();
+ bool local = !endpoint || endpoint == Endpoint::GetLocalEndpoint();
+
+ if (local) {
+ GetCheckCommand()->Execute(this, cr, nullptr, false);
+ } else {
+ Dictionary::Ptr macros = new Dictionary();
+ GetCheckCommand()->Execute(this, cr, macros, false);
+
+ if (endpoint->GetConnected()) {
+ /* perform check on remote endpoint */
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ExecuteCommand");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(this);
+
+ Dictionary::Ptr params = new Dictionary();
+ message->Set("params", params);
+ params->Set("command_type", "check_command");
+ params->Set("command", GetCheckCommand()->GetName());
+ params->Set("host", host->GetName());
+
+ if (service)
+ params->Set("service", service->GetShortName());
+
+ /*
+ * If the host/service object specifies the 'check_timeout' attribute,
+ * forward this to the remote endpoint to limit the command execution time.
+ */
+ if (!GetCheckTimeout().IsEmpty())
+ params->Set("check_timeout", GetCheckTimeout());
+
+ params->Set("macros", macros);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener)
+ listener->SyncSendMessage(endpoint, message);
+
+ /* Re-schedule the check so we don't run it again until after we've received
+ * a check result from the remote instance. The check will be re-scheduled
+ * using the proper check interval once we've received a check result.
+ */
+ SetNextCheck(Utility::GetTime() + GetCheckCommand()->GetTimeout() + 30);
+
+ /*
+ * Let the user know that there was a problem with the check if
+ * 1) The endpoint is not syncing (replay log, etc.)
+ * 2) Outside of the cold startup window (5min)
+ */
+ } else if (!endpoint->GetSyncing() && Application::GetInstance()->GetStartTime() < Utility::GetTime() - 300) {
+ /* fail to perform check on unconnected endpoint */
+ cr->SetState(ServiceUnknown);
+
+ String output = "Remote Icinga instance '" + endpoint->GetName() + "' is not connected to ";
+
+ Endpoint::Ptr localEndpoint = Endpoint::GetLocalEndpoint();
+
+ if (localEndpoint)
+ output += "'" + localEndpoint->GetName() + "'";
+ else
+ output += "this instance";
+
+ cr->SetOutput(output);
+
+ ProcessCheckResult(cr);
+ }
+
+ {
+ ObjectLock olock(this);
+ m_CheckRunning = false;
+ }
+ }
+}
+
+void Checkable::UpdateStatistics(const CheckResult::Ptr& cr, CheckableType type)
+{
+ time_t ts = cr->GetScheduleEnd();
+
+ if (type == CheckableHost) {
+ if (cr->GetActive())
+ CIB::UpdateActiveHostChecksStatistics(ts, 1);
+ else
+ CIB::UpdatePassiveHostChecksStatistics(ts, 1);
+ } else if (type == CheckableService) {
+ if (cr->GetActive())
+ CIB::UpdateActiveServiceChecksStatistics(ts, 1);
+ else
+ CIB::UpdatePassiveServiceChecksStatistics(ts, 1);
+ } else {
+ Log(LogWarning, "Checkable", "Unknown checkable type for statistic update.");
+ }
+}
+
+void Checkable::IncreasePendingChecks()
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ m_PendingChecks++;
+}
+
+void Checkable::DecreasePendingChecks()
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ m_PendingChecks--;
+ m_PendingChecksCV.notify_one();
+}
+
+int Checkable::GetPendingChecks()
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ return m_PendingChecks;
+}
+
+void Checkable::AquirePendingCheckSlot(int maxPendingChecks)
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ while (m_PendingChecks >= maxPendingChecks)
+ m_PendingChecksCV.wait(lock);
+
+ m_PendingChecks++;
+}
diff --git a/lib/icinga/checkable-comment.cpp b/lib/icinga/checkable-comment.cpp
new file mode 100644
index 0000000..71cfac6
--- /dev/null
+++ b/lib/icinga/checkable-comment.cpp
@@ -0,0 +1,75 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "remote/configobjectutility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/timer.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include <utility>
+
+using namespace icinga;
+
+
+void Checkable::RemoveAllComments()
+{
+ for (const Comment::Ptr& comment : GetComments()) {
+ Comment::RemoveComment(comment->GetName());
+ }
+}
+
+void Checkable::RemoveAckComments(const String& removedBy, double createdBefore)
+{
+ for (const Comment::Ptr& comment : GetComments()) {
+ if (comment->GetEntryType() == CommentAcknowledgement) {
+ /* Do not remove persistent comments from an acknowledgement */
+ if (comment->GetPersistent()) {
+ continue;
+ }
+
+ if (comment->GetEntryTime() > createdBefore) {
+ continue;
+ }
+
+ {
+ ObjectLock oLock (comment);
+ comment->SetRemovedBy(removedBy);
+ }
+
+ Comment::RemoveComment(comment->GetName());
+ }
+ }
+}
+
+std::set<Comment::Ptr> Checkable::GetComments() const
+{
+ std::unique_lock<std::mutex> lock(m_CommentMutex);
+ return m_Comments;
+}
+
+Comment::Ptr Checkable::GetLastComment() const
+{
+ std::unique_lock<std::mutex> lock (m_CommentMutex);
+ Comment::Ptr lastComment;
+
+ for (auto& comment : m_Comments) {
+ if (!lastComment || comment->GetEntryTime() > lastComment->GetEntryTime()) {
+ lastComment = comment;
+ }
+ }
+
+ return lastComment;
+}
+
+void Checkable::RegisterComment(const Comment::Ptr& comment)
+{
+ std::unique_lock<std::mutex> lock(m_CommentMutex);
+ m_Comments.insert(comment);
+}
+
+void Checkable::UnregisterComment(const Comment::Ptr& comment)
+{
+ std::unique_lock<std::mutex> lock(m_CommentMutex);
+ m_Comments.erase(comment);
+}
diff --git a/lib/icinga/checkable-dependency.cpp b/lib/icinga/checkable-dependency.cpp
new file mode 100644
index 0000000..58d6b57
--- /dev/null
+++ b/lib/icinga/checkable-dependency.cpp
@@ -0,0 +1,176 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "icinga/dependency.hpp"
+#include "base/logger.hpp"
+#include <unordered_map>
+
+using namespace icinga;
+
+void Checkable::AddDependency(const Dependency::Ptr& dep)
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ m_Dependencies.insert(dep);
+}
+
+void Checkable::RemoveDependency(const Dependency::Ptr& dep)
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ m_Dependencies.erase(dep);
+}
+
+std::vector<Dependency::Ptr> Checkable::GetDependencies() const
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ return std::vector<Dependency::Ptr>(m_Dependencies.begin(), m_Dependencies.end());
+}
+
+void Checkable::AddReverseDependency(const Dependency::Ptr& dep)
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ m_ReverseDependencies.insert(dep);
+}
+
+void Checkable::RemoveReverseDependency(const Dependency::Ptr& dep)
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ m_ReverseDependencies.erase(dep);
+}
+
+std::vector<Dependency::Ptr> Checkable::GetReverseDependencies() const
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ return std::vector<Dependency::Ptr>(m_ReverseDependencies.begin(), m_ReverseDependencies.end());
+}
+
+bool Checkable::IsReachable(DependencyType dt, Dependency::Ptr *failedDependency, int rstack) const
+{
+ /* Anything greater than 256 causes recursion bus errors. */
+ int limit = 256;
+
+ if (rstack > limit) {
+ Log(LogWarning, "Checkable")
+ << "Too many nested dependencies (>" << limit << ") for checkable '" << GetName() << "': Dependency failed.";
+
+ return false;
+ }
+
+ for (const Checkable::Ptr& checkable : GetParents()) {
+ if (!checkable->IsReachable(dt, failedDependency, rstack + 1))
+ return false;
+ }
+
+ /* implicit dependency on host if this is a service */
+ const auto *service = dynamic_cast<const Service *>(this);
+ if (service && (dt == DependencyState || dt == DependencyNotification)) {
+ Host::Ptr host = service->GetHost();
+
+ if (host && host->GetState() != HostUp && host->GetStateType() == StateTypeHard) {
+ if (failedDependency)
+ *failedDependency = nullptr;
+
+ return false;
+ }
+ }
+
+ auto deps = GetDependencies();
+
+ std::unordered_map<std::string, Dependency::Ptr> violated; // key: redundancy group, value: nullptr if satisfied, violating dependency otherwise
+
+ for (const Dependency::Ptr& dep : deps) {
+ std::string redundancy_group = dep->GetRedundancyGroup();
+
+ if (!dep->IsAvailable(dt)) {
+ if (redundancy_group.empty()) {
+ Log(LogDebug, "Checkable")
+ << "Non-redundant dependency '" << dep->GetName() << "' failed for checkable '" << GetName() << "': Marking as unreachable.";
+
+ if (failedDependency)
+ *failedDependency = dep;
+
+ return false;
+ }
+
+ // tentatively mark this dependency group as failed unless it is already marked;
+ // so it either passed before (don't overwrite) or already failed (so don't care)
+ // note that std::unordered_map::insert() will not overwrite an existing entry
+ violated.insert(std::make_pair(redundancy_group, dep));
+ } else if (!redundancy_group.empty()) {
+ violated[redundancy_group] = nullptr;
+ }
+ }
+
+ auto violator = std::find_if(violated.begin(), violated.end(), [](auto& v) { return v.second != nullptr; });
+ if (violator != violated.end()) {
+ Log(LogDebug, "Checkable")
+ << "All dependencies in redundancy group '" << violator->first << "' have failed for checkable '" << GetName() << "': Marking as unreachable.";
+
+ if (failedDependency)
+ *failedDependency = violator->second;
+
+ return false;
+ }
+
+ if (failedDependency)
+ *failedDependency = nullptr;
+
+ return true;
+}
+
+std::set<Checkable::Ptr> Checkable::GetParents() const
+{
+ std::set<Checkable::Ptr> parents;
+
+ for (const Dependency::Ptr& dep : GetDependencies()) {
+ Checkable::Ptr parent = dep->GetParent();
+
+ if (parent && parent.get() != this)
+ parents.insert(parent);
+ }
+
+ return parents;
+}
+
+std::set<Checkable::Ptr> Checkable::GetChildren() const
+{
+ std::set<Checkable::Ptr> parents;
+
+ for (const Dependency::Ptr& dep : GetReverseDependencies()) {
+ Checkable::Ptr service = dep->GetChild();
+
+ if (service && service.get() != this)
+ parents.insert(service);
+ }
+
+ return parents;
+}
+
+std::set<Checkable::Ptr> Checkable::GetAllChildren() const
+{
+ std::set<Checkable::Ptr> children = GetChildren();
+
+ GetAllChildrenInternal(children, 0);
+
+ return children;
+}
+
+void Checkable::GetAllChildrenInternal(std::set<Checkable::Ptr>& children, int level) const
+{
+ if (level > 32)
+ return;
+
+ std::set<Checkable::Ptr> localChildren;
+
+ for (const Checkable::Ptr& checkable : children) {
+ std::set<Checkable::Ptr> cChildren = checkable->GetChildren();
+
+ if (!cChildren.empty()) {
+ GetAllChildrenInternal(cChildren, level + 1);
+ localChildren.insert(cChildren.begin(), cChildren.end());
+ }
+
+ localChildren.insert(checkable);
+ }
+
+ children.insert(localChildren.begin(), localChildren.end());
+}
diff --git a/lib/icinga/checkable-downtime.cpp b/lib/icinga/checkable-downtime.cpp
new file mode 100644
index 0000000..d96003d
--- /dev/null
+++ b/lib/icinga/checkable-downtime.cpp
@@ -0,0 +1,64 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+void Checkable::RemoveAllDowntimes()
+{
+ for (const Downtime::Ptr& downtime : GetDowntimes()) {
+ Downtime::RemoveDowntime(downtime->GetName(), true, true, true);
+ }
+}
+
+void Checkable::TriggerDowntimes(double triggerTime)
+{
+ for (const Downtime::Ptr& downtime : GetDowntimes()) {
+ downtime->TriggerDowntime(triggerTime);
+ }
+}
+
+bool Checkable::IsInDowntime() const
+{
+ for (const Downtime::Ptr& downtime : GetDowntimes()) {
+ if (downtime->IsInEffect())
+ return true;
+ }
+
+ return false;
+}
+
+int Checkable::GetDowntimeDepth() const
+{
+ int downtime_depth = 0;
+
+ for (const Downtime::Ptr& downtime : GetDowntimes()) {
+ if (downtime->IsInEffect())
+ downtime_depth++;
+ }
+
+ return downtime_depth;
+}
+
+std::set<Downtime::Ptr> Checkable::GetDowntimes() const
+{
+ std::unique_lock<std::mutex> lock(m_DowntimeMutex);
+ return m_Downtimes;
+}
+
+void Checkable::RegisterDowntime(const Downtime::Ptr& downtime)
+{
+ std::unique_lock<std::mutex> lock(m_DowntimeMutex);
+ m_Downtimes.insert(downtime);
+}
+
+void Checkable::UnregisterDowntime(const Downtime::Ptr& downtime)
+{
+ std::unique_lock<std::mutex> lock(m_DowntimeMutex);
+ m_Downtimes.erase(downtime);
+}
diff --git a/lib/icinga/checkable-event.cpp b/lib/icinga/checkable-event.cpp
new file mode 100644
index 0000000..fb315d9
--- /dev/null
+++ b/lib/icinga/checkable-event.cpp
@@ -0,0 +1,81 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/service.hpp"
+#include "remote/apilistener.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+
+using namespace icinga;
+
+boost::signals2::signal<void (const Checkable::Ptr&)> Checkable::OnEventCommandExecuted;
+
+EventCommand::Ptr Checkable::GetEventCommand() const
+{
+ return EventCommand::GetByName(GetEventCommandRaw());
+}
+
+void Checkable::ExecuteEventHandler(const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ CONTEXT("Executing event handler for object '" << GetName() << "'");
+
+ if (!IcingaApplication::GetInstance()->GetEnableEventHandlers() || !GetEnableEventHandler())
+ return;
+
+ /* HA enabled zones. */
+ if (IsActive() && IsPaused()) {
+ Log(LogNotice, "Checkable")
+ << "Skipping event handler for HA-paused checkable '" << GetName() << "'";
+ return;
+ }
+
+ EventCommand::Ptr ec = GetEventCommand();
+
+ if (!ec)
+ return;
+
+ Log(LogNotice, "Checkable")
+ << "Executing event handler '" << ec->GetName() << "' for checkable '" << GetName() << "'";
+
+ Dictionary::Ptr macros;
+ Endpoint::Ptr endpoint = GetCommandEndpoint();
+
+ if (endpoint && !useResolvedMacros)
+ macros = new Dictionary();
+ else
+ macros = resolvedMacros;
+
+ ec->Execute(this, macros, useResolvedMacros);
+
+ if (endpoint && !GetExtension("agent_check")) {
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ExecuteCommand");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(this);
+
+ Dictionary::Ptr params = new Dictionary();
+ message->Set("params", params);
+ params->Set("command_type", "event_command");
+ params->Set("command", GetEventCommand()->GetName());
+ params->Set("host", host->GetName());
+
+ if (service)
+ params->Set("service", service->GetShortName());
+
+ params->Set("macros", macros);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener)
+ listener->SyncSendMessage(endpoint, message);
+
+ return;
+ }
+
+ OnEventCommandExecuted(this);
+}
diff --git a/lib/icinga/checkable-flapping.cpp b/lib/icinga/checkable-flapping.cpp
new file mode 100644
index 0000000..e905e05
--- /dev/null
+++ b/lib/icinga/checkable-flapping.cpp
@@ -0,0 +1,114 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+template<typename T>
+struct Bitset
+{
+public:
+ Bitset(T value)
+ : m_Data(value)
+ { }
+
+ void Modify(int index, bool bit)
+ {
+ if (bit)
+ m_Data |= 1 << index;
+ else
+ m_Data &= ~(1 << index);
+ }
+
+ bool Get(int index) const
+ {
+ return m_Data & (1 << index);
+ }
+
+ T GetValue() const
+ {
+ return m_Data;
+ }
+
+private:
+ T m_Data{0};
+};
+
+void Checkable::UpdateFlappingStatus(ServiceState newState)
+{
+ Bitset<unsigned long> stateChangeBuf = GetFlappingBuffer();
+ int oldestIndex = GetFlappingIndex();
+
+ ServiceState lastState = GetFlappingLastState();
+ bool stateChange = false;
+
+ int stateFilter = GetFlappingIgnoreStatesFilter();
+
+ /* Only count as state change if no state filter is set or the new state isn't filtered out */
+ if (stateFilter == -1 || !(ServiceStateToFlappingFilter(newState) & stateFilter)) {
+ stateChange = newState != lastState;
+ SetFlappingLastState(newState);
+ }
+
+ stateChangeBuf.Modify(oldestIndex, stateChange);
+ oldestIndex = (oldestIndex + 1) % 20;
+
+ double stateChanges = 0;
+
+ /* Iterate over our state array and compute a weighted total */
+ for (int i = 0; i < 20; i++) {
+ if (stateChangeBuf.Get((oldestIndex + i) % 20))
+ stateChanges += 0.8 + (0.02 * i);
+ }
+
+ double flappingValue = 100.0 * stateChanges / 20.0;
+
+ bool flapping;
+
+ if (GetFlapping())
+ flapping = flappingValue > GetFlappingThresholdLow();
+ else
+ flapping = flappingValue > GetFlappingThresholdHigh();
+
+ SetFlappingBuffer(stateChangeBuf.GetValue());
+ SetFlappingIndex(oldestIndex);
+ SetFlappingCurrent(flappingValue);
+
+ if (flapping != GetFlapping()) {
+ SetFlapping(flapping, true);
+
+ double ee = GetLastCheckResult()->GetExecutionEnd();
+
+ if (GetEnableFlapping() && IcingaApplication::GetInstance()->GetEnableFlapping()) {
+ OnFlappingChange(this, ee);
+ }
+
+ SetFlappingLastChange(ee);
+ }
+}
+
+bool Checkable::IsFlapping() const
+{
+ if (!GetEnableFlapping() || !IcingaApplication::GetInstance()->GetEnableFlapping())
+ return false;
+ else
+ return GetFlapping();
+}
+
+int Checkable::ServiceStateToFlappingFilter(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ return StateFilterOK;
+ case ServiceWarning:
+ return StateFilterWarning;
+ case ServiceCritical:
+ return StateFilterCritical;
+ case ServiceUnknown:
+ return StateFilterUnknown;
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
diff --git a/lib/icinga/checkable-notification.cpp b/lib/icinga/checkable-notification.cpp
new file mode 100644
index 0000000..79b5986
--- /dev/null
+++ b/lib/icinga/checkable-notification.cpp
@@ -0,0 +1,334 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/context.hpp"
+#include "base/convert.hpp"
+#include "base/lazy-init.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+boost::signals2::signal<void (const Notification::Ptr&, const Checkable::Ptr&, const std::set<User::Ptr>&,
+ const NotificationType&, const CheckResult::Ptr&, const String&, const String&,
+ const MessageOrigin::Ptr&)> Checkable::OnNotificationSentToAllUsers;
+boost::signals2::signal<void (const Notification::Ptr&, const Checkable::Ptr&, const User::Ptr&,
+ const NotificationType&, const CheckResult::Ptr&, const String&, const String&, const String&,
+ const MessageOrigin::Ptr&)> Checkable::OnNotificationSentToUser;
+
+void Checkable::ResetNotificationNumbers()
+{
+ for (const Notification::Ptr& notification : GetNotifications()) {
+ ObjectLock olock(notification);
+ notification->ResetNotificationNumber();
+ }
+}
+
+void Checkable::SendNotifications(NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text)
+{
+ String checkableName = GetName();
+
+ CONTEXT("Sending notifications for object '" << checkableName << "'");
+
+ bool force = GetForceNextNotification();
+
+ SetForceNextNotification(false);
+
+ if (!IcingaApplication::GetInstance()->GetEnableNotifications() || !GetEnableNotifications()) {
+ if (!force) {
+ Log(LogInformation, "Checkable")
+ << "Notifications are disabled for checkable '" << checkableName << "'.";
+ return;
+ }
+ }
+
+ std::set<Notification::Ptr> notifications = GetNotifications();
+
+ String notificationTypeName = Notification::NotificationTypeToString(type);
+
+ // Bail early if there are no notifications.
+ if (notifications.empty()) {
+ Log(LogNotice, "Checkable")
+ << "Skipping checkable '" << checkableName << "' which doesn't have any notification objects configured.";
+ return;
+ }
+
+ Log(LogInformation, "Checkable")
+ << "Checkable '" << checkableName << "' has " << notifications.size()
+ << " notification(s). Checking filters for type '" << notificationTypeName << "', sends will be logged.";
+
+ for (const Notification::Ptr& notification : notifications) {
+ // Re-send stashed notifications from cold startup.
+ if (ApiListener::UpdatedObjectAuthority()) {
+ try {
+ if (!notification->IsPaused()) {
+ auto stashedNotifications (notification->GetStashedNotifications());
+
+ if (stashedNotifications->GetLength()) {
+ Log(LogNotice, "Notification")
+ << "Notification '" << notification->GetName() << "': there are some stashed notifications. Stashing notification to preserve order.";
+
+ stashedNotifications->Add(new Dictionary({
+ {"notification_type", type},
+ {"cr", cr},
+ {"force", force},
+ {"reminder", false},
+ {"author", author},
+ {"text", text}
+ }));
+ } else {
+ notification->BeginExecuteNotification(type, cr, force, false, author, text);
+ }
+ } else {
+ Log(LogNotice, "Notification")
+ << "Notification '" << notification->GetName() << "': HA cluster active, this endpoint does not have the authority (paused=true). Skipping.";
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "Checkable")
+ << "Exception occurred during notification '" << notification->GetName() << "' for checkable '"
+ << GetName() << "': " << DiagnosticInformation(ex, false);
+ }
+ } else {
+ // Cold startup phase. Stash notification for later.
+ Log(LogNotice, "Notification")
+ << "Notification '" << notification->GetName() << "': object authority hasn't been updated, yet. Stashing notification.";
+
+ notification->GetStashedNotifications()->Add(new Dictionary({
+ {"notification_type", type},
+ {"cr", cr},
+ {"force", force},
+ {"reminder", false},
+ {"author", author},
+ {"text", text}
+ }));
+ }
+ }
+}
+
+std::set<Notification::Ptr> Checkable::GetNotifications() const
+{
+ std::unique_lock<std::mutex> lock(m_NotificationMutex);
+ return m_Notifications;
+}
+
+void Checkable::RegisterNotification(const Notification::Ptr& notification)
+{
+ std::unique_lock<std::mutex> lock(m_NotificationMutex);
+ m_Notifications.insert(notification);
+}
+
+void Checkable::UnregisterNotification(const Notification::Ptr& notification)
+{
+ std::unique_lock<std::mutex> lock(m_NotificationMutex);
+ m_Notifications.erase(notification);
+}
+
+void Checkable::FireSuppressedNotifications()
+{
+ if (!IsActive())
+ return;
+
+ if (IsPaused())
+ return;
+
+ if (!GetEnableNotifications())
+ return;
+
+ int suppressed_types (GetSuppressedNotifications());
+ if (!suppressed_types)
+ return;
+
+ int subtract = 0;
+
+ {
+ LazyInit<bool> wasLastParentRecoveryRecent ([this]() {
+ auto cr (GetLastCheckResult());
+
+ if (!cr) {
+ return true;
+ }
+
+ auto threshold (cr->GetExecutionStart());
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(this);
+
+ if (service) {
+ ObjectLock oLock (host);
+
+ if (!host->GetProblem() && host->GetLastStateChange() >= threshold) {
+ return true;
+ }
+ }
+
+ for (auto& dep : GetDependencies()) {
+ auto parent (dep->GetParent());
+ ObjectLock oLock (parent);
+
+ if (!parent->GetProblem() && parent->GetLastStateChange() >= threshold) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ if (suppressed_types & (NotificationProblem|NotificationRecovery)) {
+ CheckResult::Ptr cr = GetLastCheckResult();
+ NotificationType type = cr && IsStateOK(cr->GetState()) ? NotificationRecovery : NotificationProblem;
+ bool state_suppressed = NotificationReasonSuppressed(NotificationProblem) || NotificationReasonSuppressed(NotificationRecovery);
+
+ /* Only process (i.e. send or dismiss) suppressed state notifications if the following conditions are met:
+ *
+ * 1. State notifications are not suppressed at the moment. State notifications must only be removed from
+ * the suppressed notifications bitset after the reason for the suppression is gone as these bits are
+ * used as a marker for when to set the state_before_suppression attribute.
+ * 2. The checkable is in a hard state. Soft states represent a state where we are not certain yet about
+ * the actual state and wait with sending notifications. If we want to immediately send a notification,
+ * we might send a recovery notification for something that just started failing or a problem
+ * notification which might be for an intermittent problem that would have never received a
+ * notification if there was no suppression as it still was in a soft state. Both cases aren't ideal so
+ * better wait until we are certain.
+ * 3. The checkable isn't likely checked soon. For example, if a downtime ended, give the checkable a
+ * chance to recover afterwards before sending a notification.
+ * 4. No parent recovered recently. Similar to the previous condition, give the checkable a chance to
+ * recover after one of its dependencies recovered before sending a notification.
+ *
+ * If any of these conditions is not met, processing the suppressed notification is further delayed.
+ */
+ if (!state_suppressed && GetStateType() == StateTypeHard && !IsLikelyToBeCheckedSoon() && !wasLastParentRecoveryRecent.Get()) {
+ if (NotificationReasonApplies(type)) {
+ Checkable::OnNotificationsRequested(this, type, cr, "", "", nullptr);
+ }
+ subtract |= NotificationRecovery|NotificationProblem;
+ }
+ }
+
+ for (auto type : {NotificationFlappingStart, NotificationFlappingEnd}) {
+ if (suppressed_types & type) {
+ bool still_applies = NotificationReasonApplies(type);
+
+ if (still_applies) {
+ if (!NotificationReasonSuppressed(type) && !IsLikelyToBeCheckedSoon() && !wasLastParentRecoveryRecent.Get()) {
+ Checkable::OnNotificationsRequested(this, type, GetLastCheckResult(), "", "", nullptr);
+
+ subtract |= type;
+ }
+ } else {
+ subtract |= type;
+ }
+ }
+ }
+ }
+
+ if (subtract) {
+ ObjectLock olock (this);
+
+ int suppressed_types_before (GetSuppressedNotifications());
+ int suppressed_types_after (suppressed_types_before & ~subtract);
+
+ if (suppressed_types_after != suppressed_types_before) {
+ SetSuppressedNotifications(suppressed_types_after);
+ }
+ }
+}
+
+/**
+ * Re-sends all notifications previously suppressed by e.g. downtimes if the notification reason still applies.
+ */
+void Checkable::FireSuppressedNotificationsTimer(const Timer * const&)
+{
+ for (auto& host : ConfigType::GetObjectsByType<Host>()) {
+ host->FireSuppressedNotifications();
+ }
+
+ for (auto& service : ConfigType::GetObjectsByType<Service>()) {
+ service->FireSuppressedNotifications();
+ }
+}
+
+/**
+ * Returns whether sending a notification of type type right now would represent *this' current state correctly.
+ *
+ * @param type The type of notification to send (or not to send).
+ *
+ * @return Whether to send the notification.
+ */
+bool Checkable::NotificationReasonApplies(NotificationType type)
+{
+ switch (type) {
+ case NotificationProblem:
+ {
+ auto cr (GetLastCheckResult());
+ return cr && !IsStateOK(cr->GetState()) && cr->GetState() != GetStateBeforeSuppression();
+ }
+ case NotificationRecovery:
+ {
+ auto cr (GetLastCheckResult());
+ return cr && IsStateOK(cr->GetState()) && cr->GetState() != GetStateBeforeSuppression();
+ }
+ case NotificationFlappingStart:
+ return IsFlapping();
+ case NotificationFlappingEnd:
+ return !IsFlapping();
+ default:
+ VERIFY(!"Checkable#NotificationReasonStillApplies(): given type not implemented");
+ return false;
+ }
+}
+
+/**
+ * Checks if notifications of a given type should be suppressed for this Checkable at the moment.
+ *
+ * @param type The notification type for which to query the suppression status.
+ *
+ * @return true if no notification of this type should be sent.
+ */
+bool Checkable::NotificationReasonSuppressed(NotificationType type)
+{
+ switch (type) {
+ case NotificationProblem:
+ case NotificationRecovery:
+ return !IsReachable(DependencyNotification) || IsInDowntime() || IsAcknowledged();
+ case NotificationFlappingStart:
+ case NotificationFlappingEnd:
+ return IsInDowntime();
+ default:
+ return false;
+ }
+}
+
+/**
+ * E.g. we're going to re-send a stashed problem notification as *this is still not ok.
+ * But if the next check result recovers *this soon, we would send a recovery notification soon after the problem one.
+ * This is not desired, especially for lots of checkables at once.
+ * Because of that if there's likely to be a check result soon,
+ * we delay the re-sending of the stashed notification until the next check.
+ * That check either doesn't change anything and we finally re-send the stashed problem notification
+ * or recovers *this and we drop the stashed notification.
+ *
+ * @return Whether *this is likely to be checked soon
+ */
+bool Checkable::IsLikelyToBeCheckedSoon()
+{
+ if (!GetEnableActiveChecks()) {
+ return false;
+ }
+
+ // One minute unless the check interval is too short so the next check will always run during the next minute.
+ auto threshold (GetCheckInterval() - 10);
+
+ if (threshold > 60) {
+ threshold = 60;
+ } else if (threshold < 0) {
+ threshold = 0;
+ }
+
+ return GetNextCheck() <= Utility::GetTime() + threshold;
+}
diff --git a/lib/icinga/checkable-script.cpp b/lib/icinga/checkable-script.cpp
new file mode 100644
index 0000000..4a0d1d8
--- /dev/null
+++ b/lib/icinga/checkable-script.cpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "base/configobject.hpp"
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+
+using namespace icinga;
+
+static void CheckableProcessCheckResult(const CheckResult::Ptr& cr)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Checkable::Ptr self = vframe->Self;
+ REQUIRE_NOT_NULL(self);
+ self->ProcessCheckResult(cr);
+}
+
+Object::Ptr Checkable::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "process_check_result", new Function("Checkable#process_check_result", CheckableProcessCheckResult, { "cr" }, false) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/icinga/checkable.cpp b/lib/icinga/checkable.cpp
new file mode 100644
index 0000000..ddf84cd
--- /dev/null
+++ b/lib/icinga/checkable.cpp
@@ -0,0 +1,322 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/checkable-ti.cpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/timer.hpp"
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE_WITH_PROTOTYPE(Checkable, Checkable::GetPrototype());
+INITIALIZE_ONCE(&Checkable::StaticInitialize);
+
+const std::map<String, int> Checkable::m_FlappingStateFilterMap ({
+ {"OK", FlappingStateFilterOk},
+ {"Warning", FlappingStateFilterWarning},
+ {"Critical", FlappingStateFilterCritical},
+ {"Unknown", FlappingStateFilterUnknown},
+ {"Up", FlappingStateFilterOk},
+ {"Down", FlappingStateFilterCritical},
+});
+
+boost::signals2::signal<void (const Checkable::Ptr&, const String&, const String&, AcknowledgementType, bool, bool, double, double, const MessageOrigin::Ptr&)> Checkable::OnAcknowledgementSet;
+boost::signals2::signal<void (const Checkable::Ptr&, const String&, double, const MessageOrigin::Ptr&)> Checkable::OnAcknowledgementCleared;
+boost::signals2::signal<void (const Checkable::Ptr&, double)> Checkable::OnFlappingChange;
+
+static Timer::Ptr l_CheckablesFireSuppressedNotifications;
+static Timer::Ptr l_CleanDeadlinedExecutions;
+
+thread_local std::function<void(const Value& commandLine, const ProcessResult&)> Checkable::ExecuteCommandProcessFinishedHandler;
+
+void Checkable::StaticInitialize()
+{
+ /* fixed downtime start */
+ Downtime::OnDowntimeStarted.connect([](const Downtime::Ptr& downtime) { Checkable::NotifyFixedDowntimeStart(downtime); });
+ /* flexible downtime start */
+ Downtime::OnDowntimeTriggered.connect([](const Downtime::Ptr& downtime) { Checkable::NotifyFlexibleDowntimeStart(downtime); });
+ /* fixed/flexible downtime end */
+ Downtime::OnDowntimeRemoved.connect([](const Downtime::Ptr& downtime) { Checkable::NotifyDowntimeEnd(downtime); });
+}
+
+Checkable::Checkable()
+{
+ SetSchedulingOffset(Utility::Random());
+}
+
+void Checkable::OnConfigLoaded()
+{
+ ObjectImpl<Checkable>::OnConfigLoaded();
+
+ SetFlappingIgnoreStatesFilter(FilterArrayToInt(GetFlappingIgnoreStates(), m_FlappingStateFilterMap, ~0));
+}
+
+void Checkable::OnAllConfigLoaded()
+{
+ ObjectImpl<Checkable>::OnAllConfigLoaded();
+
+ Endpoint::Ptr endpoint = GetCommandEndpoint();
+
+ if (endpoint) {
+ Zone::Ptr checkableZone = static_pointer_cast<Zone>(GetZone());
+
+ if (checkableZone) {
+ Zone::Ptr cmdZone = endpoint->GetZone();
+
+ if (cmdZone != checkableZone && cmdZone->GetParent() != checkableZone) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "command_endpoint" },
+ "Command endpoint must be in zone '" + checkableZone->GetName() + "' or in a direct child zone thereof."));
+ }
+ } else {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "command_endpoint" },
+ "Checkable with command endpoint requires a zone. Please check the troubleshooting documentation."));
+ }
+ }
+}
+
+void Checkable::Start(bool runtimeCreated)
+{
+ double now = Utility::GetTime();
+
+ {
+ auto cr (GetLastCheckResult());
+
+ if (GetLastCheckStarted() > (cr ? cr->GetExecutionEnd() : 0.0)) {
+ SetNextCheck(GetLastCheckStarted());
+ }
+ }
+
+ if (GetNextCheck() < now + 60) {
+ double delta = std::min(GetCheckInterval(), 60.0);
+ delta *= (double)std::rand() / RAND_MAX;
+ SetNextCheck(now + delta);
+ }
+
+ ObjectImpl<Checkable>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, []() {
+ l_CheckablesFireSuppressedNotifications = Timer::Create();
+ l_CheckablesFireSuppressedNotifications->SetInterval(5);
+ l_CheckablesFireSuppressedNotifications->OnTimerExpired.connect(&Checkable::FireSuppressedNotificationsTimer);
+ l_CheckablesFireSuppressedNotifications->Start();
+
+ l_CleanDeadlinedExecutions = Timer::Create();
+ l_CleanDeadlinedExecutions->SetInterval(300);
+ l_CleanDeadlinedExecutions->OnTimerExpired.connect(&Checkable::CleanDeadlinedExecutions);
+ l_CleanDeadlinedExecutions->Start();
+ });
+}
+
+void Checkable::AddGroup(const String& name)
+{
+ std::unique_lock<std::mutex> lock(m_CheckableMutex);
+
+ Array::Ptr groups;
+ auto *host = dynamic_cast<Host *>(this);
+
+ if (host)
+ groups = host->GetGroups();
+ else
+ groups = static_cast<Service *>(this)->GetGroups();
+
+ if (groups && groups->Contains(name))
+ return;
+
+ if (!groups)
+ groups = new Array();
+
+ groups->Add(name);
+}
+
+AcknowledgementType Checkable::GetAcknowledgement()
+{
+ auto avalue = static_cast<AcknowledgementType>(GetAcknowledgementRaw());
+
+ if (avalue != AcknowledgementNone) {
+ double expiry = GetAcknowledgementExpiry();
+
+ if (expiry != 0 && expiry < Utility::GetTime()) {
+ avalue = AcknowledgementNone;
+ ClearAcknowledgement("");
+ }
+ }
+
+ return avalue;
+}
+
+bool Checkable::IsAcknowledged() const
+{
+ return const_cast<Checkable *>(this)->GetAcknowledgement() != AcknowledgementNone;
+}
+
+void Checkable::AcknowledgeProblem(const String& author, const String& comment, AcknowledgementType type, bool notify, bool persistent, double changeTime, double expiry, const MessageOrigin::Ptr& origin)
+{
+ SetAcknowledgementRaw(type);
+ SetAcknowledgementExpiry(expiry);
+
+ if (notify && !IsPaused())
+ OnNotificationsRequested(this, NotificationAcknowledgement, GetLastCheckResult(), author, comment, nullptr);
+
+ Log(LogInformation, "Checkable")
+ << "Acknowledgement set for checkable '" << GetName() << "'.";
+
+ OnAcknowledgementSet(this, author, comment, type, notify, persistent, changeTime, expiry, origin);
+
+ SetAcknowledgementLastChange(changeTime);
+}
+
+void Checkable::ClearAcknowledgement(const String& removedBy, double changeTime, const MessageOrigin::Ptr& origin)
+{
+ ObjectLock oLock (this);
+
+ bool wasAcked = GetAcknowledgementRaw() != AcknowledgementNone;
+
+ SetAcknowledgementRaw(AcknowledgementNone);
+ SetAcknowledgementExpiry(0);
+
+ Log(LogInformation, "Checkable")
+ << "Acknowledgement cleared for checkable '" << GetName() << "'.";
+
+ if (wasAcked) {
+ OnAcknowledgementCleared(this, removedBy, changeTime, origin);
+
+ SetAcknowledgementLastChange(changeTime);
+ }
+}
+
+Endpoint::Ptr Checkable::GetCommandEndpoint() const
+{
+ return Endpoint::GetByName(GetCommandEndpointRaw());
+}
+
+int Checkable::GetSeverity() const
+{
+ /* overridden in Host/Service class. */
+ return 0;
+}
+
+bool Checkable::GetProblem() const
+{
+ auto cr (GetLastCheckResult());
+
+ return cr && !IsStateOK(cr->GetState());
+}
+
+bool Checkable::GetHandled() const
+{
+ return GetProblem() && (IsInDowntime() || IsAcknowledged());
+}
+
+Timestamp Checkable::GetNextUpdate() const
+{
+ auto cr (GetLastCheckResult());
+ double interval, latency;
+
+ // TODO: Document this behavior.
+ if (cr) {
+ interval = GetEnableActiveChecks() && GetProblem() && GetStateType() == StateTypeSoft ? GetRetryInterval() : GetCheckInterval();
+ latency = cr->GetExecutionEnd() - cr->GetScheduleStart();
+ } else {
+ interval = GetCheckInterval();
+ latency = 0.0;
+ }
+
+ return (GetEnableActiveChecks() ? GetNextCheck() : (cr ? cr->GetExecutionEnd() : Application::GetStartTime()) + interval) + interval + 2 * latency;
+}
+
+void Checkable::NotifyFixedDowntimeStart(const Downtime::Ptr& downtime)
+{
+ if (!downtime->GetFixed())
+ return;
+
+ NotifyDowntimeInternal(downtime);
+}
+
+void Checkable::NotifyFlexibleDowntimeStart(const Downtime::Ptr& downtime)
+{
+ if (downtime->GetFixed())
+ return;
+
+ NotifyDowntimeInternal(downtime);
+}
+
+void Checkable::NotifyDowntimeInternal(const Downtime::Ptr& downtime)
+{
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ if (!checkable->IsPaused())
+ OnNotificationsRequested(checkable, NotificationDowntimeStart, checkable->GetLastCheckResult(), downtime->GetAuthor(), downtime->GetComment(), nullptr);
+}
+
+void Checkable::NotifyDowntimeEnd(const Downtime::Ptr& downtime)
+{
+ /* don't send notifications for downtimes which never triggered */
+ if (!downtime->IsTriggered())
+ return;
+
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ if (!checkable->IsPaused())
+ OnNotificationsRequested(checkable, NotificationDowntimeEnd, checkable->GetLastCheckResult(), downtime->GetAuthor(), downtime->GetComment(), nullptr);
+}
+
+void Checkable::ValidateCheckInterval(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Checkable>::ValidateCheckInterval(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "check_interval" }, "Interval must be greater than 0."));
+}
+
+void Checkable::ValidateRetryInterval(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Checkable>::ValidateRetryInterval(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "retry_interval" }, "Interval must be greater than 0."));
+}
+
+void Checkable::ValidateMaxCheckAttempts(const Lazy<int>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Checkable>::ValidateMaxCheckAttempts(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "max_check_attempts" }, "Value must be greater than 0."));
+}
+
+void Checkable::CleanDeadlinedExecutions(const Timer * const&)
+{
+ double now = Utility::GetTime();
+ Dictionary::Ptr executions;
+ Dictionary::Ptr execution;
+
+ for (auto& host : ConfigType::GetObjectsByType<Host>()) {
+ executions = host->GetExecutions();
+ if (executions) {
+ for (const String& key : executions->GetKeys()) {
+ execution = executions->Get(key);
+ if (execution->Contains("deadline") && now > execution->Get("deadline")) {
+ executions->Remove(key);
+ }
+ }
+ }
+ }
+
+ for (auto& service : ConfigType::GetObjectsByType<Service>()) {
+ executions = service->GetExecutions();
+ if (executions) {
+ for (const String& key : executions->GetKeys()) {
+ execution = executions->Get(key);
+ if (execution->Contains("deadline") && now > execution->Get("deadline")) {
+ executions->Remove(key);
+ }
+ }
+ }
+ }
+}
diff --git a/lib/icinga/checkable.hpp b/lib/icinga/checkable.hpp
new file mode 100644
index 0000000..3d48b14
--- /dev/null
+++ b/lib/icinga/checkable.hpp
@@ -0,0 +1,264 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CHECKABLE_H
+#define CHECKABLE_H
+
+#include "base/atomic.hpp"
+#include "base/timer.hpp"
+#include "base/process.hpp"
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkable-ti.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/comment.hpp"
+#include "icinga/downtime.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/messageorigin.hpp"
+#include <condition_variable>
+#include <cstdint>
+#include <functional>
+#include <limits>
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+enum DependencyType
+{
+ DependencyState,
+ DependencyCheckExecution,
+ DependencyNotification
+};
+
+/**
+ * Checkable Types
+ *
+ * @ingroup icinga
+ */
+enum CheckableType
+{
+ CheckableHost,
+ CheckableService
+};
+
+/**
+ * @ingroup icinga
+ */
+enum FlappingStateFilter
+{
+ FlappingStateFilterOk = 1,
+ FlappingStateFilterWarning = 2,
+ FlappingStateFilterCritical = 4,
+ FlappingStateFilterUnknown = 8,
+};
+
+class CheckCommand;
+class EventCommand;
+class Dependency;
+
+/**
+ * An Icinga service.
+ *
+ * @ingroup icinga
+ */
+class Checkable : public ObjectImpl<Checkable>
+{
+public:
+ DECLARE_OBJECT(Checkable);
+ DECLARE_OBJECTNAME(Checkable);
+
+ static void StaticInitialize();
+ static thread_local std::function<void(const Value& commandLine, const ProcessResult&)> ExecuteCommandProcessFinishedHandler;
+
+ Checkable();
+
+ std::set<Checkable::Ptr> GetParents() const;
+ std::set<Checkable::Ptr> GetChildren() const;
+ std::set<Checkable::Ptr> GetAllChildren() const;
+
+ void AddGroup(const String& name);
+
+ bool IsReachable(DependencyType dt = DependencyState, intrusive_ptr<Dependency> *failedDependency = nullptr, int rstack = 0) const;
+
+ AcknowledgementType GetAcknowledgement();
+
+ void AcknowledgeProblem(const String& author, const String& comment, AcknowledgementType type, bool notify = true, bool persistent = false, double changeTime = Utility::GetTime(), double expiry = 0, const MessageOrigin::Ptr& origin = nullptr);
+ void ClearAcknowledgement(const String& removedBy, double changeTime = Utility::GetTime(), const MessageOrigin::Ptr& origin = nullptr);
+
+ int GetSeverity() const override;
+ bool GetProblem() const override;
+ bool GetHandled() const override;
+ Timestamp GetNextUpdate() const override;
+
+ /* Checks */
+ intrusive_ptr<CheckCommand> GetCheckCommand() const;
+ TimePeriod::Ptr GetCheckPeriod() const;
+
+ long GetSchedulingOffset();
+ void SetSchedulingOffset(long offset);
+
+ void UpdateNextCheck(const MessageOrigin::Ptr& origin = nullptr);
+
+ bool HasBeenChecked() const;
+ virtual bool IsStateOK(ServiceState state) const = 0;
+
+ double GetLastCheck() const final;
+
+ virtual void SaveLastState(ServiceState state, double timestamp) = 0;
+
+ static void UpdateStatistics(const CheckResult::Ptr& cr, CheckableType type);
+
+ void ExecuteRemoteCheck(const Dictionary::Ptr& resolvedMacros = nullptr);
+ void ExecuteCheck();
+ enum class ProcessingResult
+ {
+ Ok,
+ NoCheckResult,
+ CheckableInactive,
+ NewerCheckResultPresent,
+ };
+ ProcessingResult ProcessCheckResult(const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin = nullptr);
+
+ Endpoint::Ptr GetCommandEndpoint() const;
+
+ static boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, const MessageOrigin::Ptr&)> OnNewCheckResult;
+ static boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, StateType, const MessageOrigin::Ptr&)> OnStateChange;
+ static boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, std::set<Checkable::Ptr>, const MessageOrigin::Ptr&)> OnReachabilityChanged;
+ static boost::signals2::signal<void (const Checkable::Ptr&, NotificationType, const CheckResult::Ptr&,
+ const String&, const String&, const MessageOrigin::Ptr&)> OnNotificationsRequested;
+ static boost::signals2::signal<void (const Notification::Ptr&, const Checkable::Ptr&, const User::Ptr&,
+ const NotificationType&, const CheckResult::Ptr&, const String&, const String&, const String&,
+ const MessageOrigin::Ptr&)> OnNotificationSentToUser;
+ static boost::signals2::signal<void (const Notification::Ptr&, const Checkable::Ptr&, const std::set<User::Ptr>&,
+ const NotificationType&, const CheckResult::Ptr&, const String&,
+ const String&, const MessageOrigin::Ptr&)> OnNotificationSentToAllUsers;
+ static boost::signals2::signal<void (const Checkable::Ptr&, const String&, const String&, AcknowledgementType,
+ bool, bool, double, double, const MessageOrigin::Ptr&)> OnAcknowledgementSet;
+ static boost::signals2::signal<void (const Checkable::Ptr&, const String&, double, const MessageOrigin::Ptr&)> OnAcknowledgementCleared;
+ static boost::signals2::signal<void (const Checkable::Ptr&, double)> OnFlappingChange;
+ static boost::signals2::signal<void (const Checkable::Ptr&)> OnNextCheckUpdated;
+ static boost::signals2::signal<void (const Checkable::Ptr&)> OnEventCommandExecuted;
+
+ static Atomic<uint_fast64_t> CurrentConcurrentChecks;
+
+ /* Downtimes */
+ int GetDowntimeDepth() const final;
+
+ void RemoveAllDowntimes();
+ void TriggerDowntimes(double triggerTime);
+ bool IsInDowntime() const;
+ bool IsAcknowledged() const;
+
+ std::set<Downtime::Ptr> GetDowntimes() const;
+ void RegisterDowntime(const Downtime::Ptr& downtime);
+ void UnregisterDowntime(const Downtime::Ptr& downtime);
+
+ /* Comments */
+ void RemoveAllComments();
+ void RemoveAckComments(const String& removedBy = String(), double createdBefore = std::numeric_limits<double>::max());
+
+ std::set<Comment::Ptr> GetComments() const;
+ Comment::Ptr GetLastComment() const;
+ void RegisterComment(const Comment::Ptr& comment);
+ void UnregisterComment(const Comment::Ptr& comment);
+
+ /* Notifications */
+ void SendNotifications(NotificationType type, const CheckResult::Ptr& cr, const String& author = "", const String& text = "");
+
+ std::set<Notification::Ptr> GetNotifications() const;
+ void RegisterNotification(const Notification::Ptr& notification);
+ void UnregisterNotification(const Notification::Ptr& notification);
+
+ void ResetNotificationNumbers();
+
+ /* Event Handler */
+ void ExecuteEventHandler(const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false);
+
+ intrusive_ptr<EventCommand> GetEventCommand() const;
+
+ /* Flapping Detection */
+ bool IsFlapping() const;
+
+ /* Dependencies */
+ void AddDependency(const intrusive_ptr<Dependency>& dep);
+ void RemoveDependency(const intrusive_ptr<Dependency>& dep);
+ std::vector<intrusive_ptr<Dependency> > GetDependencies() const;
+
+ void AddReverseDependency(const intrusive_ptr<Dependency>& dep);
+ void RemoveReverseDependency(const intrusive_ptr<Dependency>& dep);
+ std::vector<intrusive_ptr<Dependency> > GetReverseDependencies() const;
+
+ void ValidateCheckInterval(const Lazy<double>& lvalue, const ValidationUtils& value) final;
+ void ValidateRetryInterval(const Lazy<double>& lvalue, const ValidationUtils& value) final;
+ void ValidateMaxCheckAttempts(const Lazy<int>& lvalue, const ValidationUtils& value) final;
+
+ bool NotificationReasonApplies(NotificationType type);
+ bool NotificationReasonSuppressed(NotificationType type);
+ bool IsLikelyToBeCheckedSoon();
+
+ void FireSuppressedNotifications();
+
+ static void IncreasePendingChecks();
+ static void DecreasePendingChecks();
+ static int GetPendingChecks();
+ static void AquirePendingCheckSlot(int maxPendingChecks);
+
+ static Object::Ptr GetPrototype();
+
+protected:
+ void Start(bool runtimeCreated) override;
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+
+private:
+ mutable std::mutex m_CheckableMutex;
+ bool m_CheckRunning{false};
+ long m_SchedulingOffset;
+
+ static std::mutex m_StatsMutex;
+ static int m_PendingChecks;
+ static std::condition_variable m_PendingChecksCV;
+
+ /* Downtimes */
+ std::set<Downtime::Ptr> m_Downtimes;
+ mutable std::mutex m_DowntimeMutex;
+
+ static void NotifyFixedDowntimeStart(const Downtime::Ptr& downtime);
+ static void NotifyFlexibleDowntimeStart(const Downtime::Ptr& downtime);
+ static void NotifyDowntimeInternal(const Downtime::Ptr& downtime);
+
+ static void NotifyDowntimeEnd(const Downtime::Ptr& downtime);
+
+ static void FireSuppressedNotificationsTimer(const Timer * const&);
+ static void CleanDeadlinedExecutions(const Timer * const&);
+
+ /* Comments */
+ std::set<Comment::Ptr> m_Comments;
+ mutable std::mutex m_CommentMutex;
+
+ /* Notifications */
+ std::set<Notification::Ptr> m_Notifications;
+ mutable std::mutex m_NotificationMutex;
+
+ /* Dependencies */
+ mutable std::mutex m_DependencyMutex;
+ std::set<intrusive_ptr<Dependency> > m_Dependencies;
+ std::set<intrusive_ptr<Dependency> > m_ReverseDependencies;
+
+ void GetAllChildrenInternal(std::set<Checkable::Ptr>& children, int level = 0) const;
+
+ /* Flapping */
+ static const std::map<String, int> m_FlappingStateFilterMap;
+
+ void UpdateFlappingStatus(ServiceState newState);
+ static int ServiceStateToFlappingFilter(ServiceState state);
+};
+
+}
+
+#endif /* CHECKABLE_H */
+
+#include "icinga/dependency.hpp"
diff --git a/lib/icinga/checkable.ti b/lib/icinga/checkable.ti
new file mode 100644
index 0000000..6f7a5da
--- /dev/null
+++ b/lib/icinga/checkable.ti
@@ -0,0 +1,192 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/icingaapplication.hpp"
+#include "icinga/customvarobject.hpp"
+#include "base/array.hpp"
+#impl_include "icinga/checkcommand.hpp"
+#impl_include "icinga/eventcommand.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+/**
+ * The acknowledgement type of a service.
+ *
+ * @ingroup icinga
+ */
+enum AcknowledgementType
+{
+ AcknowledgementNone = 0,
+ AcknowledgementNormal = 1,
+ AcknowledgementSticky = 2
+};
+}}}
+
+abstract class Checkable : CustomVarObject
+{
+ [config, required, navigation] name(CheckCommand) check_command (CheckCommandRaw) {
+ navigate {{{
+ return CheckCommand::GetByName(GetCheckCommandRaw());
+ }}}
+ };
+ [config] int max_check_attempts {
+ default {{{ return 3; }}}
+ };
+ [config, navigation] name(TimePeriod) check_period (CheckPeriodRaw) {
+ navigate {{{
+ return TimePeriod::GetByName(GetCheckPeriodRaw());
+ }}}
+ };
+ [config] Value check_timeout;
+ [config] double check_interval {
+ default {{{ return 5 * 60; }}}
+ };
+ [config] double retry_interval {
+ default {{{ return 60; }}}
+ };
+ [config, navigation] name(EventCommand) event_command (EventCommandRaw) {
+ navigate {{{
+ return EventCommand::GetByName(GetEventCommandRaw());
+ }}}
+ };
+ [config] bool volatile;
+
+ [config] bool enable_active_checks {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_passive_checks {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_event_handler {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_notifications {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_flapping {
+ default {{{ return false; }}}
+ };
+ [config] bool enable_perfdata {
+ default {{{ return true; }}}
+ };
+
+ [config] array(String) flapping_ignore_states;
+ [no_user_view, no_user_modify] int flapping_ignore_states_filter_real (FlappingIgnoreStatesFilter);
+
+ [config, deprecated] double flapping_threshold;
+
+ [config] double flapping_threshold_low {
+ default {{{ return 25; }}}
+ };
+
+ [config] double flapping_threshold_high{
+ default {{{ return 30; }}}
+ };
+
+ [config] String notes;
+ [config] String notes_url;
+ [config] String action_url;
+ [config] String icon_image;
+ [config] String icon_image_alt;
+
+ [state] Timestamp next_check;
+ [state, no_user_view, no_user_modify] Timestamp last_check_started;
+
+ [state] int check_attempt {
+ default {{{ return 1; }}}
+ };
+ [state, enum, no_user_view, no_user_modify] ServiceState state_raw {
+ default {{{ return ServiceUnknown; }}}
+ };
+ [state, enum] StateType state_type {
+ default {{{ return StateTypeSoft; }}}
+ };
+ [state, enum, no_user_view, no_user_modify] ServiceState last_state_raw {
+ default {{{ return ServiceUnknown; }}}
+ };
+ [state, enum, no_user_view, no_user_modify] ServiceState last_hard_state_raw {
+ default {{{ return ServiceUnknown; }}}
+ };
+ [state, no_user_view, no_user_modify] "unsigned short" last_hard_states_raw {
+ default {{{ return /* current */ 99 * 100 + /* previous */ 99; }}}
+ };
+ [state, no_user_view, no_user_modify] "unsigned short" last_soft_states_raw {
+ default {{{ return /* current */ 99 * 100 + /* previous */ 99; }}}
+ };
+ [state, enum] StateType last_state_type {
+ default {{{ return StateTypeSoft; }}}
+ };
+ [state] bool last_reachable {
+ default {{{ return true; }}}
+ };
+ [state] CheckResult::Ptr last_check_result;
+ [state] Timestamp last_state_change {
+ default {{{ return Application::GetStartTime(); }}}
+ };
+ [state] Timestamp last_hard_state_change {
+ default {{{ return Application::GetStartTime(); }}}
+ };
+ [state] Timestamp last_state_unreachable;
+
+ [state] Timestamp previous_state_change {
+ default {{{ return Application::GetStartTime(); }}}
+ };
+ [no_storage] int severity {
+ get;
+ };
+ [no_storage] bool problem {
+ get;
+ };
+ [no_storage] bool handled {
+ get;
+ };
+ [no_storage] Timestamp next_update {
+ get;
+ };
+
+ [state] bool force_next_check;
+ [state] int acknowledgement (AcknowledgementRaw) {
+ default {{{ return AcknowledgementNone; }}}
+ };
+ [state] Timestamp acknowledgement_expiry;
+ [state] Timestamp acknowledgement_last_change;
+ [state] bool force_next_notification;
+ [no_storage] Timestamp last_check {
+ get;
+ };
+ [no_storage] int downtime_depth {
+ get;
+ };
+
+ [state] double flapping_current {
+ default {{{ return 0; }}}
+ };
+ [state] Timestamp flapping_last_change;
+
+ [state, enum, no_user_view, no_user_modify] ServiceState flapping_last_state {
+ default {{{ return ServiceUnknown; }}}
+ };
+ [state, no_user_view, no_user_modify] int flapping_buffer;
+ [state, no_user_view, no_user_modify] int flapping_index;
+ [state, protected] bool flapping;
+ [state, no_user_view, no_user_modify] int suppressed_notifications {
+ default {{{ return 0; }}}
+ };
+ [state, enum, no_user_view, no_user_modify] ServiceState state_before_suppression {
+ default {{{ return ServiceOK; }}}
+ };
+
+ [config, navigation] name(Endpoint) command_endpoint (CommandEndpointRaw) {
+ navigate {{{
+ return Endpoint::GetByName(GetCommandEndpointRaw());
+ }}}
+ };
+
+ [state, no_user_modify] Dictionary::Ptr executions;
+ [state, no_user_view, no_user_modify] Dictionary::Ptr pending_executions;
+};
+
+}
diff --git a/lib/icinga/checkcommand.cpp b/lib/icinga/checkcommand.cpp
new file mode 100644
index 0000000..fb8032a
--- /dev/null
+++ b/lib/icinga/checkcommand.cpp
@@ -0,0 +1,22 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkcommand.hpp"
+#include "icinga/checkcommand-ti.cpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(CheckCommand);
+
+thread_local CheckCommand::Ptr CheckCommand::ExecuteOverride;
+
+void CheckCommand::Execute(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ GetExecute()->Invoke({
+ checkable,
+ cr,
+ resolvedMacros,
+ useResolvedMacros
+ });
+}
diff --git a/lib/icinga/checkcommand.hpp b/lib/icinga/checkcommand.hpp
new file mode 100644
index 0000000..c654cf9
--- /dev/null
+++ b/lib/icinga/checkcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CHECKCOMMAND_H
+#define CHECKCOMMAND_H
+
+#include "icinga/checkcommand-ti.hpp"
+#include "icinga/checkable.hpp"
+
+namespace icinga
+{
+
+/**
+ * A command.
+ *
+ * @ingroup icinga
+ */
+class CheckCommand final : public ObjectImpl<CheckCommand>
+{
+public:
+ DECLARE_OBJECT(CheckCommand);
+ DECLARE_OBJECTNAME(CheckCommand);
+
+ static thread_local CheckCommand::Ptr ExecuteOverride;
+
+ void Execute(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false);
+};
+
+}
+
+#endif /* CHECKCOMMAND_H */
diff --git a/lib/icinga/checkcommand.ti b/lib/icinga/checkcommand.ti
new file mode 100644
index 0000000..c211f0f
--- /dev/null
+++ b/lib/icinga/checkcommand.ti
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/command.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class CheckCommand : Command
+{
+};
+
+}
diff --git a/lib/icinga/checkresult.cpp b/lib/icinga/checkresult.cpp
new file mode 100644
index 0000000..07f7219
--- /dev/null
+++ b/lib/icinga/checkresult.cpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkresult.hpp"
+#include "icinga/checkresult-ti.cpp"
+#include "base/scriptglobal.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(CheckResult);
+
+INITIALIZE_ONCE([]() {
+ ScriptGlobal::Set("Icinga.ServiceOK", ServiceOK);
+ ScriptGlobal::Set("Icinga.ServiceWarning", ServiceWarning);
+ ScriptGlobal::Set("Icinga.ServiceCritical", ServiceCritical);
+ ScriptGlobal::Set("Icinga.ServiceUnknown", ServiceUnknown);
+
+ ScriptGlobal::Set("Icinga.HostUp", HostUp);
+ ScriptGlobal::Set("Icinga.HostDown", HostDown);
+})
+
+double CheckResult::CalculateExecutionTime() const
+{
+ return GetExecutionEnd() - GetExecutionStart();
+}
+
+double CheckResult::CalculateLatency() const
+{
+ double latency = (GetScheduleEnd() - GetScheduleStart()) - CalculateExecutionTime();
+
+ if (latency < 0)
+ latency = 0;
+
+ return latency;
+}
diff --git a/lib/icinga/checkresult.hpp b/lib/icinga/checkresult.hpp
new file mode 100644
index 0000000..ac54d6b
--- /dev/null
+++ b/lib/icinga/checkresult.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CHECKRESULT_H
+#define CHECKRESULT_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkresult-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * A check result.
+ *
+ * @ingroup icinga
+ */
+class CheckResult final : public ObjectImpl<CheckResult>
+{
+public:
+ DECLARE_OBJECT(CheckResult);
+
+ double CalculateExecutionTime() const;
+ double CalculateLatency() const;
+};
+
+}
+
+#endif /* CHECKRESULT_H */
diff --git a/lib/icinga/checkresult.ti b/lib/icinga/checkresult.ti
new file mode 100644
index 0000000..09312dc
--- /dev/null
+++ b/lib/icinga/checkresult.ti
@@ -0,0 +1,72 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+/**
+ * The state of a host.
+ *
+ * @ingroup icinga
+ */
+enum HostState
+{
+ HostUp = 0,
+ HostDown = 1
+};
+
+/**
+ * The state of a service.
+ *
+ * @ingroup icinga
+ */
+enum ServiceState
+{
+ ServiceOK = 0,
+ ServiceWarning = 1,
+ ServiceCritical = 2,
+ ServiceUnknown = 3
+};
+
+/**
+ * The state type of a host or service.
+ *
+ * @ingroup icinga
+ */
+enum StateType
+{
+ StateTypeSoft = 0,
+ StateTypeHard = 1
+};
+}}}
+
+class CheckResult
+{
+ [state] Timestamp schedule_start;
+ [state] Timestamp schedule_end;
+ [state] Timestamp execution_start;
+ [state] Timestamp execution_end;
+
+ [state] Value command;
+ [state] int exit_status;
+
+ [state, enum] ServiceState "state";
+ [state, enum] ServiceState previous_hard_state;
+ [state] String output;
+ [state] Array::Ptr performance_data;
+
+ [state] bool active {
+ default {{{ return true; }}}
+ };
+
+ [state] String check_source;
+ [state] String scheduling_source;
+ [state] double ttl;
+
+ [state] Dictionary::Ptr vars_before;
+ [state] Dictionary::Ptr vars_after;
+};
+
+}
diff --git a/lib/icinga/cib.cpp b/lib/icinga/cib.cpp
new file mode 100644
index 0000000..ce71a59
--- /dev/null
+++ b/lib/icinga/cib.cpp
@@ -0,0 +1,346 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/cib.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/clusterevents.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/configtype.hpp"
+#include "base/statsfunction.hpp"
+
+using namespace icinga;
+
+RingBuffer CIB::m_ActiveHostChecksStatistics(15 * 60);
+RingBuffer CIB::m_ActiveServiceChecksStatistics(15 * 60);
+RingBuffer CIB::m_PassiveHostChecksStatistics(15 * 60);
+RingBuffer CIB::m_PassiveServiceChecksStatistics(15 * 60);
+
+void CIB::UpdateActiveHostChecksStatistics(long tv, int num)
+{
+ m_ActiveHostChecksStatistics.InsertValue(tv, num);
+}
+
+void CIB::UpdateActiveServiceChecksStatistics(long tv, int num)
+{
+ m_ActiveServiceChecksStatistics.InsertValue(tv, num);
+}
+
+int CIB::GetActiveHostChecksStatistics(long timespan)
+{
+ return m_ActiveHostChecksStatistics.UpdateAndGetValues(Utility::GetTime(), timespan);
+}
+
+int CIB::GetActiveServiceChecksStatistics(long timespan)
+{
+ return m_ActiveServiceChecksStatistics.UpdateAndGetValues(Utility::GetTime(), timespan);
+}
+
+void CIB::UpdatePassiveHostChecksStatistics(long tv, int num)
+{
+ m_PassiveServiceChecksStatistics.InsertValue(tv, num);
+}
+
+void CIB::UpdatePassiveServiceChecksStatistics(long tv, int num)
+{
+ m_PassiveServiceChecksStatistics.InsertValue(tv, num);
+}
+
+int CIB::GetPassiveHostChecksStatistics(long timespan)
+{
+ return m_PassiveHostChecksStatistics.UpdateAndGetValues(Utility::GetTime(), timespan);
+}
+
+int CIB::GetPassiveServiceChecksStatistics(long timespan)
+{
+ return m_PassiveServiceChecksStatistics.UpdateAndGetValues(Utility::GetTime(), timespan);
+}
+
+CheckableCheckStatistics CIB::CalculateHostCheckStats()
+{
+ double min_latency = -1, max_latency = 0, sum_latency = 0;
+ int count_latency = 0;
+ double min_execution_time = -1, max_execution_time = 0, sum_execution_time = 0;
+ int count_execution_time = 0;
+ bool checkresult = false;
+
+ for (const Host::Ptr& host : ConfigType::GetObjectsByType<Host>()) {
+ ObjectLock olock(host);
+
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (!cr)
+ continue;
+
+ /* set to true, we have a checkresult */
+ checkresult = true;
+
+ /* latency */
+ double latency = cr->CalculateLatency();
+
+ if (min_latency == -1 || latency < min_latency)
+ min_latency = latency;
+
+ if (latency > max_latency)
+ max_latency = latency;
+
+ sum_latency += latency;
+ count_latency++;
+
+ /* execution_time */
+ double execution_time = cr->CalculateExecutionTime();
+
+ if (min_execution_time == -1 || execution_time < min_execution_time)
+ min_execution_time = execution_time;
+
+ if (execution_time > max_execution_time)
+ max_execution_time = execution_time;
+
+ sum_execution_time += execution_time;
+ count_execution_time++;
+ }
+
+ if (!checkresult) {
+ min_latency = 0;
+ min_execution_time = 0;
+ }
+
+ CheckableCheckStatistics ccs;
+
+ ccs.min_latency = min_latency;
+ ccs.max_latency = max_latency;
+ ccs.avg_latency = sum_latency / count_latency;
+ ccs.min_execution_time = min_execution_time;
+ ccs.max_execution_time = max_execution_time;
+ ccs.avg_execution_time = sum_execution_time / count_execution_time;
+
+ return ccs;
+}
+
+CheckableCheckStatistics CIB::CalculateServiceCheckStats()
+{
+ double min_latency = -1, max_latency = 0, sum_latency = 0;
+ int count_latency = 0;
+ double min_execution_time = -1, max_execution_time = 0, sum_execution_time = 0;
+ int count_execution_time = 0;
+ bool checkresult = false;
+
+ for (const Service::Ptr& service : ConfigType::GetObjectsByType<Service>()) {
+ ObjectLock olock(service);
+
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (!cr)
+ continue;
+
+ /* set to true, we have a checkresult */
+ checkresult = true;
+
+ /* latency */
+ double latency = cr->CalculateLatency();
+
+ if (min_latency == -1 || latency < min_latency)
+ min_latency = latency;
+
+ if (latency > max_latency)
+ max_latency = latency;
+
+ sum_latency += latency;
+ count_latency++;
+
+ /* execution_time */
+ double execution_time = cr->CalculateExecutionTime();
+
+ if (min_execution_time == -1 || execution_time < min_execution_time)
+ min_execution_time = execution_time;
+
+ if (execution_time > max_execution_time)
+ max_execution_time = execution_time;
+
+ sum_execution_time += execution_time;
+ count_execution_time++;
+ }
+
+ if (!checkresult) {
+ min_latency = 0;
+ min_execution_time = 0;
+ }
+
+ CheckableCheckStatistics ccs;
+
+ ccs.min_latency = min_latency;
+ ccs.max_latency = max_latency;
+ ccs.avg_latency = sum_latency / count_latency;
+ ccs.min_execution_time = min_execution_time;
+ ccs.max_execution_time = max_execution_time;
+ ccs.avg_execution_time = sum_execution_time / count_execution_time;
+
+ return ccs;
+}
+
+ServiceStatistics CIB::CalculateServiceStats()
+{
+ ServiceStatistics ss = {};
+
+ for (const Service::Ptr& service : ConfigType::GetObjectsByType<Service>()) {
+ ObjectLock olock(service);
+
+ if (service->GetState() == ServiceOK)
+ ss.services_ok++;
+ if (service->GetState() == ServiceWarning)
+ ss.services_warning++;
+ if (service->GetState() == ServiceCritical)
+ ss.services_critical++;
+ if (service->GetState() == ServiceUnknown)
+ ss.services_unknown++;
+
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (!cr)
+ ss.services_pending++;
+
+ if (!service->IsReachable())
+ ss.services_unreachable++;
+
+ if (service->IsFlapping())
+ ss.services_flapping++;
+ if (service->IsInDowntime())
+ ss.services_in_downtime++;
+ if (service->IsAcknowledged())
+ ss.services_acknowledged++;
+
+ if (service->GetHandled())
+ ss.services_handled++;
+ if (service->GetProblem())
+ ss.services_problem++;
+ }
+
+ return ss;
+}
+
+HostStatistics CIB::CalculateHostStats()
+{
+ HostStatistics hs = {};
+
+ for (const Host::Ptr& host : ConfigType::GetObjectsByType<Host>()) {
+ ObjectLock olock(host);
+
+ if (host->IsReachable()) {
+ if (host->GetState() == HostUp)
+ hs.hosts_up++;
+ if (host->GetState() == HostDown)
+ hs.hosts_down++;
+ } else
+ hs.hosts_unreachable++;
+
+ if (!host->GetLastCheckResult())
+ hs.hosts_pending++;
+
+ if (host->IsFlapping())
+ hs.hosts_flapping++;
+ if (host->IsInDowntime())
+ hs.hosts_in_downtime++;
+ if (host->IsAcknowledged())
+ hs.hosts_acknowledged++;
+
+ if (host->GetHandled())
+ hs.hosts_handled++;
+ if (host->GetProblem())
+ hs.hosts_problem++;
+ }
+
+ return hs;
+}
+
+/*
+ * 'perfdata' must be a flat dictionary with double values
+ * 'status' dictionary can contain multiple levels of dictionaries
+ */
+std::pair<Dictionary::Ptr, Array::Ptr> CIB::GetFeatureStats()
+{
+ Dictionary::Ptr status = new Dictionary();
+ Array::Ptr perfdata = new Array();
+
+ Namespace::Ptr statsFunctions = ScriptGlobal::Get("StatsFunctions", &Empty);
+
+ if (statsFunctions) {
+ ObjectLock olock(statsFunctions);
+
+ for (const Namespace::Pair& kv : statsFunctions)
+ static_cast<Function::Ptr>(kv.second.Val)->Invoke({ status, perfdata });
+ }
+
+ return std::make_pair(status, perfdata);
+}
+
+REGISTER_STATSFUNCTION(CIB, &CIB::StatsFunc);
+
+void CIB::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata) {
+ double interval = Utility::GetTime() - Application::GetStartTime();
+
+ if (interval > 60)
+ interval = 60;
+
+ status->Set("active_host_checks", GetActiveHostChecksStatistics(interval) / interval);
+ status->Set("passive_host_checks", GetPassiveHostChecksStatistics(interval) / interval);
+ status->Set("active_host_checks_1min", GetActiveHostChecksStatistics(60));
+ status->Set("passive_host_checks_1min", GetPassiveHostChecksStatistics(60));
+ status->Set("active_host_checks_5min", GetActiveHostChecksStatistics(60 * 5));
+ status->Set("passive_host_checks_5min", GetPassiveHostChecksStatistics(60 * 5));
+ status->Set("active_host_checks_15min", GetActiveHostChecksStatistics(60 * 15));
+ status->Set("passive_host_checks_15min", GetPassiveHostChecksStatistics(60 * 15));
+
+ status->Set("active_service_checks", GetActiveServiceChecksStatistics(interval) / interval);
+ status->Set("passive_service_checks", GetPassiveServiceChecksStatistics(interval) / interval);
+ status->Set("active_service_checks_1min", GetActiveServiceChecksStatistics(60));
+ status->Set("passive_service_checks_1min", GetPassiveServiceChecksStatistics(60));
+ status->Set("active_service_checks_5min", GetActiveServiceChecksStatistics(60 * 5));
+ status->Set("passive_service_checks_5min", GetPassiveServiceChecksStatistics(60 * 5));
+ status->Set("active_service_checks_15min", GetActiveServiceChecksStatistics(60 * 15));
+ status->Set("passive_service_checks_15min", GetPassiveServiceChecksStatistics(60 * 15));
+
+ // Checker related stats
+ status->Set("remote_check_queue", ClusterEvents::GetCheckRequestQueueSize());
+ status->Set("current_pending_callbacks", Application::GetTP().GetPending());
+ status->Set("current_concurrent_checks", Checkable::CurrentConcurrentChecks.load());
+
+ CheckableCheckStatistics scs = CalculateServiceCheckStats();
+
+ status->Set("min_latency", scs.min_latency);
+ status->Set("max_latency", scs.max_latency);
+ status->Set("avg_latency", scs.avg_latency);
+ status->Set("min_execution_time", scs.min_execution_time);
+ status->Set("max_execution_time", scs.max_execution_time);
+ status->Set("avg_execution_time", scs.avg_execution_time);
+
+ ServiceStatistics ss = CalculateServiceStats();
+
+ status->Set("num_services_ok", ss.services_ok);
+ status->Set("num_services_warning", ss.services_warning);
+ status->Set("num_services_critical", ss.services_critical);
+ status->Set("num_services_unknown", ss.services_unknown);
+ status->Set("num_services_pending", ss.services_pending);
+ status->Set("num_services_unreachable", ss.services_unreachable);
+ status->Set("num_services_flapping", ss.services_flapping);
+ status->Set("num_services_in_downtime", ss.services_in_downtime);
+ status->Set("num_services_acknowledged", ss.services_acknowledged);
+ status->Set("num_services_handled", ss.services_handled);
+ status->Set("num_services_problem", ss.services_problem);
+
+ double uptime = Application::GetUptime();
+ status->Set("uptime", uptime);
+
+ HostStatistics hs = CalculateHostStats();
+
+ status->Set("num_hosts_up", hs.hosts_up);
+ status->Set("num_hosts_down", hs.hosts_down);
+ status->Set("num_hosts_pending", hs.hosts_pending);
+ status->Set("num_hosts_unreachable", hs.hosts_unreachable);
+ status->Set("num_hosts_flapping", hs.hosts_flapping);
+ status->Set("num_hosts_in_downtime", hs.hosts_in_downtime);
+ status->Set("num_hosts_acknowledged", hs.hosts_acknowledged);
+ status->Set("num_hosts_handled", hs.hosts_handled);
+ status->Set("num_hosts_problem", hs.hosts_problem);
+}
diff --git a/lib/icinga/cib.hpp b/lib/icinga/cib.hpp
new file mode 100644
index 0000000..00461e3
--- /dev/null
+++ b/lib/icinga/cib.hpp
@@ -0,0 +1,91 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CIB_H
+#define CIB_H
+
+#include "icinga/i2-icinga.hpp"
+#include "base/ringbuffer.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+
+namespace icinga
+{
+
+struct CheckableCheckStatistics {
+ double min_latency;
+ double max_latency;
+ double avg_latency;
+ double min_execution_time;
+ double max_execution_time;
+ double avg_execution_time;
+};
+
+struct ServiceStatistics {
+ double services_ok;
+ double services_warning;
+ double services_critical;
+ double services_unknown;
+ double services_pending;
+ double services_unreachable;
+ double services_flapping;
+ double services_in_downtime;
+ double services_acknowledged;
+ double services_handled;
+ double services_problem;
+};
+
+struct HostStatistics {
+ double hosts_up;
+ double hosts_down;
+ double hosts_unreachable;
+ double hosts_pending;
+ double hosts_flapping;
+ double hosts_in_downtime;
+ double hosts_acknowledged;
+ double hosts_handled;
+ double hosts_problem;
+};
+
+/**
+ * Common Information Base class. Holds some statistics (and will likely be
+ * removed/refactored).
+ *
+ * @ingroup icinga
+ */
+class CIB
+{
+public:
+ static void UpdateActiveHostChecksStatistics(long tv, int num);
+ static int GetActiveHostChecksStatistics(long timespan);
+
+ static void UpdateActiveServiceChecksStatistics(long tv, int num);
+ static int GetActiveServiceChecksStatistics(long timespan);
+
+ static void UpdatePassiveHostChecksStatistics(long tv, int num);
+ static int GetPassiveHostChecksStatistics(long timespan);
+
+ static void UpdatePassiveServiceChecksStatistics(long tv, int num);
+ static int GetPassiveServiceChecksStatistics(long timespan);
+
+ static CheckableCheckStatistics CalculateHostCheckStats();
+ static CheckableCheckStatistics CalculateServiceCheckStats();
+ static HostStatistics CalculateHostStats();
+ static ServiceStatistics CalculateServiceStats();
+
+ static std::pair<Dictionary::Ptr, Array::Ptr> GetFeatureStats();
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+private:
+ CIB();
+
+ static std::mutex m_Mutex;
+ static RingBuffer m_ActiveHostChecksStatistics;
+ static RingBuffer m_PassiveHostChecksStatistics;
+ static RingBuffer m_ActiveServiceChecksStatistics;
+ static RingBuffer m_PassiveServiceChecksStatistics;
+};
+
+}
+
+#endif /* CIB_H */
diff --git a/lib/icinga/clusterevents-check.cpp b/lib/icinga/clusterevents-check.cpp
new file mode 100644
index 0000000..40325b4
--- /dev/null
+++ b/lib/icinga/clusterevents-check.cpp
@@ -0,0 +1,379 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/clusterevents.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "remote/apilistener.hpp"
+#include "base/configuration.hpp"
+#include "base/defer.hpp"
+#include "base/serializer.hpp"
+#include "base/exception.hpp"
+#include <boost/thread/once.hpp>
+#include <thread>
+
+using namespace icinga;
+
+std::mutex ClusterEvents::m_Mutex;
+std::deque<std::function<void ()>> ClusterEvents::m_CheckRequestQueue;
+bool ClusterEvents::m_CheckSchedulerRunning;
+int ClusterEvents::m_ChecksExecutedDuringInterval;
+int ClusterEvents::m_ChecksDroppedDuringInterval;
+Timer::Ptr ClusterEvents::m_LogTimer;
+
+void ClusterEvents::RemoteCheckThreadProc()
+{
+ Utility::SetThreadName("Remote Check Scheduler");
+
+ int maxConcurrentChecks = IcingaApplication::GetInstance()->GetMaxConcurrentChecks();
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ for(;;) {
+ if (m_CheckRequestQueue.empty())
+ break;
+
+ lock.unlock();
+ Checkable::AquirePendingCheckSlot(maxConcurrentChecks);
+ lock.lock();
+
+ auto callback = m_CheckRequestQueue.front();
+ m_CheckRequestQueue.pop_front();
+ m_ChecksExecutedDuringInterval++;
+ lock.unlock();
+
+ callback();
+ Checkable::DecreasePendingChecks();
+
+ lock.lock();
+ }
+
+ m_CheckSchedulerRunning = false;
+}
+
+void ClusterEvents::EnqueueCheck(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, []() {
+ m_LogTimer = Timer::Create();
+ m_LogTimer->SetInterval(10);
+ m_LogTimer->OnTimerExpired.connect([](const Timer * const&) { LogRemoteCheckQueueInformation(); });
+ m_LogTimer->Start();
+ });
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ if (m_CheckRequestQueue.size() >= 25000) {
+ m_ChecksDroppedDuringInterval++;
+ return;
+ }
+
+ m_CheckRequestQueue.emplace_back([origin, params]() { ExecuteCheckFromQueue(origin, params); });
+
+ if (!m_CheckSchedulerRunning) {
+ std::thread t(ClusterEvents::RemoteCheckThreadProc);
+ t.detach();
+ m_CheckSchedulerRunning = true;
+ }
+}
+
+static void SendEventExecutedCommand(const Dictionary::Ptr& params, long exitStatus, const String& output,
+ double start, double end, const ApiListener::Ptr& listener, const MessageOrigin::Ptr& origin,
+ const Endpoint::Ptr& sourceEndpoint)
+{
+ Dictionary::Ptr executedParams = new Dictionary();
+ executedParams->Set("execution", params->Get("source"));
+ executedParams->Set("host", params->Get("host"));
+
+ if (params->Contains("service"))
+ executedParams->Set("service", params->Get("service"));
+
+ executedParams->Set("exit", exitStatus);
+ executedParams->Set("output", output);
+ executedParams->Set("start", start);
+ executedParams->Set("end", end);
+
+ if (origin->IsLocal()) {
+ ClusterEvents::ExecutedCommandAPIHandler(origin, executedParams);
+ } else {
+ Dictionary::Ptr executedMessage = new Dictionary();
+ executedMessage->Set("jsonrpc", "2.0");
+ executedMessage->Set("method", "event::ExecutedCommand");
+ executedMessage->Set("params", executedParams);
+
+ listener->SyncSendMessage(sourceEndpoint, executedMessage);
+ }
+}
+
+void ClusterEvents::ExecuteCheckFromQueue(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params) {
+
+ Endpoint::Ptr sourceEndpoint;
+
+ if (origin->FromClient) {
+ sourceEndpoint = origin->FromClient->GetEndpoint();
+ } else if (origin->IsLocal()){
+ sourceEndpoint = Endpoint::GetLocalEndpoint();
+ }
+
+ if (!sourceEndpoint || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone))) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'execute command' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return;
+ }
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener) {
+ Log(LogCritical, "ApiListener") << "No instance available.";
+ return;
+ }
+
+ Defer resetExecuteCommandProcessFinishedHandler ([]() {
+ Checkable::ExecuteCommandProcessFinishedHandler = nullptr;
+ });
+
+ if (params->Contains("source")) {
+ String uuid = params->Get("source");
+
+ String checkableName = params->Get("host");
+
+ if (params->Contains("service"))
+ checkableName += "!" + params->Get("service");
+
+ /* Check deadline */
+ double deadline = params->Get("deadline");
+
+ if (Utility::GetTime() > deadline) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'ExecuteCheckFromQueue' event for checkable '" << checkableName
+ << "' from '" << origin->FromClient->GetIdentity() << "': Deadline has expired.";
+ return;
+ }
+
+ Checkable::ExecuteCommandProcessFinishedHandler = [checkableName, listener, sourceEndpoint, origin, params] (const Value& commandLine, const ProcessResult& pr) {
+ if (params->Get("command_type") == "check_command") {
+ Checkable::CurrentConcurrentChecks.fetch_sub(1);
+ Checkable::DecreasePendingChecks();
+ }
+
+ if (pr.ExitStatus > 3) {
+ Process::Arguments parguments = Process::PrepareCommand(commandLine);
+ Log(LogWarning, "ApiListener")
+ << "Command for object '" << checkableName << "' (PID: " << pr.PID
+ << ", arguments: " << Process::PrettyPrintArguments(parguments) << ") terminated with exit code "
+ << pr.ExitStatus << ", output: " << pr.Output;
+ }
+
+ SendEventExecutedCommand(params, pr.ExitStatus, pr.Output, pr.ExecutionStart, pr.ExecutionEnd, listener,
+ origin, sourceEndpoint);
+ };
+ }
+
+ if (!listener->GetAcceptCommands() && !origin->IsLocal()) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring command. '" << listener->GetName() << "' does not accept commands.";
+
+ String output = "Endpoint '" + Endpoint::GetLocalEndpoint()->GetName() + "' does not accept commands.";
+
+ if (params->Contains("source")) {
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, 126, output, now, now, listener, origin, sourceEndpoint);
+ } else {
+ Host::Ptr host = new Host();
+ Dictionary::Ptr attrs = new Dictionary();
+
+ attrs->Set("__name", params->Get("host"));
+ attrs->Set("type", "Host");
+ attrs->Set("enable_active_checks", false);
+
+ Deserialize(host, attrs, false, FAConfig);
+
+ if (params->Contains("service"))
+ host->SetExtension("agent_service_name", params->Get("service"));
+
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetState(ServiceUnknown);
+ cr->SetOutput(output);
+
+ Dictionary::Ptr message = MakeCheckResultMessage(host, cr);
+ listener->SyncSendMessage(sourceEndpoint, message);
+ }
+
+ return;
+ }
+
+ /* use a virtual host object for executing the command */
+ Host::Ptr host = new Host();
+ Dictionary::Ptr attrs = new Dictionary();
+
+ attrs->Set("__name", params->Get("host"));
+ attrs->Set("type", "Host");
+
+ /*
+ * Override the check timeout if the parent caller provided the value. Compatible with older versions not
+ * passing this inside the cluster message.
+ * This happens with host/service command_endpoint agents and the 'check_timeout' attribute being specified.
+ */
+ if (params->Contains("check_timeout"))
+ attrs->Set("check_timeout", params->Get("check_timeout"));
+
+ Deserialize(host, attrs, false, FAConfig);
+
+ if (params->Contains("service"))
+ host->SetExtension("agent_service_name", params->Get("service"));
+
+ String command = params->Get("command");
+ String command_type = params->Get("command_type");
+
+ if (command_type == "check_command") {
+ if (!CheckCommand::GetByName(command)) {
+ ServiceState state = ServiceUnknown;
+ String output = "Check command '" + command + "' does not exist.";
+ double now = Utility::GetTime();
+
+ if (params->Contains("source")) {
+ SendEventExecutedCommand(params, state, output, now, now, listener, origin, sourceEndpoint);
+ } else {
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetState(state);
+ cr->SetOutput(output);
+ Dictionary::Ptr message = MakeCheckResultMessage(host, cr);
+ listener->SyncSendMessage(sourceEndpoint, message);
+ }
+
+ return;
+ }
+ } else if (command_type == "event_command") {
+ if (!EventCommand::GetByName(command)) {
+ String output = "Event command '" + command + "' does not exist.";
+ Log(LogWarning, "ClusterEvents") << output;
+
+ if (params->Contains("source")) {
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, ServiceUnknown, output, now, now, listener, origin, sourceEndpoint);
+ }
+
+ return;
+ }
+ } else if (command_type == "notification_command") {
+ if (!NotificationCommand::GetByName(command)) {
+ String output = "Notification command '" + command + "' does not exist.";
+ Log(LogWarning, "ClusterEvents") << output;
+
+ if (params->Contains("source")) {
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, ServiceUnknown, output, now, now, listener, origin, sourceEndpoint);
+ }
+
+ return;
+ }
+ }
+
+ attrs->Set(command_type, params->Get("command"));
+ attrs->Set("command_endpoint", sourceEndpoint->GetName());
+
+ Deserialize(host, attrs, false, FAConfig);
+
+ host->SetExtension("agent_check", true);
+
+ Dictionary::Ptr macros = params->Get("macros");
+
+ if (command_type == "check_command") {
+ try {
+ host->ExecuteRemoteCheck(macros);
+ } catch (const std::exception& ex) {
+ String output = "Exception occurred while checking '" + host->GetName() + "': " + DiagnosticInformation(ex);
+ ServiceState state = ServiceUnknown;
+ double now = Utility::GetTime();
+
+ if (params->Contains("source")) {
+ SendEventExecutedCommand(params, state, output, now, now, listener, origin, sourceEndpoint);
+ } else {
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetState(state);
+ cr->SetOutput(output);
+ cr->SetScheduleStart(now);
+ cr->SetScheduleEnd(now);
+ cr->SetExecutionStart(now);
+ cr->SetExecutionEnd(now);
+
+ Dictionary::Ptr message = MakeCheckResultMessage(host, cr);
+ listener->SyncSendMessage(sourceEndpoint, message);
+ }
+
+ Log(LogCritical, "checker", output);
+ }
+ } else if (command_type == "event_command") {
+ try {
+ host->ExecuteEventHandler(macros, true);
+ } catch (const std::exception& ex) {
+ if (params->Contains("source")) {
+ String output = "Exception occurred while executing event command '" + command + "' for '" +
+ host->GetName() + "': " + DiagnosticInformation(ex);
+
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, ServiceUnknown, output, now, now, listener, origin, sourceEndpoint);
+ } else {
+ throw;
+ }
+ }
+ } else if (command_type == "notification_command" && params->Contains("source")) {
+ /* Get user */
+ User::Ptr user = new User();
+ Dictionary::Ptr attrs = new Dictionary();
+ attrs->Set("__name", params->Get("user"));
+ attrs->Set("type", User::GetTypeName());
+
+ Deserialize(user, attrs, false, FAConfig);
+
+ /* Get notification */
+ Notification::Ptr notification = new Notification();
+ attrs->Clear();
+ attrs->Set("__name", params->Get("notification"));
+ attrs->Set("type", Notification::GetTypeName());
+ attrs->Set("command", command);
+
+ Deserialize(notification, attrs, false, FAConfig);
+
+ try {
+ CheckResult::Ptr cr = new CheckResult();
+ String author = macros->Get("notification_author");
+ NotificationCommand::Ptr notificationCommand = NotificationCommand::GetByName(command);
+
+ notificationCommand->Execute(notification, user, cr, NotificationType::NotificationCustom,
+ author, "");
+ } catch (const std::exception& ex) {
+ String output = "Exception occurred during notification '" + notification->GetName()
+ + "' for checkable '" + notification->GetCheckable()->GetName()
+ + "' and user '" + user->GetName() + "' using command '" + command + "': "
+ + DiagnosticInformation(ex, false);
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, ServiceUnknown, output, now, now, listener, origin, sourceEndpoint);
+ }
+ }
+}
+
+int ClusterEvents::GetCheckRequestQueueSize()
+{
+ return m_CheckRequestQueue.size();
+}
+
+void ClusterEvents::LogRemoteCheckQueueInformation() {
+ if (m_ChecksDroppedDuringInterval > 0) {
+ Log(LogCritical, "ClusterEvents")
+ << "Remote check queue ran out of slots. "
+ << m_ChecksDroppedDuringInterval << " checks dropped.";
+ m_ChecksDroppedDuringInterval = 0;
+ }
+
+ if (m_ChecksExecutedDuringInterval == 0)
+ return;
+
+ Log(LogInformation, "RemoteCheckQueue")
+ << "items: " << m_CheckRequestQueue.size()
+ << ", rate: " << m_ChecksExecutedDuringInterval / 10 << "/s "
+ << "(" << m_ChecksExecutedDuringInterval * 6 << "/min "
+ << m_ChecksExecutedDuringInterval * 6 * 5 << "/5min "
+ << m_ChecksExecutedDuringInterval * 6 * 15 << "/15min" << ");";
+
+ m_ChecksExecutedDuringInterval = 0;
+}
diff --git a/lib/icinga/clusterevents.cpp b/lib/icinga/clusterevents.cpp
new file mode 100644
index 0000000..fe5167b
--- /dev/null
+++ b/lib/icinga/clusterevents.cpp
@@ -0,0 +1,1623 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/clusterevents.hpp"
+#include "icinga/service.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/messageorigin.hpp"
+#include "remote/zone.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/eventqueue.hpp"
+#include "base/application.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/exception.hpp"
+#include "base/initialize.hpp"
+#include "base/serializer.hpp"
+#include "base/json.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+INITIALIZE_ONCE(&ClusterEvents::StaticInitialize);
+
+REGISTER_APIFUNCTION(CheckResult, event, &ClusterEvents::CheckResultAPIHandler);
+REGISTER_APIFUNCTION(SetNextCheck, event, &ClusterEvents::NextCheckChangedAPIHandler);
+REGISTER_APIFUNCTION(SetLastCheckStarted, event, &ClusterEvents::LastCheckStartedChangedAPIHandler);
+REGISTER_APIFUNCTION(SetStateBeforeSuppression, event, &ClusterEvents::StateBeforeSuppressionChangedAPIHandler);
+REGISTER_APIFUNCTION(SetSuppressedNotifications, event, &ClusterEvents::SuppressedNotificationsChangedAPIHandler);
+REGISTER_APIFUNCTION(SetSuppressedNotificationTypes, event, &ClusterEvents::SuppressedNotificationTypesChangedAPIHandler);
+REGISTER_APIFUNCTION(SetNextNotification, event, &ClusterEvents::NextNotificationChangedAPIHandler);
+REGISTER_APIFUNCTION(UpdateLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler);
+REGISTER_APIFUNCTION(ClearLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserClearedAPIHandler);
+REGISTER_APIFUNCTION(SetForceNextCheck, event, &ClusterEvents::ForceNextCheckChangedAPIHandler);
+REGISTER_APIFUNCTION(SetForceNextNotification, event, &ClusterEvents::ForceNextNotificationChangedAPIHandler);
+REGISTER_APIFUNCTION(SetAcknowledgement, event, &ClusterEvents::AcknowledgementSetAPIHandler);
+REGISTER_APIFUNCTION(ClearAcknowledgement, event, &ClusterEvents::AcknowledgementClearedAPIHandler);
+REGISTER_APIFUNCTION(ExecuteCommand, event, &ClusterEvents::ExecuteCommandAPIHandler);
+REGISTER_APIFUNCTION(SendNotifications, event, &ClusterEvents::SendNotificationsAPIHandler);
+REGISTER_APIFUNCTION(NotificationSentUser, event, &ClusterEvents::NotificationSentUserAPIHandler);
+REGISTER_APIFUNCTION(NotificationSentToAllUsers, event, &ClusterEvents::NotificationSentToAllUsersAPIHandler);
+REGISTER_APIFUNCTION(ExecutedCommand, event, &ClusterEvents::ExecutedCommandAPIHandler);
+REGISTER_APIFUNCTION(UpdateExecutions, event, &ClusterEvents::UpdateExecutionsAPIHandler);
+REGISTER_APIFUNCTION(SetRemovalInfo, event, &ClusterEvents::SetRemovalInfoAPIHandler);
+
+void ClusterEvents::StaticInitialize()
+{
+ Checkable::OnNewCheckResult.connect(&ClusterEvents::CheckResultHandler);
+ Checkable::OnNextCheckChanged.connect(&ClusterEvents::NextCheckChangedHandler);
+ Checkable::OnLastCheckStartedChanged.connect(&ClusterEvents::LastCheckStartedChangedHandler);
+ Checkable::OnStateBeforeSuppressionChanged.connect(&ClusterEvents::StateBeforeSuppressionChangedHandler);
+ Checkable::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationsChangedHandler);
+ Notification::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationTypesChangedHandler);
+ Notification::OnNextNotificationChanged.connect(&ClusterEvents::NextNotificationChangedHandler);
+ Notification::OnLastNotifiedStatePerUserUpdated.connect(&ClusterEvents::LastNotifiedStatePerUserUpdatedHandler);
+ Notification::OnLastNotifiedStatePerUserCleared.connect(&ClusterEvents::LastNotifiedStatePerUserClearedHandler);
+ Checkable::OnForceNextCheckChanged.connect(&ClusterEvents::ForceNextCheckChangedHandler);
+ Checkable::OnForceNextNotificationChanged.connect(&ClusterEvents::ForceNextNotificationChangedHandler);
+ Checkable::OnNotificationsRequested.connect(&ClusterEvents::SendNotificationsHandler);
+ Checkable::OnNotificationSentToUser.connect(&ClusterEvents::NotificationSentUserHandler);
+ Checkable::OnNotificationSentToAllUsers.connect(&ClusterEvents::NotificationSentToAllUsersHandler);
+
+ Checkable::OnAcknowledgementSet.connect(&ClusterEvents::AcknowledgementSetHandler);
+ Checkable::OnAcknowledgementCleared.connect(&ClusterEvents::AcknowledgementClearedHandler);
+
+ Comment::OnRemovalInfoChanged.connect(&ClusterEvents::SetRemovalInfoHandler);
+ Downtime::OnRemovalInfoChanged.connect(&ClusterEvents::SetRemovalInfoHandler);
+}
+
+Dictionary::Ptr ClusterEvents::MakeCheckResultMessage(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::CheckResult");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ else {
+ Value agent_service_name = checkable->GetExtension("agent_service_name");
+
+ if (!agent_service_name.IsEmpty())
+ params->Set("service", agent_service_name);
+ }
+ params->Set("cr", Serialize(cr));
+
+ message->Set("params", params);
+
+ return message;
+}
+
+void ClusterEvents::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr message = MakeCheckResultMessage(checkable, cr);
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::CheckResultAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'check result' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ CheckResult::Ptr cr;
+ Array::Ptr vperf;
+
+ if (params->Contains("cr")) {
+ cr = new CheckResult();
+ Dictionary::Ptr vcr = params->Get("cr");
+
+ if (vcr && vcr->Contains("performance_data")) {
+ vperf = vcr->Get("performance_data");
+
+ if (vperf)
+ vcr->Remove("performance_data");
+
+ Deserialize(cr, vcr, true);
+ }
+ }
+
+ if (!cr)
+ return Empty;
+
+ ArrayData rperf;
+
+ if (vperf) {
+ ObjectLock olock(vperf);
+ for (const Value& vp : vperf) {
+ Value p;
+
+ if (vp.IsObjectType<Dictionary>()) {
+ PerfdataValue::Ptr val = new PerfdataValue();
+ Deserialize(val, vp, true);
+ rperf.push_back(val);
+ } else
+ rperf.push_back(vp);
+ }
+ }
+
+ cr->SetPerformanceData(new Array(std::move(rperf)));
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable) && endpoint != checkable->GetCommandEndpoint()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'check result' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ if (!checkable->IsPaused() && Zone::GetLocalZone() == checkable->GetZone() && endpoint == checkable->GetCommandEndpoint())
+ checkable->ProcessCheckResult(cr);
+ else
+ checkable->ProcessCheckResult(cr, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::NextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("next_check", checkable->GetNextCheck());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetNextCheck");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::NextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'next check changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'next check changed' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ double nextCheck = params->Get("next_check");
+
+ if (nextCheck < Application::GetStartTime() + 60)
+ return Empty;
+
+ checkable->SetNextCheck(params->Get("next_check"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::LastCheckStartedChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("last_check_started", checkable->GetLastCheckStarted());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetLastCheckStarted");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::LastCheckStartedChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last_check_started changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last_check_started changed' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetLastCheckStarted(params->Get("last_check_started"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::StateBeforeSuppressionChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("state_before_suppression", checkable->GetStateBeforeSuppression());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetStateBeforeSuppression");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::StateBeforeSuppressionChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'state before suppression changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'state before suppression changed' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetStateBeforeSuppression(ServiceState(int(params->Get("state_before_suppression"))), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::SuppressedNotificationsChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("suppressed_notifications", checkable->GetSuppressedNotifications());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetSuppressedNotifications");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::SuppressedNotificationsChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'suppressed notifications changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'suppressed notifications changed' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetSuppressedNotifications(params->Get("suppressed_notifications"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::SuppressedNotificationTypesChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+ params->Set("suppressed_notifications", notification->GetSuppressedNotifications());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetSuppressedNotificationTypes");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::SuppressedNotificationTypesChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'suppressed notifications changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ auto notification (Notification::GetByName(params->Get("notification")));
+
+ if (!notification)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'suppressed notification types changed' message for notification '" << notification->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ notification->SetSuppressedNotifications(params->Get("suppressed_notifications"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::NextNotificationChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+ params->Set("next_notification", notification->GetNextNotification());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetNextNotification");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::NextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'next notification changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Notification::Ptr notification = Notification::GetByName(params->Get("notification"));
+
+ if (!notification)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(notification)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'next notification changed' message for notification '" << notification->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ double nextNotification = params->Get("next_notification");
+
+ if (nextNotification < Utility::GetTime())
+ return Empty;
+
+ notification->SetNextNotification(nextNotification, false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin)
+{
+ auto listener (ApiListener::GetInstance());
+
+ if (!listener) {
+ return;
+ }
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+ params->Set("user", user);
+ params->Set("state", state);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::UpdateLastNotifiedStatePerUser");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ auto endpoint (origin->FromClient->GetEndpoint());
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user updated' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user updated' message from '"
+ << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+
+ return Empty;
+ }
+
+ auto notification (Notification::GetByName(params->Get("notification")));
+
+ if (!notification) {
+ return Empty;
+ }
+
+ auto state (params->Get("state"));
+
+ if (!state.IsNumber()) {
+ return Empty;
+ }
+
+ notification->GetLastNotifiedStatePerUser()->Set(params->Get("user"), state);
+ Notification::OnLastNotifiedStatePerUserUpdated(notification, params->Get("user"), state, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::LastNotifiedStatePerUserClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin)
+{
+ auto listener (ApiListener::GetInstance());
+
+ if (!listener) {
+ return;
+ }
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ClearLastNotifiedStatePerUser");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::LastNotifiedStatePerUserClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ auto endpoint (origin->FromClient->GetEndpoint());
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user cleared' message from '"
+ << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user cleared' message from '"
+ << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+
+ return Empty;
+ }
+
+ auto notification (Notification::GetByName(params->Get("notification")));
+
+ if (!notification) {
+ return Empty;
+ }
+
+ notification->GetLastNotifiedStatePerUser()->Clear();
+ Notification::OnLastNotifiedStatePerUserCleared(notification, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("forced", checkable->GetForceNextCheck());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetForceNextCheck");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::ForceNextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'force next check changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'force next check' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetForceNextCheck(params->Get("forced"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::ForceNextNotificationChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("forced", checkable->GetForceNextNotification());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetForceNextNotification");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::ForceNextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'force next notification changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'force next notification' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetForceNextNotification(params->Get("forced"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::AcknowledgementSetHandler(const Checkable::Ptr& checkable,
+ const String& author, const String& comment, AcknowledgementType type,
+ bool notify, bool persistent, double changeTime, double expiry, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("author", author);
+ params->Set("comment", comment);
+ params->Set("acktype", type);
+ params->Set("notify", notify);
+ params->Set("persistent", persistent);
+ params->Set("expiry", expiry);
+ params->Set("change_time", changeTime);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetAcknowledgement");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::AcknowledgementSetAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'acknowledgement set' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'acknowledgement set' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ ObjectLock oLock (checkable);
+
+ if (checkable->IsAcknowledged()) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'acknowledgement set' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Checkable is already acknowledged.";
+ return Empty;
+ }
+
+ checkable->AcknowledgeProblem(params->Get("author"), params->Get("comment"),
+ static_cast<AcknowledgementType>(static_cast<int>(params->Get("acktype"))),
+ params->Get("notify"), params->Get("persistent"), params->Get("change_time"), params->Get("expiry"), origin);
+
+ return Empty;
+}
+
+void ClusterEvents::AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("author", removedBy);
+ params->Set("change_time", changeTime);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ClearAcknowledgement");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::AcknowledgementClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'acknowledgement cleared' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'acknowledgement cleared' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->ClearAcknowledgement(params->Get("author"), params->Get("change_time"), origin);
+
+ return Empty;
+}
+
+Value ClusterEvents::ExecuteCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ if (!origin->IsLocal()) {
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ /* Discard messages from anonymous clients */
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents") << "Discarding 'execute command' message from '"
+ << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Zone::Ptr originZone = endpoint->GetZone();
+
+ Zone::Ptr localZone = Zone::GetLocalZone();
+ bool fromLocalZone = originZone == localZone;
+
+ Zone::Ptr parentZone = localZone->GetParent();
+ bool fromParentZone = parentZone && originZone == parentZone;
+
+ if (!fromLocalZone && !fromParentZone) {
+ Log(LogNotice, "ClusterEvents") << "Discarding 'execute command' message from '"
+ << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+ }
+
+ String executionUuid = params->Get("source");
+
+ if (params->Contains("endpoint")) {
+ Endpoint::Ptr execEndpoint = Endpoint::GetByName(params->Get("endpoint"));
+
+ if (!execEndpoint) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'execute command' message " << executionUuid
+ << ": Endpoint " << params->Get("endpoint") << " does not exist";
+ return Empty;
+ }
+
+ if (execEndpoint != Endpoint::GetLocalEndpoint()) {
+ Zone::Ptr endpointZone = execEndpoint->GetZone();
+ Zone::Ptr localZone = Zone::GetLocalZone();
+
+ if (!endpointZone->IsChildOf(localZone)) {
+ return Empty;
+ }
+
+ /* Check if the child endpoints have Icinga version >= 2.13 */
+ for (const Zone::Ptr &zone : ConfigType::GetObjectsByType<Zone>()) {
+ /* Fetch immediate child zone members */
+ if (zone->GetParent() == localZone && zone->CanAccessObject(endpointZone)) {
+ std::set<Endpoint::Ptr> endpoints = zone->GetEndpoints();
+
+ for (const Endpoint::Ptr &childEndpoint : endpoints) {
+ if (!(childEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand)) {
+ double now = Utility::GetTime();
+ Dictionary::Ptr executedParams = new Dictionary();
+ executedParams->Set("execution", executionUuid);
+ executedParams->Set("host", params->Get("host"));
+
+ if (params->Contains("service"))
+ executedParams->Set("service", params->Get("service"));
+
+ executedParams->Set("exit", 126);
+ executedParams->Set("output",
+ "Endpoint '" + childEndpoint->GetName() + "' doesn't support executing arbitrary commands.");
+ executedParams->Set("start", now);
+ executedParams->Set("end", now);
+
+ Dictionary::Ptr executedMessage = new Dictionary();
+ executedMessage->Set("jsonrpc", "2.0");
+ executedMessage->Set("method", "event::ExecutedCommand");
+ executedMessage->Set("params", executedParams);
+
+ listener->RelayMessage(nullptr, nullptr, executedMessage, true);
+ return Empty;
+ }
+ }
+
+ Checkable::Ptr checkable;
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+ if (!host) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'execute command' message " << executionUuid
+ << ": host " << params->Get("host") << " does not exist";
+ return Empty;
+ }
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable) {
+ String checkableName = host->GetName();
+ if (params->Contains("service"))
+ checkableName += "!" + params->Get("service");
+
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'execute command' message " << executionUuid
+ << ": " << checkableName << " does not exist";
+ return Empty;
+ }
+
+ /* Return an error when the endpointZone is different than the child zone and
+ * the child zone can't access the checkable.
+ * The zones are checked to allow for the case where command_endpoint is specified in the checkable
+ * but checkable is not actually present in the agent.
+ */
+ if (!zone->CanAccessObject(checkable) && zone != endpointZone) {
+ double now = Utility::GetTime();
+ Dictionary::Ptr executedParams = new Dictionary();
+ executedParams->Set("execution", executionUuid);
+ executedParams->Set("host", params->Get("host"));
+
+ if (params->Contains("service"))
+ executedParams->Set("service", params->Get("service"));
+
+ executedParams->Set("exit", 126);
+ executedParams->Set(
+ "output",
+ "Zone '" + zone->GetName() + "' cannot access to checkable '" + checkable->GetName() + "'."
+ );
+ executedParams->Set("start", now);
+ executedParams->Set("end", now);
+
+ Dictionary::Ptr executedMessage = new Dictionary();
+ executedMessage->Set("jsonrpc", "2.0");
+ executedMessage->Set("method", "event::ExecutedCommand");
+ executedMessage->Set("params", executedParams);
+
+ listener->RelayMessage(nullptr, nullptr, executedMessage, true);
+ return Empty;
+ }
+ }
+ }
+
+ Dictionary::Ptr execMessage = new Dictionary();
+ execMessage->Set("jsonrpc", "2.0");
+ execMessage->Set("method", "event::ExecuteCommand");
+ execMessage->Set("params", params);
+
+ listener->RelayMessage(origin, endpointZone, execMessage, true);
+ return Empty;
+ }
+ }
+
+ EnqueueCheck(origin, params);
+
+ return Empty;
+}
+
+void ClusterEvents::SendNotificationsHandler(const Checkable::Ptr& checkable, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr message = MakeCheckResultMessage(checkable, cr);
+ message->Set("method", "event::SendNotifications");
+
+ Dictionary::Ptr params = message->Get("params");
+ params->Set("type", type);
+ params->Set("author", author);
+ params->Set("text", text);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::SendNotificationsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'send notification' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'send custom notification' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ CheckResult::Ptr cr;
+ Array::Ptr vperf;
+
+ if (params->Contains("cr")) {
+ cr = new CheckResult();
+ Dictionary::Ptr vcr = params->Get("cr");
+
+ if (vcr && vcr->Contains("performance_data")) {
+ vperf = vcr->Get("performance_data");
+
+ if (vperf)
+ vcr->Remove("performance_data");
+
+ Deserialize(cr, vcr, true);
+ }
+ }
+
+ NotificationType type = static_cast<NotificationType>(static_cast<int>(params->Get("type")));
+ String author = params->Get("author");
+ String text = params->Get("text");
+
+ Checkable::OnNotificationsRequested(checkable, type, cr, author, text, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::NotificationSentUserHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const User::Ptr& user,
+ NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const String& command,
+ const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("notification", notification->GetName());
+ params->Set("user", user->GetName());
+ params->Set("type", notificationType);
+ params->Set("cr", Serialize(cr));
+ params->Set("author", author);
+ params->Set("text", commentText);
+ params->Set("command", command);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::NotificationSentUser");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::NotificationSentUserAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'sent notification to user' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'send notification to user' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ CheckResult::Ptr cr;
+ Array::Ptr vperf;
+
+ if (params->Contains("cr")) {
+ cr = new CheckResult();
+ Dictionary::Ptr vcr = params->Get("cr");
+
+ if (vcr && vcr->Contains("performance_data")) {
+ vperf = vcr->Get("performance_data");
+
+ if (vperf)
+ vcr->Remove("performance_data");
+
+ Deserialize(cr, vcr, true);
+ }
+ }
+
+ NotificationType type = static_cast<NotificationType>(static_cast<int>(params->Get("type")));
+ String author = params->Get("author");
+ String text = params->Get("text");
+
+ Notification::Ptr notification = Notification::GetByName(params->Get("notification"));
+
+ if (!notification)
+ return Empty;
+
+ User::Ptr user = User::GetByName(params->Get("user"));
+
+ if (!user)
+ return Empty;
+
+ String command = params->Get("command");
+
+ Checkable::OnNotificationSentToUser(notification, checkable, user, type, cr, author, text, command, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::NotificationSentToAllUsersHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("notification", notification->GetName());
+
+ ArrayData ausers;
+ for (const User::Ptr& user : users) {
+ ausers.push_back(user->GetName());
+ }
+ params->Set("users", new Array(std::move(ausers)));
+
+ params->Set("type", notificationType);
+ params->Set("cr", Serialize(cr));
+ params->Set("author", author);
+ params->Set("text", commentText);
+
+ params->Set("last_notification", notification->GetLastNotification());
+ params->Set("next_notification", notification->GetNextNotification());
+ params->Set("notification_number", notification->GetNotificationNumber());
+ params->Set("last_problem_notification", notification->GetLastProblemNotification());
+ params->Set("no_more_notifications", notification->GetNoMoreNotifications());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::NotificationSentToAllUsers");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::NotificationSentToAllUsersAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'sent notification to all users' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'sent notification to all users' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ CheckResult::Ptr cr;
+ Array::Ptr vperf;
+
+ if (params->Contains("cr")) {
+ cr = new CheckResult();
+ Dictionary::Ptr vcr = params->Get("cr");
+
+ if (vcr && vcr->Contains("performance_data")) {
+ vperf = vcr->Get("performance_data");
+
+ if (vperf)
+ vcr->Remove("performance_data");
+
+ Deserialize(cr, vcr, true);
+ }
+ }
+
+ NotificationType type = static_cast<NotificationType>(static_cast<int>(params->Get("type")));
+ String author = params->Get("author");
+ String text = params->Get("text");
+
+ Notification::Ptr notification = Notification::GetByName(params->Get("notification"));
+
+ if (!notification)
+ return Empty;
+
+ Array::Ptr ausers = params->Get("users");
+
+ if (!ausers)
+ return Empty;
+
+ std::set<User::Ptr> users;
+
+ {
+ ObjectLock olock(ausers);
+ for (const String& auser : ausers) {
+ User::Ptr user = User::GetByName(auser);
+
+ if (!user)
+ continue;
+
+ users.insert(user);
+ }
+ }
+
+ notification->SetLastNotification(params->Get("last_notification"));
+ notification->SetNextNotification(params->Get("next_notification"));
+ notification->SetNotificationNumber(params->Get("notification_number"));
+ notification->SetLastProblemNotification(params->Get("last_problem_notification"));
+ notification->SetNoMoreNotifications(params->Get("no_more_notifications"));
+
+ ArrayData notifiedProblemUsers;
+ for (const User::Ptr& user : users) {
+ notifiedProblemUsers.push_back(user->GetName());
+ }
+
+ notification->SetNotifiedProblemUsers(new Array(std::move(notifiedProblemUsers)));
+
+ Checkable::OnNotificationSentToAllUsers(notification, checkable, users, type, cr, author, text, origin);
+
+ return Empty;
+}
+
+Value ClusterEvents::ExecutedCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ Endpoint::Ptr endpoint;
+
+ if (origin->FromClient) {
+ endpoint = origin->FromClient->GetEndpoint();
+ } else if (origin->IsLocal()) {
+ endpoint = Endpoint::GetLocalEndpoint();
+ }
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message from '" << origin->FromClient->GetIdentity()
+ << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ ObjectLock oLock (checkable);
+
+ if (!params->Contains("execution")) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Execution UUID missing.";
+ return Empty;
+ }
+
+ String uuid = params->Get("execution");
+
+ Dictionary::Ptr executions = checkable->GetExecutions();
+
+ if (!executions) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Execution '" << uuid << "' missing.";
+ return Empty;
+ }
+
+ Dictionary::Ptr execution = executions->Get(uuid);
+
+ if (!execution) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Execution '" << uuid << "' missing.";
+ return Empty;
+ }
+
+ Endpoint::Ptr command_endpoint = Endpoint::GetByName(execution->Get("endpoint"));
+ if (!command_endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message from '" << origin->FromClient->GetIdentity()
+ << "': Command endpoint does not exists.";
+
+ return Empty;
+ }
+
+ if (origin->FromZone && !command_endpoint->GetZone()->IsChildOf(origin->FromZone)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ if (params->Contains("exit"))
+ execution->Set("exit", params->Get("exit"));
+
+ if (params->Contains("output"))
+ execution->Set("output", params->Get("output"));
+
+ if (params->Contains("start"))
+ execution->Set("start", params->Get("start"));
+
+ if (params->Contains("end"))
+ execution->Set("end", params->Get("end"));
+
+ execution->Remove("pending");
+
+ /* Broadcast the update */
+ Dictionary::Ptr executionsToBroadcast = new Dictionary();
+ executionsToBroadcast->Set(uuid, execution);
+ Dictionary::Ptr updateParams = new Dictionary();
+ updateParams->Set("host", host->GetName());
+
+ if (params->Contains("service"))
+ updateParams->Set("service", params->Get("service"));
+
+ updateParams->Set("executions", executionsToBroadcast);
+
+ Dictionary::Ptr updateMessage = new Dictionary();
+ updateMessage->Set("jsonrpc", "2.0");
+ updateMessage->Set("method", "event::UpdateExecutions");
+ updateMessage->Set("params", updateParams);
+
+ listener->RelayMessage(nullptr, checkable, updateMessage, true);
+
+ return Empty;
+}
+
+Value ClusterEvents::UpdateExecutionsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message from '" << origin->FromClient->GetIdentity()
+ << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ ObjectLock oLock (checkable);
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ Dictionary::Ptr executions = checkable->GetExecutions();
+
+ if (!executions)
+ executions = new Dictionary();
+
+ Dictionary::Ptr newExecutions = params->Get("executions");
+ newExecutions->CopyTo(executions);
+ checkable->SetExecutions(executions);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ Dictionary::Ptr updateMessage = new Dictionary();
+ updateMessage->Set("jsonrpc", "2.0");
+ updateMessage->Set("method", "event::UpdateExecutions");
+ updateMessage->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, updateMessage, true);
+
+ return Empty;
+}
+
+void ClusterEvents::SetRemovalInfoHandler(const ConfigObject::Ptr& obj, const String& removedBy, double removeTime,
+ const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("object_type", obj->GetReflectionType()->GetName());
+ params->Set("object_name", obj->GetName());
+ params->Set("removed_by", removedBy);
+ params->Set("remove_time", removeTime);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetRemovalInfo");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, obj, message, true);
+}
+
+Value ClusterEvents::SetRemovalInfoAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone))) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'set removal info' message from '" << origin->FromClient->GetIdentity()
+ << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ String objectType = params->Get("object_type");
+ String objectName = params->Get("object_name");
+ String removedBy = params->Get("removed_by");
+ double removeTime = params->Get("remove_time");
+
+ if (objectType == Comment::GetTypeName()) {
+ Comment::Ptr comment = Comment::GetByName(objectName);
+
+ if (comment) {
+ comment->SetRemovalInfo(removedBy, removeTime, origin);
+ }
+ } else if (objectType == Downtime::GetTypeName()) {
+ Downtime::Ptr downtime = Downtime::GetByName(objectName);
+
+ if (downtime) {
+ downtime->SetRemovalInfo(removedBy, removeTime, origin);
+ }
+ } else {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'set removal info' message from '" << origin->FromClient->GetIdentity()
+ << "': Unknown object type.";
+ }
+
+ return Empty;
+}
diff --git a/lib/icinga/clusterevents.hpp b/lib/icinga/clusterevents.hpp
new file mode 100644
index 0000000..8daf86a
--- /dev/null
+++ b/lib/icinga/clusterevents.hpp
@@ -0,0 +1,102 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CLUSTEREVENTS_H
+#define CLUSTEREVENTS_H
+
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+class ClusterEvents
+{
+public:
+ static void StaticInitialize();
+
+ static void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin);
+ static Value CheckResultAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void NextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value NextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void LastCheckStartedChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value LastCheckStartedChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void StateBeforeSuppressionChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value StateBeforeSuppressionChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void SuppressedNotificationsChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value SuppressedNotificationsChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void SuppressedNotificationTypesChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
+ static Value SuppressedNotificationTypesChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void NextNotificationChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
+ static Value NextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin);
+ static Value LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void LastNotifiedStatePerUserClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
+ static Value LastNotifiedStatePerUserClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value ForceNextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void ForceNextNotificationChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value ForceNextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type,
+ bool notify, bool persistent, double changeTime, double expiry, const MessageOrigin::Ptr& origin);
+ static Value AcknowledgementSetAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, const MessageOrigin::Ptr& origin);
+ static Value AcknowledgementClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static Value ExecuteCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static Dictionary::Ptr MakeCheckResultMessage(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+
+ static void SendNotificationsHandler(const Checkable::Ptr& checkable, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text, const MessageOrigin::Ptr& origin);
+ static Value SendNotificationsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void NotificationSentUserHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const User::Ptr& user,
+ NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const String& command, const MessageOrigin::Ptr& origin);
+ static Value NotificationSentUserAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void NotificationSentToAllUsersHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const MessageOrigin::Ptr& origin);
+ static Value NotificationSentToAllUsersAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static Value ExecutedCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static Value UpdateExecutionsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void SetRemovalInfoHandler(const ConfigObject::Ptr& obj, const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin);
+ static Value SetRemovalInfoAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static int GetCheckRequestQueueSize();
+ static void LogRemoteCheckQueueInformation();
+
+private:
+ static std::mutex m_Mutex;
+ static std::deque<std::function<void ()>> m_CheckRequestQueue;
+ static bool m_CheckSchedulerRunning;
+ static int m_ChecksExecutedDuringInterval;
+ static int m_ChecksDroppedDuringInterval;
+ static Timer::Ptr m_LogTimer;
+
+ static void RemoteCheckThreadProc();
+ static void EnqueueCheck(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static void ExecuteCheckFromQueue(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+};
+
+}
+
+#endif /* CLUSTEREVENTS_H */
diff --git a/lib/icinga/command.cpp b/lib/icinga/command.cpp
new file mode 100644
index 0000000..8e0f357
--- /dev/null
+++ b/lib/icinga/command.cpp
@@ -0,0 +1,68 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/command.hpp"
+#include "icinga/command-ti.cpp"
+#include "icinga/macroprocessor.hpp"
+#include "base/exception.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Command);
+
+void Command::Validate(int types, const ValidationUtils& utils)
+{
+ ObjectImpl<Command>::Validate(types, utils);
+
+ Dictionary::Ptr arguments = GetArguments();
+
+ if (!(types & FAConfig))
+ return;
+
+ if (arguments) {
+ if (!GetCommandLine().IsObjectType<Array>())
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "command" }, "Attribute 'command' must be an array if the 'arguments' attribute is set."));
+
+ ObjectLock olock(arguments);
+ for (const Dictionary::Pair& kv : arguments) {
+ const Value& arginfo = kv.second;
+ Value argval;
+
+ if (arginfo.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr argdict = arginfo;
+
+ if (argdict->Contains("value")) {
+ Value argvalue = argdict->Get("value");
+
+ if (argvalue.IsString() && !MacroProcessor::ValidateMacroString(argvalue))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "arguments", kv.first, "value" }, "Validation failed: Closing $ not found in macro format string '" + argvalue + "'."));
+ }
+
+ if (argdict->Contains("set_if")) {
+ Value argsetif = argdict->Get("set_if");
+
+ if (argsetif.IsString() && !MacroProcessor::ValidateMacroString(argsetif))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "arguments", kv.first, "set_if" }, "Closing $ not found in macro format string '" + argsetif + "'."));
+ }
+ } else if (arginfo.IsString()) {
+ if (!MacroProcessor::ValidateMacroString(arginfo))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "arguments", kv.first }, "Closing $ not found in macro format string '" + arginfo + "'."));
+ }
+ }
+ }
+
+ Dictionary::Ptr env = GetEnv();
+
+ if (env) {
+ ObjectLock olock(env);
+ for (const Dictionary::Pair& kv : env) {
+ const Value& envval = kv.second;
+
+ if (!envval.IsString() || envval.IsEmpty())
+ continue;
+
+ if (!MacroProcessor::ValidateMacroString(envval))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "env", kv.first }, "Closing $ not found in macro format string '" + envval + "'."));
+ }
+ }
+}
diff --git a/lib/icinga/command.hpp b/lib/icinga/command.hpp
new file mode 100644
index 0000000..19bb050
--- /dev/null
+++ b/lib/icinga/command.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMMAND_H
+#define COMMAND_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/command-ti.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+/**
+ * A command.
+ *
+ * @ingroup icinga
+ */
+class Command : public ObjectImpl<Command>
+{
+public:
+ DECLARE_OBJECT(Command);
+
+ //virtual Dictionary::Ptr Execute(const Object::Ptr& context) = 0;
+
+ void Validate(int types, const ValidationUtils& utils) override;
+};
+
+}
+
+#endif /* COMMAND_H */
diff --git a/lib/icinga/command.ti b/lib/icinga/command.ti
new file mode 100644
index 0000000..2275955
--- /dev/null
+++ b/lib/icinga/command.ti
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "base/function.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+abstract class Command : CustomVarObject
+{
+ [config] Value command (CommandLine);
+ [config, signal_with_old_value] Value arguments;
+ [config] int timeout {
+ default {{{ return 60; }}}
+ };
+ [config, signal_with_old_value] Dictionary::Ptr env;
+ [config, required] Function::Ptr execute;
+};
+
+validator Command {
+ String command;
+ Function command;
+ Array command {
+ String "*";
+ Function "*";
+ };
+
+ Dictionary arguments {
+ String "*";
+ Function "*";
+ Dictionary "*" {
+ String key;
+ String value;
+ Function value;
+ String description;
+ Number "required";
+ Number skip_key;
+ Number repeat_key;
+ String set_if;
+ Function set_if;
+ Number order;
+ String separator;
+ };
+ };
+
+ Dictionary env {
+ String "*";
+ Function "*";
+ };
+};
+
+}
diff --git a/lib/icinga/comment.cpp b/lib/icinga/comment.cpp
new file mode 100644
index 0000000..9c0b923
--- /dev/null
+++ b/lib/icinga/comment.cpp
@@ -0,0 +1,258 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/comment.hpp"
+#include "icinga/comment-ti.cpp"
+#include "icinga/host.hpp"
+#include "remote/configobjectutility.hpp"
+#include "base/utility.hpp"
+#include "base/configtype.hpp"
+#include "base/timer.hpp"
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+static int l_NextCommentID = 1;
+static std::mutex l_CommentMutex;
+static std::map<int, String> l_LegacyCommentsCache;
+static Timer::Ptr l_CommentsExpireTimer;
+
+boost::signals2::signal<void (const Comment::Ptr&)> Comment::OnCommentAdded;
+boost::signals2::signal<void (const Comment::Ptr&)> Comment::OnCommentRemoved;
+boost::signals2::signal<void (const Comment::Ptr&, const String&, double, const MessageOrigin::Ptr&)> Comment::OnRemovalInfoChanged;
+
+REGISTER_TYPE(Comment);
+
+String CommentNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Comment::Ptr comment = dynamic_pointer_cast<Comment>(context);
+
+ if (!comment)
+ return "";
+
+ String name = comment->GetHostName();
+
+ if (!comment->GetServiceName().IsEmpty())
+ name += "!" + comment->GetServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr CommentNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Comment name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void Comment::OnAllConfigLoaded()
+{
+ ConfigObject::OnAllConfigLoaded();
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+
+ if (GetServiceName().IsEmpty())
+ m_Checkable = host;
+ else
+ m_Checkable = host->GetServiceByShortName(GetServiceName());
+
+ if (!m_Checkable)
+ BOOST_THROW_EXCEPTION(ScriptError("Comment '" + GetName() + "' references a host/service which doesn't exist.", GetDebugInfo()));
+}
+
+void Comment::Start(bool runtimeCreated)
+{
+ ObjectImpl<Comment>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, [this]() {
+ l_CommentsExpireTimer = Timer::Create();
+ l_CommentsExpireTimer->SetInterval(60);
+ l_CommentsExpireTimer->OnTimerExpired.connect([](const Timer * const&) { CommentsExpireTimerHandler(); });
+ l_CommentsExpireTimer->Start();
+ });
+
+ {
+ std::unique_lock<std::mutex> lock(l_CommentMutex);
+
+ SetLegacyId(l_NextCommentID);
+ l_LegacyCommentsCache[l_NextCommentID] = GetName();
+ l_NextCommentID++;
+ }
+
+ GetCheckable()->RegisterComment(this);
+
+ if (runtimeCreated)
+ OnCommentAdded(this);
+}
+
+void Comment::Stop(bool runtimeRemoved)
+{
+ GetCheckable()->UnregisterComment(this);
+
+ if (runtimeRemoved)
+ OnCommentRemoved(this);
+
+ ObjectImpl<Comment>::Stop(runtimeRemoved);
+}
+
+Checkable::Ptr Comment::GetCheckable() const
+{
+ return static_pointer_cast<Checkable>(m_Checkable);
+}
+
+bool Comment::IsExpired() const
+{
+ double expire_time = GetExpireTime();
+
+ return (expire_time != 0 && expire_time < Utility::GetTime());
+}
+
+int Comment::GetNextCommentID()
+{
+ std::unique_lock<std::mutex> lock(l_CommentMutex);
+
+ return l_NextCommentID;
+}
+
+String Comment::AddComment(const Checkable::Ptr& checkable, CommentType entryType, const String& author,
+ const String& text, bool persistent, double expireTime, bool sticky, const String& id, const MessageOrigin::Ptr& origin)
+{
+ String fullName;
+
+ if (id.IsEmpty())
+ fullName = checkable->GetName() + "!" + Utility::NewUniqueID();
+ else
+ fullName = id;
+
+ Dictionary::Ptr attrs = new Dictionary();
+
+ attrs->Set("author", author);
+ attrs->Set("text", text);
+ attrs->Set("persistent", persistent);
+ attrs->Set("expire_time", expireTime);
+ attrs->Set("entry_type", entryType);
+ attrs->Set("sticky", sticky);
+ attrs->Set("entry_time", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ attrs->Set("host_name", host->GetName());
+ if (service)
+ attrs->Set("service_name", service->GetShortName());
+
+ String zone = checkable->GetZoneName();
+
+ if (!zone.IsEmpty())
+ attrs->Set("zone", zone);
+
+ String config = ConfigObjectUtility::CreateObjectConfig(Comment::TypeInstance, fullName, true, nullptr, attrs);
+
+ Array::Ptr errors = new Array();
+
+ if (!ConfigObjectUtility::CreateObject(Comment::TypeInstance, fullName, config, errors, nullptr)) {
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "Comment", error);
+ }
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not create comment."));
+ }
+
+ Comment::Ptr comment = Comment::GetByName(fullName);
+
+ if (!comment)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not create comment."));
+
+ Log(LogNotice, "Comment")
+ << "Added comment '" << comment->GetName() << "'.";
+
+ return fullName;
+}
+
+void Comment::RemoveComment(const String& id, bool removedManually, const String& removedBy,
+ const MessageOrigin::Ptr& origin)
+{
+ Comment::Ptr comment = Comment::GetByName(id);
+
+ if (!comment || comment->GetPackage() != "_api")
+ return;
+
+ Log(LogNotice, "Comment")
+ << "Removed comment '" << comment->GetName() << "' from object '" << comment->GetCheckable()->GetName() << "'.";
+
+ if (removedManually) {
+ comment->SetRemovalInfo(removedBy, Utility::GetTime());
+ }
+
+ Array::Ptr errors = new Array();
+
+ if (!ConfigObjectUtility::DeleteObject(comment, false, errors, nullptr)) {
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "Comment", error);
+ }
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not remove comment."));
+ }
+}
+
+void Comment::SetRemovalInfo(const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin) {
+ {
+ ObjectLock olock(this);
+
+ SetRemovedBy(removedBy, false, origin);
+ SetRemoveTime(removeTime, false, origin);
+ }
+
+ OnRemovalInfoChanged(this, removedBy, removeTime, origin);
+}
+
+String Comment::GetCommentIDFromLegacyID(int id)
+{
+ std::unique_lock<std::mutex> lock(l_CommentMutex);
+
+ auto it = l_LegacyCommentsCache.find(id);
+
+ if (it == l_LegacyCommentsCache.end())
+ return Empty;
+
+ return it->second;
+}
+
+void Comment::CommentsExpireTimerHandler()
+{
+ std::vector<Comment::Ptr> comments;
+
+ for (const Comment::Ptr& comment : ConfigType::GetObjectsByType<Comment>()) {
+ comments.push_back(comment);
+ }
+
+ for (const Comment::Ptr& comment : comments) {
+ /* Only remove comments which are activated after daemon start. */
+ if (comment->IsActive() && comment->IsExpired()) {
+ /* Do not remove persistent comments from an acknowledgement */
+ if (comment->GetEntryType() == CommentAcknowledgement && comment->GetPersistent())
+ continue;
+
+ RemoveComment(comment->GetName());
+ }
+ }
+}
diff --git a/lib/icinga/comment.hpp b/lib/icinga/comment.hpp
new file mode 100644
index 0000000..6532084
--- /dev/null
+++ b/lib/icinga/comment.hpp
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMMENT_H
+#define COMMENT_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/comment-ti.hpp"
+#include "icinga/checkable-ti.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+/**
+ * A comment.
+ *
+ * @ingroup icinga
+ */
+class Comment final : public ObjectImpl<Comment>
+{
+public:
+ DECLARE_OBJECT(Comment);
+ DECLARE_OBJECTNAME(Comment);
+
+ static boost::signals2::signal<void (const Comment::Ptr&)> OnCommentAdded;
+ static boost::signals2::signal<void (const Comment::Ptr&)> OnCommentRemoved;
+ static boost::signals2::signal<void (const Comment::Ptr&, const String&, double, const MessageOrigin::Ptr&)> OnRemovalInfoChanged;
+
+ intrusive_ptr<Checkable> GetCheckable() const;
+
+ bool IsExpired() const;
+
+ void SetRemovalInfo(const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin = nullptr);
+
+ static int GetNextCommentID();
+
+ static String AddComment(const intrusive_ptr<Checkable>& checkable, CommentType entryType,
+ const String& author, const String& text, bool persistent, double expireTime, bool sticky = false,
+ const String& id = String(), const MessageOrigin::Ptr& origin = nullptr);
+
+ static void RemoveComment(const String& id, bool removedManually = false, const String& removedBy = "",
+ const MessageOrigin::Ptr& origin = nullptr);
+
+ static String GetCommentIDFromLegacyID(int id);
+
+protected:
+ void OnAllConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ ObjectImpl<Checkable>::Ptr m_Checkable;
+
+ static void CommentsExpireTimerHandler();
+};
+
+}
+
+#endif /* COMMENT_H */
diff --git a/lib/icinga/comment.ti b/lib/icinga/comment.ti
new file mode 100644
index 0000000..b8ad6f7
--- /dev/null
+++ b/lib/icinga/comment.ti
@@ -0,0 +1,80 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/utility.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+/**
+ * The type of a service comment.
+ *
+ * @ingroup icinga
+ */
+enum CommentType
+{
+ CommentUser = 1,
+ CommentAcknowledgement = 4
+};
+
+class CommentNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Comment : ConfigObject < CommentNameComposer
+{
+ load_after Host;
+ load_after Service;
+
+ [config, no_user_modify, protected, required, navigation(host)] name(Host) host_name {
+ navigate {{{
+ return Host::GetByName(GetHostName());
+ }}}
+ };
+ [config, no_user_modify, protected, navigation(service)] String service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+ return host->GetServiceByShortName(GetServiceName());
+ }}}
+ };
+
+ [config] Timestamp entry_time {
+ default {{{ return Utility::GetTime(); }}}
+ };
+ [config, enum] CommentType entry_type {
+ default {{{ return CommentUser; }}}
+ };
+ [config, no_user_view, no_user_modify] bool sticky;
+ [config, required] String author;
+ [config, required] String text;
+ [config] bool persistent;
+ [config] Timestamp expire_time;
+ [state] int legacy_id;
+
+ [no_user_view, no_user_modify] String removed_by;
+ [no_user_view, no_user_modify] Timestamp remove_time;
+};
+
+}
diff --git a/lib/icinga/compatutility.cpp b/lib/icinga/compatutility.cpp
new file mode 100644
index 0000000..95aed43
--- /dev/null
+++ b/lib/icinga/compatutility.cpp
@@ -0,0 +1,302 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/compatutility.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/service.hpp"
+#include "base/utility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/join.hpp>
+
+using namespace icinga;
+
+/* Used in DB IDO and Livestatus. */
+String CompatUtility::GetCommandLine(const Command::Ptr& command)
+{
+ Value commandLine = command->GetCommandLine();
+
+ String result;
+ if (commandLine.IsObjectType<Array>()) {
+ Array::Ptr args = commandLine;
+
+ ObjectLock olock(args);
+ for (const String& arg : args) {
+ // This is obviously incorrect for non-trivial cases.
+ result += " \"" + EscapeString(arg) + "\"";
+ }
+ } else if (!commandLine.IsEmpty()) {
+ result = EscapeString(Convert::ToString(commandLine));
+ } else {
+ result = "<internal>";
+ }
+
+ return result;
+}
+
+String CompatUtility::GetCommandNamePrefix(const Command::Ptr& command)
+/* Helper. */
+{
+ if (!command)
+ return Empty;
+
+ String prefix;
+ if (command->GetReflectionType() == CheckCommand::TypeInstance)
+ prefix = "check_";
+ else if (command->GetReflectionType() == NotificationCommand::TypeInstance)
+ prefix = "notification_";
+ else if (command->GetReflectionType() == EventCommand::TypeInstance)
+ prefix = "event_";
+
+ return prefix;
+}
+
+String CompatUtility::GetCommandName(const Command::Ptr& command)
+/* Used in DB IDO and Livestatus. */
+{
+ if (!command)
+ return Empty;
+
+ return GetCommandNamePrefix(command) + command->GetName();
+}
+
+/* Used in DB IDO and Livestatus. */
+String CompatUtility::GetCheckableCommandArgs(const Checkable::Ptr& checkable)
+{
+ CheckCommand::Ptr command = checkable->GetCheckCommand();
+
+ Dictionary::Ptr args = new Dictionary();
+
+ if (command) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+ String command_line = GetCommandLine(command);
+
+ Dictionary::Ptr command_vars = command->GetVars();
+
+ if (command_vars) {
+ ObjectLock olock(command_vars);
+ for (const Dictionary::Pair& kv : command_vars) {
+ String macro = "$" + kv.first + "$"; // this is too simple
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+
+ }
+ }
+
+ Dictionary::Ptr host_vars = host->GetVars();
+
+ if (host_vars) {
+ ObjectLock olock(host_vars);
+ for (const Dictionary::Pair& kv : host_vars) {
+ String macro = "$" + kv.first + "$"; // this is too simple
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+ macro = "$host.vars." + kv.first + "$";
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+ }
+ }
+
+ if (service) {
+ Dictionary::Ptr service_vars = service->GetVars();
+
+ if (service_vars) {
+ ObjectLock olock(service_vars);
+ for (const Dictionary::Pair& kv : service_vars) {
+ String macro = "$" + kv.first + "$"; // this is too simple
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+ macro = "$service.vars." + kv.first + "$";
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+ }
+ }
+ }
+
+ String arg_string;
+ ObjectLock olock(args);
+ for (const Dictionary::Pair& kv : args) {
+ arg_string += Convert::ToString(kv.first) + "=" + Convert::ToString(kv.second) + "!";
+ }
+ return arg_string;
+ }
+
+ return Empty;
+}
+
+/* Used in DB IDO and Livestatus. */
+int CompatUtility::GetCheckableNotificationLastNotification(const Checkable::Ptr& checkable)
+{
+ double last_notification = 0.0;
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ if (notification->GetLastNotification() > last_notification)
+ last_notification = notification->GetLastNotification();
+ }
+
+ return static_cast<int>(last_notification);
+}
+
+/* Used in DB IDO and Livestatus. */
+int CompatUtility::GetCheckableNotificationNextNotification(const Checkable::Ptr& checkable)
+{
+ double next_notification = 0.0;
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ if (next_notification == 0 || notification->GetNextNotification() < next_notification)
+ next_notification = notification->GetNextNotification();
+ }
+
+ return static_cast<int>(next_notification);
+}
+
+/* Used in DB IDO and Livestatus. */
+int CompatUtility::GetCheckableNotificationNotificationNumber(const Checkable::Ptr& checkable)
+{
+ int notification_number = 0;
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ if (notification->GetNotificationNumber() > notification_number)
+ notification_number = notification->GetNotificationNumber();
+ }
+
+ return notification_number;
+}
+
+/* Used in DB IDO and Livestatus. */
+double CompatUtility::GetCheckableNotificationNotificationInterval(const Checkable::Ptr& checkable)
+{
+ double notification_interval = -1;
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ if (notification_interval == -1 || notification->GetInterval() < notification_interval)
+ notification_interval = notification->GetInterval();
+ }
+
+ if (notification_interval == -1)
+ notification_interval = 60;
+
+ return notification_interval / 60.0;
+}
+
+/* Helper. */
+int CompatUtility::GetCheckableNotificationTypeFilter(const Checkable::Ptr& checkable)
+{
+ unsigned long notification_type_filter = 0;
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ ObjectLock olock(notification);
+
+ notification_type_filter |= notification->GetTypeFilter();
+ }
+
+ return notification_type_filter;
+}
+
+/* Helper. */
+int CompatUtility::GetCheckableNotificationStateFilter(const Checkable::Ptr& checkable)
+{
+ unsigned long notification_state_filter = 0;
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ ObjectLock olock(notification);
+
+ notification_state_filter |= notification->GetStateFilter();
+ }
+
+ return notification_state_filter;
+}
+
+/* Used in DB IDO and Livestatus. */
+std::set<User::Ptr> CompatUtility::GetCheckableNotificationUsers(const Checkable::Ptr& checkable)
+{
+ /* Service -> Notifications -> (Users + UserGroups -> Users) */
+ std::set<User::Ptr> allUsers;
+ std::set<User::Ptr> users;
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ ObjectLock olock(notification);
+
+ users = notification->GetUsers();
+
+ std::copy(users.begin(), users.end(), std::inserter(allUsers, allUsers.begin()));
+
+ for (const UserGroup::Ptr& ug : notification->GetUserGroups()) {
+ std::set<User::Ptr> members = ug->GetMembers();
+ std::copy(members.begin(), members.end(), std::inserter(allUsers, allUsers.begin()));
+ }
+ }
+
+ return allUsers;
+}
+
+/* Used in DB IDO and Livestatus. */
+std::set<UserGroup::Ptr> CompatUtility::GetCheckableNotificationUserGroups(const Checkable::Ptr& checkable)
+{
+ std::set<UserGroup::Ptr> usergroups;
+ /* Service -> Notifications -> UserGroups */
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ ObjectLock olock(notification);
+
+ for (const UserGroup::Ptr& ug : notification->GetUserGroups()) {
+ usergroups.insert(ug);
+ }
+ }
+
+ return usergroups;
+}
+
+/* Used in DB IDO, Livestatus, CompatLogger, GelfWriter, IcingaDB. */
+String CompatUtility::GetCheckResultOutput(const CheckResult::Ptr& cr)
+{
+ if (!cr)
+ return Empty;
+
+ String output;
+
+ String raw_output = cr->GetOutput();
+
+ size_t line_end = raw_output.Find("\n");
+
+ return raw_output.SubStr(0, line_end);
+}
+
+/* Used in DB IDO, Livestatus and IcingaDB. */
+String CompatUtility::GetCheckResultLongOutput(const CheckResult::Ptr& cr)
+{
+ if (!cr)
+ return Empty;
+
+ String long_output;
+ String output;
+
+ String raw_output = cr->GetOutput();
+
+ size_t line_end = raw_output.Find("\n");
+
+ if (line_end > 0 && line_end != String::NPos) {
+ long_output = raw_output.SubStr(line_end+1, raw_output.GetLength());
+ return EscapeString(long_output);
+ }
+
+ return Empty;
+}
+
+/* Helper for DB IDO and Livestatus. */
+String CompatUtility::EscapeString(const String& str)
+{
+ String result = str;
+ boost::algorithm::replace_all(result, "\n", "\\n");
+ return result;
+}
+
+/* Used in ExternalCommandListener. */
+String CompatUtility::UnEscapeString(const String& str)
+{
+ String result = str;
+ boost::algorithm::replace_all(result, "\\n", "\n");
+ return result;
+}
diff --git a/lib/icinga/compatutility.hpp b/lib/icinga/compatutility.hpp
new file mode 100644
index 0000000..7b96fb3
--- /dev/null
+++ b/lib/icinga/compatutility.hpp
@@ -0,0 +1,56 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMPATUTILITY_H
+#define COMPATUTILITY_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/host.hpp"
+#include "icinga/command.hpp"
+
+namespace icinga
+{
+
+/**
+ * Compatibility utility functions.
+ *
+ * @ingroup icinga
+ */
+class CompatUtility
+{
+public:
+ /* command */
+ static String GetCommandLine(const Command::Ptr& command);
+ static String GetCommandName(const Command::Ptr& command);
+
+ /* service */
+ static String GetCheckableCommandArgs(const Checkable::Ptr& checkable);
+
+ /* notification */
+ static int GetCheckableNotificationsEnabled(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationLastNotification(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationNextNotification(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationNotificationNumber(const Checkable::Ptr& checkable);
+ static double GetCheckableNotificationNotificationInterval(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationTypeFilter(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationStateFilter(const Checkable::Ptr& checkable);
+
+ static std::set<User::Ptr> GetCheckableNotificationUsers(const Checkable::Ptr& checkable);
+ static std::set<UserGroup::Ptr> GetCheckableNotificationUserGroups(const Checkable::Ptr& checkable);
+
+ /* check result */
+ static String GetCheckResultOutput(const CheckResult::Ptr& cr);
+ static String GetCheckResultLongOutput(const CheckResult::Ptr& cr);
+
+ /* misc */
+ static String EscapeString(const String& str);
+ static String UnEscapeString(const String& str);
+
+private:
+ CompatUtility();
+
+ static String GetCommandNamePrefix(const Command::Ptr& command);
+};
+
+}
+
+#endif /* COMPATUTILITY_H */
diff --git a/lib/icinga/customvarobject.cpp b/lib/icinga/customvarobject.cpp
new file mode 100644
index 0000000..fc1fd27
--- /dev/null
+++ b/lib/icinga/customvarobject.cpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "icinga/customvarobject-ti.cpp"
+#include "icinga/macroprocessor.hpp"
+#include "base/logger.hpp"
+#include "base/function.hpp"
+#include "base/exception.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(CustomVarObject);
+
+void CustomVarObject::ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ MacroProcessor::ValidateCustomVars(this, lvalue());
+}
+
+int icinga::FilterArrayToInt(const Array::Ptr& typeFilters, const std::map<String, int>& filterMap, int defaultValue)
+{
+ int resultTypeFilter;
+
+ if (!typeFilters)
+ return defaultValue;
+
+ resultTypeFilter = 0;
+
+ ObjectLock olock(typeFilters);
+ for (const Value& typeFilter : typeFilters) {
+ if (typeFilter.IsNumber()) {
+ resultTypeFilter = resultTypeFilter | typeFilter;
+ continue;
+ }
+
+ if (!typeFilter.IsString())
+ return -1;
+
+ auto it = filterMap.find(typeFilter);
+
+ if (it == filterMap.end())
+ return -1;
+
+ resultTypeFilter = resultTypeFilter | it->second;
+ }
+
+ return resultTypeFilter;
+}
+
diff --git a/lib/icinga/customvarobject.hpp b/lib/icinga/customvarobject.hpp
new file mode 100644
index 0000000..e10ef32
--- /dev/null
+++ b/lib/icinga/customvarobject.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CUSTOMVAROBJECT_H
+#define CUSTOMVAROBJECT_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/customvarobject-ti.hpp"
+#include "base/configobject.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+/**
+ * An object with custom variable attribute.
+ *
+ * @ingroup icinga
+ */
+class CustomVarObject : public ObjectImpl<CustomVarObject>
+{
+public:
+ DECLARE_OBJECT(CustomVarObject);
+
+ void ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) final;
+};
+
+int FilterArrayToInt(const Array::Ptr& typeFilters, const std::map<String, int>& filterMap, int defaultValue);
+
+}
+
+#endif /* CUSTOMVAROBJECT_H */
diff --git a/lib/icinga/customvarobject.ti b/lib/icinga/customvarobject.ti
new file mode 100644
index 0000000..3e40f66
--- /dev/null
+++ b/lib/icinga/customvarobject.ti
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+abstract class CustomVarObject : ConfigObject
+{
+ [config, signal_with_old_value] Dictionary::Ptr vars;
+};
+
+}
diff --git a/lib/icinga/dependency-apply.cpp b/lib/icinga/dependency-apply.cpp
new file mode 100644
index 0000000..8681c43
--- /dev/null
+++ b/lib/icinga/dependency-apply.cpp
@@ -0,0 +1,161 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/dependency.hpp"
+#include "icinga/service.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE([]() {
+ ApplyRule::RegisterType("Dependency", { "Host", "Service" });
+});
+
+bool Dependency::EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter)
+{
+ if (!skipFilter && !rule.EvaluateFilter(frame))
+ return false;
+
+ auto& di (rule.GetDebugInfo());
+
+#ifdef _DEBUG
+ Log(LogDebug, "Dependency")
+ << "Applying dependency '" << name << "' to object '" << checkable->GetName() << "' for rule " << di;
+#endif /* _DEBUG */
+
+ ConfigItemBuilder builder{di};
+ builder.SetType(Dependency::TypeInstance);
+ builder.SetName(name);
+ builder.SetScope(frame.Locals->ShallowClone());
+ builder.SetIgnoreOnError(rule.GetIgnoreOnError());
+
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "parent_host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "child_host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+
+ if (service)
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "child_service_name"), OpSetLiteral, MakeLiteral(service->GetShortName()), di));
+
+ String zone = checkable->GetZoneName();
+
+ if (!zone.IsEmpty())
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "zone"), OpSetLiteral, MakeLiteral(zone), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "package"), OpSetLiteral, MakeLiteral(rule.GetPackage()), di));
+
+ builder.AddExpression(new OwnedExpression(rule.GetExpression()));
+
+ ConfigItem::Ptr dependencyItem = builder.Compile();
+ dependencyItem->Register();
+
+ return true;
+}
+
+bool Dependency::EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter)
+{
+ auto& di (rule.GetDebugInfo());
+
+ CONTEXT("Evaluating 'apply' rule (" << di << ")");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ScriptFrame frame(true);
+ if (rule.GetScope())
+ rule.GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+ if (service)
+ frame.Locals->Set("service", service);
+
+ Value vinstances;
+
+ if (rule.GetFTerm()) {
+ try {
+ vinstances = rule.GetFTerm()->Evaluate(frame);
+ } catch (const std::exception&) {
+ /* Silently ignore errors here and assume there are no instances. */
+ return false;
+ }
+ } else {
+ vinstances = new Array({ "" });
+ }
+
+ bool match = false;
+
+ if (vinstances.IsObjectType<Array>()) {
+ if (!rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Dictionary iterator requires value to be a dictionary.", di));
+
+ Array::Ptr arr = vinstances;
+
+ ObjectLock olock(arr);
+ for (const Value& instance : arr) {
+ String name = rule.GetName();
+
+ if (!rule.GetFKVar().IsEmpty()) {
+ frame.Locals->Set(rule.GetFKVar(), instance);
+ name += instance;
+ }
+
+ if (EvaluateApplyRuleInstance(checkable, name, frame, rule, skipFilter))
+ match = true;
+ }
+ } else if (vinstances.IsObjectType<Dictionary>()) {
+ if (rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Array iterator requires value to be an array.", di));
+
+ Dictionary::Ptr dict = vinstances;
+
+ for (const String& key : dict->GetKeys()) {
+ frame.Locals->Set(rule.GetFKVar(), key);
+ frame.Locals->Set(rule.GetFVVar(), dict->Get(key));
+
+ if (EvaluateApplyRuleInstance(checkable, rule.GetName() + key, frame, rule, skipFilter))
+ match = true;
+ }
+ }
+
+ return match;
+}
+
+void Dependency::EvaluateApplyRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating 'apply' rules for host '" << host->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Dependency::TypeInstance, Host::TypeInstance)) {
+ if (EvaluateApplyRule(host, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedHostRules(Dependency::TypeInstance, host->GetName())) {
+ if (EvaluateApplyRule(host, *rule, true))
+ rule->AddMatch();
+ }
+}
+
+void Dependency::EvaluateApplyRules(const Service::Ptr& service)
+{
+ CONTEXT("Evaluating 'apply' rules for service '" << service->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Dependency::TypeInstance, Service::TypeInstance)) {
+ if (EvaluateApplyRule(service, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedServiceRules(Dependency::TypeInstance, service->GetHost()->GetName(), service->GetShortName())) {
+ if (EvaluateApplyRule(service, *rule, true))
+ rule->AddMatch();
+ }
+}
diff --git a/lib/icinga/dependency.cpp b/lib/icinga/dependency.cpp
new file mode 100644
index 0000000..2843b90
--- /dev/null
+++ b/lib/icinga/dependency.cpp
@@ -0,0 +1,325 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/dependency.hpp"
+#include "icinga/dependency-ti.cpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/initialize.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include <map>
+#include <sstream>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(Dependency);
+
+bool Dependency::m_AssertNoCyclesForIndividualDeps = false;
+
+struct DependencyCycleNode
+{
+ bool Visited = false;
+ bool OnStack = false;
+};
+
+struct DependencyStackFrame
+{
+ ConfigObject::Ptr Node;
+ bool Implicit;
+
+ inline DependencyStackFrame(ConfigObject::Ptr node, bool implicit = false) : Node(std::move(node)), Implicit(implicit)
+ { }
+};
+
+struct DependencyCycleGraph
+{
+ std::map<Checkable::Ptr, DependencyCycleNode> Nodes;
+ std::vector<DependencyStackFrame> Stack;
+};
+
+static void AssertNoDependencyCycle(const Checkable::Ptr& checkable, DependencyCycleGraph& graph, bool implicit = false);
+
+static void AssertNoParentDependencyCycle(const Checkable::Ptr& parent, DependencyCycleGraph& graph, bool implicit)
+{
+ if (graph.Nodes[parent].OnStack) {
+ std::ostringstream oss;
+ oss << "Dependency cycle:\n";
+
+ for (auto& frame : graph.Stack) {
+ oss << frame.Node->GetReflectionType()->GetName() << " '" << frame.Node->GetName() << "'";
+
+ if (frame.Implicit) {
+ oss << " (implicit)";
+ }
+
+ oss << "\n-> ";
+ }
+
+ oss << parent->GetReflectionType()->GetName() << " '" << parent->GetName() << "'";
+
+ if (implicit) {
+ oss << " (implicit)";
+ }
+
+ BOOST_THROW_EXCEPTION(ScriptError(oss.str()));
+ }
+
+ AssertNoDependencyCycle(parent, graph, implicit);
+}
+
+static void AssertNoDependencyCycle(const Checkable::Ptr& checkable, DependencyCycleGraph& graph, bool implicit)
+{
+ auto& node (graph.Nodes[checkable]);
+
+ if (!node.Visited) {
+ node.Visited = true;
+ node.OnStack = true;
+ graph.Stack.emplace_back(checkable, implicit);
+
+ for (auto& dep : checkable->GetDependencies()) {
+ graph.Stack.emplace_back(dep);
+ AssertNoParentDependencyCycle(dep->GetParent(), graph, false);
+ graph.Stack.pop_back();
+ }
+
+ {
+ auto service (dynamic_pointer_cast<Service>(checkable));
+
+ if (service) {
+ AssertNoParentDependencyCycle(service->GetHost(), graph, true);
+ }
+ }
+
+ graph.Stack.pop_back();
+ node.OnStack = false;
+ }
+}
+
+void Dependency::AssertNoCycles()
+{
+ DependencyCycleGraph graph;
+
+ for (auto& host : ConfigType::GetObjectsByType<Host>()) {
+ AssertNoDependencyCycle(host, graph);
+ }
+
+ for (auto& service : ConfigType::GetObjectsByType<Service>()) {
+ AssertNoDependencyCycle(service, graph);
+ }
+
+ m_AssertNoCyclesForIndividualDeps = true;
+}
+
+String DependencyNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Dependency::Ptr dependency = dynamic_pointer_cast<Dependency>(context);
+
+ if (!dependency)
+ return "";
+
+ String name = dependency->GetChildHostName();
+
+ if (!dependency->GetChildServiceName().IsEmpty())
+ name += "!" + dependency->GetChildServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr DependencyNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Dependency name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("child_host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("child_service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void Dependency::OnConfigLoaded()
+{
+ Value defaultFilter;
+
+ if (GetParentServiceName().IsEmpty())
+ defaultFilter = StateFilterUp;
+ else
+ defaultFilter = StateFilterOK | StateFilterWarning;
+
+ SetStateFilter(FilterArrayToInt(GetStates(), Notification::GetStateFilterMap(), defaultFilter));
+}
+
+void Dependency::OnAllConfigLoaded()
+{
+ ObjectImpl<Dependency>::OnAllConfigLoaded();
+
+ Host::Ptr childHost = Host::GetByName(GetChildHostName());
+
+ if (childHost) {
+ if (GetChildServiceName().IsEmpty())
+ m_Child = childHost;
+ else
+ m_Child = childHost->GetServiceByShortName(GetChildServiceName());
+ }
+
+ if (!m_Child)
+ BOOST_THROW_EXCEPTION(ScriptError("Dependency '" + GetName() + "' references a child host/service which doesn't exist.", GetDebugInfo()));
+
+ Host::Ptr parentHost = Host::GetByName(GetParentHostName());
+
+ if (parentHost) {
+ if (GetParentServiceName().IsEmpty())
+ m_Parent = parentHost;
+ else
+ m_Parent = parentHost->GetServiceByShortName(GetParentServiceName());
+ }
+
+ if (!m_Parent)
+ BOOST_THROW_EXCEPTION(ScriptError("Dependency '" + GetName() + "' references a parent host/service which doesn't exist.", GetDebugInfo()));
+
+ m_Child->AddDependency(this);
+ m_Parent->AddReverseDependency(this);
+
+ if (m_AssertNoCyclesForIndividualDeps) {
+ DependencyCycleGraph graph;
+
+ try {
+ AssertNoDependencyCycle(m_Parent, graph);
+ } catch (...) {
+ m_Child->RemoveDependency(this);
+ m_Parent->RemoveReverseDependency(this);
+ throw;
+ }
+ }
+}
+
+void Dependency::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<Dependency>::Stop(runtimeRemoved);
+
+ GetChild()->RemoveDependency(this);
+ GetParent()->RemoveReverseDependency(this);
+}
+
+bool Dependency::IsAvailable(DependencyType dt) const
+{
+ Checkable::Ptr parent = GetParent();
+
+ Host::Ptr parentHost;
+ Service::Ptr parentService;
+ tie(parentHost, parentService) = GetHostService(parent);
+
+ /* ignore if it's the same checkable object */
+ if (parent == GetChild()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Parent and child " << (parentService ? "service" : "host") << " are identical.";
+ return true;
+ }
+
+ /* ignore pending */
+ if (!parent->GetLastCheckResult()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Parent " << (parentService ? "service" : "host") << " '" << parent->GetName() << "' hasn't been checked yet.";
+ return true;
+ }
+
+ if (GetIgnoreSoftStates()) {
+ /* ignore soft states */
+ if (parent->GetStateType() == StateTypeSoft) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Parent " << (parentService ? "service" : "host") << " '" << parent->GetName() << "' is in a soft state.";
+ return true;
+ }
+ } else {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' failed: Parent " << (parentService ? "service" : "host") << " '" << parent->GetName() << "' is in a soft state.";
+ }
+
+ int state;
+
+ if (parentService)
+ state = ServiceStateToFilter(parentService->GetState());
+ else
+ state = HostStateToFilter(parentHost->GetState());
+
+ /* check state */
+ if (state & GetStateFilter()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Parent " << (parentService ? "service" : "host") << " '" << parent->GetName() << "' matches state filter.";
+ return true;
+ }
+
+ /* ignore if not in time period */
+ TimePeriod::Ptr tp = GetPeriod();
+ if (tp && !tp->IsInside(Utility::GetTime())) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Outside time period.";
+ return true;
+ }
+
+ if (dt == DependencyCheckExecution && !GetDisableChecks()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Checks are not disabled.";
+ return true;
+ } else if (dt == DependencyNotification && !GetDisableNotifications()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Notifications are not disabled";
+ return true;
+ }
+
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' failed. Parent "
+ << (parentService ? "service" : "host") << " '" << parent->GetName() << "' is "
+ << (parentService ? Service::StateToString(parentService->GetState()) : Host::StateToString(parentHost->GetState()));
+
+ return false;
+}
+
+Checkable::Ptr Dependency::GetChild() const
+{
+ return m_Child;
+}
+
+Checkable::Ptr Dependency::GetParent() const
+{
+ return m_Parent;
+}
+
+TimePeriod::Ptr Dependency::GetPeriod() const
+{
+ return TimePeriod::GetByName(GetPeriodRaw());
+}
+
+void Dependency::ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Dependency>::ValidateStates(lvalue, utils);
+
+ int sfilter = FilterArrayToInt(lvalue(), Notification::GetStateFilterMap(), 0);
+
+ if (GetParentServiceName().IsEmpty() && (sfilter & ~(StateFilterUp | StateFilterDown)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid for host dependency."));
+
+ if (!GetParentServiceName().IsEmpty() && (sfilter & ~(StateFilterOK | StateFilterWarning | StateFilterCritical | StateFilterUnknown)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid for service dependency."));
+}
+
+void Dependency::SetParent(intrusive_ptr<Checkable> parent)
+{
+ m_Parent = parent;
+}
+
+void Dependency::SetChild(intrusive_ptr<Checkable> child)
+{
+ m_Child = child;
+}
diff --git a/lib/icinga/dependency.hpp b/lib/icinga/dependency.hpp
new file mode 100644
index 0000000..6cebfaa
--- /dev/null
+++ b/lib/icinga/dependency.hpp
@@ -0,0 +1,62 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DEPENDENCY_H
+#define DEPENDENCY_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/dependency-ti.hpp"
+
+namespace icinga
+{
+
+class ApplyRule;
+struct ScriptFrame;
+class Host;
+class Service;
+
+/**
+ * A service dependency..
+ *
+ * @ingroup icinga
+ */
+class Dependency final : public ObjectImpl<Dependency>
+{
+public:
+ DECLARE_OBJECT(Dependency);
+ DECLARE_OBJECTNAME(Dependency);
+
+ intrusive_ptr<Checkable> GetParent() const;
+ intrusive_ptr<Checkable> GetChild() const;
+
+ TimePeriod::Ptr GetPeriod() const;
+
+ bool IsAvailable(DependencyType dt) const;
+
+ void ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+ static void EvaluateApplyRules(const intrusive_ptr<Host>& host);
+ static void EvaluateApplyRules(const intrusive_ptr<Service>& service);
+ static void AssertNoCycles();
+
+ /* Note: Only use them for unit test mocks. Prefer OnConfigLoaded(). */
+ void SetParent(intrusive_ptr<Checkable> parent);
+ void SetChild(intrusive_ptr<Checkable> child);
+
+protected:
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ Checkable::Ptr m_Parent;
+ Checkable::Ptr m_Child;
+
+ static bool m_AssertNoCyclesForIndividualDeps;
+
+ static bool EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter);
+ static bool EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter = false);
+};
+
+}
+
+#endif /* DEPENDENCY_H */
diff --git a/lib/icinga/dependency.ti b/lib/icinga/dependency.ti
new file mode 100644
index 0000000..41de7ba
--- /dev/null
+++ b/lib/icinga/dependency.ti
@@ -0,0 +1,101 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "icinga/checkable.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class DependencyNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Dependency : CustomVarObject < DependencyNameComposer
+{
+ load_after Host;
+ load_after Service;
+
+ [config, no_user_modify, required, navigation(child_host)] name(Host) child_host_name {
+ navigate {{{
+ return Host::GetByName(GetChildHostName());
+ }}}
+ };
+
+ [config, no_user_modify, navigation(child_service)] String child_service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetChildHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetChildHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetChildServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetChildHostName());
+ return host->GetServiceByShortName(GetChildServiceName());
+ }}}
+ };
+
+ [config, no_user_modify, required, navigation(parent_host)] name(Host) parent_host_name {
+ navigate {{{
+ return Host::GetByName(GetParentHostName());
+ }}}
+ };
+
+ [config, no_user_modify, navigation(parent_service)] String parent_service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetParentHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetParentHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetParentServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetParentHostName());
+ return host->GetServiceByShortName(GetParentServiceName());
+ }}}
+ };
+
+ [config] String redundancy_group;
+
+ [config, navigation] name(TimePeriod) period (PeriodRaw) {
+ navigate {{{
+ return TimePeriod::GetByName(GetPeriodRaw());
+ }}}
+ };
+
+ [config] array(Value) states;
+ [no_user_view, no_user_modify] int state_filter_real (StateFilter);
+
+ [config] bool ignore_soft_states {
+ default {{{ return true; }}}
+ };
+
+ [config] bool disable_checks;
+ [config] bool disable_notifications {
+ default {{{ return true; }}}
+ };
+};
+
+}
diff --git a/lib/icinga/downtime.cpp b/lib/icinga/downtime.cpp
new file mode 100644
index 0000000..2178953
--- /dev/null
+++ b/lib/icinga/downtime.cpp
@@ -0,0 +1,584 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/downtime.hpp"
+#include "icinga/downtime-ti.cpp"
+#include "icinga/host.hpp"
+#include "icinga/scheduleddowntime.hpp"
+#include "remote/configobjectutility.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/timer.hpp"
+#include <boost/thread/once.hpp>
+#include <cmath>
+#include <utility>
+
+using namespace icinga;
+
+static int l_NextDowntimeID = 1;
+static std::mutex l_DowntimeMutex;
+static std::map<int, String> l_LegacyDowntimesCache;
+static Timer::Ptr l_DowntimesOrphanedTimer;
+static Timer::Ptr l_DowntimesStartTimer;
+
+boost::signals2::signal<void (const Downtime::Ptr&)> Downtime::OnDowntimeAdded;
+boost::signals2::signal<void (const Downtime::Ptr&)> Downtime::OnDowntimeRemoved;
+boost::signals2::signal<void (const Downtime::Ptr&)> Downtime::OnDowntimeStarted;
+boost::signals2::signal<void (const Downtime::Ptr&)> Downtime::OnDowntimeTriggered;
+boost::signals2::signal<void (const Downtime::Ptr&, const String&, double, const MessageOrigin::Ptr&)> Downtime::OnRemovalInfoChanged;
+
+REGISTER_TYPE(Downtime);
+
+INITIALIZE_ONCE(&Downtime::StaticInitialize);
+
+void Downtime::StaticInitialize()
+{
+ ScriptGlobal::Set("Icinga.DowntimeNoChildren", "DowntimeNoChildren");
+ ScriptGlobal::Set("Icinga.DowntimeTriggeredChildren", "DowntimeTriggeredChildren");
+ ScriptGlobal::Set("Icinga.DowntimeNonTriggeredChildren", "DowntimeNonTriggeredChildren");
+}
+
+String DowntimeNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Downtime::Ptr downtime = dynamic_pointer_cast<Downtime>(context);
+
+ if (!downtime)
+ return "";
+
+ String name = downtime->GetHostName();
+
+ if (!downtime->GetServiceName().IsEmpty())
+ name += "!" + downtime->GetServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr DowntimeNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Downtime name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void Downtime::OnAllConfigLoaded()
+{
+ ObjectImpl<Downtime>::OnAllConfigLoaded();
+
+ if (GetServiceName().IsEmpty())
+ m_Checkable = Host::GetByName(GetHostName());
+ else
+ m_Checkable = Service::GetByNamePair(GetHostName(), GetServiceName());
+
+ if (!m_Checkable)
+ BOOST_THROW_EXCEPTION(ScriptError("Downtime '" + GetName() + "' references a host/service which doesn't exist.", GetDebugInfo()));
+}
+
+void Downtime::Start(bool runtimeCreated)
+{
+ ObjectImpl<Downtime>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, [this]() {
+ l_DowntimesStartTimer = Timer::Create();
+ l_DowntimesStartTimer->SetInterval(5);
+ l_DowntimesStartTimer->OnTimerExpired.connect([](const Timer * const&){ DowntimesStartTimerHandler(); });
+ l_DowntimesStartTimer->Start();
+
+ l_DowntimesOrphanedTimer = Timer::Create();
+ l_DowntimesOrphanedTimer->SetInterval(60);
+ l_DowntimesOrphanedTimer->OnTimerExpired.connect([](const Timer * const&) { DowntimesOrphanedTimerHandler(); });
+ l_DowntimesOrphanedTimer->Start();
+ });
+
+ {
+ std::unique_lock<std::mutex> lock(l_DowntimeMutex);
+
+ SetLegacyId(l_NextDowntimeID);
+ l_LegacyDowntimesCache[l_NextDowntimeID] = GetName();
+ l_NextDowntimeID++;
+ }
+
+ Checkable::Ptr checkable = GetCheckable();
+
+ checkable->RegisterDowntime(this);
+
+ Downtime::Ptr parent = GetByName(GetParent());
+
+ if (parent)
+ parent->RegisterChild(this);
+
+ if (runtimeCreated)
+ OnDowntimeAdded(this);
+
+ /* if this object is already in a NOT-OK state trigger
+ * this downtime now *after* it has been added (important
+ * for DB IDO, etc.)
+ */
+ if (!GetFixed() && !checkable->IsStateOK(checkable->GetStateRaw())) {
+ Log(LogNotice, "Downtime")
+ << "Checkable '" << checkable->GetName() << "' already in a NOT-OK state."
+ << " Triggering downtime now.";
+
+ TriggerDowntime(std::fmax(std::fmax(GetStartTime(), GetEntryTime()), checkable->GetLastStateChange()));
+ }
+
+ if (GetFixed() && CanBeTriggered()) {
+ /* Send notifications. */
+ OnDowntimeStarted(this);
+
+ /* Trigger fixed downtime immediately. */
+ TriggerDowntime(std::fmax(GetStartTime(), GetEntryTime()));
+ }
+}
+
+void Downtime::Stop(bool runtimeRemoved)
+{
+ GetCheckable()->UnregisterDowntime(this);
+
+ Downtime::Ptr parent = GetByName(GetParent());
+
+ if (parent)
+ parent->UnregisterChild(this);
+
+ if (runtimeRemoved)
+ OnDowntimeRemoved(this);
+
+ ObjectImpl<Downtime>::Stop(runtimeRemoved);
+}
+
+void Downtime::Pause()
+{
+ if (m_CleanupTimer) {
+ m_CleanupTimer->Stop();
+ }
+
+ ObjectImpl<Downtime>::Pause();
+}
+
+void Downtime::Resume()
+{
+ ObjectImpl<Downtime>::Resume();
+ SetupCleanupTimer();
+}
+
+Checkable::Ptr Downtime::GetCheckable() const
+{
+ return static_pointer_cast<Checkable>(m_Checkable);
+}
+
+bool Downtime::IsInEffect() const
+{
+ double now = Utility::GetTime();
+
+ if (GetFixed()) {
+ /* fixed downtimes are in effect during the entire [start..end) interval */
+ return (now >= GetStartTime() && now < GetEndTime());
+ }
+
+ double triggerTime = GetTriggerTime();
+
+ if (triggerTime == 0)
+ /* flexible downtime has not been triggered yet */
+ return false;
+
+ return (now < triggerTime + GetDuration());
+}
+
+bool Downtime::IsTriggered() const
+{
+ double now = Utility::GetTime();
+
+ double triggerTime = GetTriggerTime();
+
+ return (triggerTime > 0 && triggerTime <= now);
+}
+
+bool Downtime::IsExpired() const
+{
+ double now = Utility::GetTime();
+
+ if (GetFixed())
+ return (GetEndTime() < now);
+ else {
+ /* triggered flexible downtime not in effect anymore */
+ if (IsTriggered() && !IsInEffect())
+ return true;
+ /* flexible downtime never triggered */
+ else if (!IsTriggered() && (GetEndTime() < now))
+ return true;
+ else
+ return false;
+ }
+}
+
+bool Downtime::HasValidConfigOwner() const
+{
+ if (!ScheduledDowntime::AllConfigIsLoaded()) {
+ return true;
+ }
+
+ String configOwner = GetConfigOwner();
+ return configOwner.IsEmpty() || Zone::GetByName(GetAuthoritativeZone()) != Zone::GetLocalZone() || GetObject<ScheduledDowntime>(configOwner);
+}
+
+int Downtime::GetNextDowntimeID()
+{
+ std::unique_lock<std::mutex> lock(l_DowntimeMutex);
+
+ return l_NextDowntimeID;
+}
+
+Downtime::Ptr Downtime::AddDowntime(const Checkable::Ptr& checkable, const String& author,
+ const String& comment, double startTime, double endTime, bool fixed,
+ const String& triggeredBy, double duration,
+ const String& scheduledDowntime, const String& scheduledBy, const String& parent,
+ const String& id, const MessageOrigin::Ptr& origin)
+{
+ String fullName;
+
+ if (id.IsEmpty())
+ fullName = checkable->GetName() + "!" + Utility::NewUniqueID();
+ else
+ fullName = id;
+
+ Dictionary::Ptr attrs = new Dictionary();
+
+ attrs->Set("author", author);
+ attrs->Set("comment", comment);
+ attrs->Set("start_time", startTime);
+ attrs->Set("end_time", endTime);
+ attrs->Set("fixed", fixed);
+ attrs->Set("duration", duration);
+ attrs->Set("triggered_by", triggeredBy);
+ attrs->Set("scheduled_by", scheduledBy);
+ attrs->Set("parent", parent);
+ attrs->Set("config_owner", scheduledDowntime);
+ attrs->Set("entry_time", Utility::GetTime());
+
+ if (!scheduledDowntime.IsEmpty()) {
+ auto localZone (Zone::GetLocalZone());
+
+ if (localZone) {
+ attrs->Set("authoritative_zone", localZone->GetName());
+ }
+
+ auto sd (ScheduledDowntime::GetByName(scheduledDowntime));
+
+ if (sd) {
+ attrs->Set("config_owner_hash", sd->HashDowntimeOptions());
+ }
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ attrs->Set("host_name", host->GetName());
+ if (service)
+ attrs->Set("service_name", service->GetShortName());
+
+ String zone;
+
+ if (!scheduledDowntime.IsEmpty()) {
+ auto sdt (ScheduledDowntime::GetByName(scheduledDowntime));
+
+ if (sdt) {
+ auto sdtZone (sdt->GetZone());
+
+ if (sdtZone) {
+ zone = sdtZone->GetName();
+ }
+ }
+ }
+
+ if (zone.IsEmpty()) {
+ zone = checkable->GetZoneName();
+ }
+
+ if (!zone.IsEmpty())
+ attrs->Set("zone", zone);
+
+ String config = ConfigObjectUtility::CreateObjectConfig(Downtime::TypeInstance, fullName, true, nullptr, attrs);
+
+ Array::Ptr errors = new Array();
+
+ if (!ConfigObjectUtility::CreateObject(Downtime::TypeInstance, fullName, config, errors, nullptr)) {
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "Downtime", error);
+ }
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not create downtime."));
+ }
+
+ if (!triggeredBy.IsEmpty()) {
+ Downtime::Ptr parentDowntime = Downtime::GetByName(triggeredBy);
+ Array::Ptr triggers = parentDowntime->GetTriggers();
+
+ ObjectLock olock(triggers);
+ if (!triggers->Contains(fullName))
+ triggers->Add(fullName);
+ }
+
+ Downtime::Ptr downtime = Downtime::GetByName(fullName);
+
+ if (!downtime)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not create downtime object."));
+
+ Log(LogInformation, "Downtime")
+ << "Added downtime '" << downtime->GetName()
+ << "' between '" << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", startTime)
+ << "' and '" << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", endTime) << "', author: '"
+ << author << "', " << (fixed ? "fixed" : "flexible with " + Convert::ToString(duration) + "s duration");
+
+ return downtime;
+}
+
+void Downtime::RemoveDowntime(const String& id, bool includeChildren, bool cancelled, bool expired,
+ const String& removedBy, const MessageOrigin::Ptr& origin)
+{
+ Downtime::Ptr downtime = Downtime::GetByName(id);
+
+ if (!downtime || downtime->GetPackage() != "_api")
+ return;
+
+ String config_owner = downtime->GetConfigOwner();
+
+ if (!config_owner.IsEmpty() && !expired) {
+ BOOST_THROW_EXCEPTION(invalid_downtime_removal_error("Cannot remove downtime '" + downtime->GetName() +
+ "'. It is owned by scheduled downtime object '" + config_owner + "'"));
+ }
+
+ if (includeChildren) {
+ for (const Downtime::Ptr& child : downtime->GetChildren()) {
+ Downtime::RemoveDowntime(child->GetName(), true, true);
+ }
+ }
+
+ if (cancelled) {
+ downtime->SetRemovalInfo(removedBy, Utility::GetTime());
+ }
+
+ Array::Ptr errors = new Array();
+
+ if (!ConfigObjectUtility::DeleteObject(downtime, false, errors, nullptr)) {
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "Downtime", error);
+ }
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not remove downtime."));
+ }
+
+ String reason;
+
+ if (expired) {
+ reason = "expired at " + Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", downtime->GetEndTime());
+ } else if (cancelled) {
+ reason = "cancelled by user";
+ } else {
+ reason = "<unknown>";
+ }
+
+ Log msg (LogInformation, "Downtime");
+
+ msg << "Removed downtime '" << downtime->GetName() << "' from checkable";
+
+ {
+ auto checkable (downtime->GetCheckable());
+
+ if (checkable) {
+ msg << " '" << checkable->GetName() << "'";
+ }
+ }
+
+ msg << " (Reason: " << reason << ").";
+}
+
+void Downtime::RegisterChild(const Downtime::Ptr& downtime)
+{
+ std::unique_lock<std::mutex> lock(m_ChildrenMutex);
+ m_Children.insert(downtime);
+}
+
+void Downtime::UnregisterChild(const Downtime::Ptr& downtime)
+{
+ std::unique_lock<std::mutex> lock(m_ChildrenMutex);
+ m_Children.erase(downtime);
+}
+
+std::set<Downtime::Ptr> Downtime::GetChildren() const
+{
+ std::unique_lock<std::mutex> lock(m_ChildrenMutex);
+ return m_Children;
+}
+
+bool Downtime::CanBeTriggered()
+{
+ if (IsInEffect() && IsTriggered())
+ return false;
+
+ if (IsExpired())
+ return false;
+
+ double now = Utility::GetTime();
+
+ if (now < GetStartTime() || now > GetEndTime())
+ return false;
+
+ return true;
+}
+
+void Downtime::SetupCleanupTimer()
+{
+ if (!m_CleanupTimer) {
+ m_CleanupTimer = Timer::Create();
+
+ auto name (GetName());
+
+ m_CleanupTimer->OnTimerExpired.connect([name=std::move(name)](const Timer * const&) {
+ auto downtime (Downtime::GetByName(name));
+
+ if (downtime && downtime->IsExpired()) {
+ RemoveDowntime(name, false, false, true);
+ }
+ });
+ }
+
+ auto triggerTime (GetTriggerTime());
+
+ m_CleanupTimer->Reschedule((GetFixed() || triggerTime <= 0 ? GetEndTime() : triggerTime + GetDuration()) + 0.1);
+ m_CleanupTimer->Start();
+}
+
+void Downtime::TriggerDowntime(double triggerTime)
+{
+ if (!CanBeTriggered())
+ return;
+
+ Checkable::Ptr checkable = GetCheckable();
+
+ Log(LogInformation, "Downtime")
+ << "Triggering downtime '" << GetName() << "' for checkable '" << checkable->GetName() << "'.";
+
+ if (GetTriggerTime() == 0) {
+ SetTriggerTime(triggerTime);
+ }
+
+ {
+ ObjectLock olock (this);
+ SetupCleanupTimer();
+ }
+
+ Array::Ptr triggers = GetTriggers();
+
+ {
+ ObjectLock olock(triggers);
+ for (const String& triggerName : triggers) {
+ Downtime::Ptr downtime = Downtime::GetByName(triggerName);
+
+ if (!downtime)
+ continue;
+
+ downtime->TriggerDowntime(triggerTime);
+ }
+ }
+
+ OnDowntimeTriggered(this);
+}
+
+void Downtime::SetRemovalInfo(const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin) {
+ {
+ ObjectLock olock(this);
+
+ SetRemovedBy(removedBy, false, origin);
+ SetRemoveTime(removeTime, false, origin);
+ }
+
+ OnRemovalInfoChanged(this, removedBy, removeTime, origin);
+}
+
+String Downtime::GetDowntimeIDFromLegacyID(int id)
+{
+ std::unique_lock<std::mutex> lock(l_DowntimeMutex);
+
+ auto it = l_LegacyDowntimesCache.find(id);
+
+ if (it == l_LegacyDowntimesCache.end())
+ return Empty;
+
+ return it->second;
+}
+
+void Downtime::DowntimesStartTimerHandler()
+{
+ /* Start fixed downtimes. Flexible downtimes will be triggered on-demand. */
+ for (const Downtime::Ptr& downtime : ConfigType::GetObjectsByType<Downtime>()) {
+ if (downtime->IsActive() &&
+ downtime->CanBeTriggered() &&
+ downtime->GetFixed()) {
+ /* Send notifications. */
+ OnDowntimeStarted(downtime);
+
+ /* Trigger fixed downtime immediately. */
+ downtime->TriggerDowntime(std::fmax(downtime->GetStartTime(), downtime->GetEntryTime()));
+ }
+ }
+}
+
+void Downtime::DowntimesOrphanedTimerHandler()
+{
+ for (const Downtime::Ptr& downtime : ConfigType::GetObjectsByType<Downtime>()) {
+ /* Only remove downtimes which are activated after daemon start. */
+ if (downtime->IsActive() && !downtime->HasValidConfigOwner())
+ RemoveDowntime(downtime->GetName(), false, false, true);
+ }
+}
+
+void Downtime::ValidateStartTime(const Lazy<Timestamp>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Downtime>::ValidateStartTime(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "start_time" }, "Start time must be greater than 0."));
+}
+
+void Downtime::ValidateEndTime(const Lazy<Timestamp>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Downtime>::ValidateEndTime(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "end_time" }, "End time must be greater than 0."));
+}
+
+DowntimeChildOptions Downtime::ChildOptionsFromValue(const Value& options)
+{
+ if (options == "DowntimeNoChildren")
+ return DowntimeNoChildren;
+ else if (options == "DowntimeTriggeredChildren")
+ return DowntimeTriggeredChildren;
+ else if (options == "DowntimeNonTriggeredChildren")
+ return DowntimeNonTriggeredChildren;
+ else if (options.IsNumber()) {
+ int number = options;
+ if (number >= 0 && number <= 2)
+ return static_cast<DowntimeChildOptions>(number);
+ }
+
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid child option specified"));
+}
diff --git a/lib/icinga/downtime.hpp b/lib/icinga/downtime.hpp
new file mode 100644
index 0000000..15aa0af
--- /dev/null
+++ b/lib/icinga/downtime.hpp
@@ -0,0 +1,99 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DOWNTIME_H
+#define DOWNTIME_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/downtime-ti.hpp"
+#include "icinga/checkable-ti.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+enum DowntimeChildOptions
+{
+ DowntimeNoChildren,
+ DowntimeTriggeredChildren,
+ DowntimeNonTriggeredChildren
+};
+
+/**
+ * A downtime.
+ *
+ * @ingroup icinga
+ */
+class Downtime final : public ObjectImpl<Downtime>
+{
+public:
+ DECLARE_OBJECT(Downtime);
+ DECLARE_OBJECTNAME(Downtime);
+
+ static boost::signals2::signal<void (const Downtime::Ptr&)> OnDowntimeAdded;
+ static boost::signals2::signal<void (const Downtime::Ptr&)> OnDowntimeRemoved;
+ static boost::signals2::signal<void (const Downtime::Ptr&)> OnDowntimeStarted;
+ static boost::signals2::signal<void (const Downtime::Ptr&)> OnDowntimeTriggered;
+ static boost::signals2::signal<void (const Downtime::Ptr&, const String&, double, const MessageOrigin::Ptr&)> OnRemovalInfoChanged;
+
+ intrusive_ptr<Checkable> GetCheckable() const;
+
+ bool IsInEffect() const;
+ bool IsTriggered() const;
+ bool IsExpired() const;
+ bool HasValidConfigOwner() const;
+
+ static void StaticInitialize();
+
+ static int GetNextDowntimeID();
+
+ static Ptr AddDowntime(const intrusive_ptr<Checkable>& checkable, const String& author,
+ const String& comment, double startTime, double endTime, bool fixed,
+ const String& triggeredBy, double duration, const String& scheduledDowntime = String(),
+ const String& scheduledBy = String(), const String& parent = String(), const String& id = String(),
+ const MessageOrigin::Ptr& origin = nullptr);
+
+ static void RemoveDowntime(const String& id, bool includeChildren, bool cancelled, bool expired = false,
+ const String& removedBy = "", const MessageOrigin::Ptr& origin = nullptr);
+
+ void RegisterChild(const Downtime::Ptr& downtime);
+ void UnregisterChild(const Downtime::Ptr& downtime);
+ std::set<Downtime::Ptr> GetChildren() const;
+
+ void TriggerDowntime(double triggerTime);
+ void SetRemovalInfo(const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin = nullptr);
+
+ void OnAllConfigLoaded() override;
+
+ static String GetDowntimeIDFromLegacyID(int id);
+
+ static DowntimeChildOptions ChildOptionsFromValue(const Value& options);
+
+protected:
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+ void Pause() override;
+ void Resume() override;
+
+ void ValidateStartTime(const Lazy<Timestamp>& lvalue, const ValidationUtils& utils) override;
+ void ValidateEndTime(const Lazy<Timestamp>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ ObjectImpl<Checkable>::Ptr m_Checkable;
+
+ std::set<Downtime::Ptr> m_Children;
+ mutable std::mutex m_ChildrenMutex;
+
+ Timer::Ptr m_CleanupTimer;
+
+ bool CanBeTriggered();
+
+ void SetupCleanupTimer();
+
+ static void DowntimesStartTimerHandler();
+ static void DowntimesOrphanedTimerHandler();
+};
+
+}
+
+#endif /* DOWNTIME_H */
diff --git a/lib/icinga/downtime.ti b/lib/icinga/downtime.ti
new file mode 100644
index 0000000..21e9731
--- /dev/null
+++ b/lib/icinga/downtime.ti
@@ -0,0 +1,82 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/utility.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class DowntimeNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Downtime : ConfigObject < DowntimeNameComposer
+{
+ activation_priority -10;
+
+ load_after Host;
+ load_after Service;
+
+ [config, no_user_modify, required, navigation(host)] name(Host) host_name {
+ navigate {{{
+ return Host::GetByName(GetHostName());
+ }}}
+ };
+ [config, no_user_modify, navigation(service)] String service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+ return host->GetServiceByShortName(GetServiceName());
+ }}}
+ };
+
+ [config] Timestamp entry_time {
+ default {{{ return Utility::GetTime(); }}}
+ };
+ [config, required] String author;
+ [config, required] String comment;
+ [config] Timestamp start_time;
+ [config] Timestamp end_time;
+ [state] Timestamp trigger_time;
+ [config] bool fixed;
+ [config] Timestamp duration;
+ [config] String triggered_by;
+ [config] String scheduled_by;
+ [config] String parent;
+ [state] Array::Ptr triggers {
+ default {{{ return new Array(); }}}
+ };
+ [state] int legacy_id;
+ [state] Timestamp remove_time;
+ [no_storage] bool was_cancelled {
+ get {{{ return GetRemoveTime() > 0; }}}
+ };
+ [config] String config_owner;
+ [config] String config_owner_hash;
+ [config] String authoritative_zone;
+
+ [no_user_view, no_user_modify] String removed_by;
+};
+
+}
diff --git a/lib/icinga/envresolver.cpp b/lib/icinga/envresolver.cpp
new file mode 100644
index 0000000..633255c
--- /dev/null
+++ b/lib/icinga/envresolver.cpp
@@ -0,0 +1,20 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#include "base/string.hpp"
+#include "base/value.hpp"
+#include "icinga/envresolver.hpp"
+#include "icinga/checkresult.hpp"
+#include <cstdlib>
+
+using namespace icinga;
+
+bool EnvResolver::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const
+{
+ auto value (getenv(macro.CStr()));
+
+ if (value) {
+ *result = value;
+ }
+
+ return value;
+}
diff --git a/lib/icinga/envresolver.hpp b/lib/icinga/envresolver.hpp
new file mode 100644
index 0000000..b3f0076
--- /dev/null
+++ b/lib/icinga/envresolver.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#ifndef ENVRESOLVER_H
+#define ENVRESOLVER_H
+
+#include "base/object.hpp"
+#include "base/string.hpp"
+#include "base/value.hpp"
+#include "icinga/macroresolver.hpp"
+#include "icinga/checkresult.hpp"
+
+namespace icinga
+{
+
+/**
+ * Resolves env var names.
+ *
+ * @ingroup icinga
+ */
+class EnvResolver final : public Object, public MacroResolver
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EnvResolver);
+
+ bool ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const override;
+};
+
+}
+
+#endif /* ENVRESOLVER_H */
diff --git a/lib/icinga/eventcommand.cpp b/lib/icinga/eventcommand.cpp
new file mode 100644
index 0000000..39f2d31
--- /dev/null
+++ b/lib/icinga/eventcommand.cpp
@@ -0,0 +1,20 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/eventcommand.hpp"
+#include "icinga/eventcommand-ti.cpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(EventCommand);
+
+thread_local EventCommand::Ptr EventCommand::ExecuteOverride;
+
+void EventCommand::Execute(const Checkable::Ptr& checkable,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ GetExecute()->Invoke({
+ checkable,
+ resolvedMacros,
+ useResolvedMacros
+ });
+}
diff --git a/lib/icinga/eventcommand.hpp b/lib/icinga/eventcommand.hpp
new file mode 100644
index 0000000..67997e6
--- /dev/null
+++ b/lib/icinga/eventcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EVENTCOMMAND_H
+#define EVENTCOMMAND_H
+
+#include "icinga/eventcommand-ti.hpp"
+#include "icinga/checkable.hpp"
+
+namespace icinga
+{
+
+/**
+ * An event handler command.
+ *
+ * @ingroup icinga
+ */
+class EventCommand final : public ObjectImpl<EventCommand>
+{
+public:
+ DECLARE_OBJECT(EventCommand);
+ DECLARE_OBJECTNAME(EventCommand);
+
+ static thread_local EventCommand::Ptr ExecuteOverride;
+
+ void Execute(const Checkable::Ptr& checkable,
+ const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false);
+};
+
+}
+
+#endif /* EVENTCOMMAND_H */
diff --git a/lib/icinga/eventcommand.ti b/lib/icinga/eventcommand.ti
new file mode 100644
index 0000000..a166d1e
--- /dev/null
+++ b/lib/icinga/eventcommand.ti
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/command.hpp"
+
+
+library icinga;
+
+namespace icinga
+{
+
+class EventCommand : Command
+{
+};
+
+}
diff --git a/lib/icinga/externalcommandprocessor.cpp b/lib/icinga/externalcommandprocessor.cpp
new file mode 100644
index 0000000..9850da0
--- /dev/null
+++ b/lib/icinga/externalcommandprocessor.cpp
@@ -0,0 +1,2281 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/externalcommandprocessor.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/user.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/compatutility.hpp"
+#include "remote/apifunction.hpp"
+#include "base/convert.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include <fstream>
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+boost::signals2::signal<void(double, const String&, const std::vector<String>&)> ExternalCommandProcessor::OnNewExternalCommand;
+
+void ExternalCommandProcessor::Execute(const String& line)
+{
+ if (line.IsEmpty())
+ return;
+
+ if (line[0] != '[')
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing timestamp in command: " + line));
+
+ size_t pos = line.FindFirstOf("]");
+
+ if (pos == String::NPos)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing timestamp in command: " + line));
+
+ String timestamp = line.SubStr(1, pos - 1);
+ String args = line.SubStr(pos + 2, String::NPos);
+
+ double ts = Convert::ToDouble(timestamp);
+
+ if (ts == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid timestamp in command: " + line));
+
+ std::vector<String> argv = args.Split(";");
+
+ if (argv.empty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing arguments in command: " + line));
+
+ std::vector<String> argvExtra(argv.begin() + 1, argv.end());
+ Execute(ts, argv[0], argvExtra);
+}
+
+void ExternalCommandProcessor::Execute(double time, const String& command, const std::vector<String>& arguments)
+{
+ ExternalCommandInfo eci;
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, []() {
+ RegisterCommands();
+ });
+
+ {
+ std::unique_lock<std::mutex> lock(GetMutex());
+
+ auto it = GetCommands().find(command);
+
+ if (it == GetCommands().end())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The external command '" + command + "' does not exist."));
+
+ eci = it->second;
+ }
+
+ if (arguments.size() < eci.MinArgs)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Expected " + Convert::ToString(eci.MinArgs) + " arguments"));
+
+ size_t argnum = std::min(arguments.size(), eci.MaxArgs);
+
+ std::vector<String> realArguments;
+ realArguments.resize(argnum);
+
+ if (argnum > 0) {
+ std::copy(arguments.begin(), arguments.begin() + argnum - 1, realArguments.begin());
+
+ String last_argument;
+ for (std::vector<String>::size_type i = argnum - 1; i < arguments.size(); i++) {
+ if (!last_argument.IsEmpty())
+ last_argument += ";";
+
+ last_argument += arguments[i];
+ }
+
+ realArguments[argnum - 1] = last_argument;
+ }
+
+ OnNewExternalCommand(time, command, realArguments);
+
+ eci.Callback(time, realArguments);
+}
+
+void ExternalCommandProcessor::RegisterCommand(const String& command, const ExternalCommandCallback& callback, size_t minArgs, size_t maxArgs)
+{
+ std::unique_lock<std::mutex> lock(GetMutex());
+ ExternalCommandInfo eci;
+ eci.Callback = callback;
+ eci.MinArgs = minArgs;
+ eci.MaxArgs = (maxArgs == UINT_MAX) ? minArgs : maxArgs;
+ GetCommands()[command] = eci;
+}
+
+void ExternalCommandProcessor::RegisterCommands()
+{
+ RegisterCommand("PROCESS_HOST_CHECK_RESULT", &ExternalCommandProcessor::ProcessHostCheckResult, 3);
+ RegisterCommand("PROCESS_SERVICE_CHECK_RESULT", &ExternalCommandProcessor::ProcessServiceCheckResult, 4);
+ RegisterCommand("SCHEDULE_HOST_CHECK", &ExternalCommandProcessor::ScheduleHostCheck, 2);
+ RegisterCommand("SCHEDULE_FORCED_HOST_CHECK", &ExternalCommandProcessor::ScheduleForcedHostCheck, 2);
+ RegisterCommand("SCHEDULE_SVC_CHECK", &ExternalCommandProcessor::ScheduleSvcCheck, 3);
+ RegisterCommand("SCHEDULE_FORCED_SVC_CHECK", &ExternalCommandProcessor::ScheduleForcedSvcCheck, 3);
+ RegisterCommand("ENABLE_HOST_CHECK", &ExternalCommandProcessor::EnableHostCheck, 1);
+ RegisterCommand("DISABLE_HOST_CHECK", &ExternalCommandProcessor::DisableHostCheck, 1);
+ RegisterCommand("ENABLE_SVC_CHECK", &ExternalCommandProcessor::EnableSvcCheck, 2);
+ RegisterCommand("DISABLE_SVC_CHECK", &ExternalCommandProcessor::DisableSvcCheck, 2);
+ RegisterCommand("SHUTDOWN_PROCESS", &ExternalCommandProcessor::ShutdownProcess);
+ RegisterCommand("RESTART_PROCESS", &ExternalCommandProcessor::RestartProcess);
+ RegisterCommand("SCHEDULE_FORCED_HOST_SVC_CHECKS", &ExternalCommandProcessor::ScheduleForcedHostSvcChecks, 2);
+ RegisterCommand("SCHEDULE_HOST_SVC_CHECKS", &ExternalCommandProcessor::ScheduleHostSvcChecks, 2);
+ RegisterCommand("ENABLE_HOST_SVC_CHECKS", &ExternalCommandProcessor::EnableHostSvcChecks, 1);
+ RegisterCommand("DISABLE_HOST_SVC_CHECKS", &ExternalCommandProcessor::DisableHostSvcChecks, 1);
+ RegisterCommand("ACKNOWLEDGE_SVC_PROBLEM", &ExternalCommandProcessor::AcknowledgeSvcProblem, 7);
+ RegisterCommand("ACKNOWLEDGE_SVC_PROBLEM_EXPIRE", &ExternalCommandProcessor::AcknowledgeSvcProblemExpire, 8);
+ RegisterCommand("REMOVE_SVC_ACKNOWLEDGEMENT", &ExternalCommandProcessor::RemoveSvcAcknowledgement, 2);
+ RegisterCommand("ACKNOWLEDGE_HOST_PROBLEM", &ExternalCommandProcessor::AcknowledgeHostProblem, 6);
+ RegisterCommand("ACKNOWLEDGE_HOST_PROBLEM_EXPIRE", &ExternalCommandProcessor::AcknowledgeHostProblemExpire, 7);
+ RegisterCommand("REMOVE_HOST_ACKNOWLEDGEMENT", &ExternalCommandProcessor::RemoveHostAcknowledgement, 1);
+ RegisterCommand("DISABLE_HOST_FLAP_DETECTION", &ExternalCommandProcessor::DisableHostFlapping, 1);
+ RegisterCommand("ENABLE_HOST_FLAP_DETECTION", &ExternalCommandProcessor::EnableHostFlapping, 1);
+ RegisterCommand("DISABLE_SVC_FLAP_DETECTION", &ExternalCommandProcessor::DisableSvcFlapping, 2);
+ RegisterCommand("ENABLE_SVC_FLAP_DETECTION", &ExternalCommandProcessor::EnableSvcFlapping, 2);
+ RegisterCommand("ENABLE_HOSTGROUP_SVC_CHECKS", &ExternalCommandProcessor::EnableHostgroupSvcChecks, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_SVC_CHECKS", &ExternalCommandProcessor::DisableHostgroupSvcChecks, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_SVC_CHECKS", &ExternalCommandProcessor::EnableServicegroupSvcChecks, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_SVC_CHECKS", &ExternalCommandProcessor::DisableServicegroupSvcChecks, 1);
+ RegisterCommand("ENABLE_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::EnablePassiveHostChecks, 1);
+ RegisterCommand("DISABLE_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::DisablePassiveHostChecks, 1);
+ RegisterCommand("ENABLE_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::EnablePassiveSvcChecks, 2);
+ RegisterCommand("DISABLE_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::DisablePassiveSvcChecks, 2);
+ RegisterCommand("ENABLE_SERVICEGROUP_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::EnableServicegroupPassiveSvcChecks, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::DisableServicegroupPassiveSvcChecks, 1);
+ RegisterCommand("ENABLE_HOSTGROUP_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::EnableHostgroupPassiveSvcChecks, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::DisableHostgroupPassiveSvcChecks, 1);
+ RegisterCommand("PROCESS_FILE", &ExternalCommandProcessor::ProcessFile, 2);
+ RegisterCommand("SCHEDULE_SVC_DOWNTIME", &ExternalCommandProcessor::ScheduleSvcDowntime, 9);
+ RegisterCommand("DEL_SVC_DOWNTIME", &ExternalCommandProcessor::DelSvcDowntime, 1);
+ RegisterCommand("SCHEDULE_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleHostDowntime, 8);
+ RegisterCommand("SCHEDULE_AND_PROPAGATE_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleAndPropagateHostDowntime, 8);
+ RegisterCommand("SCHEDULE_AND_PROPAGATE_TRIGGERED_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleAndPropagateTriggeredHostDowntime, 8);
+ RegisterCommand("DEL_HOST_DOWNTIME", &ExternalCommandProcessor::DelHostDowntime, 1);
+ RegisterCommand("DEL_DOWNTIME_BY_HOST_NAME", &ExternalCommandProcessor::DelDowntimeByHostName, 1, 4);
+ RegisterCommand("SCHEDULE_HOST_SVC_DOWNTIME", &ExternalCommandProcessor::ScheduleHostSvcDowntime, 8);
+ RegisterCommand("SCHEDULE_HOSTGROUP_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleHostgroupHostDowntime, 8);
+ RegisterCommand("SCHEDULE_HOSTGROUP_SVC_DOWNTIME", &ExternalCommandProcessor::ScheduleHostgroupSvcDowntime, 8);
+ RegisterCommand("SCHEDULE_SERVICEGROUP_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleServicegroupHostDowntime, 8);
+ RegisterCommand("SCHEDULE_SERVICEGROUP_SVC_DOWNTIME", &ExternalCommandProcessor::ScheduleServicegroupSvcDowntime, 8);
+ RegisterCommand("ADD_HOST_COMMENT", &ExternalCommandProcessor::AddHostComment, 4);
+ RegisterCommand("DEL_HOST_COMMENT", &ExternalCommandProcessor::DelHostComment, 1);
+ RegisterCommand("ADD_SVC_COMMENT", &ExternalCommandProcessor::AddSvcComment, 5);
+ RegisterCommand("DEL_SVC_COMMENT", &ExternalCommandProcessor::DelSvcComment, 1);
+ RegisterCommand("DEL_ALL_HOST_COMMENTS", &ExternalCommandProcessor::DelAllHostComments, 1);
+ RegisterCommand("DEL_ALL_SVC_COMMENTS", &ExternalCommandProcessor::DelAllSvcComments, 2);
+ RegisterCommand("SEND_CUSTOM_HOST_NOTIFICATION", &ExternalCommandProcessor::SendCustomHostNotification, 4);
+ RegisterCommand("SEND_CUSTOM_SVC_NOTIFICATION", &ExternalCommandProcessor::SendCustomSvcNotification, 5);
+ RegisterCommand("DELAY_HOST_NOTIFICATION", &ExternalCommandProcessor::DelayHostNotification, 2);
+ RegisterCommand("DELAY_SVC_NOTIFICATION", &ExternalCommandProcessor::DelaySvcNotification, 3);
+ RegisterCommand("ENABLE_HOST_NOTIFICATIONS", &ExternalCommandProcessor::EnableHostNotifications, 1);
+ RegisterCommand("DISABLE_HOST_NOTIFICATIONS", &ExternalCommandProcessor::DisableHostNotifications, 1);
+ RegisterCommand("ENABLE_SVC_NOTIFICATIONS", &ExternalCommandProcessor::EnableSvcNotifications, 2);
+ RegisterCommand("DISABLE_SVC_NOTIFICATIONS", &ExternalCommandProcessor::DisableSvcNotifications, 2);
+ RegisterCommand("ENABLE_HOST_SVC_NOTIFICATIONS", &ExternalCommandProcessor::EnableHostSvcNotifications, 1);
+ RegisterCommand("DISABLE_HOST_SVC_NOTIFICATIONS", &ExternalCommandProcessor::DisableHostSvcNotifications, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_HOST_CHECKS", &ExternalCommandProcessor::DisableHostgroupHostChecks, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::DisableHostgroupPassiveHostChecks, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_HOST_CHECKS", &ExternalCommandProcessor::DisableServicegroupHostChecks, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::DisableServicegroupPassiveHostChecks, 1);
+ RegisterCommand("ENABLE_HOSTGROUP_HOST_CHECKS", &ExternalCommandProcessor::EnableHostgroupHostChecks, 1);
+ RegisterCommand("ENABLE_HOSTGROUP_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::EnableHostgroupPassiveHostChecks, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_HOST_CHECKS", &ExternalCommandProcessor::EnableServicegroupHostChecks, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::EnableServicegroupPassiveHostChecks, 1);
+ RegisterCommand("ENABLE_NOTIFICATIONS", &ExternalCommandProcessor::EnableNotifications);
+ RegisterCommand("DISABLE_NOTIFICATIONS", &ExternalCommandProcessor::DisableNotifications);
+ RegisterCommand("ENABLE_FLAP_DETECTION", &ExternalCommandProcessor::EnableFlapDetection);
+ RegisterCommand("DISABLE_FLAP_DETECTION", &ExternalCommandProcessor::DisableFlapDetection);
+ RegisterCommand("ENABLE_EVENT_HANDLERS", &ExternalCommandProcessor::EnableEventHandlers);
+ RegisterCommand("DISABLE_EVENT_HANDLERS", &ExternalCommandProcessor::DisableEventHandlers);
+ RegisterCommand("ENABLE_PERFORMANCE_DATA", &ExternalCommandProcessor::EnablePerformanceData);
+ RegisterCommand("DISABLE_PERFORMANCE_DATA", &ExternalCommandProcessor::DisablePerformanceData);
+ RegisterCommand("START_EXECUTING_SVC_CHECKS", &ExternalCommandProcessor::StartExecutingSvcChecks);
+ RegisterCommand("STOP_EXECUTING_SVC_CHECKS", &ExternalCommandProcessor::StopExecutingSvcChecks);
+ RegisterCommand("START_EXECUTING_HOST_CHECKS", &ExternalCommandProcessor::StartExecutingHostChecks);
+ RegisterCommand("STOP_EXECUTING_HOST_CHECKS", &ExternalCommandProcessor::StopExecutingHostChecks);
+ RegisterCommand("CHANGE_NORMAL_SVC_CHECK_INTERVAL", &ExternalCommandProcessor::ChangeNormalSvcCheckInterval, 3);
+ RegisterCommand("CHANGE_NORMAL_HOST_CHECK_INTERVAL", &ExternalCommandProcessor::ChangeNormalHostCheckInterval, 2);
+ RegisterCommand("CHANGE_RETRY_SVC_CHECK_INTERVAL", &ExternalCommandProcessor::ChangeRetrySvcCheckInterval, 3);
+ RegisterCommand("CHANGE_RETRY_HOST_CHECK_INTERVAL", &ExternalCommandProcessor::ChangeRetryHostCheckInterval, 2);
+ RegisterCommand("ENABLE_HOST_EVENT_HANDLER", &ExternalCommandProcessor::EnableHostEventHandler, 1);
+ RegisterCommand("DISABLE_HOST_EVENT_HANDLER", &ExternalCommandProcessor::DisableHostEventHandler, 1);
+ RegisterCommand("ENABLE_SVC_EVENT_HANDLER", &ExternalCommandProcessor::EnableSvcEventHandler, 2);
+ RegisterCommand("DISABLE_SVC_EVENT_HANDLER", &ExternalCommandProcessor::DisableSvcEventHandler, 2);
+ RegisterCommand("CHANGE_HOST_EVENT_HANDLER", &ExternalCommandProcessor::ChangeHostEventHandler, 2);
+ RegisterCommand("CHANGE_SVC_EVENT_HANDLER", &ExternalCommandProcessor::ChangeSvcEventHandler, 3);
+ RegisterCommand("CHANGE_HOST_CHECK_COMMAND", &ExternalCommandProcessor::ChangeHostCheckCommand, 2);
+ RegisterCommand("CHANGE_SVC_CHECK_COMMAND", &ExternalCommandProcessor::ChangeSvcCheckCommand, 3);
+ RegisterCommand("CHANGE_MAX_HOST_CHECK_ATTEMPTS", &ExternalCommandProcessor::ChangeMaxHostCheckAttempts, 2);
+ RegisterCommand("CHANGE_MAX_SVC_CHECK_ATTEMPTS", &ExternalCommandProcessor::ChangeMaxSvcCheckAttempts, 3);
+ RegisterCommand("CHANGE_HOST_CHECK_TIMEPERIOD", &ExternalCommandProcessor::ChangeHostCheckTimeperiod, 2);
+ RegisterCommand("CHANGE_SVC_CHECK_TIMEPERIOD", &ExternalCommandProcessor::ChangeSvcCheckTimeperiod, 3);
+ RegisterCommand("CHANGE_CUSTOM_HOST_VAR", &ExternalCommandProcessor::ChangeCustomHostVar, 3);
+ RegisterCommand("CHANGE_CUSTOM_SVC_VAR", &ExternalCommandProcessor::ChangeCustomSvcVar, 4);
+ RegisterCommand("CHANGE_CUSTOM_USER_VAR", &ExternalCommandProcessor::ChangeCustomUserVar, 3);
+ RegisterCommand("CHANGE_CUSTOM_CHECKCOMMAND_VAR", &ExternalCommandProcessor::ChangeCustomCheckcommandVar, 3);
+ RegisterCommand("CHANGE_CUSTOM_EVENTCOMMAND_VAR", &ExternalCommandProcessor::ChangeCustomEventcommandVar, 3);
+ RegisterCommand("CHANGE_CUSTOM_NOTIFICATIONCOMMAND_VAR", &ExternalCommandProcessor::ChangeCustomNotificationcommandVar, 3);
+
+ RegisterCommand("ENABLE_HOSTGROUP_HOST_NOTIFICATIONS", &ExternalCommandProcessor::EnableHostgroupHostNotifications, 1);
+ RegisterCommand("ENABLE_HOSTGROUP_SVC_NOTIFICATIONS", &ExternalCommandProcessor::EnableHostgroupSvcNotifications, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_HOST_NOTIFICATIONS", &ExternalCommandProcessor::DisableHostgroupHostNotifications, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_SVC_NOTIFICATIONS", &ExternalCommandProcessor::DisableHostgroupSvcNotifications, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_HOST_NOTIFICATIONS", &ExternalCommandProcessor::EnableServicegroupHostNotifications, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_HOST_NOTIFICATIONS", &ExternalCommandProcessor::DisableServicegroupHostNotifications, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_SVC_NOTIFICATIONS", &ExternalCommandProcessor::EnableServicegroupSvcNotifications, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_SVC_NOTIFICATIONS", &ExternalCommandProcessor::DisableServicegroupSvcNotifications, 1);
+}
+
+void ExternalCommandProcessor::ExecuteFromFile(const String& line, std::deque< std::vector<String> >& file_queue)
+{
+ if (line.IsEmpty())
+ return;
+
+ if (line[0] != '[')
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing timestamp in command: " + line));
+
+ size_t pos = line.FindFirstOf("]");
+
+ if (pos == String::NPos)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing timestamp in command: " + line));
+
+ String timestamp = line.SubStr(1, pos - 1);
+ String args = line.SubStr(pos + 2, String::NPos);
+
+ double ts = Convert::ToDouble(timestamp);
+
+ if (ts == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid timestamp in command: " + line));
+
+ std::vector<String> argv = args.Split(";");
+
+ if (argv.empty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing arguments in command: " + line));
+
+ std::vector<String> argvExtra(argv.begin() + 1, argv.end());
+
+ if (argv[0] == "PROCESS_FILE") {
+ Log(LogDebug, "ExternalCommandProcessor")
+ << "Enqueing external command file " << argvExtra[0];
+ file_queue.push_back(argvExtra);
+ } else {
+ Execute(ts, argv[0], argvExtra);
+ }
+}
+
+void ExternalCommandProcessor::ProcessHostCheckResult(double time, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot process passive host check result for non-existent host '" + arguments[0] + "'"));
+
+ if (!host->GetEnablePassiveChecks())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Got passive check result for host '" + arguments[0] + "' which has passive checks disabled."));
+
+ if (!host->IsReachable(DependencyCheckExecution)) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring passive check result for unreachable host '" << arguments[0] << "'";
+ return;
+ }
+
+ int exitStatus = Convert::ToDouble(arguments[1]);
+ CheckResult::Ptr result = new CheckResult();
+ std::pair<String, String> co = PluginUtility::ParseCheckOutput(arguments[2]);
+ result->SetOutput(co.first);
+ result->SetPerformanceData(PluginUtility::SplitPerfdata(co.second));
+
+ ServiceState state;
+
+ if (exitStatus == 0)
+ state = ServiceOK;
+ else if (exitStatus == 1)
+ state = ServiceCritical;
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid status code: " + arguments[1]));
+
+ result->SetState(state);
+
+ result->SetScheduleStart(time);
+ result->SetScheduleEnd(time);
+ result->SetExecutionStart(time);
+ result->SetExecutionEnd(time);
+
+ /* Mark this check result as passive. */
+ result->SetActive(false);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Processing passive check result for host '" << arguments[0] << "'";
+
+ host->ProcessCheckResult(result);
+}
+
+void ExternalCommandProcessor::ProcessServiceCheckResult(double time, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot process passive service check result for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (!service->GetEnablePassiveChecks())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Got passive check result for service '" + arguments[1] + "' which has passive checks disabled."));
+
+ if (!service->IsReachable(DependencyCheckExecution)) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring passive check result for unreachable service '" << arguments[1] << "'";
+ return;
+ }
+
+ int exitStatus = Convert::ToDouble(arguments[2]);
+ CheckResult::Ptr result = new CheckResult();
+ String output = CompatUtility::UnEscapeString(arguments[3]);
+ std::pair<String, String> co = PluginUtility::ParseCheckOutput(output);
+ result->SetOutput(co.first);
+ result->SetPerformanceData(PluginUtility::SplitPerfdata(co.second));
+ result->SetState(PluginUtility::ExitStatusToState(exitStatus));
+
+ result->SetScheduleStart(time);
+ result->SetScheduleEnd(time);
+ result->SetExecutionStart(time);
+ result->SetExecutionEnd(time);
+
+ /* Mark this check result as passive. */
+ result->SetActive(false);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Processing passive check result for service '" << arguments[1] << "'";
+
+ service->ProcessCheckResult(result);
+}
+
+void ExternalCommandProcessor::ScheduleHostCheck(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule host check for non-existent host '" + arguments[0] + "'"));
+
+ double planned_check = Convert::ToDouble(arguments[1]);
+
+ if (planned_check > host->GetNextCheck()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring reschedule request for host '"
+ << arguments[0] << "' (next check is already sooner than requested check time)";
+ return;
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for host '" << arguments[0] << "'";
+
+ if (planned_check < Utility::GetTime())
+ planned_check = Utility::GetTime();
+
+ host->SetNextCheck(planned_check);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(host);
+}
+
+void ExternalCommandProcessor::ScheduleForcedHostCheck(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule forced host check for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for host '" << arguments[0] << "'";
+
+ host->SetForceNextCheck(true);
+ host->SetNextCheck(Convert::ToDouble(arguments[1]));
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(host);
+}
+
+void ExternalCommandProcessor::ScheduleSvcCheck(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule service check for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ double planned_check = Convert::ToDouble(arguments[2]);
+
+ if (planned_check > service->GetNextCheck()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring reschedule request for service '"
+ << arguments[1] << "' (next check is already sooner than requested check time)";
+ return;
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for service '" << arguments[1] << "'";
+
+ if (planned_check < Utility::GetTime())
+ planned_check = Utility::GetTime();
+
+ service->SetNextCheck(planned_check);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(service);
+}
+
+void ExternalCommandProcessor::ScheduleForcedSvcCheck(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule forced service check for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for service '" << arguments[1] << "'";
+
+ service->SetForceNextCheck(true);
+ service->SetNextCheck(Convert::ToDouble(arguments[2]));
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(service);
+}
+
+void ExternalCommandProcessor::EnableHostCheck(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host checks for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_active_checks", true);
+}
+
+void ExternalCommandProcessor::DisableHostCheck(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host check non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_active_checks", false);
+}
+
+void ExternalCommandProcessor::EnableSvcCheck(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service check for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_active_checks", true);
+}
+
+void ExternalCommandProcessor::DisableSvcCheck(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service check for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_active_checks", false);
+}
+
+void ExternalCommandProcessor::ShutdownProcess(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Shutting down Icinga via external command.");
+ Application::RequestShutdown();
+}
+
+void ExternalCommandProcessor::RestartProcess(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Restarting Icinga via external command.");
+ Application::RequestRestart();
+}
+
+void ExternalCommandProcessor::ScheduleForcedHostSvcChecks(double, const std::vector<String>& arguments)
+{
+ double planned_check = Convert::ToDouble(arguments[1]);
+
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule forced host service checks for non-existent host '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for service '" << service->GetName() << "'";
+
+ service->SetNextCheck(planned_check);
+ service->SetForceNextCheck(true);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(service);
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostSvcChecks(double, const std::vector<String>& arguments)
+{
+ double planned_check = Convert::ToDouble(arguments[1]);
+
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule host service checks for non-existent host '" + arguments[0] + "'"));
+
+ if (planned_check < Utility::GetTime())
+ planned_check = Utility::GetTime();
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (planned_check > service->GetNextCheck()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring reschedule request for service '"
+ << service->GetName() << "' (next check is already sooner than requested check time)";
+ continue;
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for service '" << service->GetName() << "'";
+
+ service->SetNextCheck(planned_check);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(service);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostSvcChecks(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host service checks for non-existent host '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostSvcChecks(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host service checks for non-existent host '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::AcknowledgeSvcProblem(double, const std::vector<String>& arguments)
+{
+ bool sticky = (Convert::ToLong(arguments[2]) == 2 ? true : false);
+ bool notify = (Convert::ToLong(arguments[3]) > 0 ? true : false);
+ bool persistent = (Convert::ToLong(arguments[4]) > 0 ? true : false);
+
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+ ObjectLock oLock (service);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot acknowledge service problem for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (service->GetState() == ServiceOK)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The service '" + arguments[1] + "' is OK."));
+
+ if (service->IsAcknowledged()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The service '" + arguments[1] + "' is already acknowledged."));
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Setting acknowledgement for service '" << service->GetName() << "'" << (notify ? "" : ". Disabled notification");
+
+ Comment::AddComment(service, CommentAcknowledgement, arguments[5], arguments[6], persistent, 0, sticky);
+ service->AcknowledgeProblem(arguments[5], arguments[6], sticky ? AcknowledgementSticky : AcknowledgementNormal, notify, persistent);
+}
+
+void ExternalCommandProcessor::AcknowledgeSvcProblemExpire(double, const std::vector<String>& arguments)
+{
+ bool sticky = (Convert::ToLong(arguments[2]) == 2 ? true : false);
+ bool notify = (Convert::ToLong(arguments[3]) > 0 ? true : false);
+ bool persistent = (Convert::ToLong(arguments[4]) > 0 ? true : false);
+ double timestamp = Convert::ToDouble(arguments[5]);
+
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+ ObjectLock oLock (service);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot acknowledge service problem with expire time for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (service->GetState() == ServiceOK)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The service '" + arguments[1] + "' is OK."));
+
+ if (timestamp != 0 && timestamp <= Utility::GetTime())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Acknowledgement expire time must be in the future for service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (service->IsAcknowledged()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The service '" + arguments[1] + "' is already acknowledged."));
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Setting timed acknowledgement for service '" << service->GetName() << "'" << (notify ? "" : ". Disabled notification");
+
+ Comment::AddComment(service, CommentAcknowledgement, arguments[6], arguments[7], persistent, timestamp, sticky);
+ service->AcknowledgeProblem(arguments[6], arguments[7], sticky ? AcknowledgementSticky : AcknowledgementNormal, notify, persistent, timestamp);
+}
+
+void ExternalCommandProcessor::RemoveSvcAcknowledgement(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot remove service acknowledgement for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing acknowledgement for service '" << service->GetName() << "'";
+
+ {
+ ObjectLock olock(service);
+ service->ClearAcknowledgement("");
+ }
+
+ service->RemoveAckComments();
+}
+
+void ExternalCommandProcessor::AcknowledgeHostProblem(double, const std::vector<String>& arguments)
+{
+ bool sticky = (Convert::ToLong(arguments[1]) == 2 ? true : false);
+ bool notify = (Convert::ToLong(arguments[2]) > 0 ? true : false);
+ bool persistent = (Convert::ToLong(arguments[3]) > 0 ? true : false);
+
+ Host::Ptr host = Host::GetByName(arguments[0]);
+ ObjectLock oLock (host);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot acknowledge host problem for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Setting acknowledgement for host '" << host->GetName() << "'" << (notify ? "" : ". Disabled notification");
+
+ if (host->GetState() == HostUp)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The host '" + arguments[0] + "' is OK."));
+
+ if (host->IsAcknowledged()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The host '" + arguments[1] + "' is already acknowledged."));
+ }
+
+ Comment::AddComment(host, CommentAcknowledgement, arguments[4], arguments[5], persistent, 0, sticky);
+ host->AcknowledgeProblem(arguments[4], arguments[5], sticky ? AcknowledgementSticky : AcknowledgementNormal, notify, persistent);
+}
+
+void ExternalCommandProcessor::AcknowledgeHostProblemExpire(double, const std::vector<String>& arguments)
+{
+ bool sticky = (Convert::ToLong(arguments[1]) == 2 ? true : false);
+ bool notify = (Convert::ToLong(arguments[2]) > 0 ? true : false);
+ bool persistent = (Convert::ToLong(arguments[3]) > 0 ? true : false);
+ double timestamp = Convert::ToDouble(arguments[4]);
+
+ Host::Ptr host = Host::GetByName(arguments[0]);
+ ObjectLock oLock (host);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot acknowledge host problem with expire time for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Setting timed acknowledgement for host '" << host->GetName() << "'" << (notify ? "" : ". Disabled notification");
+
+ if (host->GetState() == HostUp)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The host '" + arguments[0] + "' is OK."));
+
+ if (timestamp != 0 && timestamp <= Utility::GetTime())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Acknowledgement expire time must be in the future for host '" + arguments[0] + "'"));
+
+ if (host->IsAcknowledged()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The host '" + arguments[1] + "' is already acknowledged."));
+ }
+
+ Comment::AddComment(host, CommentAcknowledgement, arguments[5], arguments[6], persistent, timestamp, sticky);
+ host->AcknowledgeProblem(arguments[5], arguments[6], sticky ? AcknowledgementSticky : AcknowledgementNormal, notify, persistent, timestamp);
+}
+
+void ExternalCommandProcessor::RemoveHostAcknowledgement(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot remove acknowledgement for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing acknowledgement for host '" << host->GetName() << "'";
+
+ {
+ ObjectLock olock(host);
+ host->ClearAcknowledgement("");
+ }
+ host->RemoveAckComments();
+}
+
+void ExternalCommandProcessor::EnableHostgroupSvcChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable hostgroup service checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", true);
+ }
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupSvcChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable hostgroup service checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", false);
+ }
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupSvcChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable servicegroup service checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupSvcChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable servicegroup service checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::EnablePassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable passive host checks for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_passive_checks", true);
+}
+
+void ExternalCommandProcessor::DisablePassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable passive host checks for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_passive_checks", false);
+}
+
+void ExternalCommandProcessor::EnablePassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service checks for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_passive_checks", true);
+}
+
+void ExternalCommandProcessor::DisablePassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service checks for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_passive_checks", false);
+}
+
+void ExternalCommandProcessor::EnableServicegroupPassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable servicegroup passive service checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_passive_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupPassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable servicegroup passive service checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_passive_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostgroupPassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable hostgroup passive service checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_passive_checks", true);
+ }
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupPassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable hostgroup passive service checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_passive_checks", false);
+ }
+ }
+}
+
+void ExternalCommandProcessor::ProcessFile(double, const std::vector<String>& arguments)
+{
+ std::deque< std::vector<String> > file_queue;
+ file_queue.push_back(arguments);
+
+ while (!file_queue.empty()) {
+ std::vector<String> argument = file_queue.front();
+ file_queue.pop_front();
+
+ String file = argument[0];
+ int to_delete = Convert::ToLong(argument[1]);
+
+ std::ifstream ifp;
+ ifp.exceptions(std::ifstream::badbit);
+
+ ifp.open(file.CStr(), std::ifstream::in);
+
+ while (ifp.good()) {
+ std::string line;
+ std::getline(ifp, line);
+
+ try {
+ Log(LogNotice, "compat")
+ << "Executing external command: " << line;
+
+ ExecuteFromFile(line, file_queue);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ExternalCommandProcessor")
+ << "External command failed: " << DiagnosticInformation(ex);
+ }
+ }
+
+ ifp.close();
+
+ if (to_delete > 0)
+ (void) unlink(file.CStr());
+ }
+}
+
+void ExternalCommandProcessor::ScheduleSvcDowntime(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule service downtime for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[5]);
+ int is_fixed = Convert::ToLong(arguments[4]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for service " << service->GetName();
+ (void) Downtime::AddDowntime(service, arguments[7], arguments[8],
+ Convert::ToDouble(arguments[2]), Convert::ToDouble(arguments[3]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[6]));
+}
+
+void ExternalCommandProcessor::DelSvcDowntime(double, const std::vector<String>& arguments)
+{
+ int id = Convert::ToLong(arguments[0]);
+ String rid = Downtime::GetDowntimeIDFromLegacyID(id);
+
+ try {
+ Downtime::RemoveDowntime(rid, false, true);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removed downtime ID " << arguments[0];
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ExternalCommandProcessor") << error.what();
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostDowntime(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule host downtime for non-existent host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+}
+
+void ExternalCommandProcessor::ScheduleAndPropagateHostDowntime(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule and propagate host downtime for non-existent host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+
+ /* Schedule downtime for all child hosts */
+ for (const Checkable::Ptr& child : host->GetAllChildren()) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(child);
+
+ /* ignore all service children */
+ if (service)
+ continue;
+
+ (void) Downtime::AddDowntime(child, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleAndPropagateTriggeredHostDowntime(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule and propagate triggered host downtime for non-existent host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ Downtime::Ptr parentDowntime = Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+
+ /* Schedule downtime for all child hosts and explicitely trigger them through the parent host's downtime */
+ for (const Checkable::Ptr& child : host->GetAllChildren()) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(child);
+
+ /* ignore all service children */
+ if (service)
+ continue;
+
+ (void) Downtime::AddDowntime(child, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), parentDowntime->GetName(), Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::DelHostDowntime(double, const std::vector<String>& arguments)
+{
+ int id = Convert::ToLong(arguments[0]);
+ String rid = Downtime::GetDowntimeIDFromLegacyID(id);
+
+ try {
+ Downtime::RemoveDowntime(rid, false, true);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removed downtime ID " << arguments[0];
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ExternalCommandProcessor") << error.what();
+ }
+}
+
+void ExternalCommandProcessor::DelDowntimeByHostName(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule host services downtime for non-existent host '" + arguments[0] + "'"));
+
+ String serviceName;
+ if (arguments.size() >= 2)
+ serviceName = arguments[1];
+
+ String startTime;
+ if (arguments.size() >= 3)
+ startTime = arguments[2];
+
+ String commentString;
+ if (arguments.size() >= 4)
+ commentString = arguments[3];
+
+ if (arguments.size() > 5)
+ Log(LogWarning, "ExternalCommandProcessor")
+ << ("Ignoring additional parameters for host '" + arguments[0] + "' downtime deletion.");
+
+ for (const Downtime::Ptr& downtime : host->GetDowntimes()) {
+ try {
+ String downtimeName = downtime->GetName();
+ Downtime::RemoveDowntime(downtimeName, false, true);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removed downtime '" << downtimeName << "'.";
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ExternalCommandProcessor") << error.what();
+ }
+ }
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (!serviceName.IsEmpty() && serviceName != service->GetName())
+ continue;
+
+ for (const Downtime::Ptr& downtime : service->GetDowntimes()) {
+ if (!startTime.IsEmpty() && downtime->GetStartTime() != Convert::ToDouble(startTime))
+ continue;
+
+ if (!commentString.IsEmpty() && downtime->GetComment() != commentString)
+ continue;
+
+ try {
+ String downtimeName = downtime->GetName();
+ Downtime::RemoveDowntime(downtimeName, false, true);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removed downtime '" << downtimeName << "'.";
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ExternalCommandProcessor") << error.what();
+ }
+ }
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostSvcDowntime(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule host services downtime for non-existent host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for service " << service->GetName();
+ (void) Downtime::AddDowntime(service, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostgroupHostDowntime(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule hostgroup host downtime for non-existent hostgroup '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostgroupSvcDowntime(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule hostgroup service downtime for non-existent hostgroup '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ /* Note: we can't just directly create downtimes for all the services by iterating
+ * over all hosts in the host group - otherwise we might end up creating multiple
+ * downtimes for some services. */
+
+ std::set<Service::Ptr> services;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ services.insert(service);
+ }
+ }
+
+ for (const Service::Ptr& service : services) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for service " << service->GetName();
+ (void) Downtime::AddDowntime(service, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleServicegroupHostDowntime(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule servicegroup host downtime for non-existent servicegroup '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ /* Note: we can't just directly create downtimes for all the hosts by iterating
+ * over all services in the service group - otherwise we might end up creating multiple
+ * downtimes for some hosts. */
+
+ std::set<Host::Ptr> hosts;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+ hosts.insert(host);
+ }
+
+ for (const Host::Ptr& host : hosts) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleServicegroupSvcDowntime(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule servicegroup service downtime for non-existent servicegroup '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for service " << service->GetName();
+ (void) Downtime::AddDowntime(service, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::AddHostComment(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot add host comment for non-existent host '" + arguments[0] + "'"));
+
+ if (arguments[2].IsEmpty() || arguments[3].IsEmpty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Author and comment must not be empty"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating comment for host " << host->GetName();
+ (void) Comment::AddComment(host, CommentUser, arguments[2], arguments[3], false, 0);
+}
+
+void ExternalCommandProcessor::DelHostComment(double, const std::vector<String>& arguments)
+{
+ int id = Convert::ToLong(arguments[0]);
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing comment ID " << arguments[0];
+ String rid = Comment::GetCommentIDFromLegacyID(id);
+ Comment::RemoveComment(rid);
+}
+
+void ExternalCommandProcessor::AddSvcComment(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot add service comment for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (arguments[3].IsEmpty() || arguments[4].IsEmpty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Author and comment must not be empty"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating comment for service " << service->GetName();
+ (void) Comment::AddComment(service, CommentUser, arguments[3], arguments[4], false, 0);
+}
+
+void ExternalCommandProcessor::DelSvcComment(double, const std::vector<String>& arguments)
+{
+ int id = Convert::ToLong(arguments[0]);
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing comment ID " << arguments[0];
+
+ String rid = Comment::GetCommentIDFromLegacyID(id);
+ Comment::RemoveComment(rid);
+}
+
+void ExternalCommandProcessor::DelAllHostComments(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot delete all host comments for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing all comments for host " << host->GetName();
+ host->RemoveAllComments();
+}
+
+void ExternalCommandProcessor::DelAllSvcComments(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot delete all service comments for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing all comments for service " << service->GetName();
+ service->RemoveAllComments();
+}
+
+void ExternalCommandProcessor::SendCustomHostNotification(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot send custom host notification for non-existent host '" + arguments[0] + "'"));
+
+ int options = Convert::ToLong(arguments[1]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Sending custom notification for host " << host->GetName();
+ if (options & 2) {
+ host->SetForceNextNotification(true);
+ }
+
+ Checkable::OnNotificationsRequested(host, NotificationCustom,
+ host->GetLastCheckResult(), arguments[2], arguments[3], nullptr);
+}
+
+void ExternalCommandProcessor::SendCustomSvcNotification(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot send custom service notification for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ int options = Convert::ToLong(arguments[2]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Sending custom notification for service " << service->GetName();
+
+ if (options & 2) {
+ service->SetForceNextNotification(true);
+ }
+
+ Service::OnNotificationsRequested(service, NotificationCustom,
+ service->GetLastCheckResult(), arguments[3], arguments[4], nullptr);
+}
+
+void ExternalCommandProcessor::DelayHostNotification(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot delay host notification for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Delaying notifications for host '" << host->GetName() << "'";
+
+ for (const Notification::Ptr& notification : host->GetNotifications()) {
+ notification->SetNextNotification(Convert::ToDouble(arguments[1]));
+ }
+}
+
+void ExternalCommandProcessor::DelaySvcNotification(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot delay service notification for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Delaying notifications for service " << service->GetName();
+
+ for (const Notification::Ptr& notification : service->GetNotifications()) {
+ notification->SetNextNotification(Convert::ToDouble(arguments[2]));
+ }
+}
+
+void ExternalCommandProcessor::EnableHostNotifications(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host notifications for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_notifications", true);
+}
+
+void ExternalCommandProcessor::DisableHostNotifications(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host notifications for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_notifications", false);
+}
+
+void ExternalCommandProcessor::EnableSvcNotifications(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service notifications for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_notifications", true);
+}
+
+void ExternalCommandProcessor::DisableSvcNotifications(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service notifications for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_notifications", false);
+}
+
+void ExternalCommandProcessor::EnableHostSvcNotifications(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable notifications for all services for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for all services on host '" << arguments[0] << "'";
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostSvcNotifications(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable notifications for all services for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for all services on host '" << arguments[0] << "'";
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupHostChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable hostgroup host checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_active_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupPassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable hostgroup passive host checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_passive_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupHostChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable servicegroup host checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_active_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupPassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable servicegroup passive host checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_passive_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostgroupHostChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable hostgroup host checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_active_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostgroupPassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable hostgroup passive host checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_passive_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupHostChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable servicegroup host checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_active_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupPassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable servicegroup passive host checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_passive_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostFlapping(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host flapping for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling flapping detection for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_flapping", true);
+}
+
+void ExternalCommandProcessor::DisableHostFlapping(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host flapping for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling flapping detection for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_flapping", false);
+}
+
+void ExternalCommandProcessor::EnableSvcFlapping(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service flapping for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling flapping detection for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_flapping", true);
+}
+
+void ExternalCommandProcessor::DisableSvcFlapping(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service flapping for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling flapping detection for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_flapping", false);
+}
+
+void ExternalCommandProcessor::EnableNotifications(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling notifications.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_notifications", true);
+}
+
+void ExternalCommandProcessor::DisableNotifications(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling notifications.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_notifications", false);
+}
+
+void ExternalCommandProcessor::EnableFlapDetection(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling flap detection.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_flapping", true);
+}
+
+void ExternalCommandProcessor::DisableFlapDetection(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling flap detection.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_flapping", false);
+}
+
+void ExternalCommandProcessor::EnableEventHandlers(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling event handlers.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_event_handlers", true);
+}
+
+void ExternalCommandProcessor::DisableEventHandlers(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling event handlers.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_event_handlers", false);
+}
+
+void ExternalCommandProcessor::EnablePerformanceData(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling performance data processing.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_perfdata", true);
+}
+
+void ExternalCommandProcessor::DisablePerformanceData(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling performance data processing.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_perfdata", false);
+}
+
+void ExternalCommandProcessor::StartExecutingSvcChecks(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling service checks.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_service_checks", true);
+}
+
+void ExternalCommandProcessor::StopExecutingSvcChecks(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling service checks.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_service_checks", false);
+}
+
+void ExternalCommandProcessor::StartExecutingHostChecks(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling host checks.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_host_checks", true);
+}
+
+void ExternalCommandProcessor::StopExecutingHostChecks(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling host checks.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_host_checks", false);
+}
+
+void ExternalCommandProcessor::ChangeNormalSvcCheckInterval(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot update check interval for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ double interval = Convert::ToDouble(arguments[2]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Updating check interval for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("check_interval", interval * 60);
+}
+
+void ExternalCommandProcessor::ChangeNormalHostCheckInterval(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot update check interval for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Updating check interval for host '" << arguments[0] << "'";
+
+ double interval = Convert::ToDouble(arguments[1]);
+
+ host->ModifyAttribute("check_interval", interval * 60);
+}
+
+void ExternalCommandProcessor::ChangeRetrySvcCheckInterval(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot update retry interval for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ double interval = Convert::ToDouble(arguments[2]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Updating retry interval for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("retry_interval", interval * 60);
+}
+
+void ExternalCommandProcessor::ChangeRetryHostCheckInterval(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot update retry interval for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Updating retry interval for host '" << arguments[0] << "'";
+
+ double interval = Convert::ToDouble(arguments[1]);
+
+ host->ModifyAttribute("retry_interval", interval * 60);
+}
+
+void ExternalCommandProcessor::EnableHostEventHandler(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable event handler for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling event handler for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_event_handler", true);
+}
+
+void ExternalCommandProcessor::DisableHostEventHandler(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable event handler for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling event handler for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_event_handler", false);
+}
+
+void ExternalCommandProcessor::EnableSvcEventHandler(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable event handler for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling event handler for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_event_handler", true);
+}
+
+void ExternalCommandProcessor::DisableSvcEventHandler(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable event handler for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling event handler for service '" << arguments[1] + "'";
+
+ service->ModifyAttribute("enable_event_handler", false);
+}
+
+void ExternalCommandProcessor::ChangeHostEventHandler(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change event handler for non-existent host '" + arguments[0] + "'"));
+
+ if (arguments[1].IsEmpty()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Unsetting event handler for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("event_command", "");
+ } else {
+ EventCommand::Ptr command = EventCommand::GetByName(arguments[1]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Event command '" + arguments[1] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing event handler for host '" << arguments[0] << "' to '" << arguments[1] << "'";
+
+ host->ModifyAttribute("event_command", command->GetName());
+ }
+}
+
+void ExternalCommandProcessor::ChangeSvcEventHandler(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change event handler for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (arguments[2].IsEmpty()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Unsetting event handler for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("event_command", "");
+ } else {
+ EventCommand::Ptr command = EventCommand::GetByName(arguments[2]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Event command '" + arguments[2] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing event handler for service '" << arguments[1] << "' to '" << arguments[2] << "'";
+
+ service->ModifyAttribute("event_command", command->GetName());
+ }
+}
+
+void ExternalCommandProcessor::ChangeHostCheckCommand(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change check command for non-existent host '" + arguments[0] + "'"));
+
+ CheckCommand::Ptr command = CheckCommand::GetByName(arguments[1]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Check command '" + arguments[1] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing check command for host '" << arguments[0] << "' to '" << arguments[1] << "'";
+
+ host->ModifyAttribute("check_command", command->GetName());
+}
+
+void ExternalCommandProcessor::ChangeSvcCheckCommand(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change check command for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ CheckCommand::Ptr command = CheckCommand::GetByName(arguments[2]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Check command '" + arguments[2] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing check command for service '" << arguments[1] << "' to '" << arguments[2] << "'";
+
+ service->ModifyAttribute("check_command", command->GetName());
+}
+
+void ExternalCommandProcessor::ChangeMaxHostCheckAttempts(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change max check attempts for non-existent host '" + arguments[0] + "'"));
+
+ int attempts = Convert::ToLong(arguments[1]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing max check attempts for host '" << arguments[0] << "' to '" << arguments[1] << "'";
+
+ host->ModifyAttribute("max_check_attempts", attempts);
+}
+
+void ExternalCommandProcessor::ChangeMaxSvcCheckAttempts(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change max check attempts for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ int attempts = Convert::ToLong(arguments[2]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing max check attempts for service '" << arguments[1] << "' to '" << arguments[2] << "'";
+
+ service->ModifyAttribute("max_check_attempts", attempts);
+}
+
+void ExternalCommandProcessor::ChangeHostCheckTimeperiod(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change check period for non-existent host '" + arguments[0] + "'"));
+
+ TimePeriod::Ptr tp = TimePeriod::GetByName(arguments[1]);
+
+ if (!tp)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Time period '" + arguments[1] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing check period for host '" << arguments[0] << "' to '" << arguments[1] << "'";
+
+ host->ModifyAttribute("check_period", tp->GetName());
+}
+
+void ExternalCommandProcessor::ChangeSvcCheckTimeperiod(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change check period for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ TimePeriod::Ptr tp = TimePeriod::GetByName(arguments[2]);
+
+ if (!tp)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Time period '" + arguments[2] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing check period for service '" << arguments[1] << "' to '" << arguments[2] << "'";
+
+ service->ModifyAttribute("check_period", tp->GetName());
+}
+
+void ExternalCommandProcessor::ChangeCustomHostVar(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing custom var '" << arguments[1] << "' for host '" << arguments[0] << "' to value '" << arguments[2] << "'";
+
+ host->ModifyAttribute("vars." + arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomSvcVar(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing custom var '" << arguments[2] << "' for service '" << arguments[1] << "' on host '"
+ << arguments[0] << "' to value '" << arguments[3] << "'";
+
+ service->ModifyAttribute("vars." + arguments[2], arguments[3]);
+}
+
+void ExternalCommandProcessor::ChangeCustomUserVar(double, const std::vector<String>& arguments)
+{
+ User::Ptr user = User::GetByName(arguments[0]);
+
+ if (!user)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent user '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing custom var '" << arguments[1] << "' for user '" << arguments[0] << "' to value '" << arguments[2] << "'";
+
+ user->ModifyAttribute("vars." + arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomCheckcommandVar(double, const std::vector<String>& arguments)
+{
+ CheckCommand::Ptr command = CheckCommand::GetByName(arguments[0]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent command '" + arguments[0] + "'"));
+
+ ChangeCustomCommandVarInternal(command, arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomEventcommandVar(double, const std::vector<String>& arguments)
+{
+ EventCommand::Ptr command = EventCommand::GetByName(arguments[0]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent command '" + arguments[0] + "'"));
+
+ ChangeCustomCommandVarInternal(command, arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomNotificationcommandVar(double, const std::vector<String>& arguments)
+{
+ NotificationCommand::Ptr command = NotificationCommand::GetByName(arguments[0]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent command '" + arguments[0] + "'"));
+
+ ChangeCustomCommandVarInternal(command, arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomCommandVarInternal(const Command::Ptr& command, const String& name, const Value& value)
+{
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing custom var '" << name << "' for command '" << command->GetName() << "' to value '" << value << "'";
+
+ command->ModifyAttribute("vars." + name, value);
+}
+
+void ExternalCommandProcessor::EnableHostgroupHostNotifications(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host notifications for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_notifications", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostgroupSvcNotifications(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service notifications for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", true);
+ }
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupHostNotifications(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host notifications for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_notifications", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupSvcNotifications(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service notifications for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", false);
+ }
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupHostNotifications(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host notifications for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_notifications", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupSvcNotifications(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service notifications for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupHostNotifications(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host notifications for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_notifications", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupSvcNotifications(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service notifications for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", false);
+ }
+}
+
+std::mutex& ExternalCommandProcessor::GetMutex()
+{
+ static std::mutex mtx;
+ return mtx;
+}
+
+std::map<String, ExternalCommandInfo>& ExternalCommandProcessor::GetCommands()
+{
+ static std::map<String, ExternalCommandInfo> commands;
+ return commands;
+}
+
diff --git a/lib/icinga/externalcommandprocessor.hpp b/lib/icinga/externalcommandprocessor.hpp
new file mode 100644
index 0000000..a7c5a30
--- /dev/null
+++ b/lib/icinga/externalcommandprocessor.hpp
@@ -0,0 +1,169 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EXTERNALCOMMANDPROCESSOR_H
+#define EXTERNALCOMMANDPROCESSOR_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/command.hpp"
+#include "base/string.hpp"
+#include <boost/signals2.hpp>
+#include <vector>
+
+namespace icinga
+{
+
+typedef std::function<void (double, const std::vector<String>& arguments)> ExternalCommandCallback;
+
+struct ExternalCommandInfo
+{
+ ExternalCommandCallback Callback;
+ size_t MinArgs;
+ size_t MaxArgs;
+};
+
+class ExternalCommandProcessor {
+public:
+ static void Execute(const String& line);
+ static void Execute(double time, const String& command, const std::vector<String>& arguments);
+
+ static boost::signals2::signal<void(double, const String&, const std::vector<String>&)> OnNewExternalCommand;
+
+private:
+ ExternalCommandProcessor();
+
+ static void ExecuteFromFile(const String& line, std::deque< std::vector<String> >& file_queue);
+
+ static void ProcessHostCheckResult(double time, const std::vector<String>& arguments);
+ static void ProcessServiceCheckResult(double time, const std::vector<String>& arguments);
+ static void ScheduleHostCheck(double time, const std::vector<String>& arguments);
+ static void ScheduleForcedHostCheck(double time, const std::vector<String>& arguments);
+ static void ScheduleSvcCheck(double time, const std::vector<String>& arguments);
+ static void ScheduleForcedSvcCheck(double time, const std::vector<String>& arguments);
+ static void EnableHostCheck(double time, const std::vector<String>& arguments);
+ static void DisableHostCheck(double time, const std::vector<String>& arguments);
+ static void EnableSvcCheck(double time, const std::vector<String>& arguments);
+ static void DisableSvcCheck(double time, const std::vector<String>& arguments);
+ static void ShutdownProcess(double time, const std::vector<String>& arguments);
+ static void RestartProcess(double time, const std::vector<String>& arguments);
+ static void ScheduleForcedHostSvcChecks(double time, const std::vector<String>& arguments);
+ static void ScheduleHostSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnableHostSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableHostSvcChecks(double time, const std::vector<String>& arguments);
+ static void AcknowledgeSvcProblem(double time, const std::vector<String>& arguments);
+ static void AcknowledgeSvcProblemExpire(double time, const std::vector<String>& arguments);
+ static void RemoveSvcAcknowledgement(double time, const std::vector<String>& arguments);
+ static void AcknowledgeHostProblem(double time, const std::vector<String>& arguments);
+ static void AcknowledgeHostProblemExpire(double time, const std::vector<String>& arguments);
+ static void RemoveHostAcknowledgement(double time, const std::vector<String>& arguments);
+ static void EnableHostgroupSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableHostgroupSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnableServicegroupSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableServicegroupSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnablePassiveHostChecks(double time, const std::vector<String>& arguments);
+ static void DisablePassiveHostChecks(double time, const std::vector<String>& arguments);
+ static void EnablePassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisablePassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnableServicegroupPassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableServicegroupPassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnableHostgroupPassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableHostgroupPassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void ProcessFile(double time, const std::vector<String>& arguments);
+ static void ScheduleSvcDowntime(double time, const std::vector<String>& arguments);
+ static void DelSvcDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleHostDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleAndPropagateHostDowntime(double, const std::vector<String>& arguments);
+ static void ScheduleAndPropagateTriggeredHostDowntime(double, const std::vector<String>& arguments);
+ static void DelHostDowntime(double time, const std::vector<String>& arguments);
+ static void DelDowntimeByHostName(double, const std::vector<String>& arguments);
+ static void ScheduleHostSvcDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleHostgroupHostDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleHostgroupSvcDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleServicegroupHostDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleServicegroupSvcDowntime(double time, const std::vector<String>& arguments);
+ static void AddHostComment(double time, const std::vector<String>& arguments);
+ static void DelHostComment(double time, const std::vector<String>& arguments);
+ static void AddSvcComment(double time, const std::vector<String>& arguments);
+ static void DelSvcComment(double time, const std::vector<String>& arguments);
+ static void DelAllHostComments(double time, const std::vector<String>& arguments);
+ static void DelAllSvcComments(double time, const std::vector<String>& arguments);
+ static void SendCustomHostNotification(double time, const std::vector<String>& arguments);
+ static void SendCustomSvcNotification(double time, const std::vector<String>& arguments);
+ static void DelayHostNotification(double time, const std::vector<String>& arguments);
+ static void DelaySvcNotification(double time, const std::vector<String>& arguments);
+ static void EnableHostNotifications(double time, const std::vector<String>& arguments);
+ static void DisableHostNotifications(double time, const std::vector<String>& arguments);
+ static void EnableSvcNotifications(double time, const std::vector<String>& arguments);
+ static void DisableSvcNotifications(double time, const std::vector<String>& arguments);
+ static void EnableHostSvcNotifications(double, const std::vector<String>& arguments);
+ static void DisableHostSvcNotifications(double, const std::vector<String>& arguments);
+ static void DisableHostgroupHostChecks(double, const std::vector<String>& arguments);
+ static void DisableHostgroupPassiveHostChecks(double, const std::vector<String>& arguments);
+ static void DisableServicegroupHostChecks(double, const std::vector<String>& arguments);
+ static void DisableServicegroupPassiveHostChecks(double, const std::vector<String>& arguments);
+ static void EnableHostgroupHostChecks(double, const std::vector<String>& arguments);
+ static void EnableHostgroupPassiveHostChecks(double, const std::vector<String>& arguments);
+ static void EnableServicegroupHostChecks(double, const std::vector<String>& arguments);
+ static void EnableServicegroupPassiveHostChecks(double, const std::vector<String>& arguments);
+ static void EnableSvcFlapping(double time, const std::vector<String>& arguments);
+ static void DisableSvcFlapping(double time, const std::vector<String>& arguments);
+ static void EnableHostFlapping(double time, const std::vector<String>& arguments);
+ static void DisableHostFlapping(double time, const std::vector<String>& arguments);
+ static void EnableNotifications(double time, const std::vector<String>& arguments);
+ static void DisableNotifications(double time, const std::vector<String>& arguments);
+ static void EnableFlapDetection(double time, const std::vector<String>& arguments);
+ static void DisableFlapDetection(double time, const std::vector<String>& arguments);
+ static void EnableEventHandlers(double time, const std::vector<String>& arguments);
+ static void DisableEventHandlers(double time, const std::vector<String>& arguments);
+ static void EnablePerformanceData(double time, const std::vector<String>& arguments);
+ static void DisablePerformanceData(double time, const std::vector<String>& arguments);
+ static void StartExecutingSvcChecks(double time, const std::vector<String>& arguments);
+ static void StopExecutingSvcChecks(double time, const std::vector<String>& arguments);
+ static void StartExecutingHostChecks(double time, const std::vector<String>& arguments);
+ static void StopExecutingHostChecks(double time, const std::vector<String>& arguments);
+
+ static void ChangeNormalSvcCheckInterval(double time, const std::vector<String>& arguments);
+ static void ChangeNormalHostCheckInterval(double time, const std::vector<String>& arguments);
+ static void ChangeRetrySvcCheckInterval(double time, const std::vector<String>& arguments);
+ static void ChangeRetryHostCheckInterval(double time, const std::vector<String>& arguments);
+ static void EnableHostEventHandler(double time, const std::vector<String>& arguments);
+ static void DisableHostEventHandler(double time, const std::vector<String>& arguments);
+ static void EnableSvcEventHandler(double time, const std::vector<String>& arguments);
+ static void DisableSvcEventHandler(double time, const std::vector<String>& arguments);
+ static void ChangeHostEventHandler(double time, const std::vector<String>& arguments);
+ static void ChangeSvcEventHandler(double time, const std::vector<String>& arguments);
+ static void ChangeHostCheckCommand(double time, const std::vector<String>& arguments);
+ static void ChangeSvcCheckCommand(double time, const std::vector<String>& arguments);
+ static void ChangeMaxHostCheckAttempts(double time, const std::vector<String>& arguments);
+ static void ChangeMaxSvcCheckAttempts(double time, const std::vector<String>& arguments);
+ static void ChangeHostCheckTimeperiod(double time, const std::vector<String>& arguments);
+ static void ChangeSvcCheckTimeperiod(double time, const std::vector<String>& arguments);
+ static void ChangeCustomHostVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomSvcVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomUserVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomCheckcommandVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomEventcommandVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomNotificationcommandVar(double time, const std::vector<String>& arguments);
+
+ static void EnableHostgroupHostNotifications(double time, const std::vector<String>& arguments);
+ static void EnableHostgroupSvcNotifications(double time, const std::vector<String>& arguments);
+ static void DisableHostgroupHostNotifications(double time, const std::vector<String>& arguments);
+ static void DisableHostgroupSvcNotifications(double time, const std::vector<String>& arguments);
+ static void EnableServicegroupHostNotifications(double time, const std::vector<String>& arguments);
+ static void EnableServicegroupSvcNotifications(double time, const std::vector<String>& arguments);
+ static void DisableServicegroupHostNotifications(double time, const std::vector<String>& arguments);
+ static void DisableServicegroupSvcNotifications(double time, const std::vector<String>& arguments);
+
+private:
+ static void ChangeCustomCommandVarInternal(const Command::Ptr& command, const String& name, const Value& value);
+
+ static void RegisterCommand(const String& command, const ExternalCommandCallback& callback, size_t minArgs = 0, size_t maxArgs = UINT_MAX);
+ static void RegisterCommands();
+
+ static std::mutex& GetMutex();
+ static std::map<String, ExternalCommandInfo>& GetCommands();
+
+};
+
+}
+
+#endif /* EXTERNALCOMMANDPROCESSOR_H */
diff --git a/lib/icinga/host.cpp b/lib/icinga/host.cpp
new file mode 100644
index 0000000..36149d3
--- /dev/null
+++ b/lib/icinga/host.cpp
@@ -0,0 +1,330 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/host.hpp"
+#include "icinga/host-ti.cpp"
+#include "icinga/service.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/scheduleddowntime.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/debug.hpp"
+#include "base/json.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Host);
+
+void Host::OnAllConfigLoaded()
+{
+ ObjectImpl<Host>::OnAllConfigLoaded();
+
+ String zoneName = GetZoneName();
+
+ if (!zoneName.IsEmpty()) {
+ Zone::Ptr zone = Zone::GetByName(zoneName);
+
+ if (zone && zone->IsGlobal())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Host '" + GetName() + "' cannot be put into global zone '" + zone->GetName() + "'."));
+ }
+
+ HostGroup::EvaluateObjectRules(this);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ HostGroup::Ptr hg = HostGroup::GetByName(name);
+
+ if (hg)
+ hg->ResolveGroupMembership(this, true);
+ }
+ }
+}
+
+void Host::CreateChildObjects(const Type::Ptr& childType)
+{
+ if (childType == ScheduledDowntime::TypeInstance)
+ ScheduledDowntime::EvaluateApplyRules(this);
+
+ if (childType == Notification::TypeInstance)
+ Notification::EvaluateApplyRules(this);
+
+ if (childType == Dependency::TypeInstance)
+ Dependency::EvaluateApplyRules(this);
+
+ if (childType == Service::TypeInstance)
+ Service::EvaluateApplyRules(this);
+}
+
+void Host::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<Host>::Stop(runtimeRemoved);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ HostGroup::Ptr hg = HostGroup::GetByName(name);
+
+ if (hg)
+ hg->ResolveGroupMembership(this, false);
+ }
+ }
+
+ // TODO: unregister slave services/notifications?
+}
+
+std::vector<Service::Ptr> Host::GetServices() const
+{
+ std::unique_lock<std::mutex> lock(m_ServicesMutex);
+
+ std::vector<Service::Ptr> services;
+ services.reserve(m_Services.size());
+ typedef std::pair<String, Service::Ptr> ServicePair;
+ for (const ServicePair& kv : m_Services) {
+ services.push_back(kv.second);
+ }
+
+ return services;
+}
+
+void Host::AddService(const Service::Ptr& service)
+{
+ std::unique_lock<std::mutex> lock(m_ServicesMutex);
+
+ m_Services[service->GetShortName()] = service;
+}
+
+void Host::RemoveService(const Service::Ptr& service)
+{
+ std::unique_lock<std::mutex> lock(m_ServicesMutex);
+
+ m_Services.erase(service->GetShortName());
+}
+
+int Host::GetTotalServices() const
+{
+ return GetServices().size();
+}
+
+Service::Ptr Host::GetServiceByShortName(const Value& name)
+{
+ if (name.IsScalar()) {
+ {
+ std::unique_lock<std::mutex> lock(m_ServicesMutex);
+
+ auto it = m_Services.find(name);
+
+ if (it != m_Services.end())
+ return it->second;
+ }
+
+ return nullptr;
+ } else if (name.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = name;
+ String short_name;
+
+ return Service::GetByNamePair(dict->Get("host"), dict->Get("service"));
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Host/Service name pair is invalid: " + JsonEncode(name)));
+ }
+}
+
+HostState Host::CalculateState(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ case ServiceWarning:
+ return HostUp;
+ default:
+ return HostDown;
+ }
+}
+
+HostState Host::GetState() const
+{
+ return CalculateState(GetStateRaw());
+}
+
+HostState Host::GetLastState() const
+{
+ return CalculateState(GetLastStateRaw());
+}
+
+HostState Host::GetLastHardState() const
+{
+ return CalculateState(GetLastHardStateRaw());
+}
+
+/* keep in sync with Service::GetSeverity()
+ * One could think it may be smart to use an enum and some bitmask math here.
+ * But the only thing the consuming icingaweb2 cares about is being able to
+ * sort by severity. It is therefore easier to keep them seperated here. */
+int Host::GetSeverity() const
+{
+ int severity = 0;
+
+ ObjectLock olock(this);
+ HostState state = GetState();
+
+ if (!HasBeenChecked()) {
+ severity = 16;
+ } else if (state == HostUp) {
+ severity = 0;
+ } else {
+ if (IsReachable())
+ severity = 64;
+ else
+ severity = 32;
+
+ if (IsAcknowledged())
+ severity += 512;
+ else if (IsInDowntime())
+ severity += 256;
+ else
+ severity += 2048;
+ }
+
+ olock.Unlock();
+
+ return severity;
+
+}
+
+bool Host::IsStateOK(ServiceState state) const
+{
+ return Host::CalculateState(state) == HostUp;
+}
+
+void Host::SaveLastState(ServiceState state, double timestamp)
+{
+ if (state == ServiceOK || state == ServiceWarning)
+ SetLastStateUp(timestamp);
+ else if (state == ServiceCritical)
+ SetLastStateDown(timestamp);
+}
+
+HostState Host::StateFromString(const String& state)
+{
+ if (state == "UP")
+ return HostUp;
+ else
+ return HostDown;
+}
+
+String Host::StateToString(HostState state)
+{
+ switch (state) {
+ case HostUp:
+ return "UP";
+ case HostDown:
+ return "DOWN";
+ default:
+ return "INVALID";
+ }
+}
+
+StateType Host::StateTypeFromString(const String& type)
+{
+ if (type == "SOFT")
+ return StateTypeSoft;
+ else
+ return StateTypeHard;
+}
+
+String Host::StateTypeToString(StateType type)
+{
+ if (type == StateTypeSoft)
+ return "SOFT";
+ else
+ return "HARD";
+}
+
+bool Host::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const
+{
+ if (macro == "state") {
+ *result = StateToString(GetState());
+ return true;
+ } else if (macro == "state_id") {
+ *result = GetState();
+ return true;
+ } else if (macro == "state_type") {
+ *result = StateTypeToString(GetStateType());
+ return true;
+ } else if (macro == "last_state") {
+ *result = StateToString(GetLastState());
+ return true;
+ } else if (macro == "last_state_id") {
+ *result = GetLastState();
+ return true;
+ } else if (macro == "last_state_type") {
+ *result = StateTypeToString(GetLastStateType());
+ return true;
+ } else if (macro == "last_state_change") {
+ *result = static_cast<long>(GetLastStateChange());
+ return true;
+ } else if (macro == "downtime_depth") {
+ *result = GetDowntimeDepth();
+ return true;
+ } else if (macro == "duration_sec") {
+ *result = Utility::GetTime() - GetLastStateChange();
+ return true;
+ } else if (macro == "num_services" || macro == "num_services_ok" || macro == "num_services_warning"
+ || macro == "num_services_unknown" || macro == "num_services_critical") {
+ int filter = -1;
+ int count = 0;
+
+ if (macro == "num_services_ok")
+ filter = ServiceOK;
+ else if (macro == "num_services_warning")
+ filter = ServiceWarning;
+ else if (macro == "num_services_unknown")
+ filter = ServiceUnknown;
+ else if (macro == "num_services_critical")
+ filter = ServiceCritical;
+
+ for (const Service::Ptr& service : GetServices()) {
+ if (filter != -1 && service->GetState() != filter)
+ continue;
+
+ count++;
+ }
+
+ *result = count;
+ return true;
+ }
+
+ CheckResult::Ptr cr = GetLastCheckResult();
+
+ if (cr) {
+ if (macro == "latency") {
+ *result = cr->CalculateLatency();
+ return true;
+ } else if (macro == "execution_time") {
+ *result = cr->CalculateExecutionTime();
+ return true;
+ } else if (macro == "output") {
+ *result = cr->GetOutput();
+ return true;
+ } else if (macro == "perfdata") {
+ *result = PluginUtility::FormatPerfdata(cr->GetPerformanceData());
+ return true;
+ } else if (macro == "check_source") {
+ *result = cr->GetCheckSource();
+ return true;
+ } else if (macro == "scheduling_source") {
+ *result = cr->GetSchedulingSource();
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/lib/icinga/host.hpp b/lib/icinga/host.hpp
new file mode 100644
index 0000000..d0d6c1a
--- /dev/null
+++ b/lib/icinga/host.hpp
@@ -0,0 +1,71 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HOST_H
+#define HOST_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/host-ti.hpp"
+#include "icinga/macroresolver.hpp"
+#include "icinga/checkresult.hpp"
+
+namespace icinga
+{
+
+class Service;
+
+/**
+ * An Icinga host.
+ *
+ * @ingroup icinga
+ */
+class Host final : public ObjectImpl<Host>, public MacroResolver
+{
+public:
+ DECLARE_OBJECT(Host);
+ DECLARE_OBJECTNAME(Host);
+
+ intrusive_ptr<Service> GetServiceByShortName(const Value& name);
+
+ std::vector<intrusive_ptr<Service> > GetServices() const;
+ void AddService(const intrusive_ptr<Service>& service);
+ void RemoveService(const intrusive_ptr<Service>& service);
+
+ int GetTotalServices() const;
+
+ static HostState CalculateState(ServiceState state);
+
+ HostState GetState() const override;
+ HostState GetLastState() const override;
+ HostState GetLastHardState() const override;
+ int GetSeverity() const override;
+
+ bool IsStateOK(ServiceState state) const override;
+ void SaveLastState(ServiceState state, double timestamp) override;
+
+ static HostState StateFromString(const String& state);
+ static String StateToString(HostState state);
+
+ static StateType StateTypeFromString(const String& state);
+ static String StateTypeToString(StateType state);
+
+ bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const override;
+
+ void OnAllConfigLoaded() override;
+
+protected:
+ void Stop(bool runtimeRemoved) override;
+
+ void CreateChildObjects(const Type::Ptr& childType) override;
+
+private:
+ mutable std::mutex m_ServicesMutex;
+ std::map<String, intrusive_ptr<Service> > m_Services;
+
+ static void RefreshServicesCache();
+};
+
+}
+
+#endif /* HOST_H */
+
+#include "icinga/service.hpp"
diff --git a/lib/icinga/host.ti b/lib/icinga/host.ti
new file mode 100644
index 0000000..f6624e3
--- /dev/null
+++ b/lib/icinga/host.ti
@@ -0,0 +1,48 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/customvarobject.hpp"
+#impl_include "icinga/hostgroup.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class Host : Checkable
+{
+ load_after ApiListener;
+ load_after Endpoint;
+ load_after Zone;
+
+ [config, no_user_modify, required, signal_with_old_value] array(name(HostGroup)) groups {
+ default {{{ return new Array(); }}}
+ };
+
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+
+ [config] String address;
+ [config] String address6;
+
+ [enum, no_storage] HostState "state" {
+ get;
+ };
+ [enum, no_storage] HostState last_state {
+ get;
+ };
+ [enum, no_storage] HostState last_hard_state {
+ get;
+ };
+ [state] Timestamp last_state_up;
+ [state] Timestamp last_state_down;
+};
+
+}
diff --git a/lib/icinga/hostgroup.cpp b/lib/icinga/hostgroup.cpp
new file mode 100644
index 0000000..a22f3b7
--- /dev/null
+++ b/lib/icinga/hostgroup.cpp
@@ -0,0 +1,108 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/hostgroup.hpp"
+#include "icinga/hostgroup-ti.cpp"
+#include "config/objectrule.hpp"
+#include "config/configitem.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(HostGroup);
+
+INITIALIZE_ONCE([]() {
+ ObjectRule::RegisterType("HostGroup");
+});
+
+bool HostGroup::EvaluateObjectRule(const Host::Ptr& host, const ConfigItem::Ptr& group)
+{
+ String groupName = group->GetName();
+
+ CONTEXT("Evaluating rule for group '" << groupName << "'");
+
+ ScriptFrame frame(true);
+ if (group->GetScope())
+ group->GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+
+ if (!group->GetFilter()->Evaluate(frame).GetValue().ToBool())
+ return false;
+
+ Log(LogDebug, "HostGroup")
+ << "Assigning membership for group '" << groupName << "' to host '" << host->GetName() << "'";
+
+ Array::Ptr groups = host->GetGroups();
+
+ if (groups && !groups->Contains(groupName))
+ groups->Add(groupName);
+
+ return true;
+}
+
+void HostGroup::EvaluateObjectRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating group memberships for host '" << host->GetName() << "'");
+
+ for (const ConfigItem::Ptr& group : ConfigItem::GetItems(HostGroup::TypeInstance))
+ {
+ if (!group->GetFilter())
+ continue;
+
+ EvaluateObjectRule(host, group);
+ }
+}
+
+std::set<Host::Ptr> HostGroup::GetMembers() const
+{
+ std::unique_lock<std::mutex> lock(m_HostGroupMutex);
+ return m_Members;
+}
+
+void HostGroup::AddMember(const Host::Ptr& host)
+{
+ host->AddGroup(GetName());
+
+ std::unique_lock<std::mutex> lock(m_HostGroupMutex);
+ m_Members.insert(host);
+}
+
+void HostGroup::RemoveMember(const Host::Ptr& host)
+{
+ std::unique_lock<std::mutex> lock(m_HostGroupMutex);
+ m_Members.erase(host);
+}
+
+bool HostGroup::ResolveGroupMembership(const Host::Ptr& host, bool add, int rstack) {
+
+ if (add && rstack > 20) {
+ Log(LogWarning, "HostGroup")
+ << "Too many nested groups for group '" << GetName() << "': Host '"
+ << host->GetName() << "' membership assignment failed.";
+
+ return false;
+ }
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups && groups->GetLength() > 0) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ HostGroup::Ptr group = HostGroup::GetByName(name);
+
+ if (group && !group->ResolveGroupMembership(host, add, rstack + 1))
+ return false;
+ }
+ }
+
+ if (add)
+ AddMember(host);
+ else
+ RemoveMember(host);
+
+ return true;
+}
diff --git a/lib/icinga/hostgroup.hpp b/lib/icinga/hostgroup.hpp
new file mode 100644
index 0000000..3ad5d26
--- /dev/null
+++ b/lib/icinga/hostgroup.hpp
@@ -0,0 +1,43 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HOSTGROUP_H
+#define HOSTGROUP_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/hostgroup-ti.hpp"
+#include "icinga/host.hpp"
+
+namespace icinga
+{
+
+class ConfigItem;
+
+/**
+ * An Icinga host group.
+ *
+ * @ingroup icinga
+ */
+class HostGroup final : public ObjectImpl<HostGroup>
+{
+public:
+ DECLARE_OBJECT(HostGroup);
+ DECLARE_OBJECTNAME(HostGroup);
+
+ std::set<Host::Ptr> GetMembers() const;
+ void AddMember(const Host::Ptr& host);
+ void RemoveMember(const Host::Ptr& host);
+
+ bool ResolveGroupMembership(const Host::Ptr& host, bool add = true, int rstack = 0);
+
+ static void EvaluateObjectRules(const Host::Ptr& host);
+
+private:
+ mutable std::mutex m_HostGroupMutex;
+ std::set<Host::Ptr> m_Members;
+
+ static bool EvaluateObjectRule(const Host::Ptr& host, const intrusive_ptr<ConfigItem>& item);
+};
+
+}
+
+#endif /* HOSTGROUP_H */
diff --git a/lib/icinga/hostgroup.ti b/lib/icinga/hostgroup.ti
new file mode 100644
index 0000000..b679344
--- /dev/null
+++ b/lib/icinga/hostgroup.ti
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class HostGroup : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+
+ [config, no_user_modify] array(name(HostGroup)) groups;
+ [config] String notes;
+ [config] String notes_url;
+ [config] String action_url;
+};
+
+}
diff --git a/lib/icinga/i2-icinga.hpp b/lib/icinga/i2-icinga.hpp
new file mode 100644
index 0000000..7163822
--- /dev/null
+++ b/lib/icinga/i2-icinga.hpp
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2ICINGA_H
+#define I2ICINGA_H
+
+/**
+ * @defgroup icinga Icinga library
+ *
+ * The Icinga library implements all Icinga-specific functionality that is
+ * common to all components (e.g. hosts, services, etc.).
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2ICINGA_H */
diff --git a/lib/icinga/icinga-itl.conf b/lib/icinga/icinga-itl.conf
new file mode 100644
index 0000000..22b688a
--- /dev/null
+++ b/lib/icinga/icinga-itl.conf
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+System.assert(Internal.run_with_activation_context(function() {
+ template TimePeriod "legacy-timeperiod" use (LegacyTimePeriod = Internal.LegacyTimePeriod) default {
+ update = LegacyTimePeriod
+ }
+}))
+
+var methods = [
+ "LegacyTimePeriod"
+]
+
+for (method in methods) {
+ Internal.remove(method)
+}
diff --git a/lib/icinga/icingaapplication.cpp b/lib/icinga/icingaapplication.cpp
new file mode 100644
index 0000000..94ae0ed
--- /dev/null
+++ b/lib/icinga/icingaapplication.cpp
@@ -0,0 +1,321 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/icingaapplication.hpp"
+#include "icinga/icingaapplication-ti.cpp"
+#include "icinga/cib.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "config/configcompiler.hpp"
+#include "base/atomic-file.hpp"
+#include "base/configwriter.hpp"
+#include "base/configtype.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/debug.hpp"
+#include "base/utility.hpp"
+#include "base/timer.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/initialize.hpp"
+#include "base/statsfunction.hpp"
+#include "base/loader.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+static Timer::Ptr l_RetentionTimer;
+
+REGISTER_TYPE(IcingaApplication);
+/* Ensure that the priority is lower than the basic System namespace initialization in scriptframe.cpp. */
+INITIALIZE_ONCE_WITH_PRIORITY(&IcingaApplication::StaticInitialize, InitializePriority::InitIcingaApplication);
+
+static Namespace::Ptr l_IcingaNS;
+
+void IcingaApplication::StaticInitialize()
+{
+ /* Pre-fill global constants, can be overridden with user input later in icinga-app/icinga.cpp. */
+ String node_name = Utility::GetFQDN();
+
+ if (node_name.IsEmpty()) {
+ Log(LogNotice, "IcingaApplication", "No FQDN available. Trying Hostname.");
+ node_name = Utility::GetHostName();
+
+ if (node_name.IsEmpty()) {
+ Log(LogWarning, "IcingaApplication", "No FQDN nor Hostname available. Setting Nodename to 'localhost'.");
+ node_name = "localhost";
+ }
+ }
+
+ ScriptGlobal::Set("NodeName", node_name);
+
+ ScriptGlobal::Set("ReloadTimeout", 300);
+ ScriptGlobal::Set("MaxConcurrentChecks", 512);
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+ /* Ensure that the System namespace is already initialized. Otherwise this is a programming error. */
+ VERIFY(systemNS);
+
+ systemNS->Set("ApplicationType", "IcingaApplication", true);
+ systemNS->Set("ApplicationVersion", Application::GetAppVersion(), true);
+
+ Namespace::Ptr globalNS = ScriptGlobal::GetGlobals();
+ VERIFY(globalNS);
+
+ l_IcingaNS = new Namespace(true);
+ globalNS->Set("Icinga", l_IcingaNS, true);
+}
+
+INITIALIZE_ONCE_WITH_PRIORITY([]() {
+ l_IcingaNS->Freeze();
+}, InitializePriority::FreezeNamespaces);
+
+REGISTER_STATSFUNCTION(IcingaApplication, &IcingaApplication::StatsFunc);
+
+void IcingaApplication::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const IcingaApplication::Ptr& icingaapplication : ConfigType::GetObjectsByType<IcingaApplication>()) {
+ nodes.emplace_back(icingaapplication->GetName(), new Dictionary({
+ { "node_name", icingaapplication->GetNodeName() },
+ { "enable_notifications", icingaapplication->GetEnableNotifications() },
+ { "enable_event_handlers", icingaapplication->GetEnableEventHandlers() },
+ { "enable_flapping", icingaapplication->GetEnableFlapping() },
+ { "enable_host_checks", icingaapplication->GetEnableHostChecks() },
+ { "enable_service_checks", icingaapplication->GetEnableServiceChecks() },
+ { "enable_perfdata", icingaapplication->GetEnablePerfdata() },
+ { "environment", icingaapplication->GetEnvironment() },
+ { "pid", Utility::GetPid() },
+ { "program_start", Application::GetStartTime() },
+ { "version", Application::GetAppVersion() }
+ }));
+ }
+
+ status->Set("icingaapplication", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * The entry point for the Icinga application.
+ *
+ * @returns An exit status.
+ */
+int IcingaApplication::Main()
+{
+ Log(LogDebug, "IcingaApplication", "In IcingaApplication::Main()");
+
+ /* periodically dump the program state */
+ l_RetentionTimer = Timer::Create();
+ l_RetentionTimer->SetInterval(300);
+ l_RetentionTimer->OnTimerExpired.connect([this](const Timer * const&) { DumpProgramState(); });
+ l_RetentionTimer->Start();
+
+ RunEventLoop();
+
+ Log(LogInformation, "IcingaApplication", "Icinga has shut down.");
+
+ return EXIT_SUCCESS;
+}
+
+void IcingaApplication::OnShutdown()
+{
+ {
+ ObjectLock olock(this);
+ l_RetentionTimer->Stop();
+ }
+
+ DumpProgramState();
+}
+
+static void PersistModAttrHelper(AtomicFile& fp, ConfigObject::Ptr& previousObject, const ConfigObject::Ptr& object, const String& attr, const Value& value)
+{
+ if (object != previousObject) {
+ if (previousObject) {
+ ConfigWriter::EmitRaw(fp, "\tobj.version = ");
+ ConfigWriter::EmitValue(fp, 0, previousObject->GetVersion());
+ ConfigWriter::EmitRaw(fp, "\n}\n\n");
+ }
+
+ ConfigWriter::EmitRaw(fp, "var obj = ");
+
+ Array::Ptr args1 = new Array({
+ object->GetReflectionType()->GetName(),
+ object->GetName()
+ });
+ ConfigWriter::EmitFunctionCall(fp, "get_object", args1);
+
+ ConfigWriter::EmitRaw(fp, "\nif (obj) {\n");
+ }
+
+ ConfigWriter::EmitRaw(fp, "\tobj.");
+
+ Array::Ptr args2 = new Array({
+ attr,
+ value
+ });
+ ConfigWriter::EmitFunctionCall(fp, "modify_attribute", args2);
+
+ ConfigWriter::EmitRaw(fp, "\n");
+
+ previousObject = object;
+}
+
+void IcingaApplication::DumpProgramState()
+{
+ ConfigObject::DumpObjects(Configuration::StatePath);
+ DumpModifiedAttributes();
+}
+
+void IcingaApplication::DumpModifiedAttributes()
+{
+ String path = Configuration::ModAttrPath;
+
+ try {
+ Utility::Glob(path + ".tmp.*", &Utility::Remove, GlobFile);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "IcingaApplication") << DiagnosticInformation(ex);
+ }
+
+ AtomicFile fp (path, 0644);
+
+ ConfigObject::Ptr previousObject;
+ ConfigObject::DumpModifiedAttributes([&fp, &previousObject](const ConfigObject::Ptr& object, const String& attr, const Value& value) {
+ PersistModAttrHelper(fp, previousObject, object, attr, value);
+ });
+
+ if (previousObject) {
+ ConfigWriter::EmitRaw(fp, "\tobj.version = ");
+ ConfigWriter::EmitValue(fp, 0, previousObject->GetVersion());
+ ConfigWriter::EmitRaw(fp, "\n}\n");
+ }
+
+ fp.Commit();
+}
+
+IcingaApplication::Ptr IcingaApplication::GetInstance()
+{
+ return static_pointer_cast<IcingaApplication>(Application::GetInstance());
+}
+
+bool IcingaApplication::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const
+{
+ double now = Utility::GetTime();
+
+ if (macro == "timet") {
+ *result = static_cast<long>(now);
+ return true;
+ } else if (macro == "long_date_time") {
+ *result = Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", now);
+ return true;
+ } else if (macro == "short_date_time") {
+ *result = Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", now);
+ return true;
+ } else if (macro == "date") {
+ *result = Utility::FormatDateTime("%Y-%m-%d", now);
+ return true;
+ } else if (macro == "time") {
+ *result = Utility::FormatDateTime("%H:%M:%S %z", now);
+ return true;
+ } else if (macro == "uptime") {
+ *result = Utility::FormatDuration(Application::GetUptime());
+ return true;
+ }
+
+ if (macro.Contains("num_services")) {
+ ServiceStatistics ss = CIB::CalculateServiceStats();
+
+ if (macro == "num_services_ok") {
+ *result = ss.services_ok;
+ return true;
+ } else if (macro == "num_services_warning") {
+ *result = ss.services_warning;
+ return true;
+ } else if (macro == "num_services_critical") {
+ *result = ss.services_critical;
+ return true;
+ } else if (macro == "num_services_unknown") {
+ *result = ss.services_unknown;
+ return true;
+ } else if (macro == "num_services_pending") {
+ *result = ss.services_pending;
+ return true;
+ } else if (macro == "num_services_unreachable") {
+ *result = ss.services_unreachable;
+ return true;
+ } else if (macro == "num_services_flapping") {
+ *result = ss.services_flapping;
+ return true;
+ } else if (macro == "num_services_in_downtime") {
+ *result = ss.services_in_downtime;
+ return true;
+ } else if (macro == "num_services_acknowledged") {
+ *result = ss.services_acknowledged;
+ return true;
+ } else if (macro == "num_services_handled") {
+ *result = ss.services_handled;
+ return true;
+ } else if (macro == "num_services_problem") {
+ *result = ss.services_problem;
+ return true;
+ }
+ }
+ else if (macro.Contains("num_hosts")) {
+ HostStatistics hs = CIB::CalculateHostStats();
+
+ if (macro == "num_hosts_up") {
+ *result = hs.hosts_up;
+ return true;
+ } else if (macro == "num_hosts_down") {
+ *result = hs.hosts_down;
+ return true;
+ } else if (macro == "num_hosts_pending") {
+ *result = hs.hosts_pending;
+ return true;
+ } else if (macro == "num_hosts_unreachable") {
+ *result = hs.hosts_unreachable;
+ return true;
+ } else if (macro == "num_hosts_flapping") {
+ *result = hs.hosts_flapping;
+ return true;
+ } else if (macro == "num_hosts_in_downtime") {
+ *result = hs.hosts_in_downtime;
+ return true;
+ } else if (macro == "num_hosts_acknowledged") {
+ *result = hs.hosts_acknowledged;
+ return true;
+ } else if (macro == "num_hosts_handled") {
+ *result = hs.hosts_handled;
+ return true;
+ } else if (macro == "num_hosts_problem") {
+ *result = hs.hosts_problem;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+String IcingaApplication::GetNodeName() const
+{
+ return ScriptGlobal::Get("NodeName");
+}
+
+/* Intentionally kept here, since an agent may not have the CheckerComponent loaded. */
+int IcingaApplication::GetMaxConcurrentChecks() const
+{
+ return ScriptGlobal::Get("MaxConcurrentChecks");
+}
+
+String IcingaApplication::GetEnvironment() const
+{
+ return Application::GetAppEnvironment();
+}
+
+void IcingaApplication::SetEnvironment(const String& value, bool suppress_events, const Value& cookie)
+{
+ Application::SetAppEnvironment(value);
+}
+
+void IcingaApplication::ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ MacroProcessor::ValidateCustomVars(this, lvalue());
+}
diff --git a/lib/icinga/icingaapplication.hpp b/lib/icinga/icingaapplication.hpp
new file mode 100644
index 0000000..7888fa6
--- /dev/null
+++ b/lib/icinga/icingaapplication.hpp
@@ -0,0 +1,52 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ICINGAAPPLICATION_H
+#define ICINGAAPPLICATION_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/icingaapplication-ti.hpp"
+#include "icinga/macroresolver.hpp"
+
+namespace icinga
+{
+
+/**
+ * The Icinga application.
+ *
+ * @ingroup icinga
+ */
+class IcingaApplication final : public ObjectImpl<IcingaApplication>, public MacroResolver
+{
+public:
+ DECLARE_OBJECT(IcingaApplication);
+ DECLARE_OBJECTNAME(IcingaApplication);
+
+ static void StaticInitialize();
+
+ int Main() override;
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ static IcingaApplication::Ptr GetInstance();
+
+ bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const override;
+
+ String GetNodeName() const;
+
+ int GetMaxConcurrentChecks() const;
+
+ String GetEnvironment() const override;
+ void SetEnvironment(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ void ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ void DumpProgramState();
+ void DumpModifiedAttributes();
+
+ void OnShutdown() override;
+};
+
+}
+
+#endif /* ICINGAAPPLICATION_H */
diff --git a/lib/icinga/icingaapplication.ti b/lib/icinga/icingaapplication.ti
new file mode 100644
index 0000000..1cdef74
--- /dev/null
+++ b/lib/icinga/icingaapplication.ti
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/application.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class IcingaApplication : Application
+{
+ activation_priority -50;
+
+ [config, no_storage, virtual] String environment {
+ get;
+ set;
+ default {{{ return Application::GetAppEnvironment(); }}}
+ };
+
+ [config] bool enable_notifications {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_event_handlers {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_flapping {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_host_checks {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_service_checks {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_perfdata {
+ default {{{ return true; }}}
+ };
+ [config] Dictionary::Ptr vars;
+};
+
+}
diff --git a/lib/icinga/legacytimeperiod.cpp b/lib/icinga/legacytimeperiod.cpp
new file mode 100644
index 0000000..33e6665
--- /dev/null
+++ b/lib/icinga/legacytimeperiod.cpp
@@ -0,0 +1,644 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/legacytimeperiod.hpp"
+#include "base/function.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/debug.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, LegacyTimePeriod, &LegacyTimePeriod::ScriptFunc, "tp:begin:end");
+
+/**
+ * Returns the same as mktime() but does not modify its argument and takes a const pointer.
+ *
+ * @param t struct tm to convert to time_t
+ * @return time_t representing the timestamp given by t
+ */
+static time_t mktime_const(const tm *t) {
+ tm copy = *t;
+ return mktime(&copy);
+}
+
+bool LegacyTimePeriod::IsInTimeRange(const tm *begin, const tm *end, int stride, const tm *reference)
+{
+ time_t tsbegin, tsend, tsref;
+ tsbegin = mktime_const(begin);
+ tsend = mktime_const(end);
+ tsref = mktime_const(reference);
+
+ if (tsref < tsbegin || tsref > tsend)
+ return false;
+
+ int daynumber = (tsref - tsbegin) / (24 * 60 * 60);
+
+ if (stride > 1 && daynumber % stride > 0)
+ return false;
+
+ return true;
+}
+
+/**
+ * Update all day-related fields of reference (tm_year, tm_mon, tm_mday, tm_wday, tm_yday) to reference the n-th
+ * occurrence of a weekday (given by wday) in the month represented by the original value of reference.
+ *
+ * If n is negative, counting is done from the end of the month, so for example with wday=1 and n=-1, the result will be
+ * the last Monday in the month given by reference.
+ *
+ * @param wday Weekday (0 = Sunday, 1 = Monday, ..., 6 = Saturday, like tm_wday)
+ * @param n Search the n-th weekday (given by wday) in the month given by reference
+ * @param reference Input for the current month and output for the given day of that moth
+ */
+void LegacyTimePeriod::FindNthWeekday(int wday, int n, tm *reference)
+{
+ // Work on a copy to only update specific fields of reference (as documented).
+ tm t = *reference;
+
+ int dir, seen = 0;
+
+ if (n > 0) {
+ dir = 1;
+ } else {
+ n *= -1;
+ dir = -1;
+
+ /* Negative days are relative to the next month. */
+ t.tm_mon++;
+ }
+
+ ASSERT(n > 0);
+
+ t.tm_mday = 1;
+
+ for (;;) {
+ // Always operate on 00:00:00 with automatic DST detection, otherwise days could
+ // be skipped or counted twice if +-24 hours is not on the next or previous day.
+ t.tm_hour = 0;
+ t.tm_min = 0;
+ t.tm_sec = 0;
+ t.tm_isdst = -1;
+
+ mktime(&t);
+
+ if (t.tm_wday == wday) {
+ seen++;
+
+ if (seen == n)
+ break;
+ }
+
+ t.tm_mday += dir;
+ }
+
+ reference->tm_year = t.tm_year;
+ reference->tm_mon = t.tm_mon;
+ reference->tm_mday = t.tm_mday;
+ reference->tm_wday = t.tm_wday;
+ reference->tm_yday = t.tm_yday;
+}
+
+int LegacyTimePeriod::WeekdayFromString(const String& daydef)
+{
+ if (daydef == "sunday")
+ return 0;
+ else if (daydef == "monday")
+ return 1;
+ else if (daydef == "tuesday")
+ return 2;
+ else if (daydef == "wednesday")
+ return 3;
+ else if (daydef == "thursday")
+ return 4;
+ else if (daydef == "friday")
+ return 5;
+ else if (daydef == "saturday")
+ return 6;
+ else
+ return -1;
+}
+
+int LegacyTimePeriod::MonthFromString(const String& monthdef)
+{
+ if (monthdef == "january")
+ return 0;
+ else if (monthdef == "february")
+ return 1;
+ else if (monthdef == "march")
+ return 2;
+ else if (monthdef == "april")
+ return 3;
+ else if (monthdef == "may")
+ return 4;
+ else if (monthdef == "june")
+ return 5;
+ else if (monthdef == "july")
+ return 6;
+ else if (monthdef == "august")
+ return 7;
+ else if (monthdef == "september")
+ return 8;
+ else if (monthdef == "october")
+ return 9;
+ else if (monthdef == "november")
+ return 10;
+ else if (monthdef == "december")
+ return 11;
+ else
+ return -1;
+}
+
+boost::gregorian::date LegacyTimePeriod::GetEndOfMonthDay(int year, int month)
+{
+ boost::gregorian::date d(boost::gregorian::greg_year(year), boost::gregorian::greg_month(month), 1);
+
+ return d.end_of_month();
+}
+
+/**
+ * Finds the first day on or after the day given by reference and writes the beginning and end time of that day to
+ * the output parameters begin and end.
+ *
+ * @param timespec Day to find, for example "2021-10-20", "sunday", ...
+ * @param begin if != nullptr, set to 00:00:00 on that day
+ * @param end if != nullptr, set to 24:00:00 on that day (i.e. 00:00:00 of the next day)
+ * @param reference Time to begin the search at
+ */
+void LegacyTimePeriod::ParseTimeSpec(const String& timespec, tm *begin, tm *end, const tm *reference)
+{
+ /* YYYY-MM-DD */
+ if (timespec.GetLength() == 10 && timespec[4] == '-' && timespec[7] == '-') {
+ int year = Convert::ToLong(timespec.SubStr(0, 4));
+ int month = Convert::ToLong(timespec.SubStr(5, 2));
+ int day = Convert::ToLong(timespec.SubStr(8, 2));
+
+ if (month < 1 || month > 12)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid month in time specification: " + timespec));
+ if (day < 1 || day > 31)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid day in time specification: " + timespec));
+
+ if (begin) {
+ *begin = *reference;
+ begin->tm_year = year - 1900;
+ begin->tm_mon = month - 1;
+ begin->tm_mday = day;
+ begin->tm_hour = 0;
+ begin->tm_min = 0;
+ begin->tm_sec = 0;
+ begin->tm_isdst = -1;
+ }
+
+ if (end) {
+ *end = *reference;
+ end->tm_year = year - 1900;
+ end->tm_mon = month - 1;
+ end->tm_mday = day;
+ end->tm_hour = 24;
+ end->tm_min = 0;
+ end->tm_sec = 0;
+ end->tm_isdst = -1;
+ }
+
+ return;
+ }
+
+ std::vector<String> tokens = timespec.Split(" ");
+
+ int mon = -1;
+
+ if (tokens.size() > 1 && (tokens[0] == "day" || (mon = MonthFromString(tokens[0])) != -1)) {
+ if (mon == -1)
+ mon = reference->tm_mon;
+
+ int mday = Convert::ToLong(tokens[1]);
+
+ if (begin) {
+ *begin = *reference;
+ begin->tm_mon = mon;
+ begin->tm_mday = mday;
+ begin->tm_hour = 0;
+ begin->tm_min = 0;
+ begin->tm_sec = 0;
+ begin->tm_isdst = -1;
+
+ /* day -X: Negative days are relative to the next month. */
+ if (mday < 0) {
+ boost::gregorian::date d(GetEndOfMonthDay(reference->tm_year + 1900, mon + 1)); //TODO: Refactor this mess into full Boost.DateTime
+
+ //Depending on the number, we need to substract specific days (counting starts at 0).
+ d = d - boost::gregorian::days(mday * -1 - 1);
+
+ *begin = boost::gregorian::to_tm(d);
+ begin->tm_hour = 0;
+ begin->tm_min = 0;
+ begin->tm_sec = 0;
+ }
+ }
+
+ if (end) {
+ *end = *reference;
+ end->tm_mon = mon;
+ end->tm_mday = mday;
+ end->tm_hour = 24;
+ end->tm_min = 0;
+ end->tm_sec = 0;
+ end->tm_isdst = -1;
+
+ /* day -X: Negative days are relative to the next month. */
+ if (mday < 0) {
+ boost::gregorian::date d(GetEndOfMonthDay(reference->tm_year + 1900, mon + 1)); //TODO: Refactor this mess into full Boost.DateTime
+
+ //Depending on the number, we need to substract specific days (counting starts at 0).
+ d = d - boost::gregorian::days(mday * -1 - 1);
+
+ // End date is one day in the future, starting 00:00:00
+ d = d + boost::gregorian::days(1);
+
+ *end = boost::gregorian::to_tm(d);
+ end->tm_hour = 0;
+ end->tm_min = 0;
+ end->tm_sec = 0;
+ }
+ }
+
+ return;
+ }
+
+ int wday;
+
+ if (tokens.size() >= 1 && (wday = WeekdayFromString(tokens[0])) != -1) {
+ tm myref = *reference;
+ myref.tm_isdst = -1;
+
+ if (tokens.size() > 2) {
+ mon = MonthFromString(tokens[2]);
+
+ if (mon == -1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid month in time specification: " + timespec));
+
+ myref.tm_mon = mon;
+ }
+
+ int n = 0;
+
+ if (tokens.size() > 1)
+ n = Convert::ToLong(tokens[1]);
+
+ if (begin) {
+ *begin = myref;
+
+ if (tokens.size() > 1)
+ FindNthWeekday(wday, n, begin);
+ else
+ begin->tm_mday += (7 - begin->tm_wday + wday) % 7;
+
+ begin->tm_hour = 0;
+ begin->tm_min = 0;
+ begin->tm_sec = 0;
+ }
+
+ if (end) {
+ *end = myref;
+
+ if (tokens.size() > 1)
+ FindNthWeekday(wday, n, end);
+ else
+ end->tm_mday += (7 - end->tm_wday + wday) % 7;
+
+ end->tm_hour = 0;
+ end->tm_min = 0;
+ end->tm_sec = 0;
+ end->tm_mday++;
+ }
+
+ return;
+ }
+
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid time specification: " + timespec));
+}
+
+/**
+ * Parse a range of days.
+ *
+ * The input can have the following formats:
+ * begin
+ * begin - end
+ * begin / stride
+ * begin - end / stride
+ *
+ * @param timerange Text representation of a day range or a single day, for example "2021-10-20", "monday - friday", ...
+ * @param begin Output parameter set to 00:00:00 of the first day of the range
+ * @param end Output parameter set to 24:00:00 of the last day of the range (i.e. 00:00:00 of the day after)
+ * @param stride Output parameter for the stride (for every n-th day)
+ * @param reference Expand the range relative to this timestamp
+ */
+void LegacyTimePeriod::ParseTimeRange(const String& timerange, tm *begin, tm *end, int *stride, const tm *reference)
+{
+ String def = timerange;
+
+ /* Figure out the stride. */
+ size_t pos = def.FindFirstOf('/');
+
+ if (pos != String::NPos) {
+ String strStride = def.SubStr(pos + 1).Trim();
+ *stride = Convert::ToLong(strStride);
+
+ /* Remove the stride parameter from the definition. */
+ def = def.SubStr(0, pos);
+ } else {
+ *stride = 1; /* User didn't specify anything, assume default. */
+ }
+
+ /* Figure out whether the user has specified two dates. */
+ pos = def.Find("- ");
+
+ if (pos != String::NPos) {
+ String first = def.SubStr(0, pos).Trim();
+
+ String second = def.SubStr(pos + 1).Trim();
+
+ ParseTimeSpec(first, begin, nullptr, reference);
+
+ /* If the second definition starts with a number we need
+ * to add the first word from the first definition, e.g.:
+ * day 1 - 15 --> "day 15" */
+ bool is_number = true;
+ size_t xpos = second.FindFirstOf(' ');
+ String fword = second.SubStr(0, xpos);
+
+ try {
+ Convert::ToLong(fword);
+ } catch (...) {
+ is_number = false;
+ }
+
+ if (is_number) {
+ xpos = first.FindFirstOf(' ');
+ ASSERT(xpos != String::NPos);
+ second = first.SubStr(0, xpos + 1) + second;
+ }
+
+ ParseTimeSpec(second, nullptr, end, reference);
+ } else {
+ ParseTimeSpec(def, begin, end, reference);
+ }
+}
+
+bool LegacyTimePeriod::IsInDayDefinition(const String& daydef, const tm *reference)
+{
+ tm begin, end;
+ int stride;
+
+ ParseTimeRange(daydef, &begin, &end, &stride, reference);
+
+ Log(LogDebug, "LegacyTimePeriod")
+ << "ParseTimeRange: '" << daydef << "' => " << mktime(&begin)
+ << " -> " << mktime(&end) << ", stride: " << stride;
+
+ return IsInTimeRange(&begin, &end, stride, reference);
+}
+
+static inline
+void ProcessTimeRaw(const String& in, const tm *reference, tm *out)
+{
+ *out = *reference;
+
+ auto hd (in.Split(":"));
+
+ switch (hd.size()) {
+ case 2:
+ out->tm_sec = 0;
+ break;
+ case 3:
+ out->tm_sec = Convert::ToLong(hd[2]);
+ break;
+ default:
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid time specification: " + in));
+ }
+
+ out->tm_hour = Convert::ToLong(hd[0]);
+ out->tm_min = Convert::ToLong(hd[1]);
+}
+
+void LegacyTimePeriod::ProcessTimeRangeRaw(const String& timerange, const tm *reference, tm *begin, tm *end)
+{
+ std::vector<String> times = timerange.Split("-");
+
+ if (times.size() != 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid timerange: " + timerange));
+
+ ProcessTimeRaw(times[0], reference, begin);
+ ProcessTimeRaw(times[1], reference, end);
+
+ if (begin->tm_hour * 3600 + begin->tm_min * 60 + begin->tm_sec >=
+ end->tm_hour * 3600 + end->tm_min * 60 + end->tm_sec)
+ end->tm_hour += 24;
+}
+
+Dictionary::Ptr LegacyTimePeriod::ProcessTimeRange(const String& timestamp, const tm *reference)
+{
+ tm begin, end;
+
+ ProcessTimeRangeRaw(timestamp, reference, &begin, &end);
+
+ return new Dictionary({
+ { "begin", (long)mktime(&begin) },
+ { "end", (long)mktime(&end) }
+ });
+}
+
+/**
+ * Takes a list of timeranges end expands them to concrete timestamp based on a reference time.
+ *
+ * @param timeranges String of comma separated time ranges, for example "10:00-12:00", "12:15:30-12:23:43,16:00-18:00"
+ * @param reference Starting point for searching the segments
+ * @param result For each range, a dict with keys "begin" and "end" is added
+ */
+void LegacyTimePeriod::ProcessTimeRanges(const String& timeranges, const tm *reference, const Array::Ptr& result)
+{
+ std::vector<String> ranges = timeranges.Split(",");
+
+ for (const String& range : ranges) {
+ Dictionary::Ptr segment = ProcessTimeRange(range, reference);
+
+ if (segment->Get("begin") >= segment->Get("end"))
+ continue;
+
+ result->Add(segment);
+ }
+}
+
+Dictionary::Ptr LegacyTimePeriod::FindRunningSegment(const String& daydef, const String& timeranges, const tm *reference)
+{
+ tm begin, end, iter;
+ time_t tsend, tsiter, tsref;
+ int stride;
+
+ tsref = mktime_const(reference);
+
+ ParseTimeRange(daydef, &begin, &end, &stride, reference);
+
+ iter = begin;
+
+ tsend = mktime(&end);
+
+ do {
+ if (IsInTimeRange(&begin, &end, stride, &iter)) {
+ Array::Ptr segments = new Array();
+ ProcessTimeRanges(timeranges, &iter, segments);
+
+ Dictionary::Ptr bestSegment;
+ double bestEnd = 0.0;
+
+ ObjectLock olock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ double begin = segment->Get("begin");
+ double end = segment->Get("end");
+
+ if (begin >= tsref || end < tsref)
+ continue;
+
+ if (!bestSegment || end > bestEnd) {
+ bestSegment = segment;
+ bestEnd = end;
+ }
+ }
+
+ if (bestSegment)
+ return bestSegment;
+ }
+
+ iter.tm_mday++;
+ iter.tm_hour = 0;
+ iter.tm_min = 0;
+ iter.tm_sec = 0;
+ tsiter = mktime(&iter);
+ } while (tsiter < tsend);
+
+ return nullptr;
+}
+
+Dictionary::Ptr LegacyTimePeriod::FindNextSegment(const String& daydef, const String& timeranges, const tm *reference)
+{
+ tm begin, end, iter, ref;
+ time_t tsend, tsiter, tsref;
+ int stride;
+
+ for (int pass = 1; pass <= 2; pass++) {
+ if (pass == 1) {
+ ref = *reference;
+ } else {
+ ref = end;
+ ref.tm_mday++;
+ }
+
+ tsref = mktime(&ref);
+
+ ParseTimeRange(daydef, &begin, &end, &stride, &ref);
+
+ iter = begin;
+
+ tsend = mktime(&end);
+
+ do {
+ if (IsInTimeRange(&begin, &end, stride, &iter)) {
+ Array::Ptr segments = new Array();
+ ProcessTimeRanges(timeranges, &iter, segments);
+
+ Dictionary::Ptr bestSegment;
+ double bestBegin;
+
+ ObjectLock olock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ double begin = segment->Get("begin");
+
+ if (begin < tsref)
+ continue;
+
+ if (!bestSegment || begin < bestBegin) {
+ bestSegment = segment;
+ bestBegin = begin;
+ }
+ }
+
+ if (bestSegment)
+ return bestSegment;
+ }
+
+ iter.tm_mday++;
+ iter.tm_hour = 0;
+ iter.tm_min = 0;
+ iter.tm_sec = 0;
+ tsiter = mktime(&iter);
+ } while (tsiter < tsend);
+ }
+
+ return nullptr;
+}
+
+Array::Ptr LegacyTimePeriod::ScriptFunc(const TimePeriod::Ptr& tp, double begin, double end)
+{
+ Array::Ptr segments = new Array();
+
+ Dictionary::Ptr ranges = tp->GetRanges();
+
+ if (ranges) {
+ tm tm_begin = Utility::LocalTime(begin);
+
+ // Always evaluate time periods for full days as their ranges are given per day.
+ tm_begin.tm_hour = 0;
+ tm_begin.tm_min = 0;
+ tm_begin.tm_sec = 0;
+ tm_begin.tm_isdst = -1;
+
+ // Helper to move a struct tm to midnight of the next day for the loop below.
+ // Due to DST changes, this may move the time by something else than 24 hours.
+ auto advance_to_next_day = [](tm *t) {
+ t->tm_mday++;
+ t->tm_hour = 0;
+ t->tm_min = 0;
+ t->tm_sec = 0;
+ t->tm_isdst = -1;
+
+ // Normalize fields using mktime.
+ mktime(t);
+
+ // Reset tm_isdst so that future calls figure out the correct time zone after setting tm_hour/tm_min/tm_sec.
+ t->tm_isdst = -1;
+ };
+
+ for (tm reference = tm_begin; mktime_const(&reference) <= end; advance_to_next_day(&reference)) {
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "LegacyTimePeriod")
+ << "Checking reference time " << mktime_const(&reference);
+#endif /* I2_DEBUG */
+
+ ObjectLock olock(ranges);
+ for (const Dictionary::Pair& kv : ranges) {
+ if (!IsInDayDefinition(kv.first, &reference)) {
+#ifdef I2_DEBUG
+ Log(LogDebug, "LegacyTimePeriod")
+ << "Not in day definition '" << kv.first << "'.";
+#endif /* I2_DEBUG */
+ continue;
+ }
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "LegacyTimePeriod")
+ << "In day definition '" << kv.first << "'.";
+#endif /* I2_DEBUG */
+
+ ProcessTimeRanges(kv.second, &reference, segments);
+ }
+ }
+ }
+
+ Log(LogDebug, "LegacyTimePeriod")
+ << "Legacy timeperiod update returned " << segments->GetLength() << " segments.";
+
+ return segments;
+}
diff --git a/lib/icinga/legacytimeperiod.hpp b/lib/icinga/legacytimeperiod.hpp
new file mode 100644
index 0000000..001eb5c
--- /dev/null
+++ b/lib/icinga/legacytimeperiod.hpp
@@ -0,0 +1,45 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LEGACYTIMEPERIOD_H
+#define LEGACYTIMEPERIOD_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/timeperiod.hpp"
+#include "base/dictionary.hpp"
+#include <boost/date_time/gregorian/gregorian.hpp>
+
+namespace icinga
+{
+
+/**
+ * Implements Icinga 1.x time periods.
+ *
+ * @ingroup icinga
+ */
+class LegacyTimePeriod
+{
+public:
+ static Array::Ptr ScriptFunc(const TimePeriod::Ptr& tp, double start, double end);
+
+ static bool IsInTimeRange(const tm *begin, const tm *end, int stride, const tm *reference);
+ static void FindNthWeekday(int wday, int n, tm *reference);
+ static int WeekdayFromString(const String& daydef);
+ static int MonthFromString(const String& monthdef);
+ static void ParseTimeSpec(const String& timespec, tm *begin, tm *end, const tm *reference);
+ static void ParseTimeRange(const String& timerange, tm *begin, tm *end, int *stride, const tm *reference);
+ static bool IsInDayDefinition(const String& daydef, const tm *reference);
+ static void ProcessTimeRangeRaw(const String& timerange, const tm *reference, tm *begin, tm *end);
+ static Dictionary::Ptr ProcessTimeRange(const String& timerange, const tm *reference);
+ static void ProcessTimeRanges(const String& timeranges, const tm *reference, const Array::Ptr& result);
+ static Dictionary::Ptr FindNextSegment(const String& daydef, const String& timeranges, const tm *reference);
+ static Dictionary::Ptr FindRunningSegment(const String& daydef, const String& timeranges, const tm *reference);
+
+private:
+ LegacyTimePeriod();
+
+ static boost::gregorian::date GetEndOfMonthDay(int year, int month);
+};
+
+}
+
+#endif /* LEGACYTIMEPERIOD_H */
diff --git a/lib/icinga/macroprocessor.cpp b/lib/icinga/macroprocessor.cpp
new file mode 100644
index 0000000..724a4f9
--- /dev/null
+++ b/lib/icinga/macroprocessor.cpp
@@ -0,0 +1,585 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/macroprocessor.hpp"
+#include "icinga/macroresolver.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/envresolver.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/configobject.hpp"
+#include "base/scriptframe.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/join.hpp>
+
+using namespace icinga;
+
+thread_local Dictionary::Ptr MacroResolver::OverrideMacros;
+
+Value MacroProcessor::ResolveMacros(const Value& str, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, String *missingMacro,
+ const MacroProcessor::EscapeCallback& escapeFn, const Dictionary::Ptr& resolvedMacros,
+ bool useResolvedMacros, int recursionLevel)
+{
+ if (useResolvedMacros)
+ REQUIRE_NOT_NULL(resolvedMacros);
+
+ Value result;
+
+ if (str.IsEmpty())
+ return Empty;
+
+ if (str.IsScalar()) {
+ result = InternalResolveMacros(str, resolvers, cr, missingMacro, escapeFn,
+ resolvedMacros, useResolvedMacros, recursionLevel + 1);
+ } else if (str.IsObjectType<Array>()) {
+ ArrayData resultArr;
+ Array::Ptr arr = str;
+
+ ObjectLock olock(arr);
+
+ for (const Value& arg : arr) {
+ /* Note: don't escape macros here. */
+ Value value = InternalResolveMacros(arg, resolvers, cr, missingMacro,
+ EscapeCallback(), resolvedMacros, useResolvedMacros, recursionLevel + 1);
+
+ if (value.IsObjectType<Array>())
+ resultArr.push_back(Utility::Join(value, ';'));
+ else
+ resultArr.push_back(value);
+ }
+
+ result = new Array(std::move(resultArr));
+ } else if (str.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr resultDict = new Dictionary();
+ Dictionary::Ptr dict = str;
+
+ ObjectLock olock(dict);
+
+ for (const Dictionary::Pair& kv : dict) {
+ /* Note: don't escape macros here. */
+ resultDict->Set(kv.first, InternalResolveMacros(kv.second, resolvers, cr, missingMacro,
+ EscapeCallback(), resolvedMacros, useResolvedMacros, recursionLevel + 1));
+ }
+
+ result = resultDict;
+ } else if (str.IsObjectType<Function>()) {
+ result = EvaluateFunction(str, resolvers, cr, escapeFn, resolvedMacros, useResolvedMacros, 0);
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Macro is not a string or array."));
+ }
+
+ return result;
+}
+
+static const EnvResolver::Ptr l_EnvResolver = new EnvResolver();
+
+static MacroProcessor::ResolverList GetDefaultResolvers()
+{
+ return {
+ { "icinga", IcingaApplication::GetInstance() },
+ { "env", l_EnvResolver, false }
+ };
+}
+
+bool MacroProcessor::ResolveMacro(const String& macro, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, Value *result, bool *recursive_macro)
+{
+ CONTEXT("Resolving macro '" << macro << "'");
+
+ *recursive_macro = false;
+
+ std::vector<String> tokens = macro.Split(".");
+
+ String objName;
+ if (tokens.size() > 1) {
+ objName = tokens[0];
+ tokens.erase(tokens.begin());
+ }
+
+ const auto defaultResolvers (GetDefaultResolvers());
+
+ for (auto resolverList : {&resolvers, &defaultResolvers}) {
+ for (auto& resolver : *resolverList) {
+ if (!objName.IsEmpty() && objName != resolver.Name)
+ continue;
+
+ if (objName.IsEmpty()) {
+ if (!resolver.ResolveShortMacros)
+ continue;
+
+ Dictionary::Ptr vars;
+ CustomVarObject::Ptr dobj = dynamic_pointer_cast<CustomVarObject>(resolver.Obj);
+
+ if (dobj) {
+ vars = dobj->GetVars();
+ } else {
+ auto app (dynamic_pointer_cast<IcingaApplication>(resolver.Obj));
+
+ if (app) {
+ vars = app->GetVars();
+ }
+ }
+
+ if (vars && vars->Contains(macro)) {
+ *result = vars->Get(macro);
+ *recursive_macro = true;
+ return true;
+ }
+ }
+
+ auto *mresolver = dynamic_cast<MacroResolver *>(resolver.Obj.get());
+
+ if (mresolver && mresolver->ResolveMacro(boost::algorithm::join(tokens, "."), cr, result))
+ return true;
+
+ Value ref = resolver.Obj;
+ bool valid = true;
+
+ for (const String& token : tokens) {
+ if (ref.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = ref;
+ if (dict->Contains(token)) {
+ ref = dict->Get(token);
+ continue;
+ } else {
+ valid = false;
+ break;
+ }
+ } else if (ref.IsObject()) {
+ Object::Ptr object = ref;
+
+ Type::Ptr type = object->GetReflectionType();
+
+ if (!type) {
+ valid = false;
+ break;
+ }
+
+ int field = type->GetFieldId(token);
+
+ if (field == -1) {
+ valid = false;
+ break;
+ }
+
+ ref = object->GetField(field);
+
+ Field fieldInfo = type->GetFieldInfo(field);
+
+ if (strcmp(fieldInfo.TypeName, "Timestamp") == 0)
+ ref = static_cast<long>(ref);
+ }
+ }
+
+ if (valid) {
+ if (tokens[0] == "vars" ||
+ tokens[0] == "action_url" ||
+ tokens[0] == "notes_url" ||
+ tokens[0] == "notes")
+ *recursive_macro = true;
+
+ *result = ref;
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+Value MacroProcessor::EvaluateFunction(const Function::Ptr& func, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, const MacroProcessor::EscapeCallback& escapeFn,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel)
+{
+ Dictionary::Ptr resolvers_this = new Dictionary();
+ const auto defaultResolvers (GetDefaultResolvers());
+
+ for (auto resolverList : {&resolvers, &defaultResolvers}) {
+ for (auto& resolver: *resolverList) {
+ resolvers_this->Set(resolver.Name, resolver.Obj);
+ }
+ }
+
+ auto internalResolveMacrosShim = [resolvers, cr, resolvedMacros, useResolvedMacros, recursionLevel](const std::vector<Value>& args) {
+ if (args.size() < 1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments for function"));
+
+ String missingMacro;
+
+ return MacroProcessor::InternalResolveMacros(args[0], resolvers, cr, &missingMacro, MacroProcessor::EscapeCallback(),
+ resolvedMacros, useResolvedMacros, recursionLevel);
+ };
+
+ resolvers_this->Set("macro", new Function("macro (temporary)", internalResolveMacrosShim, { "str" }));
+
+ auto internalResolveArgumentsShim = [resolvers, cr, resolvedMacros, useResolvedMacros, recursionLevel](const std::vector<Value>& args) {
+ if (args.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments for function"));
+
+ return MacroProcessor::ResolveArguments(args[0], args[1], resolvers, cr,
+ resolvedMacros, useResolvedMacros, recursionLevel + 1);
+ };
+
+ resolvers_this->Set("resolve_arguments", new Function("resolve_arguments (temporary)", internalResolveArgumentsShim, { "command", "args" }));
+
+ return func->InvokeThis(resolvers_this);
+}
+
+Value MacroProcessor::InternalResolveMacros(const String& str, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, String *missingMacro,
+ const MacroProcessor::EscapeCallback& escapeFn, const Dictionary::Ptr& resolvedMacros,
+ bool useResolvedMacros, int recursionLevel)
+{
+ CONTEXT("Resolving macros for string '" << str << "'");
+
+ if (recursionLevel > 15)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Infinite recursion detected while resolving macros"));
+
+ size_t offset, pos_first, pos_second;
+ offset = 0;
+
+ Dictionary::Ptr resolvers_this;
+
+ String result = str;
+ while ((pos_first = result.FindFirstOf("$", offset)) != String::NPos) {
+ pos_second = result.FindFirstOf("$", pos_first + 1);
+
+ if (pos_second == String::NPos)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Closing $ not found in macro format string."));
+
+ String name = result.SubStr(pos_first + 1, pos_second - pos_first - 1);
+
+ Value resolved_macro;
+ bool recursive_macro;
+ bool found;
+
+ if (useResolvedMacros) {
+ recursive_macro = false;
+ found = resolvedMacros->Contains(name);
+
+ if (found)
+ resolved_macro = resolvedMacros->Get(name);
+ } else
+ found = ResolveMacro(name, resolvers, cr, &resolved_macro, &recursive_macro);
+
+ /* $$ is an escape sequence for $. */
+ if (name.IsEmpty()) {
+ resolved_macro = "$";
+ found = true;
+ }
+
+ if (resolved_macro.IsObjectType<Function>()) {
+ resolved_macro = EvaluateFunction(resolved_macro, resolvers, cr, escapeFn,
+ resolvedMacros, useResolvedMacros, recursionLevel + 1);
+ }
+
+ if (!found) {
+ if (!missingMacro)
+ Log(LogWarning, "MacroProcessor")
+ << "Macro '" << name << "' is not defined.";
+ else
+ *missingMacro = name;
+ }
+
+ /* recursively resolve macros in the macro if it was a user macro */
+ if (recursive_macro) {
+ if (resolved_macro.IsObjectType<Array>()) {
+ Array::Ptr arr = resolved_macro;
+ ArrayData resolved_arr;
+
+ ObjectLock olock(arr);
+ for (const Value& value : arr) {
+ if (value.IsScalar()) {
+ resolved_arr.push_back(InternalResolveMacros(value,
+ resolvers, cr, missingMacro, EscapeCallback(), nullptr,
+ false, recursionLevel + 1));
+ } else
+ resolved_arr.push_back(value);
+ }
+
+ resolved_macro = new Array(std::move(resolved_arr));
+ } else if (resolved_macro.IsString()) {
+ resolved_macro = InternalResolveMacros(resolved_macro,
+ resolvers, cr, missingMacro, EscapeCallback(), nullptr,
+ false, recursionLevel + 1);
+ }
+ }
+
+ if (!useResolvedMacros && found && resolvedMacros)
+ resolvedMacros->Set(name, resolved_macro);
+
+ if (escapeFn)
+ resolved_macro = escapeFn(resolved_macro);
+
+ /* we're done if this is the only macro and there are no other non-macro parts in the string */
+ if (pos_first == 0 && pos_second == str.GetLength() - 1)
+ return resolved_macro;
+ else if (resolved_macro.IsObjectType<Array>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Mixing both strings and non-strings in macros is not allowed."));
+
+ if (resolved_macro.IsObjectType<Array>()) {
+ /* don't allow mixing strings and arrays in macro strings */
+ if (pos_first != 0 || pos_second != str.GetLength() - 1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Mixing both strings and non-strings in macros is not allowed."));
+
+ return resolved_macro;
+ }
+
+ String resolved_macro_str = resolved_macro;
+
+ result.Replace(pos_first, pos_second - pos_first + 1, resolved_macro_str);
+ offset = pos_first + resolved_macro_str.GetLength();
+ }
+
+ return result;
+}
+
+
+bool MacroProcessor::ValidateMacroString(const String& macro)
+{
+ if (macro.IsEmpty())
+ return true;
+
+ size_t pos_first, pos_second, offset;
+ offset = 0;
+
+ while ((pos_first = macro.FindFirstOf("$", offset)) != String::NPos) {
+ pos_second = macro.FindFirstOf("$", pos_first + 1);
+
+ if (pos_second == String::NPos)
+ return false;
+
+ offset = pos_second + 1;
+ }
+
+ return true;
+}
+
+void MacroProcessor::ValidateCustomVars(const ConfigObject::Ptr& object, const Dictionary::Ptr& value)
+{
+ if (!value)
+ return;
+
+ /* string, array, dictionary */
+ ObjectLock olock(value);
+ for (const Dictionary::Pair& kv : value) {
+ const Value& varval = kv.second;
+
+ if (varval.IsObjectType<Dictionary>()) {
+ /* only one dictonary level */
+ Dictionary::Ptr varval_dict = varval;
+
+ ObjectLock xlock(varval_dict);
+ for (const Dictionary::Pair& kv_var : varval_dict) {
+ if (!kv_var.second.IsString())
+ continue;
+
+ if (!ValidateMacroString(kv_var.second))
+ BOOST_THROW_EXCEPTION(ValidationError(object.get(), { "vars", kv.first, kv_var.first }, "Closing $ not found in macro format string '" + kv_var.second + "'."));
+ }
+ } else if (varval.IsObjectType<Array>()) {
+ /* check all array entries */
+ Array::Ptr varval_arr = varval;
+
+ ObjectLock ylock (varval_arr);
+ for (const Value& arrval : varval_arr) {
+ if (!arrval.IsString())
+ continue;
+
+ if (!ValidateMacroString(arrval)) {
+ BOOST_THROW_EXCEPTION(ValidationError(object.get(), { "vars", kv.first }, "Closing $ not found in macro format string '" + arrval + "'."));
+ }
+ }
+ } else {
+ if (!varval.IsString())
+ continue;
+
+ if (!ValidateMacroString(varval))
+ BOOST_THROW_EXCEPTION(ValidationError(object.get(), { "vars", kv.first }, "Closing $ not found in macro format string '" + varval + "'."));
+ }
+ }
+}
+
+void MacroProcessor::AddArgumentHelper(const Array::Ptr& args, const String& key, const String& value,
+ bool add_key, bool add_value, const Value& separator)
+{
+ if (add_key && separator.GetType() != ValueEmpty && add_value) {
+ args->Add(key + separator + value);
+ } else {
+ if (add_key)
+ args->Add(key);
+
+ if (add_value)
+ args->Add(value);
+ }
+}
+
+Value MacroProcessor::EscapeMacroShellArg(const Value& value)
+{
+ String result;
+
+ if (value.IsObjectType<Array>()) {
+ Array::Ptr arr = value;
+
+ ObjectLock olock(arr);
+ for (const Value& arg : arr) {
+ if (result.GetLength() > 0)
+ result += " ";
+
+ result += Utility::EscapeShellArg(arg);
+ }
+ } else
+ result = Utility::EscapeShellArg(value);
+
+ return result;
+}
+
+struct CommandArgument
+{
+ int Order{0};
+ bool SkipKey{false};
+ bool RepeatKey{true};
+ bool SkipValue{false};
+ String Key;
+ Value Separator;
+ Value AValue;
+
+ bool operator<(const CommandArgument& rhs) const
+ {
+ return Order < rhs.Order;
+ }
+};
+
+Value MacroProcessor::ResolveArguments(const Value& command, const Dictionary::Ptr& arguments,
+ const MacroProcessor::ResolverList& resolvers, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel)
+{
+ if (useResolvedMacros)
+ REQUIRE_NOT_NULL(resolvedMacros);
+
+ Value resolvedCommand;
+ if (!arguments || command.IsObjectType<Array>() || command.IsObjectType<Function>())
+ resolvedCommand = MacroProcessor::ResolveMacros(command, resolvers, cr, nullptr,
+ EscapeMacroShellArg, resolvedMacros, useResolvedMacros, recursionLevel + 1);
+ else {
+ resolvedCommand = new Array({ command });
+ }
+
+ if (arguments) {
+ std::vector<CommandArgument> args;
+
+ ObjectLock olock(arguments);
+ for (const Dictionary::Pair& kv : arguments) {
+ const Value& arginfo = kv.second;
+
+ CommandArgument arg;
+ arg.Key = kv.first;
+
+ bool required = false;
+ Value argval;
+
+ if (arginfo.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr argdict = arginfo;
+ if (argdict->Contains("key"))
+ arg.Key = argdict->Get("key");
+ argval = argdict->Get("value");
+ if (argdict->Contains("required"))
+ required = argdict->Get("required");
+ arg.SkipKey = argdict->Get("skip_key");
+ if (argdict->Contains("repeat_key"))
+ arg.RepeatKey = argdict->Get("repeat_key");
+ arg.Order = argdict->Get("order");
+ arg.Separator = argdict->Get("separator");
+
+ Value set_if = argdict->Get("set_if");
+
+ if (!set_if.IsEmpty()) {
+ String missingMacro;
+ Value set_if_resolved = MacroProcessor::ResolveMacros(set_if, resolvers,
+ cr, &missingMacro, MacroProcessor::EscapeCallback(), resolvedMacros,
+ useResolvedMacros, recursionLevel + 1);
+
+ if (!missingMacro.IsEmpty())
+ continue;
+
+ int value;
+
+ if (set_if_resolved == "true")
+ value = 1;
+ else if (set_if_resolved == "false")
+ value = 0;
+ else {
+ try {
+ value = Convert::ToLong(set_if_resolved);
+ } catch (const std::exception& ex) {
+ /* tried to convert a string */
+ Log(LogWarning, "PluginUtility")
+ << "Error evaluating set_if value '" << set_if_resolved
+ << "' used in argument '" << arg.Key << "': " << ex.what();
+ continue;
+ }
+ }
+
+ if (!value)
+ continue;
+ }
+ }
+ else
+ argval = arginfo;
+
+ if (argval.IsEmpty())
+ arg.SkipValue = true;
+
+ String missingMacro;
+ arg.AValue = MacroProcessor::ResolveMacros(argval, resolvers,
+ cr, &missingMacro, MacroProcessor::EscapeCallback(), resolvedMacros,
+ useResolvedMacros, recursionLevel + 1);
+
+ if (!missingMacro.IsEmpty()) {
+ if (required) {
+ BOOST_THROW_EXCEPTION(ScriptError("Non-optional macro '" + missingMacro + "' used in argument '" +
+ arg.Key + "' is missing."));
+ }
+
+ continue;
+ }
+
+ args.emplace_back(std::move(arg));
+ }
+
+ std::sort(args.begin(), args.end());
+
+ Array::Ptr command_arr = resolvedCommand;
+ for (const CommandArgument& arg : args) {
+
+ if (arg.AValue.IsObjectType<Dictionary>()) {
+ Log(LogWarning, "PluginUtility")
+ << "Tried to use dictionary in argument '" << arg.Key << "'.";
+ continue;
+ } else if (arg.AValue.IsObjectType<Array>()) {
+ bool first = true;
+ Array::Ptr arr = static_cast<Array::Ptr>(arg.AValue);
+
+ ObjectLock olock(arr);
+ for (const Value& value : arr) {
+ bool add_key;
+
+ if (first) {
+ first = false;
+ add_key = !arg.SkipKey;
+ } else
+ add_key = !arg.SkipKey && arg.RepeatKey;
+
+ AddArgumentHelper(command_arr, arg.Key, value, add_key, !arg.SkipValue, arg.Separator);
+ }
+ } else
+ AddArgumentHelper(command_arr, arg.Key, arg.AValue, !arg.SkipKey, !arg.SkipValue, arg.Separator);
+ }
+ }
+
+ return resolvedCommand;
+}
diff --git a/lib/icinga/macroprocessor.hpp b/lib/icinga/macroprocessor.hpp
new file mode 100644
index 0000000..7e74821
--- /dev/null
+++ b/lib/icinga/macroprocessor.hpp
@@ -0,0 +1,75 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MACROPROCESSOR_H
+#define MACROPROCESSOR_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkable.hpp"
+#include "base/value.hpp"
+#include <vector>
+#include <utility>
+
+namespace icinga
+{
+
+/**
+ * Resolves macros.
+ *
+ * @ingroup icinga
+ */
+class MacroProcessor
+{
+public:
+ struct ResolverSpec
+ {
+ String Name;
+ Object::Ptr Obj;
+
+ // Whether to resolve not only e.g. $host.address$, but also just $address$
+ bool ResolveShortMacros;
+
+ inline ResolverSpec(String name, Object::Ptr obj, bool resolveShortMacros = true)
+ : Name(std::move(name)), Obj(std::move(obj)), ResolveShortMacros(resolveShortMacros)
+ {
+ }
+ };
+
+ typedef std::function<Value (const Value&)> EscapeCallback;
+ typedef std::vector<ResolverSpec> ResolverList;
+
+ static Value ResolveMacros(const Value& str, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr = nullptr, String *missingMacro = nullptr,
+ const EscapeCallback& escapeFn = EscapeCallback(),
+ const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false, int recursionLevel = 0);
+
+ static Value ResolveArguments(const Value& command, const Dictionary::Ptr& arguments,
+ const MacroProcessor::ResolverList& resolvers, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel = 0);
+
+ static bool ValidateMacroString(const String& macro);
+ static void ValidateCustomVars(const ConfigObject::Ptr& object, const Dictionary::Ptr& value);
+
+private:
+ MacroProcessor();
+
+ static bool ResolveMacro(const String& macro, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, Value *result, bool *recursive_macro);
+ static Value InternalResolveMacros(const String& str,
+ const ResolverList& resolvers, const CheckResult::Ptr& cr,
+ String *missingMacro, const EscapeCallback& escapeFn,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros,
+ int recursionLevel = 0);
+ static Value EvaluateFunction(const Function::Ptr& func, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, const MacroProcessor::EscapeCallback& escapeFn,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel);
+
+ static void AddArgumentHelper(const Array::Ptr& args, const String& key, const String& value,
+ bool add_key, bool add_value, const Value& separator);
+ static Value EscapeMacroShellArg(const Value& value);
+
+};
+
+}
+
+#endif /* MACROPROCESSOR_H */
diff --git a/lib/icinga/macroresolver.hpp b/lib/icinga/macroresolver.hpp
new file mode 100644
index 0000000..62cd41d
--- /dev/null
+++ b/lib/icinga/macroresolver.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MACRORESOLVER_H
+#define MACRORESOLVER_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkresult.hpp"
+#include "base/dictionary.hpp"
+#include "base/string.hpp"
+
+namespace icinga
+{
+
+/**
+ * Resolves macros.
+ *
+ * @ingroup icinga
+ */
+class MacroResolver
+{
+public:
+ DECLARE_PTR_TYPEDEFS(MacroResolver);
+
+ static thread_local Dictionary::Ptr OverrideMacros;
+
+ virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const = 0;
+};
+
+}
+
+#endif /* MACRORESOLVER_H */
diff --git a/lib/icinga/notification-apply.cpp b/lib/icinga/notification-apply.cpp
new file mode 100644
index 0000000..f5b3764
--- /dev/null
+++ b/lib/icinga/notification-apply.cpp
@@ -0,0 +1,161 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/notification.hpp"
+#include "icinga/service.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE([]() {
+ ApplyRule::RegisterType("Notification", { "Host", "Service" });
+});
+
+bool Notification::EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter)
+{
+ if (!skipFilter && !rule.EvaluateFilter(frame))
+ return false;
+
+ auto& di (rule.GetDebugInfo());
+
+#ifdef _DEBUG
+ Log(LogDebug, "Notification")
+ << "Applying notification '" << name << "' to object '" << checkable->GetName() << "' for rule " << di;
+#endif /* _DEBUG */
+
+ ConfigItemBuilder builder{di};
+ builder.SetType(Notification::TypeInstance);
+ builder.SetName(name);
+ builder.SetScope(frame.Locals->ShallowClone());
+ builder.SetIgnoreOnError(rule.GetIgnoreOnError());
+
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+
+ if (service)
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "service_name"), OpSetLiteral, MakeLiteral(service->GetShortName()), di));
+
+ String zone = checkable->GetZoneName();
+
+ if (!zone.IsEmpty())
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "zone"), OpSetLiteral, MakeLiteral(zone), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "package"), OpSetLiteral, MakeLiteral(rule.GetPackage()), di));
+
+ builder.AddExpression(new OwnedExpression(rule.GetExpression()));
+
+ ConfigItem::Ptr notificationItem = builder.Compile();
+ notificationItem->Register();
+
+ return true;
+}
+
+bool Notification::EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter)
+{
+ auto& di (rule.GetDebugInfo());
+
+ CONTEXT("Evaluating 'apply' rule (" << di << ")");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ScriptFrame frame(true);
+ if (rule.GetScope())
+ rule.GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+ if (service)
+ frame.Locals->Set("service", service);
+
+ Value vinstances;
+
+ if (rule.GetFTerm()) {
+ try {
+ vinstances = rule.GetFTerm()->Evaluate(frame);
+ } catch (const std::exception&) {
+ /* Silently ignore errors here and assume there are no instances. */
+ return false;
+ }
+ } else {
+ vinstances = new Array({ "" });
+ }
+
+ bool match = false;
+
+ if (vinstances.IsObjectType<Array>()) {
+ if (!rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Dictionary iterator requires value to be a dictionary.", di));
+
+ Array::Ptr arr = vinstances;
+
+ ObjectLock olock(arr);
+ for (const Value& instance : arr) {
+ String name = rule.GetName();
+
+ if (!rule.GetFKVar().IsEmpty()) {
+ frame.Locals->Set(rule.GetFKVar(), instance);
+ name += instance;
+ }
+
+ if (EvaluateApplyRuleInstance(checkable, name, frame, rule, skipFilter))
+ match = true;
+ }
+ } else if (vinstances.IsObjectType<Dictionary>()) {
+ if (rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Array iterator requires value to be an array.", di));
+
+ Dictionary::Ptr dict = vinstances;
+
+ for (const String& key : dict->GetKeys()) {
+ frame.Locals->Set(rule.GetFKVar(), key);
+ frame.Locals->Set(rule.GetFVVar(), dict->Get(key));
+
+ if (EvaluateApplyRuleInstance(checkable, rule.GetName() + key, frame, rule, skipFilter))
+ match = true;
+ }
+ }
+
+ return match;
+}
+
+void Notification::EvaluateApplyRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating 'apply' rules for host '" << host->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Notification::TypeInstance, Host::TypeInstance))
+ {
+ if (EvaluateApplyRule(host, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedHostRules(Notification::TypeInstance, host->GetName())) {
+ if (EvaluateApplyRule(host, *rule, true))
+ rule->AddMatch();
+ }
+}
+
+void Notification::EvaluateApplyRules(const Service::Ptr& service)
+{
+ CONTEXT("Evaluating 'apply' rules for service '" << service->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Notification::TypeInstance, Service::TypeInstance)) {
+ if (EvaluateApplyRule(service, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedServiceRules(Notification::TypeInstance, service->GetHost()->GetName(), service->GetShortName())) {
+ if (EvaluateApplyRule(service, *rule, true))
+ rule->AddMatch();
+ }
+}
diff --git a/lib/icinga/notification.cpp b/lib/icinga/notification.cpp
new file mode 100644
index 0000000..ab8d42b
--- /dev/null
+++ b/lib/icinga/notification.cpp
@@ -0,0 +1,812 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/notification.hpp"
+#include "icinga/notification-ti.cpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/service.hpp"
+#include "remote/apilistener.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include "base/initialize.hpp"
+#include "base/scriptglobal.hpp"
+#include <algorithm>
+
+using namespace icinga;
+
+REGISTER_TYPE(Notification);
+INITIALIZE_ONCE(&Notification::StaticInitialize);
+
+std::map<String, int> Notification::m_StateFilterMap;
+std::map<String, int> Notification::m_TypeFilterMap;
+
+boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> Notification::OnNextNotificationChanged;
+boost::signals2::signal<void (const Notification::Ptr&, const String&, uint_fast8_t, const MessageOrigin::Ptr&)> Notification::OnLastNotifiedStatePerUserUpdated;
+boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> Notification::OnLastNotifiedStatePerUserCleared;
+
+String NotificationNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Notification::Ptr notification = dynamic_pointer_cast<Notification>(context);
+
+ if (!notification)
+ return "";
+
+ String name = notification->GetHostName();
+
+ if (!notification->GetServiceName().IsEmpty())
+ name += "!" + notification->GetServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr NotificationNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Notification name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void Notification::StaticInitialize()
+{
+ ScriptGlobal::Set("Icinga.OK", "OK");
+ ScriptGlobal::Set("Icinga.Warning", "Warning");
+ ScriptGlobal::Set("Icinga.Critical", "Critical");
+ ScriptGlobal::Set("Icinga.Unknown", "Unknown");
+ ScriptGlobal::Set("Icinga.Up", "Up");
+ ScriptGlobal::Set("Icinga.Down", "Down");
+
+ ScriptGlobal::Set("Icinga.DowntimeStart", "DowntimeStart");
+ ScriptGlobal::Set("Icinga.DowntimeEnd", "DowntimeEnd");
+ ScriptGlobal::Set("Icinga.DowntimeRemoved", "DowntimeRemoved");
+ ScriptGlobal::Set("Icinga.Custom", "Custom");
+ ScriptGlobal::Set("Icinga.Acknowledgement", "Acknowledgement");
+ ScriptGlobal::Set("Icinga.Problem", "Problem");
+ ScriptGlobal::Set("Icinga.Recovery", "Recovery");
+ ScriptGlobal::Set("Icinga.FlappingStart", "FlappingStart");
+ ScriptGlobal::Set("Icinga.FlappingEnd", "FlappingEnd");
+
+ m_StateFilterMap["OK"] = StateFilterOK;
+ m_StateFilterMap["Warning"] = StateFilterWarning;
+ m_StateFilterMap["Critical"] = StateFilterCritical;
+ m_StateFilterMap["Unknown"] = StateFilterUnknown;
+ m_StateFilterMap["Up"] = StateFilterUp;
+ m_StateFilterMap["Down"] = StateFilterDown;
+
+ m_TypeFilterMap["DowntimeStart"] = NotificationDowntimeStart;
+ m_TypeFilterMap["DowntimeEnd"] = NotificationDowntimeEnd;
+ m_TypeFilterMap["DowntimeRemoved"] = NotificationDowntimeRemoved;
+ m_TypeFilterMap["Custom"] = NotificationCustom;
+ m_TypeFilterMap["Acknowledgement"] = NotificationAcknowledgement;
+ m_TypeFilterMap["Problem"] = NotificationProblem;
+ m_TypeFilterMap["Recovery"] = NotificationRecovery;
+ m_TypeFilterMap["FlappingStart"] = NotificationFlappingStart;
+ m_TypeFilterMap["FlappingEnd"] = NotificationFlappingEnd;
+}
+
+void Notification::OnConfigLoaded()
+{
+ ObjectImpl<Notification>::OnConfigLoaded();
+
+ SetTypeFilter(FilterArrayToInt(GetTypes(), GetTypeFilterMap(), ~0));
+ SetStateFilter(FilterArrayToInt(GetStates(), GetStateFilterMap(), ~0));
+}
+
+void Notification::OnAllConfigLoaded()
+{
+ ObjectImpl<Notification>::OnAllConfigLoaded();
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+
+ if (GetServiceName().IsEmpty())
+ m_Checkable = host;
+ else
+ m_Checkable = host->GetServiceByShortName(GetServiceName());
+
+ if (!m_Checkable)
+ BOOST_THROW_EXCEPTION(ScriptError("Notification object refers to a host/service which doesn't exist.", GetDebugInfo()));
+
+ GetCheckable()->RegisterNotification(this);
+}
+
+void Notification::Start(bool runtimeCreated)
+{
+ Checkable::Ptr obj = GetCheckable();
+
+ if (obj)
+ obj->RegisterNotification(this);
+
+ if (ApiListener::IsHACluster() && GetNextNotification() < Utility::GetTime() + 60)
+ SetNextNotification(Utility::GetTime() + 60, true);
+
+ for (const UserGroup::Ptr& group : GetUserGroups())
+ group->AddNotification(this);
+
+ ObjectImpl<Notification>::Start(runtimeCreated);
+}
+
+void Notification::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<Notification>::Stop(runtimeRemoved);
+
+ Checkable::Ptr obj = GetCheckable();
+
+ if (obj)
+ obj->UnregisterNotification(this);
+
+ for (const UserGroup::Ptr& group : GetUserGroups())
+ group->RemoveNotification(this);
+}
+
+Checkable::Ptr Notification::GetCheckable() const
+{
+ return static_pointer_cast<Checkable>(m_Checkable);
+}
+
+NotificationCommand::Ptr Notification::GetCommand() const
+{
+ return NotificationCommand::GetByName(GetCommandRaw());
+}
+
+std::set<User::Ptr> Notification::GetUsers() const
+{
+ std::set<User::Ptr> result;
+
+ Array::Ptr users = GetUsersRaw();
+
+ if (users) {
+ ObjectLock olock(users);
+
+ for (const String& name : users) {
+ User::Ptr user = User::GetByName(name);
+
+ if (!user)
+ continue;
+
+ result.insert(user);
+ }
+ }
+
+ return result;
+}
+
+std::set<UserGroup::Ptr> Notification::GetUserGroups() const
+{
+ std::set<UserGroup::Ptr> result;
+
+ Array::Ptr groups = GetUserGroupsRaw();
+
+ if (groups) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ UserGroup::Ptr ug = UserGroup::GetByName(name);
+
+ if (!ug)
+ continue;
+
+ result.insert(ug);
+ }
+ }
+
+ return result;
+}
+
+TimePeriod::Ptr Notification::GetPeriod() const
+{
+ return TimePeriod::GetByName(GetPeriodRaw());
+}
+
+void Notification::UpdateNotificationNumber()
+{
+ SetNotificationNumber(GetNotificationNumber() + 1);
+}
+
+void Notification::ResetNotificationNumber()
+{
+ SetNotificationNumber(0);
+}
+
+void Notification::BeginExecuteNotification(NotificationType type, const CheckResult::Ptr& cr, bool force, bool reminder, const String& author, const String& text)
+{
+ String notificationName = GetName();
+ String notificationTypeName = NotificationTypeToString(type);
+
+ Log(LogNotice, "Notification")
+ << "Attempting to send " << (reminder ? "reminder " : "")
+ << "notifications of type '" << notificationTypeName
+ << "' for notification object '" << notificationName << "'.";
+
+ if (type == NotificationRecovery) {
+ auto states (GetLastNotifiedStatePerUser());
+
+ states->Clear();
+ OnLastNotifiedStatePerUserCleared(this, nullptr);
+ }
+
+ Checkable::Ptr checkable = GetCheckable();
+
+ if (!force) {
+ TimePeriod::Ptr tp = GetPeriod();
+
+ if (tp && !tp->IsInside(Utility::GetTime())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '" << notificationName
+ << "': not in timeperiod '" << tp->GetName() << "'";
+
+ if (!reminder) {
+ switch (type) {
+ case NotificationProblem:
+ case NotificationRecovery:
+ case NotificationFlappingStart:
+ case NotificationFlappingEnd:
+ {
+ /* If a non-reminder notification was suppressed, but just because of its time period,
+ * stash it into a notification types bitmask for maybe re-sending later.
+ */
+
+ ObjectLock olock (this);
+ int suppressedTypesBefore (GetSuppressedNotifications());
+ int suppressedTypesAfter (suppressedTypesBefore | type);
+
+ for (int conflict : {NotificationProblem | NotificationRecovery, NotificationFlappingStart | NotificationFlappingEnd}) {
+ /* E.g. problem and recovery notifications neutralize each other. */
+
+ if ((suppressedTypesAfter & conflict) == conflict) {
+ suppressedTypesAfter &= ~conflict;
+ }
+ }
+
+ if (suppressedTypesAfter != suppressedTypesBefore) {
+ SetSuppressedNotifications(suppressedTypesAfter);
+ }
+ }
+ default:
+ ; // Cheating the compiler on "5 enumeration values not handled in switch"
+ }
+ }
+
+ return;
+ }
+
+ double now = Utility::GetTime();
+ Dictionary::Ptr times = GetTimes();
+
+ if (times && type == NotificationProblem) {
+ Value timesBegin = times->Get("begin");
+ Value timesEnd = times->Get("end");
+
+ if (timesBegin != Empty && timesBegin >= 0 && now < checkable->GetLastHardStateChange() + timesBegin) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << "': before specified begin time (" << Utility::FormatDuration(timesBegin) << ")";
+
+ /* we need to adjust the next notification time
+ * delaying the first notification
+ */
+ SetNextNotification(checkable->GetLastHardStateChange() + timesBegin + 1.0);
+
+ /*
+ * We need to set no more notifications to false, in case
+ * some notifications were sent previously
+ */
+ SetNoMoreNotifications(false);
+
+ return;
+ }
+
+ if (timesEnd != Empty && timesEnd >= 0 && now > checkable->GetLastHardStateChange() + timesEnd) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << "': after specified end time (" << Utility::FormatDuration(timesEnd) << ")";
+ return;
+ }
+ }
+
+ unsigned long ftype = type;
+
+ Log(LogDebug, "Notification")
+ << "Type '" << NotificationTypeToString(type)
+ << "', TypeFilter: " << NotificationFilterToString(GetTypeFilter(), GetTypeFilterMap())
+ << " (FType=" << ftype << ", TypeFilter=" << GetTypeFilter() << ")";
+
+ if (!(ftype & GetTypeFilter())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << "': type '"
+ << NotificationTypeToString(type) << "' does not match type filter: "
+ << NotificationFilterToString(GetTypeFilter(), GetTypeFilterMap()) << ".";
+
+ /* Ensure to reset no_more_notifications on Recovery notifications,
+ * even if the admin did not configure them in the filter.
+ */
+ {
+ ObjectLock olock(this);
+ if (type == NotificationRecovery && GetInterval() <= 0)
+ SetNoMoreNotifications(false);
+ }
+
+ return;
+ }
+
+ /* Check state filters for problem notifications. Recovery notifications will be filtered away later. */
+ if (type == NotificationProblem) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ unsigned long fstate;
+ String stateStr;
+
+ if (service) {
+ fstate = ServiceStateToFilter(service->GetState());
+ stateStr = NotificationServiceStateToString(service->GetState());
+ } else {
+ fstate = HostStateToFilter(host->GetState());
+ stateStr = NotificationHostStateToString(host->GetState());
+ }
+
+ Log(LogDebug, "Notification")
+ << "State '" << stateStr << "', StateFilter: " << NotificationFilterToString(GetStateFilter(), GetStateFilterMap())
+ << " (FState=" << fstate << ", StateFilter=" << GetStateFilter() << ")";
+
+ if (!(fstate & GetStateFilter())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << "': state '" << stateStr
+ << "' does not match state filter: " << NotificationFilterToString(GetStateFilter(), GetStateFilterMap()) << ".";
+ return;
+ }
+ }
+ } else {
+ Log(LogNotice, "Notification")
+ << "Not checking " << (reminder ? "reminder " : "") << "notification filters for notification object '"
+ << notificationName << "': Notification was forced.";
+ }
+
+ {
+ ObjectLock olock(this);
+
+ UpdateNotificationNumber();
+ double now = Utility::GetTime();
+ SetLastNotification(now);
+
+ if (type == NotificationProblem && GetInterval() <= 0)
+ SetNoMoreNotifications(true);
+ else
+ SetNoMoreNotifications(false);
+
+ if (type == NotificationProblem && GetInterval() > 0)
+ SetNextNotification(now + GetInterval());
+
+ if (type == NotificationProblem)
+ SetLastProblemNotification(now);
+ }
+
+ std::set<User::Ptr> allUsers;
+
+ std::set<User::Ptr> users = GetUsers();
+ std::copy(users.begin(), users.end(), std::inserter(allUsers, allUsers.begin()));
+
+ for (const UserGroup::Ptr& ug : GetUserGroups()) {
+ std::set<User::Ptr> members = ug->GetMembers();
+ std::copy(members.begin(), members.end(), std::inserter(allUsers, allUsers.begin()));
+ }
+
+ std::set<User::Ptr> allNotifiedUsers;
+ Array::Ptr notifiedProblemUsers = GetNotifiedProblemUsers();
+
+ for (const User::Ptr& user : allUsers) {
+ String userName = user->GetName();
+
+ if (!user->GetEnableNotifications()) {
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': Disabled notifications for user '"
+ << userName << "'. Not sending notification.";
+ continue;
+ }
+
+ if (!CheckNotificationUserFilters(type, user, force, reminder)) {
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': Filters for user '" << userName << "' not matched. Not sending notification.";
+ continue;
+ }
+
+ /* on recovery, check if user was notified before */
+ if (type == NotificationRecovery) {
+ if (!notifiedProblemUsers->Contains(userName) && (NotificationProblem & user->GetTypeFilter())) {
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': We did not notify user '" << userName
+ << "' (Problem types enabled) for a problem before. Not sending Recovery notification.";
+ continue;
+ }
+ }
+
+ /* on acknowledgement, check if user was notified before */
+ if (type == NotificationAcknowledgement) {
+ if (!notifiedProblemUsers->Contains(userName) && (NotificationProblem & user->GetTypeFilter())) {
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': We did not notify user '" << userName
+ << "' (Problem types enabled) for a problem before. Not sending acknowledgement notification.";
+ continue;
+ }
+ }
+
+ if (type == NotificationProblem && !reminder && !checkable->GetVolatile()) {
+ auto [host, service] = GetHostService(checkable);
+ uint_fast8_t state = service ? service->GetState() : host->GetState();
+
+ if (state == (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
+ auto stateStr (service ? NotificationServiceStateToString(service->GetState()) : NotificationHostStateToString(host->GetState()));
+
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': We already notified user '" << userName << "' for a " << stateStr
+ << " problem. Likely after that another state change notification was filtered out by config. Not sending duplicate '"
+ << stateStr << "' notification.";
+
+ continue;
+ }
+ }
+
+ Log(LogInformation, "Notification")
+ << "Sending " << (reminder ? "reminder " : "") << "'" << NotificationTypeToString(type) << "' notification '"
+ << notificationName << "' for user '" << userName << "'";
+
+ // Explicitly use Notification::Ptr to keep the reference counted while the callback is active
+ Notification::Ptr notification (this);
+ Utility::QueueAsyncCallback([notification, type, user, cr, force, author, text]() {
+ notification->ExecuteNotificationHelper(type, user, cr, force, author, text);
+ });
+
+ /* collect all notified users */
+ allNotifiedUsers.insert(user);
+
+ if (type == NotificationProblem) {
+ auto [host, service] = GetHostService(checkable);
+ uint_fast8_t state = service ? service->GetState() : host->GetState();
+
+ if (state != (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
+ GetLastNotifiedStatePerUser()->Set(userName, state);
+ OnLastNotifiedStatePerUserUpdated(this, userName, state, nullptr);
+ }
+ }
+
+ /* store all notified users for later recovery checks */
+ if (type == NotificationProblem && !notifiedProblemUsers->Contains(userName))
+ notifiedProblemUsers->Add(userName);
+ }
+
+ /* if this was a recovery notification, reset all notified users */
+ if (type == NotificationRecovery)
+ notifiedProblemUsers->Clear();
+
+ /* used in db_ido for notification history */
+ Service::OnNotificationSentToAllUsers(this, checkable, allNotifiedUsers, type, cr, author, text, nullptr);
+}
+
+bool Notification::CheckNotificationUserFilters(NotificationType type, const User::Ptr& user, bool force, bool reminder)
+{
+ String notificationName = GetName();
+ String userName = user->GetName();
+
+ if (!force) {
+ TimePeriod::Ptr tp = user->GetPeriod();
+
+ if (tp && !tp->IsInside(Utility::GetTime())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << " and user '" << userName
+ << "': user period not in timeperiod '" << tp->GetName() << "'";
+ return false;
+ }
+
+ unsigned long ftype = type;
+
+ Log(LogDebug, "Notification")
+ << "User '" << userName << "' notification '" << notificationName
+ << "', Type '" << NotificationTypeToString(type)
+ << "', TypeFilter: " << NotificationFilterToString(user->GetTypeFilter(), GetTypeFilterMap())
+ << " (FType=" << ftype << ", TypeFilter=" << GetTypeFilter() << ")";
+
+
+ if (!(ftype & user->GetTypeFilter())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << " and user '" << userName << "': type '"
+ << NotificationTypeToString(type) << "' does not match type filter: "
+ << NotificationFilterToString(user->GetTypeFilter(), GetTypeFilterMap()) << ".";
+ return false;
+ }
+
+ /* check state filters it this is not a recovery notification */
+ if (type != NotificationRecovery) {
+ Checkable::Ptr checkable = GetCheckable();
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ unsigned long fstate;
+ String stateStr;
+
+ if (service) {
+ fstate = ServiceStateToFilter(service->GetState());
+ stateStr = NotificationServiceStateToString(service->GetState());
+ } else {
+ fstate = HostStateToFilter(host->GetState());
+ stateStr = NotificationHostStateToString(host->GetState());
+ }
+
+ Log(LogDebug, "Notification")
+ << "User '" << userName << "' notification '" << notificationName
+ << "', State '" << stateStr << "', StateFilter: "
+ << NotificationFilterToString(user->GetStateFilter(), GetStateFilterMap())
+ << " (FState=" << fstate << ", StateFilter=" << user->GetStateFilter() << ")";
+
+ if (!(fstate & user->GetStateFilter())) {
+ Log(LogNotice, "Notification")
+ << "Not " << (reminder ? "reminder " : "") << "sending notifications for notification object '"
+ << notificationName << " and user '" << userName << "': state '" << stateStr
+ << "' does not match state filter: " << NotificationFilterToString(user->GetStateFilter(), GetStateFilterMap()) << ".";
+ return false;
+ }
+ }
+ } else {
+ Log(LogNotice, "Notification")
+ << "Not checking " << (reminder ? "reminder " : "") << "notification filters for notification object '"
+ << notificationName << "' and user '" << userName << "': Notification was forced.";
+ }
+
+ return true;
+}
+
+void Notification::ExecuteNotificationHelper(NotificationType type, const User::Ptr& user, const CheckResult::Ptr& cr, bool force, const String& author, const String& text)
+{
+ String notificationName = GetName();
+ String userName = user->GetName();
+ String checkableName = GetCheckable()->GetName();
+
+ NotificationCommand::Ptr command = GetCommand();
+
+ if (!command) {
+ Log(LogDebug, "Notification")
+ << "No command found for notification '" << notificationName << "'. Skipping execution.";
+ return;
+ }
+
+ String commandName = command->GetName();
+
+ try {
+ command->Execute(this, user, cr, type, author, text);
+
+ /* required by compatlogger */
+ Service::OnNotificationSentToUser(this, GetCheckable(), user, type, cr, author, text, commandName, nullptr);
+
+ Log(LogInformation, "Notification")
+ << "Completed sending '" << NotificationTypeToString(type)
+ << "' notification '" << notificationName
+ << "' for checkable '" << checkableName
+ << "' and user '" << userName << "' using command '" << commandName << "'.";
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "Notification")
+ << "Exception occurred during notification '" << notificationName
+ << "' for checkable '" << checkableName
+ << "' and user '" << userName << "' using command '" << commandName << "': "
+ << DiagnosticInformation(ex, false);
+ }
+}
+
+int icinga::ServiceStateToFilter(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ return StateFilterOK;
+ case ServiceWarning:
+ return StateFilterWarning;
+ case ServiceCritical:
+ return StateFilterCritical;
+ case ServiceUnknown:
+ return StateFilterUnknown;
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
+
+int icinga::HostStateToFilter(HostState state)
+{
+ switch (state) {
+ case HostUp:
+ return StateFilterUp;
+ case HostDown:
+ return StateFilterDown;
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
+
+String Notification::NotificationFilterToString(int filter, const std::map<String, int>& filterMap)
+{
+ std::vector<String> sFilters;
+
+ typedef std::pair<String, int> kv_pair;
+ for (const kv_pair& kv : filterMap) {
+ if (filter & kv.second)
+ sFilters.push_back(kv.first);
+ }
+
+ return Utility::NaturalJoin(sFilters);
+}
+
+/*
+ * Main interface to translate NotificationType values into strings.
+ */
+String Notification::NotificationTypeToString(NotificationType type)
+{
+ auto typeMap = Notification::m_TypeFilterMap;
+
+ auto it = std::find_if(typeMap.begin(), typeMap.end(),
+ [&type](const std::pair<String, int>& p) {
+ return p.second == type;
+ });
+
+ if (it == typeMap.end())
+ return Empty;
+
+ return it->first;
+}
+
+
+/*
+ * Compat interface used in external features.
+ */
+String Notification::NotificationTypeToStringCompat(NotificationType type)
+{
+ switch (type) {
+ case NotificationDowntimeStart:
+ return "DOWNTIMESTART";
+ case NotificationDowntimeEnd:
+ return "DOWNTIMEEND";
+ case NotificationDowntimeRemoved:
+ return "DOWNTIMECANCELLED";
+ case NotificationCustom:
+ return "CUSTOM";
+ case NotificationAcknowledgement:
+ return "ACKNOWLEDGEMENT";
+ case NotificationProblem:
+ return "PROBLEM";
+ case NotificationRecovery:
+ return "RECOVERY";
+ case NotificationFlappingStart:
+ return "FLAPPINGSTART";
+ case NotificationFlappingEnd:
+ return "FLAPPINGEND";
+ default:
+ return "UNKNOWN_NOTIFICATION";
+ }
+}
+
+String Notification::NotificationServiceStateToString(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ return "OK";
+ case ServiceWarning:
+ return "Warning";
+ case ServiceCritical:
+ return "Critical";
+ case ServiceUnknown:
+ return "Unknown";
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
+
+String Notification::NotificationHostStateToString(HostState state)
+{
+ switch (state) {
+ case HostUp:
+ return "Up";
+ case HostDown:
+ return "Down";
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
+
+void Notification::Validate(int types, const ValidationUtils& utils)
+{
+ ObjectImpl<Notification>::Validate(types, utils);
+
+ if (!(types & FAConfig))
+ return;
+
+ Array::Ptr users = GetUsersRaw();
+ Array::Ptr groups = GetUserGroupsRaw();
+
+ if ((!users || users->GetLength() == 0) && (!groups || groups->GetLength() == 0))
+ BOOST_THROW_EXCEPTION(ValidationError(this, std::vector<String>(), "Validation failed: No users/user_groups specified."));
+}
+
+void Notification::ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Notification>::ValidateStates(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), GetStateFilterMap(), 0);
+
+ if (GetServiceName().IsEmpty() && (filter == -1 || (filter & ~(StateFilterUp | StateFilterDown)) != 0))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid."));
+
+ if (!GetServiceName().IsEmpty() && (filter == -1 || (filter & ~(StateFilterOK | StateFilterWarning | StateFilterCritical | StateFilterUnknown)) != 0))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid."));
+}
+
+void Notification::ValidateTypes(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Notification>::ValidateTypes(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), GetTypeFilterMap(), 0);
+
+ if (filter == -1 || (filter & ~(NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved |
+ NotificationCustom | NotificationAcknowledgement | NotificationProblem | NotificationRecovery |
+ NotificationFlappingStart | NotificationFlappingEnd)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "types" }, "Type filter is invalid."));
+}
+
+void Notification::ValidateTimes(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Notification>::ValidateTimes(lvalue, utils);
+
+ Dictionary::Ptr times = lvalue();
+
+ if (!times)
+ return;
+
+ double begin;
+ double end;
+
+ try {
+ begin = Convert::ToDouble(times->Get("begin"));
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "times" }, "'begin' is invalid, must be duration or number." ));
+ }
+
+ try {
+ end = Convert::ToDouble(times->Get("end"));
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "times" }, "'end' is invalid, must be duration or number." ));
+ }
+
+ /* Also solve logical errors where begin > end. */
+ if (begin > 0 && end > 0 && begin > end)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "times" }, "'begin' must be smaller than 'end'."));
+}
+
+Endpoint::Ptr Notification::GetCommandEndpoint() const
+{
+ return Endpoint::GetByName(GetCommandEndpointRaw());
+}
+
+const std::map<String, int>& Notification::GetStateFilterMap()
+{
+ return m_StateFilterMap;
+}
+
+const std::map<String, int>& Notification::GetTypeFilterMap()
+{
+ return m_TypeFilterMap;
+}
diff --git a/lib/icinga/notification.hpp b/lib/icinga/notification.hpp
new file mode 100644
index 0000000..1b6cbed
--- /dev/null
+++ b/lib/icinga/notification.hpp
@@ -0,0 +1,135 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NOTIFICATION_H
+#define NOTIFICATION_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/notification-ti.hpp"
+#include "icinga/checkable-ti.hpp"
+#include "icinga/user.hpp"
+#include "icinga/usergroup.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/checkresult.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/messageorigin.hpp"
+#include "base/array.hpp"
+#include <cstdint>
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+enum NotificationFilter
+{
+ StateFilterOK = 1,
+ StateFilterWarning = 2,
+ StateFilterCritical = 4,
+ StateFilterUnknown = 8,
+
+ StateFilterUp = 16,
+ StateFilterDown = 32
+};
+
+/**
+ * The notification type.
+ *
+ * @ingroup icinga
+ */
+enum NotificationType
+{
+ NotificationDowntimeStart = 1,
+ NotificationDowntimeEnd = 2,
+ NotificationDowntimeRemoved = 4,
+ NotificationCustom = 8,
+ NotificationAcknowledgement = 16,
+ NotificationProblem = 32,
+ NotificationRecovery = 64,
+ NotificationFlappingStart = 128,
+ NotificationFlappingEnd = 256
+};
+
+class NotificationCommand;
+class ApplyRule;
+struct ScriptFrame;
+class Host;
+class Service;
+
+/**
+ * An Icinga notification specification.
+ *
+ * @ingroup icinga
+ */
+class Notification final : public ObjectImpl<Notification>
+{
+public:
+ DECLARE_OBJECT(Notification);
+ DECLARE_OBJECTNAME(Notification);
+
+ static void StaticInitialize();
+
+ intrusive_ptr<Checkable> GetCheckable() const;
+ intrusive_ptr<NotificationCommand> GetCommand() const;
+ TimePeriod::Ptr GetPeriod() const;
+ std::set<User::Ptr> GetUsers() const;
+ std::set<UserGroup::Ptr> GetUserGroups() const;
+
+ void UpdateNotificationNumber();
+ void ResetNotificationNumber();
+
+ void BeginExecuteNotification(NotificationType type, const CheckResult::Ptr& cr, bool force,
+ bool reminder = false, const String& author = "", const String& text = "");
+
+ Endpoint::Ptr GetCommandEndpoint() const;
+
+ // Logging, etc.
+ static String NotificationTypeToString(NotificationType type);
+ // Compat, used for notifications, etc.
+ static String NotificationTypeToStringCompat(NotificationType type);
+ static String NotificationFilterToString(int filter, const std::map<String, int>& filterMap);
+
+ static String NotificationServiceStateToString(ServiceState state);
+ static String NotificationHostStateToString(HostState state);
+
+ static boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> OnNextNotificationChanged;
+ static boost::signals2::signal<void (const Notification::Ptr&, const String&, uint_fast8_t, const MessageOrigin::Ptr&)> OnLastNotifiedStatePerUserUpdated;
+ static boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> OnLastNotifiedStatePerUserCleared;
+
+ void Validate(int types, const ValidationUtils& utils) override;
+
+ void ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateTypes(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateTimes(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+ static void EvaluateApplyRules(const intrusive_ptr<Host>& host);
+ static void EvaluateApplyRules(const intrusive_ptr<Service>& service);
+
+ static const std::map<String, int>& GetStateFilterMap();
+ static const std::map<String, int>& GetTypeFilterMap();
+
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ ObjectImpl<Checkable>::Ptr m_Checkable;
+
+ bool CheckNotificationUserFilters(NotificationType type, const User::Ptr& user, bool force, bool reminder);
+
+ void ExecuteNotificationHelper(NotificationType type, const User::Ptr& user, const CheckResult::Ptr& cr, bool force, const String& author = "", const String& text = "");
+
+ static bool EvaluateApplyRuleInstance(const intrusive_ptr<Checkable>& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter);
+ static bool EvaluateApplyRule(const intrusive_ptr<Checkable>& checkable, const ApplyRule& rule, bool skipFilter = false);
+
+ static std::map<String, int> m_StateFilterMap;
+ static std::map<String, int> m_TypeFilterMap;
+};
+
+int ServiceStateToFilter(ServiceState state);
+int HostStateToFilter(HostState state);
+
+}
+
+#endif /* NOTIFICATION_H */
diff --git a/lib/icinga/notification.ti b/lib/icinga/notification.ti
new file mode 100644
index 0000000..be07846
--- /dev/null
+++ b/lib/icinga/notification.ti
@@ -0,0 +1,111 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#impl_include "icinga/notificationcommand.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class NotificationNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Notification : CustomVarObject < NotificationNameComposer
+{
+ load_after Host;
+ load_after Service;
+
+ [config, protected, required, navigation] name(NotificationCommand) command (CommandRaw) {
+ navigate {{{
+ return NotificationCommand::GetByName(GetCommandRaw());
+ }}}
+ };
+ [config] double interval {
+ default {{{ return 1800; }}}
+ };
+ [config, navigation] name(TimePeriod) period (PeriodRaw) {
+ navigate {{{
+ return TimePeriod::GetByName(GetPeriodRaw());
+ }}}
+ };
+ [config, signal_with_old_value] array(name(User)) users (UsersRaw);
+ [config, signal_with_old_value] array(name(UserGroup)) user_groups (UserGroupsRaw);
+ [config] Dictionary::Ptr times;
+ [config] array(Value) types;
+ [no_user_view, no_user_modify] int type_filter_real (TypeFilter);
+ [config] array(Value) states;
+ [no_user_view, no_user_modify] int state_filter_real (StateFilter);
+ [config, no_user_modify, protected, required, navigation(host)] name(Host) host_name {
+ navigate {{{
+ return Host::GetByName(GetHostName());
+ }}}
+ };
+ [config, protected, no_user_modify, navigation(service)] String service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+ return host->GetServiceByShortName(GetServiceName());
+ }}}
+ };
+
+ [state, no_user_modify] Array::Ptr notified_problem_users {
+ default {{{ return new Array(); }}}
+ };
+
+ [state, no_user_modify] bool no_more_notifications {
+ default {{{ return false; }}}
+ };
+
+ [state, no_user_view, no_user_modify] Array::Ptr stashed_notifications {
+ default {{{ return new Array(); }}}
+ };
+
+ [state] Timestamp last_notification;
+ [state] Timestamp next_notification;
+ [state] int notification_number;
+ [state] Timestamp last_problem_notification;
+
+ [state, no_user_view, no_user_modify] int suppressed_notifications {
+ default {{{ return 0; }}}
+ };
+
+ [state, no_user_view, no_user_modify] Dictionary::Ptr last_notified_state_per_user {
+ default {{{ return new Dictionary(); }}}
+ };
+
+ [config, navigation] name(Endpoint) command_endpoint (CommandEndpointRaw) {
+ navigate {{{
+ return Endpoint::GetByName(GetCommandEndpointRaw());
+ }}}
+ };
+};
+
+validator Notification {
+ Dictionary times {
+ Number begin;
+ Number end;
+ };
+};
+
+}
diff --git a/lib/icinga/notificationcommand.cpp b/lib/icinga/notificationcommand.cpp
new file mode 100644
index 0000000..d4a5fd6
--- /dev/null
+++ b/lib/icinga/notificationcommand.cpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/notificationcommand.hpp"
+#include "icinga/notificationcommand-ti.cpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(NotificationCommand);
+
+thread_local NotificationCommand::Ptr NotificationCommand::ExecuteOverride;
+
+Dictionary::Ptr NotificationCommand::Execute(const Notification::Ptr& notification,
+ const User::Ptr& user, const CheckResult::Ptr& cr, const NotificationType& type,
+ const String& author, const String& comment, const Dictionary::Ptr& resolvedMacros,
+ bool useResolvedMacros)
+{
+ return GetExecute()->Invoke({
+ notification,
+ user,
+ cr,
+ type,
+ author,
+ comment,
+ resolvedMacros,
+ useResolvedMacros,
+ });
+}
diff --git a/lib/icinga/notificationcommand.hpp b/lib/icinga/notificationcommand.hpp
new file mode 100644
index 0000000..f0f6899
--- /dev/null
+++ b/lib/icinga/notificationcommand.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NOTIFICATIONCOMMAND_H
+#define NOTIFICATIONCOMMAND_H
+
+#include "icinga/notificationcommand-ti.hpp"
+#include "icinga/notification.hpp"
+
+namespace icinga
+{
+
+class Notification;
+
+/**
+ * A notification command.
+ *
+ * @ingroup icinga
+ */
+class NotificationCommand final : public ObjectImpl<NotificationCommand>
+{
+public:
+ DECLARE_OBJECT(NotificationCommand);
+ DECLARE_OBJECTNAME(NotificationCommand);
+
+ static thread_local NotificationCommand::Ptr ExecuteOverride;
+
+ virtual Dictionary::Ptr Execute(const intrusive_ptr<Notification>& notification,
+ const User::Ptr& user, const CheckResult::Ptr& cr, const NotificationType& type,
+ const String& author, const String& comment,
+ const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false);
+};
+
+}
+
+#endif /* NOTIFICATIONCOMMAND_H */
diff --git a/lib/icinga/notificationcommand.ti b/lib/icinga/notificationcommand.ti
new file mode 100644
index 0000000..51207a3
--- /dev/null
+++ b/lib/icinga/notificationcommand.ti
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/command.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class NotificationCommand : Command
+{
+};
+
+}
diff --git a/lib/icinga/objectutils.cpp b/lib/icinga/objectutils.cpp
new file mode 100644
index 0000000..559ca43
--- /dev/null
+++ b/lib/icinga/objectutils.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/objectutils.hpp"
+#include "icinga/host.hpp"
+#include "icinga/user.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/usergroup.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION(Icinga, get_host, &Host::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_service, &ObjectUtils::GetService, "host:name");
+REGISTER_FUNCTION(Icinga, get_services, &ObjectUtils::GetServices, "host");
+REGISTER_FUNCTION(Icinga, get_user, &User::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_check_command, &CheckCommand::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_event_command, &EventCommand::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_notification_command, &NotificationCommand::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_host_group, &HostGroup::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_service_group, &ServiceGroup::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_user_group, &UserGroup::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_time_period, &TimePeriod::GetByName, "name");
+
+Service::Ptr ObjectUtils::GetService(const Value& host, const String& name)
+{
+ Host::Ptr hostObj;
+
+ if (host.IsObjectType<Host>())
+ hostObj = host;
+ else
+ hostObj = Host::GetByName(host);
+
+ if (!hostObj)
+ return nullptr;
+
+ return hostObj->GetServiceByShortName(name);
+}
+
+Array::Ptr ObjectUtils::GetServices(const Value& host)
+{
+ Host::Ptr hostObj;
+
+ if (host.IsObjectType<Host>())
+ hostObj = host;
+ else
+ hostObj = Host::GetByName(host);
+
+ if (!hostObj)
+ return nullptr;
+
+ return Array::FromVector(hostObj->GetServices());
+}
diff --git a/lib/icinga/objectutils.hpp b/lib/icinga/objectutils.hpp
new file mode 100644
index 0000000..42e2953
--- /dev/null
+++ b/lib/icinga/objectutils.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTUTILS_H
+#define OBJECTUTILS_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include "base/array.hpp"
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+class ObjectUtils
+{
+public:
+ static Service::Ptr GetService(const Value& host, const String& name);
+ static Array::Ptr GetServices(const Value& host);
+
+private:
+ ObjectUtils();
+};
+
+}
+
+#endif /* OBJECTUTILS_H */
diff --git a/lib/icinga/pluginutility.cpp b/lib/icinga/pluginutility.cpp
new file mode 100644
index 0000000..4dc46f7
--- /dev/null
+++ b/lib/icinga/pluginutility.cpp
@@ -0,0 +1,218 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/pluginutility.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/convert.hpp"
+#include "base/process.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/trim.hpp>
+
+using namespace icinga;
+
+void PluginUtility::ExecuteCommand(const Command::Ptr& commandObj, const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MacroProcessor::ResolverList& macroResolvers,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int timeout,
+ const std::function<void(const Value& commandLine, const ProcessResult&)>& callback)
+{
+ Value raw_command = commandObj->GetCommandLine();
+ Dictionary::Ptr raw_arguments = commandObj->GetArguments();
+
+ Value command;
+
+ try {
+ command = MacroProcessor::ResolveArguments(raw_command, raw_arguments,
+ macroResolvers, cr, resolvedMacros, useResolvedMacros);
+ } catch (const std::exception& ex) {
+ String message = DiagnosticInformation(ex);
+
+ Log(LogWarning, "PluginUtility", message);
+
+ if (callback) {
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.ExecutionStart = Utility::GetTime();
+ pr.ExecutionEnd = pr.ExecutionStart;
+ pr.ExitStatus = 3; /* Unknown */
+ pr.Output = message;
+ callback(Empty, pr);
+ }
+
+ return;
+ }
+
+ Dictionary::Ptr envMacros = new Dictionary();
+
+ Dictionary::Ptr env = commandObj->GetEnv();
+
+ if (env) {
+ ObjectLock olock(env);
+ for (const Dictionary::Pair& kv : env) {
+ String name = kv.second;
+
+ String missingMacro;
+ Value value = MacroProcessor::ResolveMacros(name, macroResolvers, cr,
+ &missingMacro, MacroProcessor::EscapeCallback(), resolvedMacros,
+ useResolvedMacros);
+
+#ifdef I2_DEBUG
+ if (!missingMacro.IsEmpty())
+ Log(LogDebug, "PluginUtility")
+ << "Macro '" << name << "' is not defined.";
+#endif /* I2_DEBUG */
+
+ if (value.IsObjectType<Array>())
+ value = Utility::Join(value, ';');
+
+ envMacros->Set(kv.first, value);
+ }
+ }
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ Process::Ptr process = new Process(Process::PrepareCommand(command), envMacros);
+
+ process->SetTimeout(timeout);
+ process->SetAdjustPriority(true);
+
+ process->Run([callback, command](const ProcessResult& pr) { callback(command, pr); });
+}
+
+ServiceState PluginUtility::ExitStatusToState(int exitStatus)
+{
+ switch (exitStatus) {
+ case 0:
+ return ServiceOK;
+ case 1:
+ return ServiceWarning;
+ case 2:
+ return ServiceCritical;
+ default:
+ return ServiceUnknown;
+ }
+}
+
+std::pair<String, String> PluginUtility::ParseCheckOutput(const String& output)
+{
+ String text;
+ String perfdata;
+
+ std::vector<String> lines = output.Split("\r\n");
+
+ for (const String& line : lines) {
+ size_t delim = line.FindFirstOf("|");
+
+ if (!text.IsEmpty())
+ text += "\n";
+
+ if (delim != String::NPos && line.FindFirstOf("=", delim) != String::NPos) {
+ text += line.SubStr(0, delim);
+
+ if (!perfdata.IsEmpty())
+ perfdata += " ";
+
+ perfdata += line.SubStr(delim + 1, line.GetLength());
+ } else {
+ text += line;
+ }
+ }
+
+ boost::algorithm::trim(perfdata);
+
+ return std::make_pair(text, perfdata);
+}
+
+Array::Ptr PluginUtility::SplitPerfdata(const String& perfdata)
+{
+ ArrayData result;
+
+ size_t begin = 0;
+ String multi_prefix;
+
+ for (;;) {
+ size_t eqp = perfdata.FindFirstOf('=', begin);
+
+ if (eqp == String::NPos)
+ break;
+
+ String label = perfdata.SubStr(begin, eqp - begin);
+ boost::algorithm::trim_left(label);
+
+ if (label.GetLength() > 2 && label[0] == '\'' && label[label.GetLength() - 1] == '\'')
+ label = label.SubStr(1, label.GetLength() - 2);
+
+ size_t multi_index = label.RFind("::");
+
+ if (multi_index != String::NPos)
+ multi_prefix = "";
+
+ size_t spq = perfdata.FindFirstOf(' ', eqp);
+
+ if (spq == String::NPos)
+ spq = perfdata.GetLength();
+
+ String value = perfdata.SubStr(eqp + 1, spq - eqp - 1);
+
+ if (!multi_prefix.IsEmpty())
+ label = multi_prefix + "::" + label;
+
+ String pdv;
+ if (label.FindFirstOf(" ") != String::NPos)
+ pdv = "'" + label + "'=" + value;
+ else
+ pdv = label + "=" + value;
+
+ result.emplace_back(std::move(pdv));
+
+ if (multi_index != String::NPos)
+ multi_prefix = label.SubStr(0, multi_index);
+
+ begin = spq + 1;
+ }
+
+ return new Array(std::move(result));
+}
+
+String PluginUtility::FormatPerfdata(const Array::Ptr& perfdata, bool normalize)
+{
+ if (!perfdata)
+ return "";
+
+ std::ostringstream result;
+
+ ObjectLock olock(perfdata);
+
+ bool first = true;
+ for (const Value& pdv : perfdata) {
+ if (!first)
+ result << " ";
+ else
+ first = false;
+
+ if (pdv.IsObjectType<PerfdataValue>()) {
+ result << static_cast<PerfdataValue::Ptr>(pdv)->Format();
+ } else if (normalize) {
+ PerfdataValue::Ptr normalized;
+
+ try {
+ normalized = PerfdataValue::Parse(pdv);
+ } catch (const std::invalid_argument& ex) {
+ Log(LogDebug, "PerfdataValue") << ex.what();
+ }
+
+ if (normalized) {
+ result << normalized->Format();
+ } else {
+ result << pdv;
+ }
+ } else {
+ result << pdv;
+ }
+ }
+
+ return result.str();
+}
diff --git a/lib/icinga/pluginutility.hpp b/lib/icinga/pluginutility.hpp
new file mode 100644
index 0000000..3f6a844
--- /dev/null
+++ b/lib/icinga/pluginutility.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PLUGINUTILITY_H
+#define PLUGINUTILITY_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+struct ProcessResult;
+
+/**
+ * Utility functions for plugin-based checks.
+ *
+ * @ingroup icinga
+ */
+class PluginUtility
+{
+public:
+ static void ExecuteCommand(const Command::Ptr& commandObj, const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MacroProcessor::ResolverList& macroResolvers,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int timeout,
+ const std::function<void(const Value& commandLine, const ProcessResult&)>& callback = std::function<void(const Value& commandLine, const ProcessResult&)>());
+
+ static ServiceState ExitStatusToState(int exitStatus);
+ static std::pair<String, String> ParseCheckOutput(const String& output);
+
+ static Array::Ptr SplitPerfdata(const String& perfdata);
+ static String FormatPerfdata(const Array::Ptr& perfdata, bool normalize = false);
+
+private:
+ PluginUtility();
+};
+
+}
+
+#endif /* PLUGINUTILITY_H */
diff --git a/lib/icinga/scheduleddowntime-apply.cpp b/lib/icinga/scheduleddowntime-apply.cpp
new file mode 100644
index 0000000..4f8aa47
--- /dev/null
+++ b/lib/icinga/scheduleddowntime-apply.cpp
@@ -0,0 +1,159 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/scheduleddowntime.hpp"
+#include "icinga/service.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE([]() {
+ ApplyRule::RegisterType("ScheduledDowntime", { "Host", "Service" });
+});
+
+bool ScheduledDowntime::EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter)
+{
+ if (!skipFilter && !rule.EvaluateFilter(frame))
+ return false;
+
+ auto& di (rule.GetDebugInfo());
+
+#ifdef _DEBUG
+ Log(LogDebug, "ScheduledDowntime")
+ << "Applying scheduled downtime '" << rule.GetName() << "' to object '" << checkable->GetName() << "' for rule " << di;
+#endif /* _DEBUG */
+
+ ConfigItemBuilder builder{di};
+ builder.SetType(ScheduledDowntime::TypeInstance);
+ builder.SetName(name);
+ builder.SetScope(frame.Locals->ShallowClone());
+ builder.SetIgnoreOnError(rule.GetIgnoreOnError());
+
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+
+ if (service)
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "service_name"), OpSetLiteral, MakeLiteral(service->GetShortName()), di));
+
+ String zone = checkable->GetZoneName();
+
+ if (!zone.IsEmpty())
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "zone"), OpSetLiteral, MakeLiteral(zone), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "package"), OpSetLiteral, MakeLiteral(rule.GetPackage()), di));
+
+ builder.AddExpression(new OwnedExpression(rule.GetExpression()));
+
+ ConfigItem::Ptr downtimeItem = builder.Compile();
+ downtimeItem->Register();
+
+ return true;
+}
+
+bool ScheduledDowntime::EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter)
+{
+ auto& di (rule.GetDebugInfo());
+
+ CONTEXT("Evaluating 'apply' rule (" << di << ")");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ScriptFrame frame(true);
+ if (rule.GetScope())
+ rule.GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+ if (service)
+ frame.Locals->Set("service", service);
+
+ Value vinstances;
+
+ if (rule.GetFTerm()) {
+ try {
+ vinstances = rule.GetFTerm()->Evaluate(frame);
+ } catch (const std::exception&) {
+ /* Silently ignore errors here and assume there are no instances. */
+ return false;
+ }
+ } else {
+ vinstances = new Array({ "" });
+ }
+
+ bool match = false;
+
+ if (vinstances.IsObjectType<Array>()) {
+ if (!rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Dictionary iterator requires value to be a dictionary.", di));
+
+ Array::Ptr arr = vinstances;
+
+ ObjectLock olock(arr);
+ for (const Value& instance : arr) {
+ String name = rule.GetName();
+
+ if (!rule.GetFKVar().IsEmpty()) {
+ frame.Locals->Set(rule.GetFKVar(), instance);
+ name += instance;
+ }
+
+ if (EvaluateApplyRuleInstance(checkable, name, frame, rule, skipFilter))
+ match = true;
+ }
+ } else if (vinstances.IsObjectType<Dictionary>()) {
+ if (rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Array iterator requires value to be an array.", di));
+
+ Dictionary::Ptr dict = vinstances;
+
+ for (const String& key : dict->GetKeys()) {
+ frame.Locals->Set(rule.GetFKVar(), key);
+ frame.Locals->Set(rule.GetFVVar(), dict->Get(key));
+
+ if (EvaluateApplyRuleInstance(checkable, rule.GetName() + key, frame, rule, skipFilter))
+ match = true;
+ }
+ }
+
+ return match;
+}
+
+void ScheduledDowntime::EvaluateApplyRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating 'apply' rules for host '" << host->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(ScheduledDowntime::TypeInstance, Host::TypeInstance)) {
+ if (EvaluateApplyRule(host, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedHostRules(ScheduledDowntime::TypeInstance, host->GetName())) {
+ if (EvaluateApplyRule(host, *rule, true))
+ rule->AddMatch();
+ }
+}
+
+void ScheduledDowntime::EvaluateApplyRules(const Service::Ptr& service)
+{
+ CONTEXT("Evaluating 'apply' rules for service '" << service->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(ScheduledDowntime::TypeInstance, Service::TypeInstance)) {
+ if (EvaluateApplyRule(service, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedServiceRules(ScheduledDowntime::TypeInstance, service->GetHost()->GetName(), service->GetShortName())) {
+ if (EvaluateApplyRule(service, *rule, true))
+ rule->AddMatch();
+ }
+}
diff --git a/lib/icinga/scheduleddowntime.cpp b/lib/icinga/scheduleddowntime.cpp
new file mode 100644
index 0000000..f23d3e4
--- /dev/null
+++ b/lib/icinga/scheduleddowntime.cpp
@@ -0,0 +1,393 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/scheduleddowntime.hpp"
+#include "icinga/scheduleddowntime-ti.cpp"
+#include "icinga/legacytimeperiod.hpp"
+#include "icinga/downtime.hpp"
+#include "icinga/service.hpp"
+#include "base/timer.hpp"
+#include "base/tlsutility.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/objectlock.hpp"
+#include "base/object-packer.hpp"
+#include "base/serializer.hpp"
+#include "base/convert.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include <boost/thread/once.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_TYPE(ScheduledDowntime);
+
+static Timer::Ptr l_Timer;
+
+String ScheduledDowntimeNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ ScheduledDowntime::Ptr downtime = dynamic_pointer_cast<ScheduledDowntime>(context);
+
+ if (!downtime)
+ return "";
+
+ String name = downtime->GetHostName();
+
+ if (!downtime->GetServiceName().IsEmpty())
+ name += "!" + downtime->GetServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr ScheduledDowntimeNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid ScheduledDowntime name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void ScheduledDowntime::OnAllConfigLoaded()
+{
+ ObjectImpl<ScheduledDowntime>::OnAllConfigLoaded();
+
+ if (!GetCheckable())
+ BOOST_THROW_EXCEPTION(ScriptError("ScheduledDowntime '" + GetName() + "' references a host/service which doesn't exist.", GetDebugInfo()));
+
+ m_AllConfigLoaded.store(true);
+}
+
+void ScheduledDowntime::Start(bool runtimeCreated)
+{
+ ObjectImpl<ScheduledDowntime>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, [this]() {
+ l_Timer = Timer::Create();
+ l_Timer->SetInterval(60);
+ l_Timer->OnTimerExpired.connect([](const Timer * const&) { TimerProc(); });
+ l_Timer->Start();
+ });
+
+ if (!IsPaused())
+ Utility::QueueAsyncCallback([this]() { CreateNextDowntime(); });
+}
+
+void ScheduledDowntime::TimerProc()
+{
+ for (const ScheduledDowntime::Ptr& sd : ConfigType::GetObjectsByType<ScheduledDowntime>()) {
+ if (sd->IsActive() && !sd->IsPaused()) {
+ try {
+ sd->CreateNextDowntime();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ScheduledDowntime")
+ << "Exception occurred during creation of next downtime for scheduled downtime '"
+ << sd->GetName() << "': " << DiagnosticInformation(ex, false);
+ continue;
+ }
+
+ try {
+ sd->RemoveObsoleteDowntimes();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ScheduledDowntime")
+ << "Exception occurred during removal of obsolete downtime for scheduled downtime '"
+ << sd->GetName() << "': " << DiagnosticInformation(ex, false);
+ }
+ }
+ }
+}
+
+Checkable::Ptr ScheduledDowntime::GetCheckable() const
+{
+ Host::Ptr host = Host::GetByName(GetHostName());
+
+ if (GetServiceName().IsEmpty())
+ return host;
+ else
+ return host->GetServiceByShortName(GetServiceName());
+}
+
+std::pair<double, double> ScheduledDowntime::FindRunningSegment(double minEnd)
+{
+ time_t refts = Utility::GetTime();
+ tm reference = Utility::LocalTime(refts);
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Finding running scheduled downtime segment for time " << refts
+ << " (minEnd " << (minEnd > 0 ? Utility::FormatDateTime("%c", minEnd) : "-") << ")";
+
+ Dictionary::Ptr ranges = GetRanges();
+
+ if (!ranges)
+ return std::make_pair(0, 0);
+
+ Array::Ptr segments = new Array();
+
+ Dictionary::Ptr bestSegment;
+ double bestBegin = 0.0, bestEnd = 0.0;
+ double now = Utility::GetTime();
+
+ ObjectLock olock(ranges);
+
+ /* Find the longest lasting (and longer than minEnd, if given) segment that's already running */
+ for (const Dictionary::Pair& kv : ranges) {
+ Log(LogDebug, "ScheduledDowntime")
+ << "Evaluating (running?) segment: " << kv.first << ": " << kv.second;
+
+ Dictionary::Ptr segment = LegacyTimePeriod::FindRunningSegment(kv.first, kv.second, &reference);
+
+ if (!segment)
+ continue;
+
+ double begin = segment->Get("begin");
+ double end = segment->Get("end");
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Considering (running?) segment: " << Utility::FormatDateTime("%c", begin) << " -> " << Utility::FormatDateTime("%c", end);
+
+ if (begin >= now || end < now) {
+ Log(LogDebug, "ScheduledDowntime") << "not running.";
+ continue;
+ }
+ if (minEnd && end <= minEnd) {
+ Log(LogDebug, "ScheduledDowntime") << "ending too early.";
+ continue;
+ }
+
+ if (!bestSegment || end > bestEnd) {
+ Log(LogDebug, "ScheduledDowntime") << "(best match yet)";
+ bestSegment = segment;
+ bestBegin = begin;
+ bestEnd = end;
+ }
+ }
+
+ if (bestSegment)
+ return std::make_pair(bestBegin, bestEnd);
+
+ return std::make_pair(0, 0);
+}
+
+std::pair<double, double> ScheduledDowntime::FindNextSegment()
+{
+ time_t refts = Utility::GetTime();
+ tm reference = Utility::LocalTime(refts);
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Finding next scheduled downtime segment for time " << refts;
+
+ Dictionary::Ptr ranges = GetRanges();
+
+ if (!ranges)
+ return std::make_pair(0, 0);
+
+ Array::Ptr segments = new Array();
+
+ Dictionary::Ptr bestSegment;
+ double bestBegin = 0.0, bestEnd = 0.0;
+ double now = Utility::GetTime();
+
+ ObjectLock olock(ranges);
+
+ /* Find the segment starting earliest */
+ for (const Dictionary::Pair& kv : ranges) {
+ Log(LogDebug, "ScheduledDowntime")
+ << "Evaluating segment: " << kv.first << ": " << kv.second;
+
+ Dictionary::Ptr segment = LegacyTimePeriod::FindNextSegment(kv.first, kv.second, &reference);
+
+ if (!segment)
+ continue;
+
+ double begin = segment->Get("begin");
+ double end = segment->Get("end");
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Considering segment: " << Utility::FormatDateTime("%c", begin) << " -> " << Utility::FormatDateTime("%c", end);
+
+ if (begin < now) {
+ Log(LogDebug, "ScheduledDowntime") << "already running.";
+ continue;
+ }
+
+ if (!bestSegment || begin < bestBegin) {
+ Log(LogDebug, "ScheduledDowntime") << "(best match yet)";
+ bestSegment = segment;
+ bestBegin = begin;
+ bestEnd = end;
+ }
+ }
+
+ if (bestSegment)
+ return std::make_pair(bestBegin, bestEnd);
+
+ return std::make_pair(0, 0);
+}
+
+void ScheduledDowntime::CreateNextDowntime()
+{
+ /* HA enabled zones. */
+ if (IsActive() && IsPaused()) {
+ Log(LogNotice, "Checkable")
+ << "Skipping downtime creation for HA-paused Scheduled Downtime object '" << GetName() << "'";
+ return;
+ }
+
+ double minEnd = 0;
+ auto downtimeOptionsHash (HashDowntimeOptions());
+
+ for (const Downtime::Ptr& downtime : GetCheckable()->GetDowntimes()) {
+ if (downtime->GetScheduledBy() != GetName())
+ continue;
+
+ auto configOwnerHash (downtime->GetConfigOwnerHash());
+ if (!configOwnerHash.IsEmpty() && configOwnerHash != downtimeOptionsHash)
+ continue;
+
+ double end = downtime->GetEndTime();
+ if (end > minEnd)
+ minEnd = end;
+
+ if (downtime->GetStartTime() < Utility::GetTime())
+ continue;
+
+ /* We've found a downtime that is owned by us and that hasn't started yet - we're done. */
+ return;
+ }
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Creating new Downtime for ScheduledDowntime \"" << GetName() << "\"";
+
+ std::pair<double, double> segment = FindRunningSegment(minEnd);
+ if (segment.first == 0 && segment.second == 0) {
+ segment = FindNextSegment();
+ if (segment.first == 0 && segment.second == 0)
+ return;
+ }
+
+ Downtime::Ptr downtime = Downtime::AddDowntime(GetCheckable(), GetAuthor(), GetComment(),
+ segment.first, segment.second,
+ GetFixed(), String(), GetDuration(), GetName(), GetName());
+ String downtimeName = downtime->GetName();
+
+ int childOptions = Downtime::ChildOptionsFromValue(GetChildOptions());
+ if (childOptions > 0) {
+ /* 'DowntimeTriggeredChildren' schedules child downtimes triggered by the parent downtime.
+ * 'DowntimeNonTriggeredChildren' schedules non-triggered downtimes for all children.
+ */
+ String triggerName;
+ if (childOptions == 1)
+ triggerName = downtimeName;
+
+ Log(LogNotice, "ScheduledDowntime")
+ << "Processing child options " << childOptions << " for downtime " << downtimeName;
+
+ for (const Checkable::Ptr& child : GetCheckable()->GetAllChildren()) {
+ Log(LogNotice, "ScheduledDowntime")
+ << "Scheduling downtime for child object " << child->GetName();
+
+ Downtime::Ptr childDowntime = Downtime::AddDowntime(child, GetAuthor(), GetComment(),
+ segment.first, segment.second, GetFixed(), triggerName, GetDuration(), GetName(), GetName());
+
+ Log(LogNotice, "ScheduledDowntime")
+ << "Add child downtime '" << childDowntime->GetName() << "'.";
+ }
+ }
+}
+
+void ScheduledDowntime::RemoveObsoleteDowntimes()
+{
+ auto name (GetName());
+ auto downtimeOptionsHash (HashDowntimeOptions());
+
+ // Just to be sure start and removal don't happen at the same time
+ auto threshold (Utility::GetTime() + 5 * 60);
+
+ for (const Downtime::Ptr& downtime : GetCheckable()->GetDowntimes()) {
+ if (downtime->GetScheduledBy() == name && downtime->GetStartTime() > threshold) {
+ auto configOwnerHash (downtime->GetConfigOwnerHash());
+
+ if (!configOwnerHash.IsEmpty() && configOwnerHash != downtimeOptionsHash)
+ Downtime::RemoveDowntime(downtime->GetName(), false, true);
+ }
+ }
+}
+
+void ScheduledDowntime::ValidateRanges(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<ScheduledDowntime>::ValidateRanges(lvalue, utils);
+
+ if (!lvalue())
+ return;
+
+ /* create a fake time environment to validate the definitions */
+ time_t refts = Utility::GetTime();
+ tm reference = Utility::LocalTime(refts);
+ Array::Ptr segments = new Array();
+
+ ObjectLock olock(lvalue());
+ for (const Dictionary::Pair& kv : lvalue()) {
+ try {
+ tm begin_tm, end_tm;
+ int stride;
+ LegacyTimePeriod::ParseTimeRange(kv.first, &begin_tm, &end_tm, &stride, &reference);
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "ranges" }, "Invalid time specification '" + kv.first + "': " + ex.what()));
+ }
+
+ try {
+ LegacyTimePeriod::ProcessTimeRanges(kv.second, &reference, segments);
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "ranges" }, "Invalid time range definition '" + kv.second + "': " + ex.what()));
+ }
+ }
+}
+
+void ScheduledDowntime::ValidateChildOptions(const Lazy<Value>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<ScheduledDowntime>::ValidateChildOptions(lvalue, utils);
+
+ try {
+ Downtime::ChildOptionsFromValue(lvalue());
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "child_options" }, "Invalid child_options specified"));
+ }
+}
+
+static const std::set<String> l_SDDowntimeOptions ({
+ "author", "child_options", "comment", "duration", "fixed", "ranges", "vars"
+});
+
+String ScheduledDowntime::HashDowntimeOptions()
+{
+ Dictionary::Ptr allOpts = Serialize(this, FAConfig);
+ Dictionary::Ptr opts = new Dictionary();
+
+ for (auto& opt : l_SDDowntimeOptions) {
+ opts->Set(opt, allOpts->Get(opt));
+ }
+
+ return SHA256(PackObject(opts));
+}
+
+bool ScheduledDowntime::AllConfigIsLoaded()
+{
+ return m_AllConfigLoaded.load();
+}
+
+std::atomic<bool> ScheduledDowntime::m_AllConfigLoaded (false);
diff --git a/lib/icinga/scheduleddowntime.hpp b/lib/icinga/scheduleddowntime.hpp
new file mode 100644
index 0000000..e701236
--- /dev/null
+++ b/lib/icinga/scheduleddowntime.hpp
@@ -0,0 +1,60 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SCHEDULEDDOWNTIME_H
+#define SCHEDULEDDOWNTIME_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/scheduleddowntime-ti.hpp"
+#include "icinga/checkable.hpp"
+#include <atomic>
+
+namespace icinga
+{
+
+class ApplyRule;
+struct ScriptFrame;
+class Host;
+class Service;
+
+/**
+ * An Icinga scheduled downtime specification.
+ *
+ * @ingroup icinga
+ */
+class ScheduledDowntime final : public ObjectImpl<ScheduledDowntime>
+{
+public:
+ DECLARE_OBJECT(ScheduledDowntime);
+ DECLARE_OBJECTNAME(ScheduledDowntime);
+
+ Checkable::Ptr GetCheckable() const;
+
+ static void EvaluateApplyRules(const intrusive_ptr<Host>& host);
+ static void EvaluateApplyRules(const intrusive_ptr<Service>& service);
+ static bool AllConfigIsLoaded();
+
+ void ValidateRanges(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateChildOptions(const Lazy<Value>& lvalue, const ValidationUtils& utils) override;
+ String HashDowntimeOptions();
+
+protected:
+ void OnAllConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+
+private:
+ static void TimerProc();
+
+ std::pair<double, double> FindRunningSegment(double minEnd = 0);
+ std::pair<double, double> FindNextSegment();
+ void CreateNextDowntime();
+ void RemoveObsoleteDowntimes();
+
+ static std::atomic<bool> m_AllConfigLoaded;
+
+ static bool EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter);
+ static bool EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter = false);
+};
+
+}
+
+#endif /* SCHEDULEDDOWNTIME_H */
diff --git a/lib/icinga/scheduleddowntime.ti b/lib/icinga/scheduleddowntime.ti
new file mode 100644
index 0000000..1653f27
--- /dev/null
+++ b/lib/icinga/scheduleddowntime.ti
@@ -0,0 +1,76 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class ScheduledDowntimeNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class ScheduledDowntime : CustomVarObject < ScheduledDowntimeNameComposer
+{
+ // Scheduled Downtimes have a dependency on Downtimes. This is to make sure ScheduledDowntimes are activated after
+ // the Downtimes (and other checkables)
+ activation_priority 20;
+
+ load_after Host;
+ load_after Service;
+
+ [config, protected, no_user_modify, required, navigation(host)] name(Host) host_name {
+ navigate {{{
+ return Host::GetByName(GetHostName());
+ }}}
+ };
+ [config, protected, no_user_modify, navigation(service)] String service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+ return host->GetServiceByShortName(GetServiceName());
+ }}}
+ };
+
+ [config, required] String author;
+ [config, required] String comment;
+
+ [config] double duration;
+ [config] bool fixed {
+ default {{{ return true; }}}
+ };
+
+ [config] Value child_options {
+ default {{{ return "DowntimeNoChildren"; }}}
+ };
+
+ [config, required] Dictionary::Ptr ranges;
+};
+
+validator ScheduledDowntime {
+ Dictionary ranges {
+ String "*";
+ };
+};
+
+}
diff --git a/lib/icinga/service-apply.cpp b/lib/icinga/service-apply.cpp
new file mode 100644
index 0000000..4419e0b
--- /dev/null
+++ b/lib/icinga/service-apply.cpp
@@ -0,0 +1,133 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE([]() {
+ ApplyRule::RegisterType("Service", { "Host" });
+});
+
+bool Service::EvaluateApplyRuleInstance(const Host::Ptr& host, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter)
+{
+ if (!skipFilter && !rule.EvaluateFilter(frame))
+ return false;
+
+ auto& di (rule.GetDebugInfo());
+
+#ifdef _DEBUG
+ Log(LogDebug, "Service")
+ << "Applying service '" << name << "' to host '" << host->GetName() << "' for rule " << di;
+#endif /* _DEBUG */
+
+ ConfigItemBuilder builder{di};
+ builder.SetType(Service::TypeInstance);
+ builder.SetName(name);
+ builder.SetScope(frame.Locals->ShallowClone());
+ builder.SetIgnoreOnError(rule.GetIgnoreOnError());
+
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "name"), OpSetLiteral, MakeLiteral(name), di));
+
+ String zone = host->GetZoneName();
+
+ if (!zone.IsEmpty())
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "zone"), OpSetLiteral, MakeLiteral(zone), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "package"), OpSetLiteral, MakeLiteral(rule.GetPackage()), di));
+
+ builder.AddExpression(new OwnedExpression(rule.GetExpression()));
+
+ ConfigItem::Ptr serviceItem = builder.Compile();
+ serviceItem->Register();
+
+ return true;
+}
+
+bool Service::EvaluateApplyRule(const Host::Ptr& host, const ApplyRule& rule, bool skipFilter)
+{
+ auto& di (rule.GetDebugInfo());
+
+ CONTEXT("Evaluating 'apply' rule (" << di << ")");
+
+ ScriptFrame frame(true);
+ if (rule.GetScope())
+ rule.GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+
+ Value vinstances;
+
+ if (rule.GetFTerm()) {
+ try {
+ vinstances = rule.GetFTerm()->Evaluate(frame);
+ } catch (const std::exception&) {
+ /* Silently ignore errors here and assume there are no instances. */
+ return false;
+ }
+ } else {
+ vinstances = new Array({ "" });
+ }
+
+ bool match = false;
+
+ if (vinstances.IsObjectType<Array>()) {
+ if (!rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Dictionary iterator requires value to be a dictionary.", di));
+
+ Array::Ptr arr = vinstances;
+
+ ObjectLock olock(arr);
+ for (const Value& instance : arr) {
+ String name = rule.GetName();
+
+ if (!rule.GetFKVar().IsEmpty()) {
+ frame.Locals->Set(rule.GetFKVar(), instance);
+ name += instance;
+ }
+
+ if (EvaluateApplyRuleInstance(host, name, frame, rule, skipFilter))
+ match = true;
+ }
+ } else if (vinstances.IsObjectType<Dictionary>()) {
+ if (rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Array iterator requires value to be an array.", di));
+
+ Dictionary::Ptr dict = vinstances;
+
+ for (const String& key : dict->GetKeys()) {
+ frame.Locals->Set(rule.GetFKVar(), key);
+ frame.Locals->Set(rule.GetFVVar(), dict->Get(key));
+
+ if (EvaluateApplyRuleInstance(host, rule.GetName() + key, frame, rule, skipFilter))
+ match = true;
+ }
+ }
+
+ return match;
+}
+
+void Service::EvaluateApplyRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating 'apply' rules for host '" << host->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Service::TypeInstance, Host::TypeInstance)) {
+ if (EvaluateApplyRule(host, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedHostRules(Service::TypeInstance, host->GetName())) {
+ if (EvaluateApplyRule(host, *rule, true))
+ rule->AddMatch();
+ }
+}
diff --git a/lib/icinga/service.cpp b/lib/icinga/service.cpp
new file mode 100644
index 0000000..d831136
--- /dev/null
+++ b/lib/icinga/service.cpp
@@ -0,0 +1,287 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "icinga/service-ti.cpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/scheduleddowntime.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Service);
+
+boost::signals2::signal<void (const Service::Ptr&, const CheckResult::Ptr&, const MessageOrigin::Ptr&)> Service::OnHostProblemChanged;
+
+String ServiceNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Service::Ptr service = dynamic_pointer_cast<Service>(context);
+
+ if (!service)
+ return "";
+
+ return service->GetHostName() + "!" + shortName;
+}
+
+Dictionary::Ptr ServiceNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Service name."));
+
+ return new Dictionary({
+ { "host_name", tokens[0] },
+ { "name", tokens[1] }
+ });
+}
+
+void Service::OnAllConfigLoaded()
+{
+ ObjectImpl<Service>::OnAllConfigLoaded();
+
+ String zoneName = GetZoneName();
+
+ if (!zoneName.IsEmpty()) {
+ Zone::Ptr zone = Zone::GetByName(zoneName);
+
+ if (zone && zone->IsGlobal())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Service '" + GetName() + "' cannot be put into global zone '" + zone->GetName() + "'."));
+ }
+
+ m_Host = Host::GetByName(GetHostName());
+
+ if (m_Host)
+ m_Host->AddService(this);
+
+ ServiceGroup::EvaluateObjectRules(this);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(name);
+
+ if (sg)
+ sg->ResolveGroupMembership(this, true);
+ }
+ }
+}
+
+void Service::CreateChildObjects(const Type::Ptr& childType)
+{
+ if (childType == ScheduledDowntime::TypeInstance)
+ ScheduledDowntime::EvaluateApplyRules(this);
+
+ if (childType == Notification::TypeInstance)
+ Notification::EvaluateApplyRules(this);
+
+ if (childType == Dependency::TypeInstance)
+ Dependency::EvaluateApplyRules(this);
+}
+
+Service::Ptr Service::GetByNamePair(const String& hostName, const String& serviceName)
+{
+ if (!hostName.IsEmpty()) {
+ Host::Ptr host = Host::GetByName(hostName);
+
+ if (!host)
+ return nullptr;
+
+ return host->GetServiceByShortName(serviceName);
+ } else {
+ return Service::GetByName(serviceName);
+ }
+}
+
+Host::Ptr Service::GetHost() const
+{
+ return m_Host;
+}
+
+/* keep in sync with Host::GetSeverity()
+ * One could think it may be smart to use an enum and some bitmask math here.
+ * But the only thing the consuming icingaweb2 cares about is being able to
+ * sort by severity. It is therefore easier to keep them seperated here. */
+int Service::GetSeverity() const
+{
+ int severity;
+
+ ObjectLock olock(this);
+ ServiceState state = GetStateRaw();
+
+ if (!HasBeenChecked()) {
+ severity = 16;
+ } else if (state == ServiceOK) {
+ severity = 0;
+ } else {
+ switch (state) {
+ case ServiceWarning:
+ severity = 32;
+ break;
+ case ServiceUnknown:
+ severity = 64;
+ break;
+ case ServiceCritical:
+ severity = 128;
+ break;
+ default:
+ severity = 256;
+ }
+
+ Host::Ptr host = GetHost();
+ ObjectLock hlock (host);
+ if (host->GetState() != HostUp) {
+ severity += 1024;
+ } else {
+ if (IsAcknowledged())
+ severity += 512;
+ else if (IsInDowntime())
+ severity += 256;
+ else
+ severity += 2048;
+ }
+ hlock.Unlock();
+ }
+
+ olock.Unlock();
+
+ return severity;
+}
+
+bool Service::GetHandled() const
+{
+ return Checkable::GetHandled() || (m_Host && m_Host->GetProblem());
+}
+
+bool Service::IsStateOK(ServiceState state) const
+{
+ return state == ServiceOK;
+}
+
+void Service::SaveLastState(ServiceState state, double timestamp)
+{
+ if (state == ServiceOK)
+ SetLastStateOK(timestamp);
+ else if (state == ServiceWarning)
+ SetLastStateWarning(timestamp);
+ else if (state == ServiceCritical)
+ SetLastStateCritical(timestamp);
+ else if (state == ServiceUnknown)
+ SetLastStateUnknown(timestamp);
+}
+
+ServiceState Service::StateFromString(const String& state)
+{
+ if (state == "OK")
+ return ServiceOK;
+ else if (state == "WARNING")
+ return ServiceWarning;
+ else if (state == "CRITICAL")
+ return ServiceCritical;
+ else
+ return ServiceUnknown;
+}
+
+String Service::StateToString(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ return "OK";
+ case ServiceWarning:
+ return "WARNING";
+ case ServiceCritical:
+ return "CRITICAL";
+ case ServiceUnknown:
+ default:
+ return "UNKNOWN";
+ }
+}
+
+StateType Service::StateTypeFromString(const String& type)
+{
+ if (type == "SOFT")
+ return StateTypeSoft;
+ else
+ return StateTypeHard;
+}
+
+String Service::StateTypeToString(StateType type)
+{
+ if (type == StateTypeSoft)
+ return "SOFT";
+ else
+ return "HARD";
+}
+
+bool Service::ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const
+{
+ if (macro == "state") {
+ *result = StateToString(GetState());
+ return true;
+ } else if (macro == "state_id") {
+ *result = GetState();
+ return true;
+ } else if (macro == "state_type") {
+ *result = StateTypeToString(GetStateType());
+ return true;
+ } else if (macro == "last_state") {
+ *result = StateToString(GetLastState());
+ return true;
+ } else if (macro == "last_state_id") {
+ *result = GetLastState();
+ return true;
+ } else if (macro == "last_state_type") {
+ *result = StateTypeToString(GetLastStateType());
+ return true;
+ } else if (macro == "last_state_change") {
+ *result = static_cast<long>(GetLastStateChange());
+ return true;
+ } else if (macro == "downtime_depth") {
+ *result = GetDowntimeDepth();
+ return true;
+ } else if (macro == "duration_sec") {
+ *result = Utility::GetTime() - GetLastStateChange();
+ return true;
+ }
+
+ if (cr) {
+ if (macro == "latency") {
+ *result = cr->CalculateLatency();
+ return true;
+ } else if (macro == "execution_time") {
+ *result = cr->CalculateExecutionTime();
+ return true;
+ } else if (macro == "output") {
+ *result = cr->GetOutput();
+ return true;
+ } else if (macro == "perfdata") {
+ *result = PluginUtility::FormatPerfdata(cr->GetPerformanceData());
+ return true;
+ } else if (macro == "check_source") {
+ *result = cr->GetCheckSource();
+ return true;
+ } else if (macro == "scheduling_source") {
+ *result = cr->GetSchedulingSource();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+std::pair<Host::Ptr, Service::Ptr> icinga::GetHostService(const Checkable::Ptr& checkable)
+{
+ Service::Ptr service = dynamic_pointer_cast<Service>(checkable);
+
+ if (service)
+ return std::make_pair(service->GetHost(), service);
+ else
+ return std::make_pair(static_pointer_cast<Host>(checkable), nullptr);
+}
diff --git a/lib/icinga/service.hpp b/lib/icinga/service.hpp
new file mode 100644
index 0000000..ac27c3d
--- /dev/null
+++ b/lib/icinga/service.hpp
@@ -0,0 +1,65 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERVICE_H
+#define SERVICE_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/service-ti.hpp"
+#include "icinga/macroresolver.hpp"
+#include "icinga/host.hpp"
+#include <tuple>
+
+using std::tie;
+
+namespace icinga
+{
+
+/**
+ * An Icinga service.
+ *
+ * @ingroup icinga
+ */
+class Service final : public ObjectImpl<Service>, public MacroResolver
+{
+public:
+ DECLARE_OBJECT(Service);
+ DECLARE_OBJECTNAME(Service);
+
+ static Service::Ptr GetByNamePair(const String& hostName, const String& serviceName);
+
+ Host::Ptr GetHost() const override;
+ int GetSeverity() const override;
+ bool GetHandled() const override;
+
+ bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const override;
+
+ bool IsStateOK(ServiceState state) const override;
+ void SaveLastState(ServiceState state, double timestamp) override;
+
+ static ServiceState StateFromString(const String& state);
+ static String StateToString(ServiceState state);
+
+ static StateType StateTypeFromString(const String& state);
+ static String StateTypeToString(StateType state);
+
+ static void EvaluateApplyRules(const Host::Ptr& host);
+
+ void OnAllConfigLoaded() override;
+
+ static boost::signals2::signal<void (const Service::Ptr&, const CheckResult::Ptr&, const MessageOrigin::Ptr&)> OnHostProblemChanged;
+
+protected:
+ void CreateChildObjects(const Type::Ptr& childType) override;
+
+private:
+ Host::Ptr m_Host;
+
+ static bool EvaluateApplyRuleInstance(const Host::Ptr& host, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter);
+ static bool EvaluateApplyRule(const Host::Ptr& host, const ApplyRule& rule, bool skipFilter = false);
+};
+
+std::pair<Host::Ptr, Service::Ptr> GetHostService(const Checkable::Ptr& checkable);
+
+}
+
+#endif /* SERVICE_H */
diff --git a/lib/icinga/service.ti b/lib/icinga/service.ti
new file mode 100644
index 0000000..12c2d8c
--- /dev/null
+++ b/lib/icinga/service.ti
@@ -0,0 +1,71 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/customvarobject.hpp"
+#impl_include "icinga/servicegroup.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class ServiceNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Service : Checkable < ServiceNameComposer
+{
+ load_after ApiListener;
+ load_after Endpoint;
+ load_after Host;
+ load_after Zone;
+
+ [config, no_user_modify, required, signal_with_old_value] array(name(ServiceGroup)) groups {
+ default {{{ return new Array(); }}}
+ };
+
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetShortName();
+ else
+ return displayName;
+ }}}
+ };
+ [config, no_user_modify, required] name(Host) host_name;
+ [no_storage, navigation] Host::Ptr host {
+ get;
+ navigate {{{
+ return GetHost();
+ }}}
+ };
+ [enum, no_storage] ServiceState "state" {
+ get {{{
+ return GetStateRaw();
+ }}}
+ };
+ [enum, no_storage] ServiceState last_state {
+ get {{{
+ return GetLastStateRaw();
+ }}}
+ };
+ [enum, no_storage] ServiceState last_hard_state {
+ get {{{
+ return GetLastHardStateRaw();
+ }}}
+ };
+ [state] Timestamp last_state_ok (LastStateOK);
+ [state] Timestamp last_state_warning;
+ [state] Timestamp last_state_critical;
+ [state] Timestamp last_state_unknown;
+};
+
+}
diff --git a/lib/icinga/servicegroup.cpp b/lib/icinga/servicegroup.cpp
new file mode 100644
index 0000000..d21f852
--- /dev/null
+++ b/lib/icinga/servicegroup.cpp
@@ -0,0 +1,111 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/servicegroup.hpp"
+#include "icinga/servicegroup-ti.cpp"
+#include "config/objectrule.hpp"
+#include "config/configitem.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(ServiceGroup);
+
+INITIALIZE_ONCE([]() {
+ ObjectRule::RegisterType("ServiceGroup");
+});
+
+bool ServiceGroup::EvaluateObjectRule(const Service::Ptr& service, const ConfigItem::Ptr& group)
+{
+ String groupName = group->GetName();
+
+ CONTEXT("Evaluating rule for group '" << groupName << "'");
+
+ Host::Ptr host = service->GetHost();
+
+ ScriptFrame frame(true);
+ if (group->GetScope())
+ group->GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+ frame.Locals->Set("service", service);
+
+ if (!group->GetFilter()->Evaluate(frame).GetValue().ToBool())
+ return false;
+
+ Log(LogDebug, "ServiceGroup")
+ << "Assigning membership for group '" << groupName << "' to service '" << service->GetName() << "'";
+
+ Array::Ptr groups = service->GetGroups();
+
+ if (groups && !groups->Contains(groupName))
+ groups->Add(groupName);
+
+ return true;
+}
+
+void ServiceGroup::EvaluateObjectRules(const Service::Ptr& service)
+{
+ CONTEXT("Evaluating group membership for service '" << service->GetName() << "'");
+
+ for (const ConfigItem::Ptr& group : ConfigItem::GetItems(ServiceGroup::TypeInstance))
+ {
+ if (!group->GetFilter())
+ continue;
+
+ EvaluateObjectRule(service, group);
+ }
+}
+
+std::set<Service::Ptr> ServiceGroup::GetMembers() const
+{
+ std::unique_lock<std::mutex> lock(m_ServiceGroupMutex);
+ return m_Members;
+}
+
+void ServiceGroup::AddMember(const Service::Ptr& service)
+{
+ service->AddGroup(GetName());
+
+ std::unique_lock<std::mutex> lock(m_ServiceGroupMutex);
+ m_Members.insert(service);
+}
+
+void ServiceGroup::RemoveMember(const Service::Ptr& service)
+{
+ std::unique_lock<std::mutex> lock(m_ServiceGroupMutex);
+ m_Members.erase(service);
+}
+
+bool ServiceGroup::ResolveGroupMembership(const Service::Ptr& service, bool add, int rstack) {
+
+ if (add && rstack > 20) {
+ Log(LogWarning, "ServiceGroup")
+ << "Too many nested groups for group '" << GetName() << "': Service '"
+ << service->GetName() << "' membership assignment failed.";
+
+ return false;
+ }
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups && groups->GetLength() > 0) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ ServiceGroup::Ptr group = ServiceGroup::GetByName(name);
+
+ if (group && !group->ResolveGroupMembership(service, add, rstack + 1))
+ return false;
+ }
+ }
+
+ if (add)
+ AddMember(service);
+ else
+ RemoveMember(service);
+
+ return true;
+}
diff --git a/lib/icinga/servicegroup.hpp b/lib/icinga/servicegroup.hpp
new file mode 100644
index 0000000..f2d0ab7
--- /dev/null
+++ b/lib/icinga/servicegroup.hpp
@@ -0,0 +1,43 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERVICEGROUP_H
+#define SERVICEGROUP_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/servicegroup-ti.hpp"
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+class ConfigItem;
+
+/**
+ * An Icinga service group.
+ *
+ * @ingroup icinga
+ */
+class ServiceGroup final : public ObjectImpl<ServiceGroup>
+{
+public:
+ DECLARE_OBJECT(ServiceGroup);
+ DECLARE_OBJECTNAME(ServiceGroup);
+
+ std::set<Service::Ptr> GetMembers() const;
+ void AddMember(const Service::Ptr& service);
+ void RemoveMember(const Service::Ptr& service);
+
+ bool ResolveGroupMembership(const Service::Ptr& service, bool add = true, int rstack = 0);
+
+ static void EvaluateObjectRules(const Service::Ptr& service);
+
+private:
+ mutable std::mutex m_ServiceGroupMutex;
+ std::set<Service::Ptr> m_Members;
+
+ static bool EvaluateObjectRule(const Service::Ptr& service, const intrusive_ptr<ConfigItem>& group);
+};
+
+}
+
+#endif /* SERVICEGROUP_H */
diff --git a/lib/icinga/servicegroup.ti b/lib/icinga/servicegroup.ti
new file mode 100644
index 0000000..7daf9d4
--- /dev/null
+++ b/lib/icinga/servicegroup.ti
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class ServiceGroup : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+
+ [config, no_user_modify] array(name(ServiceGroup)) groups;
+ [config] String notes;
+ [config] String notes_url;
+ [config] String action_url;
+};
+
+}
diff --git a/lib/icinga/timeperiod.cpp b/lib/icinga/timeperiod.cpp
new file mode 100644
index 0000000..db3272e
--- /dev/null
+++ b/lib/icinga/timeperiod.cpp
@@ -0,0 +1,399 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/timeperiod.hpp"
+#include "icinga/timeperiod-ti.cpp"
+#include "icinga/legacytimeperiod.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include "base/timer.hpp"
+#include "base/utility.hpp"
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE(TimePeriod);
+
+static Timer::Ptr l_UpdateTimer;
+
+void TimePeriod::Start(bool runtimeCreated)
+{
+ ObjectImpl<TimePeriod>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, [this]() {
+ l_UpdateTimer = Timer::Create();
+ l_UpdateTimer->SetInterval(300);
+ l_UpdateTimer->OnTimerExpired.connect([](const Timer * const&) { UpdateTimerHandler(); });
+ l_UpdateTimer->Start();
+ });
+
+ /* Pre-fill the time period for the next 24 hours. */
+ double now = Utility::GetTime();
+ UpdateRegion(now, now + 24 * 3600, true);
+#ifdef _DEBUG
+ Dump();
+#endif /* _DEBUG */
+}
+
+void TimePeriod::AddSegment(double begin, double end)
+{
+ ASSERT(OwnsLock());
+
+ Log(LogDebug, "TimePeriod")
+ << "Adding segment '" << Utility::FormatDateTime("%c", begin) << "' <-> '"
+ << Utility::FormatDateTime("%c", end) << "' to TimePeriod '" << GetName() << "'";
+
+ if (GetValidBegin().IsEmpty() || begin < GetValidBegin())
+ SetValidBegin(begin);
+
+ if (GetValidEnd().IsEmpty() || end > GetValidEnd())
+ SetValidEnd(end);
+
+ Array::Ptr segments = GetSegments();
+
+ if (segments) {
+ /* Try to merge the new segment into an existing segment. */
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ if (segment->Get("begin") <= begin && segment->Get("end") >= end)
+ return; /* New segment is fully contained in this segment. */
+
+ if (segment->Get("begin") >= begin && segment->Get("end") <= end) {
+ segment->Set("begin", begin);
+ segment->Set("end", end); /* Extend an existing segment to both sides */
+ return;
+ }
+
+ if (segment->Get("end") >= begin && segment->Get("end") <= end) {
+ segment->Set("end", end); /* Extend an existing segment to right. */
+ return;
+ }
+
+ if (segment->Get("begin") >= begin && segment->Get("begin") <= end) {
+ segment->Set("begin", begin); /* Extend an existing segment to left. */
+ return;
+ }
+
+ }
+ }
+
+ /* Create new segment if we weren't able to merge this into an existing segment. */
+ Dictionary::Ptr segment = new Dictionary({
+ { "begin", begin },
+ { "end", end }
+ });
+
+ if (!segments) {
+ segments = new Array();
+ SetSegments(segments);
+ }
+
+ segments->Add(segment);
+}
+
+void TimePeriod::AddSegment(const Dictionary::Ptr& segment)
+{
+ AddSegment(segment->Get("begin"), segment->Get("end"));
+}
+
+void TimePeriod::RemoveSegment(double begin, double end)
+{
+ ASSERT(OwnsLock());
+
+ Log(LogDebug, "TimePeriod")
+ << "Removing segment '" << Utility::FormatDateTime("%c", begin) << "' <-> '"
+ << Utility::FormatDateTime("%c", end) << "' from TimePeriod '" << GetName() << "'";
+
+ if (GetValidBegin().IsEmpty() || begin < GetValidBegin())
+ SetValidBegin(begin);
+
+ if (GetValidEnd().IsEmpty() || end > GetValidEnd())
+ SetValidEnd(end);
+
+ Array::Ptr segments = GetSegments();
+
+ if (!segments)
+ return;
+
+ Array::Ptr newSegments = new Array();
+
+ /* Try to split or adjust an existing segment. */
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ /* Fully contained in the specified range? */
+ if (segment->Get("begin") >= begin && segment->Get("end") <= end)
+ // Don't add the old segment, because the segment is fully contained into our range
+ continue;
+
+ /* Not overlapping at all? */
+ if (segment->Get("end") < begin || segment->Get("begin") > end) {
+ newSegments->Add(segment);
+ continue;
+ }
+
+ /* Cut between */
+ if (segment->Get("begin") < begin && segment->Get("end") > end) {
+ newSegments->Add(new Dictionary({
+ { "begin", segment->Get("begin") },
+ { "end", begin }
+ }));
+
+ newSegments->Add(new Dictionary({
+ { "begin", end },
+ { "end", segment->Get("end") }
+ }));
+ // Don't add the old segment, because we have now two new segments and a gap between
+ continue;
+ }
+
+ /* Adjust the begin/end timestamps so as to not overlap with the specified range. */
+ if (segment->Get("begin") > begin && segment->Get("begin") < end)
+ segment->Set("begin", end);
+
+ if (segment->Get("end") > begin && segment->Get("end") < end)
+ segment->Set("end", begin);
+
+ newSegments->Add(segment);
+ }
+
+ SetSegments(newSegments);
+
+#ifdef _DEBUG
+ Dump();
+#endif /* _DEBUG */
+}
+
+void TimePeriod::RemoveSegment(const Dictionary::Ptr& segment)
+{
+ RemoveSegment(segment->Get("begin"), segment->Get("end"));
+}
+
+void TimePeriod::PurgeSegments(double end)
+{
+ ASSERT(OwnsLock());
+
+ Log(LogDebug, "TimePeriod")
+ << "Purging segments older than '" << Utility::FormatDateTime("%c", end)
+ << "' from TimePeriod '" << GetName() << "'";
+
+ if (GetValidBegin().IsEmpty() || end < GetValidBegin())
+ return;
+
+ SetValidBegin(end);
+
+ Array::Ptr segments = GetSegments();
+
+ if (!segments)
+ return;
+
+ Array::Ptr newSegments = new Array();
+
+ /* Remove old segments. */
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ if (segment->Get("end") >= end)
+ newSegments->Add(segment);
+ }
+
+ SetSegments(newSegments);
+}
+
+void TimePeriod::Merge(const TimePeriod::Ptr& timeperiod, bool include)
+{
+ Log(LogDebug, "TimePeriod")
+ << "Merge TimePeriod '" << GetName() << "' with '" << timeperiod->GetName() << "' "
+ << "Method: " << (include ? "include" : "exclude");
+
+ Array::Ptr segments = timeperiod->GetSegments();
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ ObjectLock ilock(this);
+ for (const Dictionary::Ptr& segment : segments) {
+ include ? AddSegment(segment) : RemoveSegment(segment);
+ }
+ }
+}
+
+void TimePeriod::UpdateRegion(double begin, double end, bool clearExisting)
+{
+ if (clearExisting) {
+ ObjectLock olock(this);
+ SetSegments(new Array());
+ } else {
+ if (begin < GetValidEnd())
+ begin = GetValidEnd();
+
+ if (end < GetValidEnd())
+ return;
+ }
+
+ Array::Ptr segments = GetUpdate()->Invoke({ this, begin, end });
+
+ {
+ ObjectLock olock(this);
+ RemoveSegment(begin, end);
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ AddSegment(segment);
+ }
+ }
+ }
+
+ bool preferInclude = GetPreferIncludes();
+
+ /* First handle the non preferred timeranges */
+ Array::Ptr timeranges = preferInclude ? GetExcludes() : GetIncludes();
+
+ if (timeranges) {
+ ObjectLock olock(timeranges);
+ for (const String& name : timeranges) {
+ const TimePeriod::Ptr timeperiod = TimePeriod::GetByName(name);
+
+ if (timeperiod)
+ Merge(timeperiod, !preferInclude);
+ }
+ }
+
+ /* Preferred timeranges must be handled at the end */
+ timeranges = preferInclude ? GetIncludes() : GetExcludes();
+
+ if (timeranges) {
+ ObjectLock olock(timeranges);
+ for (const String& name : timeranges) {
+ const TimePeriod::Ptr timeperiod = TimePeriod::GetByName(name);
+
+ if (timeperiod)
+ Merge(timeperiod, preferInclude);
+ }
+ }
+}
+
+bool TimePeriod::GetIsInside() const
+{
+ return IsInside(Utility::GetTime());
+}
+
+bool TimePeriod::IsInside(double ts) const
+{
+ ObjectLock olock(this);
+
+ if (GetValidBegin().IsEmpty() || ts < GetValidBegin() || GetValidEnd().IsEmpty() || ts > GetValidEnd())
+ return true; /* Assume that all invalid regions are "inside". */
+
+ Array::Ptr segments = GetSegments();
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ if (ts > segment->Get("begin") && ts < segment->Get("end"))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+double TimePeriod::FindNextTransition(double begin)
+{
+ ObjectLock olock(this);
+
+ Array::Ptr segments = GetSegments();
+
+ double closestTransition = -1;
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ if (segment->Get("begin") > begin && (segment->Get("begin") < closestTransition || closestTransition == -1))
+ closestTransition = segment->Get("begin");
+
+ if (segment->Get("end") > begin && (segment->Get("end") < closestTransition || closestTransition == -1))
+ closestTransition = segment->Get("end");
+ }
+ }
+
+ return closestTransition;
+}
+
+void TimePeriod::UpdateTimerHandler()
+{
+ double now = Utility::GetTime();
+
+ for (const TimePeriod::Ptr& tp : ConfigType::GetObjectsByType<TimePeriod>()) {
+ if (!tp->IsActive())
+ continue;
+
+ double valid_end;
+
+ {
+ ObjectLock olock(tp);
+ tp->PurgeSegments(now - 3600);
+
+ valid_end = tp->GetValidEnd();
+ }
+
+ tp->UpdateRegion(valid_end, now + 24 * 3600, false);
+#ifdef _DEBUG
+ tp->Dump();
+#endif /* _DEBUG */
+ }
+}
+
+void TimePeriod::Dump()
+{
+ ObjectLock olock(this);
+
+ Array::Ptr segments = GetSegments();
+
+ Log(LogDebug, "TimePeriod")
+ << "Dumping TimePeriod '" << GetName() << "'";
+
+ Log(LogDebug, "TimePeriod")
+ << "Valid from '" << Utility::FormatDateTime("%c", GetValidBegin())
+ << "' until '" << Utility::FormatDateTime("%c", GetValidEnd());
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ Log(LogDebug, "TimePeriod")
+ << "Segment: " << Utility::FormatDateTime("%c", segment->Get("begin")) << " <-> "
+ << Utility::FormatDateTime("%c", segment->Get("end"));
+ }
+ }
+
+ Log(LogDebug, "TimePeriod", "---");
+}
+
+void TimePeriod::ValidateRanges(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ if (!lvalue())
+ return;
+
+ /* create a fake time environment to validate the definitions */
+ time_t refts = Utility::GetTime();
+ tm reference = Utility::LocalTime(refts);
+ Array::Ptr segments = new Array();
+
+ ObjectLock olock(lvalue());
+ for (const Dictionary::Pair& kv : lvalue()) {
+ try {
+ tm begin_tm, end_tm;
+ int stride;
+ LegacyTimePeriod::ParseTimeRange(kv.first, &begin_tm, &end_tm, &stride, &reference);
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "ranges" }, "Invalid time specification '" + kv.first + "': " + ex.what()));
+ }
+
+ try {
+ LegacyTimePeriod::ProcessTimeRanges(kv.second, &reference, segments);
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "ranges" }, "Invalid time range definition '" + kv.second + "': " + ex.what()));
+ }
+ }
+}
diff --git a/lib/icinga/timeperiod.hpp b/lib/icinga/timeperiod.hpp
new file mode 100644
index 0000000..a5a2f73
--- /dev/null
+++ b/lib/icinga/timeperiod.hpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TIMEPERIOD_H
+#define TIMEPERIOD_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/timeperiod-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * A time period.
+ *
+ * @ingroup icinga
+ */
+class TimePeriod final : public ObjectImpl<TimePeriod>
+{
+public:
+ DECLARE_OBJECT(TimePeriod);
+ DECLARE_OBJECTNAME(TimePeriod);
+
+ void Start(bool runtimeCreated) override;
+
+ void UpdateRegion(double begin, double end, bool clearExisting);
+
+ bool GetIsInside() const override;
+
+ bool IsInside(double ts) const;
+ double FindNextTransition(double begin);
+
+ void ValidateRanges(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ void AddSegment(double s, double end);
+ void AddSegment(const Dictionary::Ptr& segment);
+ void RemoveSegment(double begin, double end);
+ void RemoveSegment(const Dictionary::Ptr& segment);
+ void PurgeSegments(double end);
+
+ void Merge(const TimePeriod::Ptr& timeperiod, bool include = true);
+
+ void Dump();
+
+ static void UpdateTimerHandler();
+};
+
+}
+
+#endif /* TIMEPERIOD_H */
diff --git a/lib/icinga/timeperiod.ti b/lib/icinga/timeperiod.ti
new file mode 100644
index 0000000..bba272e
--- /dev/null
+++ b/lib/icinga/timeperiod.ti
@@ -0,0 +1,47 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "base/function.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class TimePeriod : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+ [config, signal_with_old_value] Dictionary::Ptr ranges;
+ [config, required] Function::Ptr update;
+ [config] bool prefer_includes {
+ default {{{ return true; }}}
+ };
+ [config, required, signal_with_old_value] array(name(TimePeriod)) excludes {
+ default {{{ return new Array(); }}}
+ };
+ [config, required, signal_with_old_value] array(name(TimePeriod)) includes {
+ default {{{ return new Array(); }}}
+ };
+ [state, no_user_modify] Value valid_begin;
+ [state, no_user_modify] Value valid_end;
+ [state, no_user_modify] Array::Ptr segments;
+ [no_storage] bool is_inside {
+ get;
+ };
+};
+
+validator TimePeriod {
+ Dictionary ranges {
+ String "*";
+ };
+};
+
+}
diff --git a/lib/icinga/user.cpp b/lib/icinga/user.cpp
new file mode 100644
index 0000000..4d99db7
--- /dev/null
+++ b/lib/icinga/user.cpp
@@ -0,0 +1,103 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/user.hpp"
+#include "icinga/user-ti.cpp"
+#include "icinga/usergroup.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/usergroup.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(User);
+
+void User::OnConfigLoaded()
+{
+ ObjectImpl<User>::OnConfigLoaded();
+
+ SetTypeFilter(FilterArrayToInt(GetTypes(), Notification::GetTypeFilterMap(), ~0));
+ SetStateFilter(FilterArrayToInt(GetStates(), Notification::GetStateFilterMap(), ~0));
+}
+
+void User::OnAllConfigLoaded()
+{
+ ObjectImpl<User>::OnAllConfigLoaded();
+
+ UserGroup::EvaluateObjectRules(this);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ UserGroup::Ptr ug = UserGroup::GetByName(name);
+
+ if (ug)
+ ug->ResolveGroupMembership(this, true);
+ }
+ }
+}
+
+void User::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<User>::Stop(runtimeRemoved);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ UserGroup::Ptr ug = UserGroup::GetByName(name);
+
+ if (ug)
+ ug->ResolveGroupMembership(this, false);
+ }
+ }
+}
+
+void User::AddGroup(const String& name)
+{
+ std::unique_lock<std::mutex> lock(m_UserMutex);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups && groups->Contains(name))
+ return;
+
+ if (!groups)
+ groups = new Array();
+
+ groups->Add(name);
+}
+
+TimePeriod::Ptr User::GetPeriod() const
+{
+ return TimePeriod::GetByName(GetPeriodRaw());
+}
+
+void User::ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<User>::ValidateStates(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), Notification::GetStateFilterMap(), 0);
+
+ if (filter == -1 || (filter & ~(StateFilterUp | StateFilterDown | StateFilterOK | StateFilterWarning | StateFilterCritical | StateFilterUnknown)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid."));
+}
+
+void User::ValidateTypes(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<User>::ValidateTypes(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), Notification::GetTypeFilterMap(), 0);
+
+ if (filter == -1 || (filter & ~(NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved |
+ NotificationCustom | NotificationAcknowledgement | NotificationProblem | NotificationRecovery |
+ NotificationFlappingStart | NotificationFlappingEnd)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "types" }, "Type filter is invalid."));
+}
diff --git a/lib/icinga/user.hpp b/lib/icinga/user.hpp
new file mode 100644
index 0000000..14e59c2
--- /dev/null
+++ b/lib/icinga/user.hpp
@@ -0,0 +1,44 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef USER_H
+#define USER_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/user-ti.hpp"
+#include "icinga/timeperiod.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+/**
+ * A User.
+ *
+ * @ingroup icinga
+ */
+class User final : public ObjectImpl<User>
+{
+public:
+ DECLARE_OBJECT(User);
+ DECLARE_OBJECTNAME(User);
+
+ void AddGroup(const String& name);
+
+ /* Notifications */
+ TimePeriod::Ptr GetPeriod() const;
+
+ void ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateTypes(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void Stop(bool runtimeRemoved) override;
+
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+private:
+ mutable std::mutex m_UserMutex;
+};
+
+}
+
+#endif /* USER_H */
diff --git a/lib/icinga/user.ti b/lib/icinga/user.ti
new file mode 100644
index 0000000..8b8c43a
--- /dev/null
+++ b/lib/icinga/user.ti
@@ -0,0 +1,47 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "base/array.hpp"
+#impl_include "icinga/usergroup.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class User : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+ [config, no_user_modify, required, signal_with_old_value] array(name(UserGroup)) groups {
+ default {{{ return new Array(); }}}
+ };
+ [config, navigation] name(TimePeriod) period (PeriodRaw) {
+ navigate {{{
+ return TimePeriod::GetByName(GetPeriodRaw());
+ }}}
+ };
+
+ [config] array(Value) types;
+ [no_user_view, no_user_modify] int type_filter_real (TypeFilter);
+ [config] array(Value) states;
+ [no_user_view, no_user_modify] int state_filter_real (StateFilter);
+
+ [config] String email;
+ [config] String pager;
+
+ [config] bool enable_notifications {
+ default {{{ return true; }}}
+ };
+
+ [state] Timestamp last_notification;
+};
+
+}
diff --git a/lib/icinga/usergroup.cpp b/lib/icinga/usergroup.cpp
new file mode 100644
index 0000000..27ae45b
--- /dev/null
+++ b/lib/icinga/usergroup.cpp
@@ -0,0 +1,128 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/usergroup.hpp"
+#include "icinga/usergroup-ti.cpp"
+#include "icinga/notification.hpp"
+#include "config/objectrule.hpp"
+#include "config/configitem.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(UserGroup);
+
+INITIALIZE_ONCE([]() {
+ ObjectRule::RegisterType("UserGroup");
+});
+
+bool UserGroup::EvaluateObjectRule(const User::Ptr& user, const ConfigItem::Ptr& group)
+{
+ String groupName = group->GetName();
+
+ CONTEXT("Evaluating rule for group '" << groupName << "'");
+
+ ScriptFrame frame(true);
+ if (group->GetScope())
+ group->GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("user", user);
+
+ if (!group->GetFilter()->Evaluate(frame).GetValue().ToBool())
+ return false;
+
+ Log(LogDebug, "UserGroup")
+ << "Assigning membership for group '" << groupName << "' to user '" << user->GetName() << "'";
+
+ Array::Ptr groups = user->GetGroups();
+
+ if (groups && !groups->Contains(groupName))
+ groups->Add(groupName);
+
+ return true;
+}
+
+void UserGroup::EvaluateObjectRules(const User::Ptr& user)
+{
+ CONTEXT("Evaluating group membership for user '" << user->GetName() << "'");
+
+ for (const ConfigItem::Ptr& group : ConfigItem::GetItems(UserGroup::TypeInstance))
+ {
+ if (!group->GetFilter())
+ continue;
+
+ EvaluateObjectRule(user, group);
+ }
+}
+
+std::set<User::Ptr> UserGroup::GetMembers() const
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ return m_Members;
+}
+
+void UserGroup::AddMember(const User::Ptr& user)
+{
+ user->AddGroup(GetName());
+
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ m_Members.insert(user);
+}
+
+void UserGroup::RemoveMember(const User::Ptr& user)
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ m_Members.erase(user);
+}
+
+std::set<Notification::Ptr> UserGroup::GetNotifications() const
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ return m_Notifications;
+}
+
+void UserGroup::AddNotification(const Notification::Ptr& notification)
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ m_Notifications.insert(notification);
+}
+
+void UserGroup::RemoveNotification(const Notification::Ptr& notification)
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ m_Notifications.erase(notification);
+}
+
+bool UserGroup::ResolveGroupMembership(const User::Ptr& user, bool add, int rstack) {
+
+ if (add && rstack > 20) {
+ Log(LogWarning, "UserGroup")
+ << "Too many nested groups for group '" << GetName() << "': User '"
+ << user->GetName() << "' membership assignment failed.";
+
+ return false;
+ }
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups && groups->GetLength() > 0) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ UserGroup::Ptr group = UserGroup::GetByName(name);
+
+ if (group && !group->ResolveGroupMembership(user, add, rstack + 1))
+ return false;
+ }
+ }
+
+ if (add)
+ AddMember(user);
+ else
+ RemoveMember(user);
+
+ return true;
+}
+
diff --git a/lib/icinga/usergroup.hpp b/lib/icinga/usergroup.hpp
new file mode 100644
index 0000000..c6f82a1
--- /dev/null
+++ b/lib/icinga/usergroup.hpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef USERGROUP_H
+#define USERGROUP_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/usergroup-ti.hpp"
+#include "icinga/user.hpp"
+
+namespace icinga
+{
+
+class ConfigItem;
+class Notification;
+
+/**
+ * An Icinga user group.
+ *
+ * @ingroup icinga
+ */
+class UserGroup final : public ObjectImpl<UserGroup>
+{
+public:
+ DECLARE_OBJECT(UserGroup);
+ DECLARE_OBJECTNAME(UserGroup);
+
+ std::set<User::Ptr> GetMembers() const;
+ void AddMember(const User::Ptr& user);
+ void RemoveMember(const User::Ptr& user);
+
+ std::set<intrusive_ptr<Notification>> GetNotifications() const;
+ void AddNotification(const intrusive_ptr<Notification>& notification);
+ void RemoveNotification(const intrusive_ptr<Notification>& notification);
+
+ bool ResolveGroupMembership(const User::Ptr& user, bool add = true, int rstack = 0);
+
+ static void EvaluateObjectRules(const User::Ptr& user);
+
+private:
+ mutable std::mutex m_UserGroupMutex;
+ std::set<User::Ptr> m_Members;
+ std::set<intrusive_ptr<Notification>> m_Notifications;
+
+ static bool EvaluateObjectRule(const User::Ptr& user, const intrusive_ptr<ConfigItem>& group);
+};
+
+}
+
+#endif /* USERGROUP_H */
diff --git a/lib/icinga/usergroup.ti b/lib/icinga/usergroup.ti
new file mode 100644
index 0000000..e955c5e
--- /dev/null
+++ b/lib/icinga/usergroup.ti
@@ -0,0 +1,25 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class UserGroup : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+
+ [config, no_user_modify] array(name(UserGroup)) groups;
+};
+
+}
diff --git a/lib/icingadb/CMakeLists.txt b/lib/icingadb/CMakeLists.txt
new file mode 100644
index 0000000..de8e4ad
--- /dev/null
+++ b/lib/icingadb/CMakeLists.txt
@@ -0,0 +1,32 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(icingadb.ti icingadb-ti.cpp icingadb-ti.hpp)
+
+mkembedconfig_target(icingadb-itl.conf icingadb-itl.cpp)
+
+set(icingadb_SOURCES
+ icingadb.cpp icingadb-objects.cpp icingadb-stats.cpp icingadb-utility.cpp redisconnection.cpp icingadb-ti.hpp
+ icingadbchecktask.cpp icingadb-itl.cpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(icingadb icingadb icingadb_SOURCES)
+endif()
+
+add_library(icingadb OBJECT ${icingadb_SOURCES})
+
+include_directories(${icinga2_SOURCE_DIR}/third-party)
+
+add_dependencies(icingadb base config icinga remote)
+
+set_target_properties (
+ icingadb PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/icingadb.conf
+ ${CMAKE_INSTALL_SYSCONFDIR}/icinga2/features-available
+)
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/icingadb/icingadb-itl.conf b/lib/icingadb/icingadb-itl.conf
new file mode 100644
index 0000000..5f3950e
--- /dev/null
+++ b/lib/icingadb/icingadb-itl.conf
@@ -0,0 +1,24 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+System.assert(Internal.run_with_activation_context(function() {
+ template CheckCommand "icingadb-check-command" use (checkFunc = Internal.IcingadbCheck) {
+ execute = checkFunc
+ }
+
+ object CheckCommand "icingadb" {
+ import "icingadb-check-command"
+
+ vars.icingadb_name = "icingadb"
+
+ vars.icingadb_full_dump_duration_warning = 5m
+ vars.icingadb_full_dump_duration_critical = 10m
+ vars.icingadb_full_sync_duration_warning = 5m
+ vars.icingadb_full_sync_duration_critical = 10m
+ vars.icingadb_redis_backlog_warning = 5m
+ vars.icingadb_redis_backlog_critical = 15m
+ vars.icingadb_database_backlog_warning = 5m
+ vars.icingadb_database_backlog_critical = 15m
+ }
+}))
+
+Internal.remove("IcingadbCheck")
diff --git a/lib/icingadb/icingadb-objects.cpp b/lib/icingadb/icingadb-objects.cpp
new file mode 100644
index 0000000..ff7a833
--- /dev/null
+++ b/lib/icingadb/icingadb-objects.cpp
@@ -0,0 +1,2966 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadb.hpp"
+#include "icingadb/redisconnection.hpp"
+#include "base/configtype.hpp"
+#include "base/configobject.hpp"
+#include "base/defer.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/shared.hpp"
+#include "base/tlsutility.hpp"
+#include "base/initialize.hpp"
+#include "base/convert.hpp"
+#include "base/array.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include "base/object-packer.hpp"
+#include "icinga/command.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/usergroup.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/pluginutility.hpp"
+#include "remote/zone.hpp"
+#include <algorithm>
+#include <chrono>
+#include <cmath>
+#include <cstdint>
+#include <iterator>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <set>
+#include <utility>
+#include <type_traits>
+
+using namespace icinga;
+
+using Prio = RedisConnection::QueryPriority;
+
+std::unordered_set<Type*> IcingaDB::m_IndexedTypes;
+
+INITIALIZE_ONCE(&IcingaDB::ConfigStaticInitialize);
+
+std::vector<Type::Ptr> IcingaDB::GetTypes()
+{
+ // The initial config sync will queue the types in the following order.
+ return {
+ // Sync them first to get their states ASAP.
+ Host::TypeInstance,
+ Service::TypeInstance,
+
+ // Then sync them for similar reasons.
+ Downtime::TypeInstance,
+ Comment::TypeInstance,
+
+ HostGroup::TypeInstance,
+ ServiceGroup::TypeInstance,
+ CheckCommand::TypeInstance,
+ Endpoint::TypeInstance,
+ EventCommand::TypeInstance,
+ Notification::TypeInstance,
+ NotificationCommand::TypeInstance,
+ TimePeriod::TypeInstance,
+ User::TypeInstance,
+ UserGroup::TypeInstance,
+ Zone::TypeInstance
+ };
+}
+
+void IcingaDB::ConfigStaticInitialize()
+{
+ for (auto& type : GetTypes()) {
+ m_IndexedTypes.emplace(type.get());
+ }
+
+ /* triggered in ProcessCheckResult(), requires UpdateNextCheck() to be called before */
+ Checkable::OnStateChange.connect([](const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr&) {
+ IcingaDB::StateChangeHandler(checkable, cr, type);
+ });
+
+ Checkable::OnAcknowledgementSet.connect([](const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool, bool persistent, double changeTime, double expiry, const MessageOrigin::Ptr&) {
+ AcknowledgementSetHandler(checkable, author, comment, type, persistent, changeTime, expiry);
+ });
+ Checkable::OnAcknowledgementCleared.connect([](const Checkable::Ptr& checkable, const String& removedBy, double changeTime, const MessageOrigin::Ptr&) {
+ AcknowledgementClearedHandler(checkable, removedBy, changeTime);
+ });
+
+ Checkable::OnReachabilityChanged.connect([](const Checkable::Ptr&, const CheckResult::Ptr&, std::set<Checkable::Ptr> children, const MessageOrigin::Ptr&) {
+ IcingaDB::ReachabilityChangeHandler(children);
+ });
+
+ /* triggered on create, update and delete objects */
+ ConfigObject::OnActiveChanged.connect([](const ConfigObject::Ptr& object, const Value&) {
+ IcingaDB::VersionChangedHandler(object);
+ });
+ ConfigObject::OnVersionChanged.connect([](const ConfigObject::Ptr& object, const Value&) {
+ IcingaDB::VersionChangedHandler(object);
+ });
+
+ /* downtime start */
+ Downtime::OnDowntimeTriggered.connect(&IcingaDB::DowntimeStartedHandler);
+ /* fixed/flexible downtime end or remove */
+ Downtime::OnDowntimeRemoved.connect(&IcingaDB::DowntimeRemovedHandler);
+
+ Checkable::OnNotificationSentToAllUsers.connect([](
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ const NotificationType& type, const CheckResult::Ptr& cr, const String& author, const String& text,
+ const MessageOrigin::Ptr&
+ ) {
+ IcingaDB::NotificationSentToAllUsersHandler(notification, checkable, users, type, cr, author, text);
+ });
+
+ Comment::OnCommentAdded.connect(&IcingaDB::CommentAddedHandler);
+ Comment::OnCommentRemoved.connect(&IcingaDB::CommentRemovedHandler);
+
+ Checkable::OnFlappingChange.connect(&IcingaDB::FlappingChangeHandler);
+
+ Checkable::OnNewCheckResult.connect([](const Checkable::Ptr& checkable, const CheckResult::Ptr&, const MessageOrigin::Ptr&) {
+ IcingaDB::NewCheckResultHandler(checkable);
+ });
+
+ Checkable::OnNextCheckUpdated.connect([](const Checkable::Ptr& checkable) {
+ IcingaDB::NextCheckUpdatedHandler(checkable);
+ });
+
+ Service::OnHostProblemChanged.connect([](const Service::Ptr& service, const CheckResult::Ptr&, const MessageOrigin::Ptr&) {
+ IcingaDB::HostProblemChangedHandler(service);
+ });
+
+ Notification::OnUsersRawChangedWithOldValue.connect([](const Notification::Ptr& notification, const Value& oldValues, const Value& newValues) {
+ IcingaDB::NotificationUsersChangedHandler(notification, oldValues, newValues);
+ });
+ Notification::OnUserGroupsRawChangedWithOldValue.connect([](const Notification::Ptr& notification, const Value& oldValues, const Value& newValues) {
+ IcingaDB::NotificationUserGroupsChangedHandler(notification, oldValues, newValues);
+ });
+ TimePeriod::OnRangesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+ IcingaDB::TimePeriodRangesChangedHandler(timeperiod, oldValues, newValues);
+ });
+ TimePeriod::OnIncludesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+ IcingaDB::TimePeriodIncludesChangedHandler(timeperiod, oldValues, newValues);
+ });
+ TimePeriod::OnExcludesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+ IcingaDB::TimePeriodExcludesChangedHandler(timeperiod, oldValues, newValues);
+ });
+ User::OnGroupsChangedWithOldValue.connect([](const User::Ptr& user, const Value& oldValues, const Value& newValues) {
+ IcingaDB::UserGroupsChangedHandler(user, oldValues, newValues);
+ });
+ Host::OnGroupsChangedWithOldValue.connect([](const Host::Ptr& host, const Value& oldValues, const Value& newValues) {
+ IcingaDB::HostGroupsChangedHandler(host, oldValues, newValues);
+ });
+ Service::OnGroupsChangedWithOldValue.connect([](const Service::Ptr& service, const Value& oldValues, const Value& newValues) {
+ IcingaDB::ServiceGroupsChangedHandler(service, oldValues, newValues);
+ });
+ Command::OnEnvChangedWithOldValue.connect([](const ConfigObject::Ptr& command, const Value& oldValues, const Value& newValues) {
+ IcingaDB::CommandEnvChangedHandler(command, oldValues, newValues);
+ });
+ Command::OnArgumentsChangedWithOldValue.connect([](const ConfigObject::Ptr& command, const Value& oldValues, const Value& newValues) {
+ IcingaDB::CommandArgumentsChangedHandler(command, oldValues, newValues);
+ });
+ CustomVarObject::OnVarsChangedWithOldValue.connect([](const ConfigObject::Ptr& object, const Value& oldValues, const Value& newValues) {
+ IcingaDB::CustomVarsChangedHandler(object, oldValues, newValues);
+ });
+}
+
+void IcingaDB::UpdateAllConfigObjects()
+{
+ m_Rcon->Sync();
+ m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "5"}, Prio::Heartbeat);
+
+ Log(LogInformation, "IcingaDB") << "Starting initial config/status dump";
+ double startTime = Utility::GetTime();
+
+ SetOngoingDumpStart(startTime);
+
+ Defer resetOngoingDumpStart ([this]() {
+ SetOngoingDumpStart(0);
+ });
+
+ // Use a Workqueue to pack objects in parallel
+ WorkQueue upq(25000, Configuration::Concurrency, LogNotice);
+ upq.SetName("IcingaDB:ConfigDump");
+
+ std::vector<Type::Ptr> types = GetTypes();
+
+ m_Rcon->SuppressQueryKind(Prio::CheckResult);
+ m_Rcon->SuppressQueryKind(Prio::RuntimeStateSync);
+
+ Defer unSuppress ([this]() {
+ m_Rcon->UnsuppressQueryKind(Prio::RuntimeStateSync);
+ m_Rcon->UnsuppressQueryKind(Prio::CheckResult);
+ });
+
+ // Add a new type=* state=wip entry to the stream and remove all previous entries (MAXLEN 1).
+ m_Rcon->FireAndForgetQuery({"XADD", "icinga:dump", "MAXLEN", "1", "*", "key", "*", "state", "wip"}, Prio::Config);
+
+ const std::vector<String> globalKeys = {
+ m_PrefixConfigObject + "customvar",
+ m_PrefixConfigObject + "action:url",
+ m_PrefixConfigObject + "notes:url",
+ m_PrefixConfigObject + "icon:image",
+ };
+ DeleteKeys(m_Rcon, globalKeys, Prio::Config);
+ DeleteKeys(m_Rcon, {"icinga:nextupdate:host", "icinga:nextupdate:service"}, Prio::Config);
+ m_Rcon->Sync();
+
+ Defer resetDumpedGlobals ([this]() {
+ m_DumpedGlobals.CustomVar.Reset();
+ m_DumpedGlobals.ActionUrl.Reset();
+ m_DumpedGlobals.NotesUrl.Reset();
+ m_DumpedGlobals.IconImage.Reset();
+ });
+
+ upq.ParallelFor(types, false, [this](const Type::Ptr& type) {
+ String lcType = type->GetName().ToLower();
+ ConfigType *ctype = dynamic_cast<ConfigType *>(type.get());
+ if (!ctype)
+ return;
+
+ auto& rcon (m_Rcons.at(ctype));
+
+ std::vector<String> keys = GetTypeOverwriteKeys(lcType);
+ DeleteKeys(rcon, keys, Prio::Config);
+
+ WorkQueue upqObjectType(25000, Configuration::Concurrency, LogNotice);
+ upqObjectType.SetName("IcingaDB:ConfigDump:" + lcType);
+
+ std::map<String, String> redisCheckSums;
+ String configCheckSum = m_PrefixConfigCheckSum + lcType;
+
+ upqObjectType.Enqueue([&rcon, &configCheckSum, &redisCheckSums]() {
+ String cursor = "0";
+
+ do {
+ Array::Ptr res = rcon->GetResultOfQuery({
+ "HSCAN", configCheckSum, cursor, "COUNT", "1000"
+ }, Prio::Config);
+
+ AddKvsToMap(res->Get(1), redisCheckSums);
+
+ cursor = res->Get(0);
+ } while (cursor != "0");
+ });
+
+ auto objectChunks (ChunkObjects(ctype->GetObjects(), 500));
+ String configObject = m_PrefixConfigObject + lcType;
+
+ // Skimmed away attributes and checksums HMSETs' keys and values by Redis key.
+ std::map<String, std::vector<std::vector<String>>> ourContentRaw {{configCheckSum, {}}, {configObject, {}}};
+ std::mutex ourContentMutex;
+
+ upqObjectType.ParallelFor(objectChunks, [&](decltype(objectChunks)::const_reference chunk) {
+ std::map<String, std::vector<String>> hMSets;
+ // Two values are appended per object: Object ID (Hash encoded) and Object State (IcingaDB::SerializeState() -> JSON encoded)
+ std::vector<String> states = {"HMSET", m_PrefixConfigObject + lcType + ":state"};
+ // Two values are appended per object: Object ID (Hash encoded) and State Checksum ({ "checksum": checksum } -> JSON encoded)
+ std::vector<String> statesChksms = {"HMSET", m_PrefixConfigCheckSum + lcType + ":state"};
+ std::vector<std::vector<String> > transaction = {{"MULTI"}};
+ std::vector<String> hostZAdds = {"ZADD", "icinga:nextupdate:host"}, serviceZAdds = {"ZADD", "icinga:nextupdate:service"};
+
+ auto skimObjects ([&]() {
+ std::lock_guard<std::mutex> l (ourContentMutex);
+
+ for (auto& kv : ourContentRaw) {
+ auto pos (hMSets.find(kv.first));
+
+ if (pos != hMSets.end()) {
+ kv.second.emplace_back(std::move(pos->second));
+ hMSets.erase(pos);
+ }
+ }
+ });
+
+ bool dumpState = (lcType == "host" || lcType == "service");
+
+ size_t bulkCounter = 0;
+ for (const ConfigObject::Ptr& object : chunk) {
+ if (lcType != GetLowerCaseTypeNameDB(object))
+ continue;
+
+ std::vector<Dictionary::Ptr> runtimeUpdates;
+ CreateConfigUpdate(object, lcType, hMSets, runtimeUpdates, false);
+
+ // Write out inital state for checkables
+ if (dumpState) {
+ String objectKey = GetObjectIdentifier(object);
+ Dictionary::Ptr state = SerializeState(dynamic_pointer_cast<Checkable>(object));
+
+ states.emplace_back(objectKey);
+ states.emplace_back(JsonEncode(state));
+
+ statesChksms.emplace_back(objectKey);
+ statesChksms.emplace_back(JsonEncode(new Dictionary({{"checksum", HashValue(state)}})));
+ }
+
+ bulkCounter++;
+ if (!(bulkCounter % 100)) {
+ skimObjects();
+
+ for (auto& kv : hMSets) {
+ if (!kv.second.empty()) {
+ kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
+ transaction.emplace_back(std::move(kv.second));
+ }
+ }
+
+ if (states.size() > 2) {
+ transaction.emplace_back(std::move(states));
+ transaction.emplace_back(std::move(statesChksms));
+ states = {"HMSET", m_PrefixConfigObject + lcType + ":state"};
+ statesChksms = {"HMSET", m_PrefixConfigCheckSum + lcType + ":state"};
+ }
+
+ hMSets = decltype(hMSets)();
+
+ if (transaction.size() > 1) {
+ transaction.push_back({"EXEC"});
+ rcon->FireAndForgetQueries(std::move(transaction), Prio::Config);
+ transaction = {{"MULTI"}};
+ }
+ }
+
+ auto checkable (dynamic_pointer_cast<Checkable>(object));
+
+ if (checkable && checkable->GetEnableActiveChecks()) {
+ auto zAdds (dynamic_pointer_cast<Service>(checkable) ? &serviceZAdds : &hostZAdds);
+
+ zAdds->emplace_back(Convert::ToString(checkable->GetNextUpdate()));
+ zAdds->emplace_back(GetObjectIdentifier(checkable));
+
+ if (zAdds->size() >= 102u) {
+ std::vector<String> header (zAdds->begin(), zAdds->begin() + 2u);
+
+ rcon->FireAndForgetQuery(std::move(*zAdds), Prio::CheckResult);
+
+ *zAdds = std::move(header);
+ }
+ }
+ }
+
+ skimObjects();
+
+ for (auto& kv : hMSets) {
+ if (!kv.second.empty()) {
+ kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
+ transaction.emplace_back(std::move(kv.second));
+ }
+ }
+
+ if (states.size() > 2) {
+ transaction.emplace_back(std::move(states));
+ transaction.emplace_back(std::move(statesChksms));
+ }
+
+ if (transaction.size() > 1) {
+ transaction.push_back({"EXEC"});
+ rcon->FireAndForgetQueries(std::move(transaction), Prio::Config);
+ }
+
+ for (auto zAdds : {&hostZAdds, &serviceZAdds}) {
+ if (zAdds->size() > 2u) {
+ rcon->FireAndForgetQuery(std::move(*zAdds), Prio::CheckResult);
+ }
+ }
+
+ Log(LogNotice, "IcingaDB")
+ << "Dumped " << bulkCounter << " objects of type " << lcType;
+ });
+
+ upqObjectType.Join();
+
+ if (upqObjectType.HasExceptions()) {
+ for (boost::exception_ptr exc : upqObjectType.GetExceptions()) {
+ if (exc) {
+ boost::rethrow_exception(exc);
+ }
+ }
+ }
+
+ std::map<String, std::map<String, String>> ourContent;
+
+ for (auto& source : ourContentRaw) {
+ auto& dest (ourContent[source.first]);
+
+ upqObjectType.Enqueue([&]() {
+ for (auto& hMSet : source.second) {
+ for (decltype(hMSet.size()) i = 0, stop = hMSet.size() - 1u; i < stop; i += 2u) {
+ dest.emplace(std::move(hMSet[i]), std::move(hMSet[i + 1u]));
+ }
+
+ hMSet.clear();
+ }
+
+ source.second.clear();
+ });
+ }
+
+ upqObjectType.Join();
+ ourContentRaw.clear();
+
+ auto& ourCheckSums (ourContent[configCheckSum]);
+ auto& ourObjects (ourContent[configObject]);
+ std::vector<String> setChecksum, setObject, delChecksum, delObject;
+
+ auto redisCurrent (redisCheckSums.begin());
+ auto redisEnd (redisCheckSums.end());
+ auto ourCurrent (ourCheckSums.begin());
+ auto ourEnd (ourCheckSums.end());
+
+ auto flushSets ([&]() {
+ auto affectedConfig (setObject.size() / 2u);
+
+ setChecksum.insert(setChecksum.begin(), {"HMSET", configCheckSum});
+ setObject.insert(setObject.begin(), {"HMSET", configObject});
+
+ std::vector<std::vector<String>> transaction;
+
+ transaction.emplace_back(std::vector<String>{"MULTI"});
+ transaction.emplace_back(std::move(setChecksum));
+ transaction.emplace_back(std::move(setObject));
+ transaction.emplace_back(std::vector<String>{"EXEC"});
+
+ setChecksum.clear();
+ setObject.clear();
+
+ rcon->FireAndForgetQueries(std::move(transaction), Prio::Config, {affectedConfig});
+ });
+
+ auto flushDels ([&]() {
+ auto affectedConfig (delObject.size());
+
+ delChecksum.insert(delChecksum.begin(), {"HDEL", configCheckSum});
+ delObject.insert(delObject.begin(), {"HDEL", configObject});
+
+ std::vector<std::vector<String>> transaction;
+
+ transaction.emplace_back(std::vector<String>{"MULTI"});
+ transaction.emplace_back(std::move(delChecksum));
+ transaction.emplace_back(std::move(delObject));
+ transaction.emplace_back(std::vector<String>{"EXEC"});
+
+ delChecksum.clear();
+ delObject.clear();
+
+ rcon->FireAndForgetQueries(std::move(transaction), Prio::Config, {affectedConfig});
+ });
+
+ auto setOne ([&]() {
+ setChecksum.emplace_back(ourCurrent->first);
+ setChecksum.emplace_back(ourCurrent->second);
+ setObject.emplace_back(ourCurrent->first);
+ setObject.emplace_back(ourObjects[ourCurrent->first]);
+
+ if (setChecksum.size() == 100u) {
+ flushSets();
+ }
+ });
+
+ auto delOne ([&]() {
+ delChecksum.emplace_back(redisCurrent->first);
+ delObject.emplace_back(redisCurrent->first);
+
+ if (delChecksum.size() == 100u) {
+ flushDels();
+ }
+ });
+
+ for (;;) {
+ if (redisCurrent == redisEnd) {
+ for (; ourCurrent != ourEnd; ++ourCurrent) {
+ setOne();
+ }
+
+ break;
+ } else if (ourCurrent == ourEnd) {
+ for (; redisCurrent != redisEnd; ++redisCurrent) {
+ delOne();
+ }
+
+ break;
+ } else if (redisCurrent->first < ourCurrent->first) {
+ delOne();
+ ++redisCurrent;
+ } else if (redisCurrent->first > ourCurrent->first) {
+ setOne();
+ ++ourCurrent;
+ } else {
+ if (redisCurrent->second != ourCurrent->second) {
+ setOne();
+ }
+
+ ++redisCurrent;
+ ++ourCurrent;
+ }
+ }
+
+ if (delChecksum.size()) {
+ flushDels();
+ }
+
+ if (setChecksum.size()) {
+ flushSets();
+ }
+
+ for (auto& key : GetTypeDumpSignalKeys(type)) {
+ rcon->FireAndForgetQuery({"XADD", "icinga:dump", "*", "key", key, "state", "done"}, Prio::Config);
+ }
+ rcon->Sync();
+ });
+
+ upq.Join();
+
+ if (upq.HasExceptions()) {
+ for (boost::exception_ptr exc : upq.GetExceptions()) {
+ try {
+ if (exc) {
+ boost::rethrow_exception(exc);
+ }
+ } catch(const std::exception& e) {
+ Log(LogCritical, "IcingaDB")
+ << "Exception during ConfigDump: " << e.what();
+ }
+ }
+ }
+
+ for (auto& key : globalKeys) {
+ m_Rcon->FireAndForgetQuery({"XADD", "icinga:dump", "*", "key", key, "state", "done"}, Prio::Config);
+ }
+
+ m_Rcon->FireAndForgetQuery({"XADD", "icinga:dump", "*", "key", "*", "state", "done"}, Prio::Config);
+
+ // enqueue a callback that will notify us once all previous queries were executed and wait for this event
+ std::promise<void> p;
+ m_Rcon->EnqueueCallback([&p](boost::asio::yield_context& yc) { p.set_value(); }, Prio::Config);
+ p.get_future().wait();
+
+ auto endTime (Utility::GetTime());
+ auto took (endTime - startTime);
+
+ SetLastdumpTook(took);
+ SetLastdumpEnd(endTime);
+
+ Log(LogInformation, "IcingaDB")
+ << "Initial config/status dump finished in " << took << " seconds.";
+}
+
+std::vector<std::vector<intrusive_ptr<ConfigObject>>> IcingaDB::ChunkObjects(std::vector<intrusive_ptr<ConfigObject>> objects, size_t chunkSize) {
+ std::vector<std::vector<intrusive_ptr<ConfigObject>>> chunks;
+ auto offset (objects.begin());
+ auto end (objects.end());
+
+ chunks.reserve((std::distance(offset, end) + chunkSize - 1) / chunkSize);
+
+ while (std::distance(offset, end) >= chunkSize) {
+ auto until (offset + chunkSize);
+ chunks.emplace_back(offset, until);
+ offset = until;
+ }
+
+ if (offset != end) {
+ chunks.emplace_back(offset, end);
+ }
+
+ return chunks;
+}
+
+void IcingaDB::DeleteKeys(const RedisConnection::Ptr& conn, const std::vector<String>& keys, RedisConnection::QueryPriority priority) {
+ std::vector<String> query = {"DEL"};
+ for (auto& key : keys) {
+ query.emplace_back(key);
+ }
+
+ conn->FireAndForgetQuery(std::move(query), priority);
+}
+
+std::vector<String> IcingaDB::GetTypeOverwriteKeys(const String& type)
+{
+ std::vector<String> keys = {
+ m_PrefixConfigObject + type + ":customvar",
+ };
+
+ if (type == "host" || type == "service" || type == "user") {
+ keys.emplace_back(m_PrefixConfigObject + type + "group:member");
+ keys.emplace_back(m_PrefixConfigObject + type + ":state");
+ keys.emplace_back(m_PrefixConfigCheckSum + type + ":state");
+ } else if (type == "timeperiod") {
+ keys.emplace_back(m_PrefixConfigObject + type + ":override:include");
+ keys.emplace_back(m_PrefixConfigObject + type + ":override:exclude");
+ keys.emplace_back(m_PrefixConfigObject + type + ":range");
+ } else if (type == "notification") {
+ keys.emplace_back(m_PrefixConfigObject + type + ":user");
+ keys.emplace_back(m_PrefixConfigObject + type + ":usergroup");
+ keys.emplace_back(m_PrefixConfigObject + type + ":recipient");
+ } else if (type == "checkcommand" || type == "notificationcommand" || type == "eventcommand") {
+ keys.emplace_back(m_PrefixConfigObject + type + ":envvar");
+ keys.emplace_back(m_PrefixConfigCheckSum + type + ":envvar");
+ keys.emplace_back(m_PrefixConfigObject + type + ":argument");
+ keys.emplace_back(m_PrefixConfigCheckSum + type + ":argument");
+ }
+
+ return keys;
+}
+
+std::vector<String> IcingaDB::GetTypeDumpSignalKeys(const Type::Ptr& type)
+{
+ String lcType = type->GetName().ToLower();
+ std::vector<String> keys = {m_PrefixConfigObject + lcType};
+
+ if (CustomVarObject::TypeInstance->IsAssignableFrom(type)) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":customvar");
+ }
+
+ if (type == Host::TypeInstance || type == Service::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + "group:member");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":state");
+ } else if (type == User::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + "group:member");
+ } else if (type == TimePeriod::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":override:include");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":override:exclude");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":range");
+ } else if (type == Notification::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":user");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":usergroup");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":recipient");
+ } else if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":envvar");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":argument");
+ }
+
+ return keys;
+}
+
+template<typename ConfigType>
+static ConfigObject::Ptr GetObjectByName(const String& name)
+{
+ return ConfigObject::GetObject<ConfigType>(name);
+}
+
+void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const String typeName, std::map<String, std::vector<String>>& hMSets,
+ std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate)
+{
+ String objectKey = GetObjectIdentifier(object);
+ String objectKeyName = typeName + "_id";
+
+ Type::Ptr type = object->GetReflectionType();
+
+ CustomVarObject::Ptr customVarObject = dynamic_pointer_cast<CustomVarObject>(object);
+
+ if (customVarObject) {
+ auto vars(SerializeVars(customVarObject->GetVars()));
+ if (vars) {
+ auto& typeCvs (hMSets[m_PrefixConfigObject + typeName + ":customvar"]);
+ auto& allCvs (hMSets[m_PrefixConfigObject + "customvar"]);
+
+ ObjectLock varsLock(vars);
+ Array::Ptr varsArray(new Array);
+
+ varsArray->Reserve(vars->GetLength());
+
+ for (auto& kv : vars) {
+ if (runtimeUpdate || m_DumpedGlobals.CustomVar.IsNew(kv.first)) {
+ allCvs.emplace_back(kv.first);
+ allCvs.emplace_back(JsonEncode(kv.second));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, kv.first, m_PrefixConfigObject + "customvar", kv.second);
+ }
+ }
+
+ String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
+ typeCvs.emplace_back(id);
+
+ Dictionary::Ptr data = new Dictionary({{objectKeyName, objectKey}, {"environment_id", m_EnvironmentId}, {"customvar_id", kv.first}});
+ typeCvs.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":customvar", data);
+ }
+ }
+ }
+ }
+
+ if (type == Host::TypeInstance || type == Service::TypeInstance) {
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ String actionUrl = checkable->GetActionUrl();
+ String notesUrl = checkable->GetNotesUrl();
+ String iconImage = checkable->GetIconImage();
+ if (!actionUrl.IsEmpty()) {
+ auto& actionUrls (hMSets[m_PrefixConfigObject + "action:url"]);
+
+ auto id (HashValue(new Array({m_EnvironmentId, actionUrl})));
+
+ if (runtimeUpdate || m_DumpedGlobals.ActionUrl.IsNew(id)) {
+ actionUrls.emplace_back(std::move(id));
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"action_url", actionUrl}});
+ actionUrls.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, actionUrls.at(actionUrls.size() - 2u), m_PrefixConfigObject + "action:url", data);
+ }
+ }
+ }
+ if (!notesUrl.IsEmpty()) {
+ auto& notesUrls (hMSets[m_PrefixConfigObject + "notes:url"]);
+
+ auto id (HashValue(new Array({m_EnvironmentId, notesUrl})));
+
+ if (runtimeUpdate || m_DumpedGlobals.NotesUrl.IsNew(id)) {
+ notesUrls.emplace_back(std::move(id));
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"notes_url", notesUrl}});
+ notesUrls.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, notesUrls.at(notesUrls.size() - 2u), m_PrefixConfigObject + "notes:url", data);
+ }
+ }
+ }
+ if (!iconImage.IsEmpty()) {
+ auto& iconImages (hMSets[m_PrefixConfigObject + "icon:image"]);
+
+ auto id (HashValue(new Array({m_EnvironmentId, iconImage})));
+
+ if (runtimeUpdate || m_DumpedGlobals.IconImage.IsNew(id)) {
+ iconImages.emplace_back(std::move(id));
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"icon_image", iconImage}});
+ iconImages.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, iconImages.at(iconImages.size() - 2u), m_PrefixConfigObject + "icon:image", data);
+ }
+ }
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ConfigObject::Ptr (*getGroup)(const String& name);
+ Array::Ptr groups;
+ if (service) {
+ groups = service->GetGroups();
+ getGroup = &::GetObjectByName<ServiceGroup>;
+ } else {
+ groups = host->GetGroups();
+ getGroup = &::GetObjectByName<HostGroup>;
+ }
+
+ if (groups) {
+ ObjectLock groupsLock(groups);
+ Array::Ptr groupIds(new Array);
+
+ groupIds->Reserve(groups->GetLength());
+
+ auto& members (hMSets[m_PrefixConfigObject + typeName + "group:member"]);
+
+ for (auto& group : groups) {
+ auto groupObj ((*getGroup)(group));
+ String groupId = GetObjectIdentifier(groupObj);
+ String id = HashValue(new Array({m_EnvironmentId, groupObj->GetName(), object->GetName()}));
+ members.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{objectKeyName, objectKey}, {"environment_id", m_EnvironmentId}, {typeName + "group_id", groupId}});
+ members.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + "group:member", data);
+ }
+
+ groupIds->Add(groupId);
+ }
+ }
+
+ return;
+ }
+
+ if (type == TimePeriod::TypeInstance) {
+ TimePeriod::Ptr timeperiod = static_pointer_cast<TimePeriod>(object);
+
+ Dictionary::Ptr ranges = timeperiod->GetRanges();
+ if (ranges) {
+ ObjectLock rangesLock(ranges);
+ Array::Ptr rangeIds(new Array);
+ auto& typeRanges (hMSets[m_PrefixConfigObject + typeName + ":range"]);
+
+ rangeIds->Reserve(ranges->GetLength());
+
+ for (auto& kv : ranges) {
+ String rangeId = HashValue(new Array({m_EnvironmentId, kv.first, kv.second}));
+ rangeIds->Add(rangeId);
+
+ String id = HashValue(new Array({m_EnvironmentId, kv.first, kv.second, object->GetName()}));
+ typeRanges.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"range_key", kv.first}, {"range_value", kv.second}});
+ typeRanges.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":range", data);
+ }
+ }
+ }
+
+ Array::Ptr includes;
+ ConfigObject::Ptr (*getInclude)(const String& name);
+ includes = timeperiod->GetIncludes();
+ getInclude = &::GetObjectByName<TimePeriod>;
+
+ Array::Ptr includeChecksums = new Array();
+
+ ObjectLock includesLock(includes);
+ ObjectLock includeChecksumsLock(includeChecksums);
+
+ includeChecksums->Reserve(includes->GetLength());
+
+
+ auto& includs (hMSets[m_PrefixConfigObject + typeName + ":override:include"]);
+ for (auto include : includes) {
+ auto includeTp ((*getInclude)(include.Get<String>()));
+ String includeId = GetObjectIdentifier(includeTp);
+ includeChecksums->Add(includeId);
+
+ String id = HashValue(new Array({m_EnvironmentId, includeTp->GetName(), object->GetName()}));
+ includs.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"include_id", includeId}});
+ includs.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":override:include", data);
+ }
+ }
+
+ Array::Ptr excludes;
+ ConfigObject::Ptr (*getExclude)(const String& name);
+
+ excludes = timeperiod->GetExcludes();
+ getExclude = &::GetObjectByName<TimePeriod>;
+
+ Array::Ptr excludeChecksums = new Array();
+
+ ObjectLock excludesLock(excludes);
+ ObjectLock excludeChecksumsLock(excludeChecksums);
+
+ excludeChecksums->Reserve(excludes->GetLength());
+
+ auto& excluds (hMSets[m_PrefixConfigObject + typeName + ":override:exclude"]);
+
+ for (auto exclude : excludes) {
+ auto excludeTp ((*getExclude)(exclude.Get<String>()));
+ String excludeId = GetObjectIdentifier(excludeTp);
+ excludeChecksums->Add(excludeId);
+
+ String id = HashValue(new Array({m_EnvironmentId, excludeTp->GetName(), object->GetName()}));
+ excluds.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"exclude_id", excludeId}});
+ excluds.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":override:exclude", data);
+ }
+ }
+
+ return;
+ }
+
+ if (type == User::TypeInstance) {
+ User::Ptr user = static_pointer_cast<User>(object);
+ Array::Ptr groups = user->GetGroups();
+
+ if (groups) {
+ ObjectLock groupsLock(groups);
+ Array::Ptr groupIds(new Array);
+
+ groupIds->Reserve(groups->GetLength());
+
+ auto& members (hMSets[m_PrefixConfigObject + typeName + "group:member"]);
+ auto& notificationRecipients (hMSets[m_PrefixConfigObject + "notification:recipient"]);
+
+ for (auto& group : groups) {
+ UserGroup::Ptr groupObj = UserGroup::GetByName(group);
+ String groupId = GetObjectIdentifier(groupObj);
+ String id = HashValue(new Array({m_EnvironmentId, groupObj->GetName(), object->GetName()}));
+ members.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{"user_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", groupId}});
+ members.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + "group:member", data);
+
+ // Recipients are handled by notifications during initial dumps and only need to be handled here during runtime (e.g. User creation).
+ for (auto& notification : groupObj->GetNotifications()) {
+ String recipientId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), groupObj->GetName(), notification->GetName()}));
+ notificationRecipients.emplace_back(recipientId);
+ Dictionary::Ptr recipientData = new Dictionary({{"notification_id", GetObjectIdentifier(notification)}, {"environment_id", m_EnvironmentId}, {"user_id", objectKey}, {"usergroup_id", groupId}});
+ notificationRecipients.emplace_back(JsonEncode(recipientData));
+
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, recipientId, m_PrefixConfigObject + "notification:recipient", recipientData);
+ }
+ }
+
+ groupIds->Add(groupId);
+ }
+ }
+
+ return;
+ }
+
+ if (type == Notification::TypeInstance) {
+ Notification::Ptr notification = static_pointer_cast<Notification>(object);
+
+ std::set<User::Ptr> users = notification->GetUsers();
+ Array::Ptr userIds = new Array();
+
+ auto usergroups(notification->GetUserGroups());
+ Array::Ptr usergroupIds = new Array();
+
+ userIds->Reserve(users.size());
+
+ auto& usrs (hMSets[m_PrefixConfigObject + typeName + ":user"]);
+ auto& notificationRecipients (hMSets[m_PrefixConfigObject + typeName + ":recipient"]);
+
+ for (auto& user : users) {
+ String userId = GetObjectIdentifier(user);
+ String id = HashValue(new Array({m_EnvironmentId, "user", user->GetName(), object->GetName()}));
+ usrs.emplace_back(id);
+ notificationRecipients.emplace_back(id);
+
+ Dictionary::Ptr data = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}});
+ String dataJson = JsonEncode(data);
+ usrs.emplace_back(dataJson);
+ notificationRecipients.emplace_back(dataJson);
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":user", data);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", data);
+ }
+
+ userIds->Add(userId);
+ }
+
+ usergroupIds->Reserve(usergroups.size());
+
+ auto& groups (hMSets[m_PrefixConfigObject + typeName + ":usergroup"]);
+
+ for (auto& usergroup : usergroups) {
+ String usergroupId = GetObjectIdentifier(usergroup);
+ String id = HashValue(new Array({m_EnvironmentId, "usergroup", usergroup->GetName(), object->GetName()}));
+ groups.emplace_back(id);
+ notificationRecipients.emplace_back(id);
+
+ Dictionary::Ptr groupData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", usergroupId}});
+ String groupDataJson = JsonEncode(groupData);
+ groups.emplace_back(groupDataJson);
+ notificationRecipients.emplace_back(groupDataJson);
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":usergroup", groupData);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", groupData);
+ }
+
+ for (const User::Ptr& user : usergroup->GetMembers()) {
+ String userId = GetObjectIdentifier(user);
+ String recipientId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), usergroup->GetName(), notification->GetName()}));
+ notificationRecipients.emplace_back(recipientId);
+ Dictionary::Ptr userData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}, {"usergroup_id", usergroupId}});
+ notificationRecipients.emplace_back(JsonEncode(userData));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, recipientId, m_PrefixConfigObject + typeName + ":recipient", userData);
+ }
+ }
+
+ usergroupIds->Add(usergroupId);
+ }
+
+ return;
+ }
+
+ if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+ Command::Ptr command = static_pointer_cast<Command>(object);
+
+ Dictionary::Ptr arguments = command->GetArguments();
+ if (arguments) {
+ ObjectLock argumentsLock(arguments);
+ auto& typeArgs (hMSets[m_PrefixConfigObject + typeName + ":argument"]);
+ auto& argChksms (hMSets[m_PrefixConfigCheckSum + typeName + ":argument"]);
+
+ for (auto& kv : arguments) {
+ Dictionary::Ptr values;
+ if (kv.second.IsObjectType<Dictionary>()) {
+ values = kv.second;
+ values = values->ShallowClone();
+ } else if (kv.second.IsObjectType<Array>()) {
+ values = new Dictionary({{"value", JsonEncode(kv.second)}});
+ } else {
+ values = new Dictionary({{"value", kv.second}});
+ }
+
+ for (const char *attr : {"value", "set_if", "separator"}) {
+ Value value;
+
+ // Stringify if set.
+ if (values->Get(attr, &value)) {
+ switch (value.GetType()) {
+ case ValueEmpty:
+ case ValueString:
+ break;
+ case ValueObject:
+ values->Set(attr, value.Get<Object::Ptr>()->ToString());
+ break;
+ default:
+ values->Set(attr, JsonEncode(value));
+ }
+ }
+ }
+
+ for (const char *attr : {"repeat_key", "required", "skip_key"}) {
+ Value value;
+
+ // Boolify if set.
+ if (values->Get(attr, &value)) {
+ values->Set(attr, value.ToBool());
+ }
+ }
+
+ {
+ Value order;
+
+ // Intify if set.
+ if (values->Get("order", &order)) {
+ values->Set("order", (int)order);
+ }
+ }
+
+ values->Set(objectKeyName, objectKey);
+ values->Set("argument_key", kv.first);
+ values->Set("environment_id", m_EnvironmentId);
+
+ String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
+
+ typeArgs.emplace_back(id);
+ typeArgs.emplace_back(JsonEncode(values));
+
+ argChksms.emplace_back(id);
+ String checksum = HashValue(kv.second);
+ argChksms.emplace_back(JsonEncode(new Dictionary({{"checksum", checksum}})));
+
+ if (runtimeUpdate) {
+ values->Set("checksum", checksum);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":argument", values);
+ }
+ }
+ }
+
+ Dictionary::Ptr envvars = command->GetEnv();
+ if (envvars) {
+ ObjectLock envvarsLock(envvars);
+ Array::Ptr envvarIds(new Array);
+ auto& typeVars (hMSets[m_PrefixConfigObject + typeName + ":envvar"]);
+ auto& varChksms (hMSets[m_PrefixConfigCheckSum + typeName + ":envvar"]);
+
+ envvarIds->Reserve(envvars->GetLength());
+
+ for (auto& kv : envvars) {
+ Dictionary::Ptr values;
+ if (kv.second.IsObjectType<Dictionary>()) {
+ values = kv.second;
+ values = values->ShallowClone();
+ } else if (kv.second.IsObjectType<Array>()) {
+ values = new Dictionary({{"value", JsonEncode(kv.second)}});
+ } else {
+ values = new Dictionary({{"value", kv.second}});
+ }
+
+ {
+ Value value;
+
+ // JsonEncode() the value if it's set.
+ if (values->Get("value", &value)) {
+ values->Set("value", JsonEncode(value));
+ }
+ }
+
+ values->Set(objectKeyName, objectKey);
+ values->Set("envvar_key", kv.first);
+ values->Set("environment_id", m_EnvironmentId);
+
+ String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
+
+ typeVars.emplace_back(id);
+ typeVars.emplace_back(JsonEncode(values));
+
+ varChksms.emplace_back(id);
+ String checksum = HashValue(kv.second);
+ varChksms.emplace_back(JsonEncode(new Dictionary({{"checksum", checksum}})));
+
+ if (runtimeUpdate) {
+ values->Set("checksum", checksum);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":envvar", values);
+ }
+ }
+ }
+
+ return;
+ }
+}
+
+/**
+ * Update the state information of a checkable in Redis.
+ *
+ * What is updated exactly depends on the mode parameter:
+ * - Volatile: Update the volatile state information stored in icinga:host:state or icinga:service:state as well as
+ * the corresponding checksum stored in icinga:checksum:host:state or icinga:checksum:service:state.
+ * - RuntimeOnly: Write a runtime update to the icinga:runtime:state stream. It is up to the caller to ensure that
+ * identical volatile state information was already written before to avoid inconsistencies. This mode is only
+ * useful to upgrade a previous Volatile to a Full operation, otherwise Full should be used.
+ * - Full: Perform an update of all state information in Redis, that is updating the volatile information and sending
+ * a corresponding runtime update so that this state update gets written through to the persistent database by a
+ * running icingadb process.
+ *
+ * @param checkable State of this checkable is updated in Redis
+ * @param mode Mode of operation (StateUpdate::Volatile, StateUpdate::RuntimeOnly, or StateUpdate::Full)
+ */
+void IcingaDB::UpdateState(const Checkable::Ptr& checkable, StateUpdate mode)
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ String objectType = GetLowerCaseTypeNameDB(checkable);
+ String objectKey = GetObjectIdentifier(checkable);
+
+ Dictionary::Ptr stateAttrs = SerializeState(checkable);
+
+ String redisStateKey = m_PrefixConfigObject + objectType + ":state";
+ String redisChecksumKey = m_PrefixConfigCheckSum + objectType + ":state";
+ String checksum = HashValue(stateAttrs);
+
+ if (mode & StateUpdate::Volatile) {
+ m_Rcon->FireAndForgetQueries({
+ {"HSET", redisStateKey, objectKey, JsonEncode(stateAttrs)},
+ {"HSET", redisChecksumKey, objectKey, JsonEncode(new Dictionary({{"checksum", checksum}}))},
+ }, Prio::RuntimeStateSync);
+ }
+
+ if (mode & StateUpdate::RuntimeOnly) {
+ ObjectLock olock(stateAttrs);
+
+ std::vector<String> streamadd({
+ "XADD", "icinga:runtime:state", "MAXLEN", "~", "1000000", "*",
+ "runtime_type", "upsert",
+ "redis_key", redisStateKey,
+ "checksum", checksum,
+ });
+
+ for (const Dictionary::Pair& kv : stateAttrs) {
+ streamadd.emplace_back(kv.first);
+ streamadd.emplace_back(IcingaToStreamValue(kv.second));
+ }
+
+ m_Rcon->FireAndForgetQuery(std::move(streamadd), Prio::RuntimeStateStream, {0, 1});
+ }
+}
+
+// Used to update a single object, used for runtime updates
+void IcingaDB::SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpdate)
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ String typeName = GetLowerCaseTypeNameDB(object);
+
+ std::map<String, std::vector<String>> hMSets;
+ std::vector<Dictionary::Ptr> runtimeUpdates;
+
+ CreateConfigUpdate(object, typeName, hMSets, runtimeUpdates, runtimeUpdate);
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+ if (checkable) {
+ UpdateState(checkable, runtimeUpdate ? StateUpdate::Full : StateUpdate::Volatile);
+ }
+
+ std::vector<std::vector<String> > transaction = {{"MULTI"}};
+
+ for (auto& kv : hMSets) {
+ if (!kv.second.empty()) {
+ kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
+ transaction.emplace_back(std::move(kv.second));
+ }
+ }
+
+ for (auto& objectAttributes : runtimeUpdates) {
+ std::vector<String> xAdd({"XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*"});
+ ObjectLock olock(objectAttributes);
+
+ for (const Dictionary::Pair& kv : objectAttributes) {
+ String value = IcingaToStreamValue(kv.second);
+ if (!value.IsEmpty()) {
+ xAdd.emplace_back(kv.first);
+ xAdd.emplace_back(value);
+ }
+ }
+
+ transaction.emplace_back(std::move(xAdd));
+ }
+
+ if (transaction.size() > 1) {
+ transaction.push_back({"EXEC"});
+ m_Rcon->FireAndForgetQueries(std::move(transaction), Prio::Config, {1});
+ }
+
+ if (checkable) {
+ SendNextUpdate(checkable);
+ }
+}
+
+void IcingaDB::AddObjectDataToRuntimeUpdates(std::vector<Dictionary::Ptr>& runtimeUpdates, const String& objectKey,
+ const String& redisKey, const Dictionary::Ptr& data)
+{
+ Dictionary::Ptr dataClone = data->ShallowClone();
+ dataClone->Set("id", objectKey);
+ dataClone->Set("redis_key", redisKey);
+ dataClone->Set("runtime_type", "upsert");
+ runtimeUpdates.emplace_back(dataClone);
+}
+
+// Takes object and collects IcingaDB relevant attributes and computes checksums. Returns whether the object is relevant
+// for IcingaDB.
+bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& attributes, Dictionary::Ptr& checksums)
+{
+ auto originalAttrs (object->GetOriginalAttributes());
+
+ if (originalAttrs) {
+ originalAttrs = originalAttrs->ShallowClone();
+ }
+
+ attributes->Set("name_checksum", SHA1(object->GetName()));
+ attributes->Set("environment_id", m_EnvironmentId);
+ attributes->Set("name", object->GetName());
+ attributes->Set("original_attributes", originalAttrs);
+
+ Zone::Ptr ObjectsZone;
+ Type::Ptr type = object->GetReflectionType();
+
+ if (type == Endpoint::TypeInstance) {
+ ObjectsZone = static_cast<Endpoint*>(object.get())->GetZone();
+ } else {
+ ObjectsZone = static_pointer_cast<Zone>(object->GetZone());
+ }
+
+ if (ObjectsZone) {
+ attributes->Set("zone_id", GetObjectIdentifier(ObjectsZone));
+ attributes->Set("zone_name", ObjectsZone->GetName());
+ }
+
+ if (type == Endpoint::TypeInstance) {
+ return true;
+ }
+
+ if (type == Zone::TypeInstance) {
+ Zone::Ptr zone = static_pointer_cast<Zone>(object);
+
+ attributes->Set("is_global", zone->GetGlobal());
+
+ Zone::Ptr parent = zone->GetParent();
+ if (parent) {
+ attributes->Set("parent_id", GetObjectIdentifier(parent));
+ }
+
+ auto parentsRaw (zone->GetAllParentsRaw());
+ attributes->Set("depth", parentsRaw.size());
+
+ return true;
+ }
+
+ if (type == Host::TypeInstance || type == Service::TypeInstance) {
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+ auto checkTimeout (checkable->GetCheckTimeout());
+
+ attributes->Set("checkcommand_name", checkable->GetCheckCommand()->GetName());
+ attributes->Set("max_check_attempts", checkable->GetMaxCheckAttempts());
+ attributes->Set("check_timeout", checkTimeout.IsEmpty() ? checkable->GetCheckCommand()->GetTimeout() : (double)checkTimeout);
+ attributes->Set("check_interval", checkable->GetCheckInterval());
+ attributes->Set("check_retry_interval", checkable->GetRetryInterval());
+ attributes->Set("active_checks_enabled", checkable->GetEnableActiveChecks());
+ attributes->Set("passive_checks_enabled", checkable->GetEnablePassiveChecks());
+ attributes->Set("event_handler_enabled", checkable->GetEnableEventHandler());
+ attributes->Set("notifications_enabled", checkable->GetEnableNotifications());
+ attributes->Set("flapping_enabled", checkable->GetEnableFlapping());
+ attributes->Set("flapping_threshold_low", checkable->GetFlappingThresholdLow());
+ attributes->Set("flapping_threshold_high", checkable->GetFlappingThresholdHigh());
+ attributes->Set("perfdata_enabled", checkable->GetEnablePerfdata());
+ attributes->Set("is_volatile", checkable->GetVolatile());
+ attributes->Set("notes", checkable->GetNotes());
+ attributes->Set("icon_image_alt", checkable->GetIconImageAlt());
+
+ attributes->Set("checkcommand_id", GetObjectIdentifier(checkable->GetCheckCommand()));
+
+ Endpoint::Ptr commandEndpoint = checkable->GetCommandEndpoint();
+ if (commandEndpoint) {
+ attributes->Set("command_endpoint_id", GetObjectIdentifier(commandEndpoint));
+ attributes->Set("command_endpoint_name", commandEndpoint->GetName());
+ }
+
+ TimePeriod::Ptr timePeriod = checkable->GetCheckPeriod();
+ if (timePeriod) {
+ attributes->Set("check_timeperiod_id", GetObjectIdentifier(timePeriod));
+ attributes->Set("check_timeperiod_name", timePeriod->GetName());
+ }
+
+ EventCommand::Ptr eventCommand = checkable->GetEventCommand();
+ if (eventCommand) {
+ attributes->Set("eventcommand_id", GetObjectIdentifier(eventCommand));
+ attributes->Set("eventcommand_name", eventCommand->GetName());
+ }
+
+ String actionUrl = checkable->GetActionUrl();
+ String notesUrl = checkable->GetNotesUrl();
+ String iconImage = checkable->GetIconImage();
+ if (!actionUrl.IsEmpty())
+ attributes->Set("action_url_id", HashValue(new Array({m_EnvironmentId, actionUrl})));
+ if (!notesUrl.IsEmpty())
+ attributes->Set("notes_url_id", HashValue(new Array({m_EnvironmentId, notesUrl})));
+ if (!iconImage.IsEmpty())
+ attributes->Set("icon_image_id", HashValue(new Array({m_EnvironmentId, iconImage})));
+
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ if (service) {
+ attributes->Set("host_id", GetObjectIdentifier(service->GetHost()));
+ attributes->Set("display_name", service->GetDisplayName());
+
+ // Overwrite name here, `object->name` is 'HostName!ServiceName' but we only want the name of the Service
+ attributes->Set("name", service->GetShortName());
+ } else {
+ attributes->Set("display_name", host->GetDisplayName());
+ attributes->Set("address", host->GetAddress());
+ attributes->Set("address6", host->GetAddress6());
+ }
+
+ return true;
+ }
+
+ if (type == User::TypeInstance) {
+ User::Ptr user = static_pointer_cast<User>(object);
+
+ attributes->Set("display_name", user->GetDisplayName());
+ attributes->Set("email", user->GetEmail());
+ attributes->Set("pager", user->GetPager());
+ attributes->Set("notifications_enabled", user->GetEnableNotifications());
+ attributes->Set("states", user->GetStates());
+ attributes->Set("types", user->GetTypes());
+
+ if (user->GetPeriod())
+ attributes->Set("timeperiod_id", GetObjectIdentifier(user->GetPeriod()));
+
+ return true;
+ }
+
+ if (type == TimePeriod::TypeInstance) {
+ TimePeriod::Ptr timeperiod = static_pointer_cast<TimePeriod>(object);
+
+ attributes->Set("display_name", timeperiod->GetDisplayName());
+ attributes->Set("prefer_includes", timeperiod->GetPreferIncludes());
+ return true;
+ }
+
+ if (type == Notification::TypeInstance) {
+ Notification::Ptr notification = static_pointer_cast<Notification>(object);
+
+ Host::Ptr host;
+ Service::Ptr service;
+
+ tie(host, service) = GetHostService(notification->GetCheckable());
+
+ attributes->Set("notificationcommand_id", GetObjectIdentifier(notification->GetCommand()));
+
+ attributes->Set("host_id", GetObjectIdentifier(host));
+ if (service)
+ attributes->Set("service_id", GetObjectIdentifier(service));
+
+ TimePeriod::Ptr timeperiod = notification->GetPeriod();
+ if (timeperiod)
+ attributes->Set("timeperiod_id", GetObjectIdentifier(timeperiod));
+
+ if (notification->GetTimes()) {
+ auto begin (notification->GetTimes()->Get("begin"));
+ auto end (notification->GetTimes()->Get("end"));
+
+ if (begin != Empty && (double)begin >= 0) {
+ attributes->Set("times_begin", std::round((double)begin));
+ }
+
+ if (end != Empty && (double)end >= 0) {
+ attributes->Set("times_end", std::round((double)end));
+ }
+ }
+
+ attributes->Set("notification_interval", std::max(0.0, std::round(notification->GetInterval())));
+ attributes->Set("states", notification->GetStates());
+ attributes->Set("types", notification->GetTypes());
+
+ return true;
+ }
+
+ if (type == Comment::TypeInstance) {
+ Comment::Ptr comment = static_pointer_cast<Comment>(object);
+
+ attributes->Set("author", comment->GetAuthor());
+ attributes->Set("text", comment->GetText());
+ attributes->Set("entry_type", comment->GetEntryType());
+ attributes->Set("entry_time", TimestampToMilliseconds(comment->GetEntryTime()));
+ attributes->Set("is_persistent", comment->GetPersistent());
+ attributes->Set("is_sticky", comment->GetSticky());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(comment->GetCheckable());
+
+ attributes->Set("host_id", GetObjectIdentifier(host));
+ if (service) {
+ attributes->Set("object_type", "service");
+ attributes->Set("service_id", GetObjectIdentifier(service));
+ } else
+ attributes->Set("object_type", "host");
+
+ auto expireTime (comment->GetExpireTime());
+
+ if (expireTime > 0) {
+ attributes->Set("expire_time", TimestampToMilliseconds(expireTime));
+ }
+
+ return true;
+ }
+
+ if (type == Downtime::TypeInstance) {
+ Downtime::Ptr downtime = static_pointer_cast<Downtime>(object);
+
+ attributes->Set("author", downtime->GetAuthor());
+ attributes->Set("comment", downtime->GetComment());
+ attributes->Set("entry_time", TimestampToMilliseconds(downtime->GetEntryTime()));
+ attributes->Set("scheduled_start_time", TimestampToMilliseconds(downtime->GetStartTime()));
+ attributes->Set("scheduled_end_time", TimestampToMilliseconds(downtime->GetEndTime()));
+ attributes->Set("scheduled_duration", TimestampToMilliseconds(std::max(0.0, downtime->GetEndTime() - downtime->GetStartTime())));
+ attributes->Set("flexible_duration", TimestampToMilliseconds(std::max(0.0, downtime->GetDuration())));
+ attributes->Set("is_flexible", !downtime->GetFixed());
+ attributes->Set("is_in_effect", downtime->IsInEffect());
+ if (downtime->IsInEffect()) {
+ attributes->Set("start_time", TimestampToMilliseconds(downtime->GetTriggerTime()));
+
+ attributes->Set("end_time", TimestampToMilliseconds(
+ downtime->GetFixed() ? downtime->GetEndTime() : (downtime->GetTriggerTime() + std::max(0.0, downtime->GetDuration()))
+ ));
+ }
+
+ auto duration = downtime->GetDuration();
+ if (downtime->GetFixed()) {
+ duration = downtime->GetEndTime() - downtime->GetStartTime();
+ }
+ attributes->Set("duration", TimestampToMilliseconds(std::max(0.0, duration)));
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(downtime->GetCheckable());
+
+ attributes->Set("host_id", GetObjectIdentifier(host));
+ if (service) {
+ attributes->Set("object_type", "service");
+ attributes->Set("service_id", GetObjectIdentifier(service));
+ } else
+ attributes->Set("object_type", "host");
+
+ auto triggeredBy (Downtime::GetByName(downtime->GetTriggeredBy()));
+ if (triggeredBy) {
+ attributes->Set("triggered_by_id", GetObjectIdentifier(triggeredBy));
+ }
+
+ auto scheduledBy (downtime->GetScheduledBy());
+ if (!scheduledBy.IsEmpty()) {
+ attributes->Set("scheduled_by", scheduledBy);
+ }
+
+ auto parent (Downtime::GetByName(downtime->GetParent()));
+ if (parent) {
+ attributes->Set("parent_id", GetObjectIdentifier(parent));
+ }
+
+ return true;
+ }
+
+ if (type == UserGroup::TypeInstance) {
+ UserGroup::Ptr userGroup = static_pointer_cast<UserGroup>(object);
+
+ attributes->Set("display_name", userGroup->GetDisplayName());
+
+ return true;
+ }
+
+ if (type == HostGroup::TypeInstance) {
+ HostGroup::Ptr hostGroup = static_pointer_cast<HostGroup>(object);
+
+ attributes->Set("display_name", hostGroup->GetDisplayName());
+
+ return true;
+ }
+
+ if (type == ServiceGroup::TypeInstance) {
+ ServiceGroup::Ptr serviceGroup = static_pointer_cast<ServiceGroup>(object);
+
+ attributes->Set("display_name", serviceGroup->GetDisplayName());
+
+ return true;
+ }
+
+ if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+ Command::Ptr command = static_pointer_cast<Command>(object);
+
+ attributes->Set("command", JsonEncode(command->GetCommandLine()));
+ attributes->Set("timeout", std::max(0, command->GetTimeout()));
+
+ return true;
+ }
+
+ return false;
+}
+
+/* Creates a config update with computed checksums etc.
+ * Writes attributes, customVars and checksums into the respective supplied vectors. Adds two values to each vector
+ * (if applicable), first the key then the value. To use in a Redis command the command (e.g. HSET) and the key (e.g.
+ * icinga:config:object:downtime) need to be prepended. There is nothing to indicate success or failure.
+ */
+void
+IcingaDB::CreateConfigUpdate(const ConfigObject::Ptr& object, const String typeName, std::map<String, std::vector<String>>& hMSets,
+ std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate)
+{
+ /* TODO: This isn't essentially correct as we don't keep track of config objects ourselves. This would avoid duplicated config updates at startup.
+ if (!runtimeUpdate && m_ConfigDumpInProgress)
+ return;
+ */
+
+ if (m_Rcon == nullptr)
+ return;
+
+ Dictionary::Ptr attr = new Dictionary;
+ Dictionary::Ptr chksm = new Dictionary;
+
+ if (!PrepareObject(object, attr, chksm))
+ return;
+
+ InsertObjectDependencies(object, typeName, hMSets, runtimeUpdates, runtimeUpdate);
+
+ String objectKey = GetObjectIdentifier(object);
+ auto& attrs (hMSets[m_PrefixConfigObject + typeName]);
+ auto& chksms (hMSets[m_PrefixConfigCheckSum + typeName]);
+
+ attrs.emplace_back(objectKey);
+ attrs.emplace_back(JsonEncode(attr));
+
+ String checksum = HashValue(attr);
+ chksms.emplace_back(objectKey);
+ chksms.emplace_back(JsonEncode(new Dictionary({{"checksum", checksum}})));
+
+ /* Send an update event to subscribers. */
+ if (runtimeUpdate) {
+ attr->Set("checksum", checksum);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, objectKey, m_PrefixConfigObject + typeName, attr);
+ }
+}
+
+void IcingaDB::SendConfigDelete(const ConfigObject::Ptr& object)
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ Type::Ptr type = object->GetReflectionType();
+ String typeName = type->GetName().ToLower();
+ String objectKey = GetObjectIdentifier(object);
+
+ m_Rcon->FireAndForgetQueries({
+ {"HDEL", m_PrefixConfigObject + typeName, objectKey},
+ {"HDEL", m_PrefixConfigCheckSum + typeName, objectKey},
+ {
+ "XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*",
+ "redis_key", m_PrefixConfigObject + typeName, "id", objectKey, "runtime_type", "delete"
+ }
+ }, Prio::Config);
+
+ CustomVarObject::Ptr customVarObject = dynamic_pointer_cast<CustomVarObject>(object);
+
+ if (customVarObject) {
+ Dictionary::Ptr vars = customVarObject->GetVars();
+ SendCustomVarsChanged(object, vars, nullptr);
+ }
+
+ if (type == Host::TypeInstance || type == Service::TypeInstance) {
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ m_Rcon->FireAndForgetQuery({
+ "ZREM",
+ service ? "icinga:nextupdate:service" : "icinga:nextupdate:host",
+ GetObjectIdentifier(checkable)
+ }, Prio::CheckResult);
+
+ m_Rcon->FireAndForgetQueries({
+ {"HDEL", m_PrefixConfigObject + typeName + ":state", objectKey},
+ {"HDEL", m_PrefixConfigCheckSum + typeName + ":state", objectKey}
+ }, Prio::RuntimeStateSync);
+
+ if (service) {
+ SendGroupsChanged<ServiceGroup>(checkable, service->GetGroups(), nullptr);
+ } else {
+ SendGroupsChanged<HostGroup>(checkable, host->GetGroups(), nullptr);
+ }
+
+ return;
+ }
+
+ if (type == TimePeriod::TypeInstance) {
+ TimePeriod::Ptr timeperiod = static_pointer_cast<TimePeriod>(object);
+ SendTimePeriodRangesChanged(timeperiod, timeperiod->GetRanges(), nullptr);
+ SendTimePeriodIncludesChanged(timeperiod, timeperiod->GetIncludes(), nullptr);
+ SendTimePeriodExcludesChanged(timeperiod, timeperiod->GetExcludes(), nullptr);
+ return;
+ }
+
+ if (type == User::TypeInstance) {
+ User::Ptr user = static_pointer_cast<User>(object);
+ SendGroupsChanged<UserGroup>(user, user->GetGroups(), nullptr);
+ return;
+ }
+
+ if (type == Notification::TypeInstance) {
+ Notification::Ptr notification = static_pointer_cast<Notification>(object);
+ SendNotificationUsersChanged(notification, notification->GetUsersRaw(), nullptr);
+ SendNotificationUserGroupsChanged(notification, notification->GetUserGroupsRaw(), nullptr);
+ return;
+ }
+
+ if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+ Command::Ptr command = static_pointer_cast<Command>(object);
+ SendCommandArgumentsChanged(command, command->GetArguments(), nullptr);
+ SendCommandEnvChanged(command, command->GetEnv(), nullptr);
+ return;
+ }
+}
+
+static inline
+unsigned short GetPreviousState(const Checkable::Ptr& checkable, const Service::Ptr& service, StateType type)
+{
+ auto phs ((type == StateTypeHard ? checkable->GetLastHardStatesRaw() : checkable->GetLastSoftStatesRaw()) % 100u);
+
+ if (service) {
+ return phs;
+ } else {
+ return phs == 99 ? phs : Host::CalculateState(ServiceState(phs));
+ }
+}
+
+void IcingaDB::SendStateChange(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+ if (!checkable)
+ return;
+
+ if (!cr)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+
+ tie(host, service) = GetHostService(checkable);
+
+ UpdateState(checkable, StateUpdate::RuntimeOnly);
+
+ int hard_state;
+ if (!cr) {
+ hard_state = 99;
+ } else {
+ hard_state = service ? Convert::ToLong(service->GetLastHardState()) : Convert::ToLong(host->GetLastHardState());
+ }
+
+ auto eventTime (cr->GetExecutionEnd());
+ auto eventTs (TimestampToMilliseconds(eventTime));
+
+ Array::Ptr rawId = new Array({m_EnvironmentId, object->GetName()});
+ rawId->Add(eventTs);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:state", "*",
+ "id", HashValue(rawId),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "state_type", Convert::ToString(type),
+ "soft_state", Convert::ToString(cr ? service ? Convert::ToLong(cr->GetState()) : Convert::ToLong(Host::CalculateState(cr->GetState())) : 99),
+ "hard_state", Convert::ToString(hard_state),
+ "check_attempt", Convert::ToString(checkable->GetCheckAttempt()),
+ "previous_soft_state", Convert::ToString(GetPreviousState(checkable, service, StateTypeSoft)),
+ "previous_hard_state", Convert::ToString(GetPreviousState(checkable, service, StateTypeHard)),
+ "max_check_attempts", Convert::ToString(checkable->GetMaxCheckAttempts()),
+ "event_time", Convert::ToString(eventTs),
+ "event_id", CalcEventID("state_change", object, eventTime),
+ "event_type", "state_change"
+ });
+
+ if (cr) {
+ auto output (cr->GetOutput());
+ auto pos (output.Find("\n"));
+
+ if (pos != String::NPos) {
+ auto longOutput (output.SubStr(pos + 1u));
+ output.erase(output.Begin() + pos, output.End());
+
+ xAdd.emplace_back("long_output");
+ xAdd.emplace_back(Utility::ValidateUTF8(std::move(longOutput)));
+ }
+
+ xAdd.emplace_back("output");
+ xAdd.emplace_back(Utility::ValidateUTF8(std::move(output)));
+ xAdd.emplace_back("check_source");
+ xAdd.emplace_back(cr->GetCheckSource());
+ xAdd.emplace_back("scheduling_source");
+ xAdd.emplace_back(cr->GetSchedulingSource());
+ }
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendSentNotification(
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text, double sendTime
+)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ auto finalText = text;
+ if (finalText == "" && cr) {
+ finalText = cr->GetOutput();
+ }
+
+ auto usersAmount (users.size());
+ auto sendTs (TimestampToMilliseconds(sendTime));
+
+ Array::Ptr rawId = new Array({m_EnvironmentId, notification->GetName()});
+ rawId->Add(GetNotificationTypeByEnum(type));
+ rawId->Add(sendTs);
+
+ auto notificationHistoryId (HashValue(rawId));
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:notification", "*",
+ "id", notificationHistoryId,
+ "environment_id", m_EnvironmentId,
+ "notification_id", GetObjectIdentifier(notification),
+ "host_id", GetObjectIdentifier(host),
+ "type", Convert::ToString(type),
+ "state", Convert::ToString(cr ? service ? Convert::ToLong(cr->GetState()) : Convert::ToLong(Host::CalculateState(cr->GetState())) : 99),
+ "previous_hard_state", Convert::ToString(cr ? Convert::ToLong(service ? cr->GetPreviousHardState() : Host::CalculateState(cr->GetPreviousHardState())) : 99),
+ "author", Utility::ValidateUTF8(author),
+ "text", Utility::ValidateUTF8(finalText),
+ "users_notified", Convert::ToString(usersAmount),
+ "send_time", Convert::ToString(sendTs),
+ "event_id", CalcEventID("notification", notification, sendTime, type),
+ "event_type", "notification"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ if (!users.empty()) {
+ Array::Ptr users_notified = new Array();
+ for (const User::Ptr& user : users) {
+ users_notified->Add(GetObjectIdentifier(user));
+ }
+ xAdd.emplace_back("users_notified_ids");
+ xAdd.emplace_back(JsonEncode(users_notified));
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendStartedDowntime(const Downtime::Ptr& downtime)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ SendConfigUpdate(downtime, true);
+
+ auto checkable (downtime->GetCheckable());
+ auto triggeredBy (Downtime::GetByName(downtime->GetTriggeredBy()));
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ /* Update checkable state as in_downtime may have changed. */
+ UpdateState(checkable, StateUpdate::Full);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:downtime", "*",
+ "downtime_id", GetObjectIdentifier(downtime),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "entry_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEntryTime())),
+ "author", Utility::ValidateUTF8(downtime->GetAuthor()),
+ "comment", Utility::ValidateUTF8(downtime->GetComment()),
+ "is_flexible", Convert::ToString((unsigned short)!downtime->GetFixed()),
+ "flexible_duration", Convert::ToString(TimestampToMilliseconds(std::max(0.0, downtime->GetDuration()))),
+ "scheduled_start_time", Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())),
+ "scheduled_end_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())),
+ "has_been_cancelled", Convert::ToString((unsigned short)downtime->GetWasCancelled()),
+ "trigger_time", Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())),
+ "cancel_time", Convert::ToString(TimestampToMilliseconds(downtime->GetRemoveTime())),
+ "event_id", CalcEventID("downtime_start", downtime),
+ "event_type", "downtime_start"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ if (triggeredBy) {
+ xAdd.emplace_back("triggered_by_id");
+ xAdd.emplace_back(GetObjectIdentifier(triggeredBy));
+ }
+
+ if (downtime->GetFixed()) {
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())));
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())));
+ } else {
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())));
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime() + std::max(0.0, downtime->GetDuration()))));
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ auto parent (Downtime::GetByName(downtime->GetParent()));
+
+ if (parent) {
+ xAdd.emplace_back("parent_id");
+ xAdd.emplace_back(GetObjectIdentifier(parent));
+ }
+
+ auto scheduledBy (downtime->GetScheduledBy());
+
+ if (!scheduledBy.IsEmpty()) {
+ xAdd.emplace_back("scheduled_by");
+ xAdd.emplace_back(scheduledBy);
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendRemovedDowntime(const Downtime::Ptr& downtime)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ auto checkable (downtime->GetCheckable());
+ auto triggeredBy (Downtime::GetByName(downtime->GetTriggeredBy()));
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ // Downtime never got triggered (didn't send "downtime_start") so we don't want to send "downtime_end"
+ if (downtime->GetTriggerTime() == 0)
+ return;
+
+ /* Update checkable state as in_downtime may have changed. */
+ UpdateState(checkable, StateUpdate::Full);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:downtime", "*",
+ "downtime_id", GetObjectIdentifier(downtime),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "entry_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEntryTime())),
+ "author", Utility::ValidateUTF8(downtime->GetAuthor()),
+ "cancelled_by", Utility::ValidateUTF8(downtime->GetRemovedBy()),
+ "comment", Utility::ValidateUTF8(downtime->GetComment()),
+ "is_flexible", Convert::ToString((unsigned short)!downtime->GetFixed()),
+ "flexible_duration", Convert::ToString(TimestampToMilliseconds(std::max(0.0, downtime->GetDuration()))),
+ "scheduled_start_time", Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())),
+ "scheduled_end_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())),
+ "has_been_cancelled", Convert::ToString((unsigned short)downtime->GetWasCancelled()),
+ "trigger_time", Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())),
+ "cancel_time", Convert::ToString(TimestampToMilliseconds(downtime->GetRemoveTime())),
+ "event_id", CalcEventID("downtime_end", downtime),
+ "event_type", "downtime_end"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ if (triggeredBy) {
+ xAdd.emplace_back("triggered_by_id");
+ xAdd.emplace_back(GetObjectIdentifier(triggeredBy));
+ }
+
+ if (downtime->GetFixed()) {
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())));
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())));
+ } else {
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())));
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime() + std::max(0.0, downtime->GetDuration()))));
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ auto parent (Downtime::GetByName(downtime->GetParent()));
+
+ if (parent) {
+ xAdd.emplace_back("parent_id");
+ xAdd.emplace_back(GetObjectIdentifier(parent));
+ }
+
+ auto scheduledBy (downtime->GetScheduledBy());
+
+ if (!scheduledBy.IsEmpty()) {
+ xAdd.emplace_back("scheduled_by");
+ xAdd.emplace_back(scheduledBy);
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendAddedComment(const Comment::Ptr& comment)
+{
+ if (comment->GetEntryType() != CommentUser || !GetActive())
+ return;
+
+ auto checkable (comment->GetCheckable());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:comment", "*",
+ "comment_id", GetObjectIdentifier(comment),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "entry_time", Convert::ToString(TimestampToMilliseconds(comment->GetEntryTime())),
+ "author", Utility::ValidateUTF8(comment->GetAuthor()),
+ "comment", Utility::ValidateUTF8(comment->GetText()),
+ "entry_type", Convert::ToString(comment->GetEntryType()),
+ "is_persistent", Convert::ToString((unsigned short)comment->GetPersistent()),
+ "is_sticky", Convert::ToString((unsigned short)comment->GetSticky()),
+ "event_id", CalcEventID("comment_add", comment),
+ "event_type", "comment_add"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ {
+ auto expireTime (comment->GetExpireTime());
+
+ if (expireTime > 0) {
+ xAdd.emplace_back("expire_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(expireTime)));
+ }
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+ UpdateState(checkable, StateUpdate::Full);
+}
+
+void IcingaDB::SendRemovedComment(const Comment::Ptr& comment)
+{
+ if (comment->GetEntryType() != CommentUser || !GetActive()) {
+ return;
+ }
+
+ double removeTime = comment->GetRemoveTime();
+ bool wasRemoved = removeTime > 0;
+
+ double expireTime = comment->GetExpireTime();
+ bool hasExpireTime = expireTime > 0;
+ bool isExpired = hasExpireTime && expireTime <= Utility::GetTime();
+
+ if (!wasRemoved && !isExpired) {
+ /* The comment object disappeared for no apparent reason, most likely because it simply was deleted instead
+ * of using the proper remove-comment API action. In this case, information that should normally be set is
+ * missing and a proper history event cannot be generated.
+ */
+ return;
+ }
+
+ auto checkable (comment->GetCheckable());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:comment", "*",
+ "comment_id", GetObjectIdentifier(comment),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "entry_time", Convert::ToString(TimestampToMilliseconds(comment->GetEntryTime())),
+ "author", Utility::ValidateUTF8(comment->GetAuthor()),
+ "comment", Utility::ValidateUTF8(comment->GetText()),
+ "entry_type", Convert::ToString(comment->GetEntryType()),
+ "is_persistent", Convert::ToString((unsigned short)comment->GetPersistent()),
+ "is_sticky", Convert::ToString((unsigned short)comment->GetSticky()),
+ "event_id", CalcEventID("comment_remove", comment),
+ "event_type", "comment_remove"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ if (wasRemoved) {
+ xAdd.emplace_back("remove_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(removeTime)));
+ xAdd.emplace_back("has_been_removed");
+ xAdd.emplace_back("1");
+ xAdd.emplace_back("removed_by");
+ xAdd.emplace_back(Utility::ValidateUTF8(comment->GetRemovedBy()));
+ } else {
+ xAdd.emplace_back("has_been_removed");
+ xAdd.emplace_back("0");
+ }
+
+ if (hasExpireTime) {
+ xAdd.emplace_back("expire_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(expireTime)));
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+ UpdateState(checkable, StateUpdate::Full);
+}
+
+void IcingaDB::SendFlappingChange(const Checkable::Ptr& checkable, double changeTime, double flappingLastChange)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:flapping", "*",
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "flapping_threshold_low", Convert::ToString(checkable->GetFlappingThresholdLow()),
+ "flapping_threshold_high", Convert::ToString(checkable->GetFlappingThresholdHigh())
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ long long startTime;
+
+ if (checkable->IsFlapping()) {
+ startTime = TimestampToMilliseconds(changeTime);
+
+ xAdd.emplace_back("event_type");
+ xAdd.emplace_back("flapping_start");
+ xAdd.emplace_back("percent_state_change_start");
+ xAdd.emplace_back(Convert::ToString(checkable->GetFlappingCurrent()));
+ } else {
+ startTime = TimestampToMilliseconds(flappingLastChange);
+
+ xAdd.emplace_back("event_type");
+ xAdd.emplace_back("flapping_end");
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(changeTime)));
+ xAdd.emplace_back("percent_state_change_end");
+ xAdd.emplace_back(Convert::ToString(checkable->GetFlappingCurrent()));
+ }
+
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(startTime));
+ xAdd.emplace_back("event_id");
+ xAdd.emplace_back(CalcEventID(checkable->IsFlapping() ? "flapping_start" : "flapping_end", checkable, startTime));
+ xAdd.emplace_back("id");
+ xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), startTime})));
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendNextUpdate(const Checkable::Ptr& checkable)
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ if (checkable->GetEnableActiveChecks()) {
+ m_Rcon->FireAndForgetQuery(
+ {
+ "ZADD",
+ dynamic_pointer_cast<Service>(checkable) ? "icinga:nextupdate:service" : "icinga:nextupdate:host",
+ Convert::ToString(checkable->GetNextUpdate()),
+ GetObjectIdentifier(checkable)
+ },
+ Prio::CheckResult
+ );
+ } else {
+ m_Rcon->FireAndForgetQuery(
+ {
+ "ZREM",
+ dynamic_pointer_cast<Service>(checkable) ? "icinga:nextupdate:service" : "icinga:nextupdate:host",
+ GetObjectIdentifier(checkable)
+ },
+ Prio::CheckResult
+ );
+ }
+}
+
+void IcingaDB::SendAcknowledgementSet(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ /* Update checkable state as is_acknowledged may have changed. */
+ UpdateState(checkable, StateUpdate::Full);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:acknowledgement", "*",
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "event_type", "ack_set",
+ "author", author,
+ "comment", comment,
+ "is_sticky", Convert::ToString((unsigned short)(type == AcknowledgementSticky)),
+ "is_persistent", Convert::ToString((unsigned short)persistent)
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ if (expiry > 0) {
+ xAdd.emplace_back("expire_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(expiry)));
+ }
+
+ long long setTime = TimestampToMilliseconds(changeTime);
+
+ xAdd.emplace_back("set_time");
+ xAdd.emplace_back(Convert::ToString(setTime));
+ xAdd.emplace_back("event_id");
+ xAdd.emplace_back(CalcEventID("ack_set", checkable, setTime));
+ xAdd.emplace_back("id");
+ xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), setTime})));
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendAcknowledgementCleared(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, double ackLastChange)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ /* Update checkable state as is_acknowledged may have changed. */
+ UpdateState(checkable, StateUpdate::Full);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:acknowledgement", "*",
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "clear_time", Convert::ToString(TimestampToMilliseconds(changeTime)),
+ "event_type", "ack_clear"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ long long setTime = TimestampToMilliseconds(ackLastChange);
+
+ xAdd.emplace_back("set_time");
+ xAdd.emplace_back(Convert::ToString(setTime));
+ xAdd.emplace_back("event_id");
+ xAdd.emplace_back(CalcEventID("ack_clear", checkable, setTime));
+ xAdd.emplace_back("id");
+ xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), setTime})));
+
+ if (!removedBy.IsEmpty()) {
+ xAdd.emplace_back("cleared_by");
+ xAdd.emplace_back(removedBy);
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::ForwardHistoryEntries()
+{
+ using clock = std::chrono::steady_clock;
+
+ const std::chrono::seconds logInterval (10);
+ auto nextLog (clock::now() + logInterval);
+
+ auto logPeriodically ([this, logInterval, &nextLog]() {
+ if (clock::now() > nextLog) {
+ nextLog += logInterval;
+
+ auto size (m_HistoryBulker.Size());
+
+ Log(size > m_HistoryBulker.GetBulkSize() ? LogInformation : LogNotice, "IcingaDB")
+ << "Pending history queries: " << size;
+ }
+ });
+
+ for (;;) {
+ logPeriodically();
+
+ auto haystack (m_HistoryBulker.ConsumeMany());
+
+ if (haystack.empty()) {
+ if (!GetActive()) {
+ break;
+ }
+
+ continue;
+ }
+
+ uintmax_t attempts = 0;
+
+ auto logFailure ([&haystack, &attempts](const char* err = nullptr) {
+ Log msg (LogNotice, "IcingaDB");
+
+ msg << "history: " << haystack.size() << " queries failed temporarily (attempt #" << ++attempts << ")";
+
+ if (err) {
+ msg << ": " << err;
+ }
+ });
+
+ for (;;) {
+ logPeriodically();
+
+ if (m_Rcon && m_Rcon->IsConnected()) {
+ try {
+ m_Rcon->GetResultsOfQueries(haystack, Prio::History, {0, 0, haystack.size()});
+ break;
+ } catch (const std::exception& ex) {
+ logFailure(ex.what());
+ } catch (...) {
+ logFailure();
+ }
+ } else {
+ logFailure("not connected to Redis");
+ }
+
+ if (!GetActive()) {
+ Log(LogCritical, "IcingaDB") << "history: " << haystack.size() << " queries failed (attempt #" << attempts
+ << ") while we're about to shut down. Giving up and discarding additional "
+ << m_HistoryBulker.Size() << " queued history queries.";
+
+ return;
+ }
+
+ Utility::Sleep(2);
+ }
+ }
+}
+
+void IcingaDB::SendNotificationUsersChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedUsers = GetArrayDeletedValues(oldValues, newValues);
+
+ for (const auto& userName : deletedUsers) {
+ String id = HashValue(new Array({m_EnvironmentId, "user", userName, notification->GetName()}));
+ DeleteRelationship(id, "notification:user");
+ DeleteRelationship(id, "notification:recipient");
+ }
+}
+
+void IcingaDB::SendNotificationUserGroupsChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedUserGroups = GetArrayDeletedValues(oldValues, newValues);
+
+ for (const auto& userGroupName : deletedUserGroups) {
+ UserGroup::Ptr userGroup = UserGroup::GetByName(userGroupName);
+ String id = HashValue(new Array({m_EnvironmentId, "usergroup", userGroupName, notification->GetName()}));
+ DeleteRelationship(id, "notification:usergroup");
+ DeleteRelationship(id, "notification:recipient");
+
+ for (const User::Ptr& user : userGroup->GetMembers()) {
+ String userId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), userGroupName, notification->GetName()}));
+ DeleteRelationship(userId, "notification:recipient");
+ }
+ }
+}
+
+void IcingaDB::SendTimePeriodRangesChanged(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+ String typeName = GetLowerCaseTypeNameDB(timeperiod);
+
+ for (const auto& rangeKey : deletedKeys) {
+ String id = HashValue(new Array({m_EnvironmentId, rangeKey, oldValues->Get(rangeKey), timeperiod->GetName()}));
+ DeleteRelationship(id, "timeperiod:range");
+ }
+}
+
+void IcingaDB::SendTimePeriodIncludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedIncludes = GetArrayDeletedValues(oldValues, newValues);
+
+ for (const auto& includeName : deletedIncludes) {
+ String id = HashValue(new Array({m_EnvironmentId, includeName, timeperiod->GetName()}));
+ DeleteRelationship(id, "timeperiod:override:include");
+ }
+}
+
+void IcingaDB::SendTimePeriodExcludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedExcludes = GetArrayDeletedValues(oldValues, newValues);
+
+ for (const auto& excludeName : deletedExcludes) {
+ String id = HashValue(new Array({m_EnvironmentId, excludeName, timeperiod->GetName()}));
+ DeleteRelationship(id, "timeperiod:override:exclude");
+ }
+}
+
+template<typename T>
+void IcingaDB::SendGroupsChanged(const ConfigObject::Ptr& object, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedGroups = GetArrayDeletedValues(oldValues, newValues);
+ String typeName = GetLowerCaseTypeNameDB(object);
+
+ for (const auto& groupName : deletedGroups) {
+ typename T::Ptr group = ConfigObject::GetObject<T>(groupName);
+ String id = HashValue(new Array({m_EnvironmentId, group->GetName(), object->GetName()}));
+ DeleteRelationship(id, typeName + "group:member");
+
+ if (std::is_same<T, UserGroup>::value) {
+ UserGroup::Ptr userGroup = dynamic_pointer_cast<UserGroup>(group);
+
+ for (const auto& notification : userGroup->GetNotifications()) {
+ String userId = HashValue(new Array({m_EnvironmentId, "usergroupuser", object->GetName(), groupName, notification->GetName()}));
+ DeleteRelationship(userId, "notification:recipient");
+ }
+ }
+ }
+}
+
+void IcingaDB::SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+ String typeName = GetLowerCaseTypeNameDB(command);
+
+ for (const auto& envvarKey : deletedKeys) {
+ String id = HashValue(new Array({m_EnvironmentId, envvarKey, command->GetName()}));
+ DeleteRelationship(id, typeName + ":envvar", true);
+ }
+}
+
+void IcingaDB::SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+ String typeName = GetLowerCaseTypeNameDB(command);
+
+ for (const auto& argumentKey : deletedKeys) {
+ String id = HashValue(new Array({m_EnvironmentId, argumentKey, command->GetName()}));
+ DeleteRelationship(id, typeName + ":argument", true);
+ }
+}
+
+void IcingaDB::SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ if (m_IndexedTypes.find(object->GetReflectionType().get()) == m_IndexedTypes.end()) {
+ return;
+ }
+
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ Dictionary::Ptr oldVars = SerializeVars(oldValues);
+ Dictionary::Ptr newVars = SerializeVars(newValues);
+
+ std::vector<String> deletedVars = GetDictionaryDeletedKeys(oldVars, newVars);
+ String typeName = GetLowerCaseTypeNameDB(object);
+
+ for (const auto& varId : deletedVars) {
+ String id = HashValue(new Array({m_EnvironmentId, varId, object->GetName()}));
+ DeleteRelationship(id, typeName + ":customvar");
+ }
+}
+
+Dictionary::Ptr IcingaDB::SerializeState(const Checkable::Ptr& checkable)
+{
+ Dictionary::Ptr attrs = new Dictionary();
+
+ Host::Ptr host;
+ Service::Ptr service;
+
+ tie(host, service) = GetHostService(checkable);
+
+ String id = GetObjectIdentifier(checkable);
+
+ /*
+ * As there is a 1:1 relationship between host and host state, the host ID ('host_id')
+ * is also used as the host state ID ('id'). These are duplicated to 1) avoid having
+ * special handling for this in Icinga DB and 2) to have both a primary key and a foreign key
+ * in the SQL database in the end. In the database 'host_id' ends up as foreign key 'host_state.host_id'
+ * referring to 'host.id' while 'id' ends up as the primary key 'host_state.id'. This also applies for service.
+ */
+ attrs->Set("id", id);
+ attrs->Set("environment_id", m_EnvironmentId);
+ attrs->Set("state_type", checkable->HasBeenChecked() ? checkable->GetStateType() : StateTypeHard);
+
+ // TODO: last_hard/soft_state should be "previous".
+ if (service) {
+ attrs->Set("service_id", id);
+ auto state = service->HasBeenChecked() ? service->GetState() : 99;
+ attrs->Set("soft_state", state);
+ attrs->Set("hard_state", service->HasBeenChecked() ? service->GetLastHardState() : 99);
+ attrs->Set("severity", service->GetSeverity());
+ attrs->Set("host_id", GetObjectIdentifier(host));
+ } else {
+ attrs->Set("host_id", id);
+ auto state = host->HasBeenChecked() ? host->GetState() : 99;
+ attrs->Set("soft_state", state);
+ attrs->Set("hard_state", host->HasBeenChecked() ? host->GetLastHardState() : 99);
+ attrs->Set("severity", host->GetSeverity());
+ }
+
+ attrs->Set("previous_soft_state", GetPreviousState(checkable, service, StateTypeSoft));
+ attrs->Set("previous_hard_state", GetPreviousState(checkable, service, StateTypeHard));
+ attrs->Set("check_attempt", checkable->GetCheckAttempt());
+
+ attrs->Set("is_active", checkable->IsActive());
+
+ CheckResult::Ptr cr = checkable->GetLastCheckResult();
+
+ if (cr) {
+ String rawOutput = cr->GetOutput();
+ if (!rawOutput.IsEmpty()) {
+ size_t lineBreak = rawOutput.Find("\n");
+ String output = rawOutput.SubStr(0, lineBreak);
+ if (!output.IsEmpty())
+ attrs->Set("output", rawOutput.SubStr(0, lineBreak));
+
+ if (lineBreak > 0 && lineBreak != String::NPos) {
+ String longOutput = rawOutput.SubStr(lineBreak+1, rawOutput.GetLength());
+ if (!longOutput.IsEmpty())
+ attrs->Set("long_output", longOutput);
+ }
+ }
+
+ String perfData = PluginUtility::FormatPerfdata(cr->GetPerformanceData());
+ if (!perfData.IsEmpty())
+ attrs->Set("performance_data", perfData);
+
+ String normedPerfData = PluginUtility::FormatPerfdata(cr->GetPerformanceData(), true);
+ if (!normedPerfData.IsEmpty())
+ attrs->Set("normalized_performance_data", normedPerfData);
+
+ if (!cr->GetCommand().IsEmpty())
+ attrs->Set("check_commandline", FormatCommandLine(cr->GetCommand()));
+ attrs->Set("execution_time", TimestampToMilliseconds(fmax(0.0, cr->CalculateExecutionTime())));
+ attrs->Set("latency", TimestampToMilliseconds(cr->CalculateLatency()));
+ attrs->Set("check_source", cr->GetCheckSource());
+ attrs->Set("scheduling_source", cr->GetSchedulingSource());
+ }
+
+ attrs->Set("is_problem", checkable->GetProblem());
+ attrs->Set("is_handled", checkable->GetHandled());
+ attrs->Set("is_reachable", checkable->IsReachable());
+ attrs->Set("is_flapping", checkable->IsFlapping());
+
+ attrs->Set("is_acknowledged", checkable->GetAcknowledgement());
+ if (checkable->IsAcknowledged()) {
+ Timestamp entry = 0;
+ Comment::Ptr AckComment;
+ for (const Comment::Ptr& c : checkable->GetComments()) {
+ if (c->GetEntryType() == CommentAcknowledgement) {
+ if (c->GetEntryTime() > entry) {
+ entry = c->GetEntryTime();
+ AckComment = c;
+ }
+ }
+ }
+ if (AckComment != nullptr) {
+ attrs->Set("acknowledgement_comment_id", GetObjectIdentifier(AckComment));
+ }
+ }
+
+ {
+ auto lastComment (checkable->GetLastComment());
+
+ if (lastComment) {
+ attrs->Set("last_comment_id", GetObjectIdentifier(lastComment));
+ }
+ }
+
+ attrs->Set("in_downtime", checkable->IsInDowntime());
+
+ if (checkable->GetCheckTimeout().IsEmpty())
+ attrs->Set("check_timeout", TimestampToMilliseconds(checkable->GetCheckCommand()->GetTimeout()));
+ else
+ attrs->Set("check_timeout", TimestampToMilliseconds(checkable->GetCheckTimeout()));
+
+ long long lastCheck = TimestampToMilliseconds(checkable->GetLastCheck());
+ if (lastCheck > 0)
+ attrs->Set("last_update", lastCheck);
+
+ attrs->Set("last_state_change", TimestampToMilliseconds(checkable->GetLastStateChange()));
+ attrs->Set("next_check", TimestampToMilliseconds(checkable->GetNextCheck()));
+ attrs->Set("next_update", TimestampToMilliseconds(checkable->GetNextUpdate()));
+
+ return attrs;
+}
+
+std::vector<String>
+IcingaDB::UpdateObjectAttrs(const ConfigObject::Ptr& object, int fieldType,
+ const String& typeNameOverride)
+{
+ Type::Ptr type = object->GetReflectionType();
+ Dictionary::Ptr attrs(new Dictionary);
+
+ for (int fid = 0; fid < type->GetFieldCount(); fid++) {
+ Field field = type->GetFieldInfo(fid);
+
+ if ((field.Attributes & fieldType) == 0)
+ continue;
+
+ Value val = object->GetField(fid);
+
+ /* hide attributes which shouldn't be user-visible */
+ if (field.Attributes & FANoUserView)
+ continue;
+
+ /* hide internal navigation fields */
+ if (field.Attributes & FANavigation && !(field.Attributes & (FAConfig | FAState)))
+ continue;
+
+ attrs->Set(field.Name, Serialize(val));
+ }
+
+ /* Downtimes require in_effect, which is not an attribute */
+ Downtime::Ptr downtime = dynamic_pointer_cast<Downtime>(object);
+ if (downtime) {
+ attrs->Set("in_effect", Serialize(downtime->IsInEffect()));
+ attrs->Set("trigger_time", Serialize(TimestampToMilliseconds(downtime->GetTriggerTime())));
+ }
+
+
+ /* Use the name checksum as unique key. */
+ String typeName = type->GetName().ToLower();
+ if (!typeNameOverride.IsEmpty())
+ typeName = typeNameOverride.ToLower();
+
+ return {GetObjectIdentifier(object), JsonEncode(attrs)};
+ //m_Rcon->FireAndForgetQuery({"HSET", keyPrefix + typeName, GetObjectIdentifier(object), JsonEncode(attrs)});
+}
+
+void IcingaDB::StateChangeHandler(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type)
+{
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendStateChange(object, cr, type);
+ }
+}
+
+void IcingaDB::ReachabilityChangeHandler(const std::set<Checkable::Ptr>& children)
+{
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ for (auto& checkable : children) {
+ rw->UpdateState(checkable, StateUpdate::Full);
+ }
+ }
+}
+
+void IcingaDB::VersionChangedHandler(const ConfigObject::Ptr& object)
+{
+ Type::Ptr type = object->GetReflectionType();
+
+ if (m_IndexedTypes.find(type.get()) == m_IndexedTypes.end()) {
+ return;
+ }
+
+ if (object->IsActive()) {
+ // Create or update the object config
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ if (rw)
+ rw->SendConfigUpdate(object, true);
+ }
+ } else if (!object->IsActive() &&
+ object->GetExtension("ConfigObjectDeleted")) { // same as in apilistener-configsync.cpp
+ // Delete object config
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ if (rw)
+ rw->SendConfigDelete(object);
+ }
+ }
+}
+
+void IcingaDB::DowntimeStartedHandler(const Downtime::Ptr& downtime)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendStartedDowntime(downtime);
+ }
+}
+
+void IcingaDB::DowntimeRemovedHandler(const Downtime::Ptr& downtime)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendRemovedDowntime(downtime);
+ }
+}
+
+void IcingaDB::NotificationSentToAllUsersHandler(
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text
+)
+{
+ auto rws (ConfigType::GetObjectsByType<IcingaDB>());
+ auto sendTime (notification->GetLastNotification());
+
+ if (!rws.empty()) {
+ for (auto& rw : rws) {
+ rw->SendSentNotification(notification, checkable, users, type, cr, author, text, sendTime);
+ }
+ }
+}
+
+void IcingaDB::CommentAddedHandler(const Comment::Ptr& comment)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendAddedComment(comment);
+ }
+}
+
+void IcingaDB::CommentRemovedHandler(const Comment::Ptr& comment)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendRemovedComment(comment);
+ }
+}
+
+void IcingaDB::FlappingChangeHandler(const Checkable::Ptr& checkable, double changeTime)
+{
+ auto flappingLastChange (checkable->GetFlappingLastChange());
+
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendFlappingChange(checkable, changeTime, flappingLastChange);
+ }
+}
+
+void IcingaDB::NewCheckResultHandler(const Checkable::Ptr& checkable)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->UpdateState(checkable, StateUpdate::Volatile);
+ rw->SendNextUpdate(checkable);
+ }
+}
+
+void IcingaDB::NextCheckUpdatedHandler(const Checkable::Ptr& checkable)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->UpdateState(checkable, StateUpdate::Volatile);
+ rw->SendNextUpdate(checkable);
+ }
+}
+
+void IcingaDB::HostProblemChangedHandler(const Service::Ptr& service) {
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ /* Host state changes affect is_handled and severity of services. */
+ rw->UpdateState(service, StateUpdate::Full);
+ }
+}
+
+void IcingaDB::AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry)
+{
+ auto rws (ConfigType::GetObjectsByType<IcingaDB>());
+
+ if (!rws.empty()) {
+ for (auto& rw : rws) {
+ rw->SendAcknowledgementSet(checkable, author, comment, type, persistent, changeTime, expiry);
+ }
+ }
+}
+
+void IcingaDB::AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime)
+{
+ auto rws (ConfigType::GetObjectsByType<IcingaDB>());
+
+ if (!rws.empty()) {
+ auto rb (Shared<String>::Make(removedBy));
+ auto ackLastChange (checkable->GetAcknowledgementLastChange());
+
+ for (auto& rw : rws) {
+ rw->SendAcknowledgementCleared(checkable, *rb, changeTime, ackLastChange);
+ }
+ }
+}
+
+void IcingaDB::NotificationUsersChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendNotificationUsersChanged(notification, oldValues, newValues);
+ }
+}
+
+void IcingaDB::NotificationUserGroupsChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendNotificationUserGroupsChanged(notification, oldValues, newValues);
+ }
+}
+
+void IcingaDB::TimePeriodRangesChangedHandler(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendTimePeriodRangesChanged(timeperiod, oldValues, newValues);
+ }
+}
+
+void IcingaDB::TimePeriodIncludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendTimePeriodIncludesChanged(timeperiod, oldValues, newValues);
+ }
+}
+
+void IcingaDB::TimePeriodExcludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendTimePeriodExcludesChanged(timeperiod, oldValues, newValues);
+ }
+}
+
+void IcingaDB::UserGroupsChangedHandler(const User::Ptr& user, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendGroupsChanged<UserGroup>(user, oldValues, newValues);
+ }
+}
+
+void IcingaDB::HostGroupsChangedHandler(const Host::Ptr& host, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendGroupsChanged<HostGroup>(host, oldValues, newValues);
+ }
+}
+
+void IcingaDB::ServiceGroupsChangedHandler(const Service::Ptr& service, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendGroupsChanged<ServiceGroup>(service, oldValues, newValues);
+ }
+}
+
+void IcingaDB::CommandEnvChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendCommandEnvChanged(command, oldValues, newValues);
+ }
+}
+
+void IcingaDB::CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendCommandArgumentsChanged(command, oldValues, newValues);
+ }
+}
+
+void IcingaDB::CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendCustomVarsChanged(object, oldValues, newValues);
+ }
+}
+
+void IcingaDB::DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum) {
+ Log(LogNotice, "IcingaDB") << "Deleting relationship '" << redisKeyWithoutPrefix << " -> '" << id << "'";
+
+ String redisKey = m_PrefixConfigObject + redisKeyWithoutPrefix;
+
+ std::vector<std::vector<String>> queries;
+
+ if (hasChecksum) {
+ queries.push_back({"HDEL", m_PrefixConfigCheckSum + redisKeyWithoutPrefix, id});
+ }
+
+ queries.push_back({"HDEL", redisKey, id});
+ queries.push_back({
+ "XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*",
+ "redis_key", redisKey, "id", id, "runtime_type", "delete"
+ });
+
+ m_Rcon->FireAndForgetQueries(queries, Prio::Config);
+}
diff --git a/lib/icingadb/icingadb-stats.cpp b/lib/icingadb/icingadb-stats.cpp
new file mode 100644
index 0000000..476756b
--- /dev/null
+++ b/lib/icingadb/icingadb-stats.cpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadb.hpp"
+#include "base/application.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/statsfunction.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+Dictionary::Ptr IcingaDB::GetStats()
+{
+ Dictionary::Ptr stats = new Dictionary();
+
+ //TODO: Figure out if more stats can be useful here.
+ Namespace::Ptr statsFunctions = ScriptGlobal::Get("StatsFunctions", &Empty);
+
+ if (!statsFunctions)
+ Dictionary::Ptr();
+
+ ObjectLock olock(statsFunctions);
+
+ for (auto& kv : statsFunctions)
+ {
+ Function::Ptr func = kv.second.Val;
+
+ if (!func)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid status function name."));
+
+ Dictionary::Ptr status = new Dictionary();
+ Array::Ptr perfdata = new Array();
+ func->Invoke({ status, perfdata });
+
+ stats->Set(kv.first, new Dictionary({
+ { "status", status },
+ { "perfdata", Serialize(perfdata, FAState) }
+ }));
+ }
+
+ typedef Dictionary::Ptr DP;
+ DP app = DP(DP(DP(stats->Get("IcingaApplication"))->Get("status"))->Get("icingaapplication"))->Get("app");
+
+ app->Set("program_start", TimestampToMilliseconds(Application::GetStartTime()));
+
+ auto localEndpoint (Endpoint::GetLocalEndpoint());
+ if (localEndpoint) {
+ app->Set("endpoint_id", GetObjectIdentifier(localEndpoint));
+ }
+
+ return stats;
+}
+
diff --git a/lib/icingadb/icingadb-utility.cpp b/lib/icingadb/icingadb-utility.cpp
new file mode 100644
index 0000000..b247ed8
--- /dev/null
+++ b/lib/icingadb/icingadb-utility.cpp
@@ -0,0 +1,319 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadb.hpp"
+#include "base/configtype.hpp"
+#include "base/object-packer.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/tlsutility.hpp"
+#include "base/initialize.hpp"
+#include "base/objectlock.hpp"
+#include "base/array.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/convert.hpp"
+#include "base/json.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/host.hpp"
+#include <boost/algorithm/string.hpp>
+#include <map>
+#include <utility>
+#include <vector>
+
+using namespace icinga;
+
+String IcingaDB::FormatCheckSumBinary(const String& str)
+{
+ char output[20*2+1];
+ for (int i = 0; i < 20; i++)
+ sprintf(output + 2 * i, "%02x", str[i]);
+
+ return output;
+}
+
+String IcingaDB::FormatCommandLine(const Value& commandLine)
+{
+ String result;
+ if (commandLine.IsObjectType<Array>()) {
+ Array::Ptr args = commandLine;
+ bool first = true;
+
+ ObjectLock olock(args);
+ for (const Value& arg : args) {
+ String token = "'" + Convert::ToString(arg) + "'";
+
+ if (first)
+ first = false;
+ else
+ result += String(1, ' ');
+
+ result += token;
+ }
+ } else if (!commandLine.IsEmpty()) {
+ result = commandLine;
+ boost::algorithm::replace_all(result, "\'", "\\'");
+ result = "'" + result + "'";
+ }
+
+ return result;
+}
+
+String IcingaDB::GetObjectIdentifier(const ConfigObject::Ptr& object)
+{
+ String identifier = object->GetIcingadbIdentifier();
+ if (identifier.IsEmpty()) {
+ identifier = HashValue(new Array({m_EnvironmentId, object->GetName()}));
+ object->SetIcingadbIdentifier(identifier);
+ }
+
+ return identifier;
+}
+
+/**
+ * Calculates a deterministic history event ID like SHA1(env, eventType, x...[, nt][, eventTime])
+ *
+ * Where SHA1(env, x...) = GetObjectIdentifier(object)
+ */
+String IcingaDB::CalcEventID(const char* eventType, const ConfigObject::Ptr& object, double eventTime, NotificationType nt)
+{
+ Array::Ptr rawId = new Array({object->GetName()});
+ rawId->Insert(0, m_EnvironmentId);
+ rawId->Insert(1, eventType);
+
+ if (nt) {
+ rawId->Add(GetNotificationTypeByEnum(nt));
+ }
+
+ if (eventTime) {
+ rawId->Add(TimestampToMilliseconds(eventTime));
+ }
+
+ return HashValue(std::move(rawId));
+}
+
+static const std::set<String> metadataWhitelist ({"package", "source_location", "templates"});
+
+/**
+ * Prepare custom vars for being written to Redis
+ *
+ * object.vars = {
+ * "disks": {
+ * "disk": {},
+ * "disk /": {
+ * "disk_partitions": "/"
+ * }
+ * }
+ * }
+ *
+ * return {
+ * SHA1(PackObject([
+ * EnvironmentId,
+ * "disks",
+ * {
+ * "disk": {},
+ * "disk /": {
+ * "disk_partitions": "/"
+ * }
+ * }
+ * ])): {
+ * "environment_id": EnvironmentId,
+ * "name_checksum": SHA1("disks"),
+ * "name": "disks",
+ * "value": {
+ * "disk": {},
+ * "disk /": {
+ * "disk_partitions": "/"
+ * }
+ * }
+ * }
+ * }
+ *
+ * @param Dictionary Config object with custom vars
+ *
+ * @return JSON-like data structure for Redis
+ */
+Dictionary::Ptr IcingaDB::SerializeVars(const Dictionary::Ptr& vars)
+{
+ if (!vars)
+ return nullptr;
+
+ Dictionary::Ptr res = new Dictionary();
+
+ ObjectLock olock(vars);
+
+ for (auto& kv : vars) {
+ res->Set(
+ SHA1(PackObject((Array::Ptr)new Array({m_EnvironmentId, kv.first, kv.second}))),
+ (Dictionary::Ptr)new Dictionary({
+ {"environment_id", m_EnvironmentId},
+ {"name_checksum", SHA1(kv.first)},
+ {"name", kv.first},
+ {"value", JsonEncode(kv.second)},
+ })
+ );
+ }
+
+ return res;
+}
+
+const char* IcingaDB::GetNotificationTypeByEnum(NotificationType type)
+{
+ switch (type) {
+ case NotificationDowntimeStart:
+ return "downtime_start";
+ case NotificationDowntimeEnd:
+ return "downtime_end";
+ case NotificationDowntimeRemoved:
+ return "downtime_removed";
+ case NotificationCustom:
+ return "custom";
+ case NotificationAcknowledgement:
+ return "acknowledgement";
+ case NotificationProblem:
+ return "problem";
+ case NotificationRecovery:
+ return "recovery";
+ case NotificationFlappingStart:
+ return "flapping_start";
+ case NotificationFlappingEnd:
+ return "flapping_end";
+ }
+
+ VERIFY(!"Invalid notification type.");
+}
+
+static const std::set<String> propertiesBlacklistEmpty;
+
+String IcingaDB::HashValue(const Value& value)
+{
+ return HashValue(value, propertiesBlacklistEmpty);
+}
+
+String IcingaDB::HashValue(const Value& value, const std::set<String>& propertiesBlacklist, bool propertiesWhitelist)
+{
+ Value temp;
+ bool mutabl;
+
+ Type::Ptr type = value.GetReflectionType();
+
+ if (ConfigObject::TypeInstance->IsAssignableFrom(type)) {
+ temp = Serialize(value, FAConfig);
+ mutabl = true;
+ } else {
+ temp = value;
+ mutabl = false;
+ }
+
+ if (propertiesBlacklist.size() && temp.IsObject()) {
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>((Object::Ptr)temp);
+
+ if (dict) {
+ if (!mutabl)
+ dict = dict->ShallowClone();
+
+ ObjectLock olock(dict);
+
+ if (propertiesWhitelist) {
+ auto current = dict->Begin();
+ auto propertiesBlacklistEnd = propertiesBlacklist.end();
+
+ while (current != dict->End()) {
+ if (propertiesBlacklist.find(current->first) == propertiesBlacklistEnd) {
+ dict->Remove(current++);
+ } else {
+ ++current;
+ }
+ }
+ } else {
+ for (auto& property : propertiesBlacklist)
+ dict->Remove(property);
+ }
+
+ if (!mutabl)
+ temp = dict;
+ }
+ }
+
+ return SHA1(PackObject(temp));
+}
+
+String IcingaDB::GetLowerCaseTypeNameDB(const ConfigObject::Ptr& obj)
+{
+ return obj->GetReflectionType()->GetName().ToLower();
+}
+
+long long IcingaDB::TimestampToMilliseconds(double timestamp) {
+ return static_cast<long long>(timestamp * 1000);
+}
+
+String IcingaDB::IcingaToStreamValue(const Value& value)
+{
+ switch (value.GetType()) {
+ case ValueBoolean:
+ return Convert::ToString(int(value));
+ case ValueString:
+ return Utility::ValidateUTF8(value);
+ case ValueNumber:
+ case ValueEmpty:
+ return Convert::ToString(value);
+ default:
+ return JsonEncode(value);
+ }
+}
+
+// Returns the items that exist in "arrayOld" but not in "arrayNew"
+std::vector<Value> IcingaDB::GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew) {
+ std::vector<Value> deletedValues;
+
+ if (!arrayOld) {
+ return deletedValues;
+ }
+
+ if (!arrayNew) {
+ ObjectLock olock (arrayOld);
+ return std::vector<Value>(arrayOld->Begin(), arrayOld->End());
+ }
+
+ std::vector<Value> vectorOld;
+ {
+ ObjectLock olock (arrayOld);
+ vectorOld.assign(arrayOld->Begin(), arrayOld->End());
+ }
+ std::sort(vectorOld.begin(), vectorOld.end());
+ vectorOld.erase(std::unique(vectorOld.begin(), vectorOld.end()), vectorOld.end());
+
+ std::vector<Value> vectorNew;
+ {
+ ObjectLock olock (arrayNew);
+ vectorNew.assign(arrayNew->Begin(), arrayNew->End());
+ }
+ std::sort(vectorNew.begin(), vectorNew.end());
+ vectorNew.erase(std::unique(vectorNew.begin(), vectorNew.end()), vectorNew.end());
+
+ std::set_difference(vectorOld.begin(), vectorOld.end(), vectorNew.begin(), vectorNew.end(), std::back_inserter(deletedValues));
+
+ return deletedValues;
+}
+
+// Returns the keys that exist in "dictOld" but not in "dictNew"
+std::vector<String> IcingaDB::GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew) {
+ std::vector<String> deletedKeys;
+
+ if (!dictOld) {
+ return deletedKeys;
+ }
+
+ std::vector<String> oldKeys = dictOld->GetKeys();
+
+ if (!dictNew) {
+ return oldKeys;
+ }
+
+ std::vector<String> newKeys = dictNew->GetKeys();
+
+ std::set_difference(oldKeys.begin(), oldKeys.end(), newKeys.begin(), newKeys.end(), std::back_inserter(deletedKeys));
+
+ return deletedKeys;
+}
diff --git a/lib/icingadb/icingadb.cpp b/lib/icingadb/icingadb.cpp
new file mode 100644
index 0000000..6d5ded9
--- /dev/null
+++ b/lib/icingadb/icingadb.cpp
@@ -0,0 +1,311 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadb.hpp"
+#include "icingadb/icingadb-ti.cpp"
+#include "icingadb/redisconnection.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/eventqueue.hpp"
+#include "base/configuration.hpp"
+#include "base/json.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/statsfunction.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include <boost/algorithm/string.hpp>
+#include <fstream>
+#include <memory>
+#include <utility>
+
+using namespace icinga;
+
+#define MAX_EVENTS_DEFAULT 5000
+
+using Prio = RedisConnection::QueryPriority;
+
+String IcingaDB::m_EnvironmentId;
+std::mutex IcingaDB::m_EnvironmentIdInitMutex;
+
+REGISTER_TYPE(IcingaDB);
+
+IcingaDB::IcingaDB()
+ : m_Rcon(nullptr)
+{
+ m_RconLocked.store(nullptr);
+
+ m_WorkQueue.SetName("IcingaDB");
+
+ m_PrefixConfigObject = "icinga:";
+ m_PrefixConfigCheckSum = "icinga:checksum:";
+}
+
+void IcingaDB::Validate(int types, const ValidationUtils& utils)
+{
+ ObjectImpl<IcingaDB>::Validate(types, utils);
+
+ if (!(types & FAConfig))
+ return;
+
+ if (GetEnableTls() && GetCertPath().IsEmpty() != GetKeyPath().IsEmpty()) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, std::vector<String>(), "Validation failed: Either both a client certificate (cert_path) and its private key (key_path) or none of them must be given."));
+ }
+
+ try {
+ InitEnvironmentId();
+ } catch (const std::exception& e) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, std::vector<String>(),
+ String("Validation failed: ") + e.what()));
+ }
+}
+
+/**
+ * Starts the component.
+ */
+void IcingaDB::Start(bool runtimeCreated)
+{
+ ObjectImpl<IcingaDB>::Start(runtimeCreated);
+
+ VERIFY(!m_EnvironmentId.IsEmpty());
+ PersistEnvironmentId();
+
+ Log(LogInformation, "IcingaDB")
+ << "'" << GetName() << "' started.";
+
+ m_ConfigDumpInProgress = false;
+ m_ConfigDumpDone = false;
+
+ m_WorkQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); });
+
+ m_Rcon = new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(),
+ GetEnableTls(), GetInsecureNoverify(), GetCertPath(), GetKeyPath(), GetCaPath(), GetCrlPath(),
+ GetTlsProtocolmin(), GetCipherList(), GetConnectTimeout(), GetDebugInfo());
+ m_RconLocked.store(m_Rcon);
+
+ for (const Type::Ptr& type : GetTypes()) {
+ auto ctype (dynamic_cast<ConfigType*>(type.get()));
+ if (!ctype)
+ continue;
+
+ RedisConnection::Ptr con = new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(),
+ GetEnableTls(), GetInsecureNoverify(), GetCertPath(), GetKeyPath(), GetCaPath(), GetCrlPath(),
+ GetTlsProtocolmin(), GetCipherList(), GetConnectTimeout(), GetDebugInfo(), m_Rcon);
+
+ con->SetConnectedCallback([this, con](boost::asio::yield_context& yc) {
+ con->SetConnectedCallback(nullptr);
+
+ size_t pending = --m_PendingRcons;
+ Log(LogDebug, "IcingaDB") << pending << " pending child connections remaining";
+ if (pending == 0) {
+ m_WorkQueue.Enqueue([this]() { OnConnectedHandler(); });
+ }
+ });
+
+ m_Rcons[ctype] = std::move(con);
+ }
+
+ m_PendingRcons = m_Rcons.size();
+
+ m_Rcon->SetConnectedCallback([this](boost::asio::yield_context& yc) {
+ m_Rcon->SetConnectedCallback(nullptr);
+
+ for (auto& kv : m_Rcons) {
+ kv.second->Start();
+ }
+ });
+ m_Rcon->Start();
+
+ m_StatsTimer = Timer::Create();
+ m_StatsTimer->SetInterval(1);
+ m_StatsTimer->OnTimerExpired.connect([this](const Timer * const&) { PublishStatsTimerHandler(); });
+ m_StatsTimer->Start();
+
+ m_WorkQueue.SetName("IcingaDB");
+
+ m_Rcon->SuppressQueryKind(Prio::CheckResult);
+ m_Rcon->SuppressQueryKind(Prio::RuntimeStateSync);
+
+ Ptr keepAlive (this);
+
+ m_HistoryThread = std::async(std::launch::async, [this, keepAlive]() { ForwardHistoryEntries(); });
+}
+
+void IcingaDB::ExceptionHandler(boost::exception_ptr exp)
+{
+ Log(LogCritical, "IcingaDB", "Exception during redis query. Verify that Redis is operational.");
+
+ Log(LogDebug, "IcingaDB")
+ << "Exception during redis operation: " << DiagnosticInformation(exp);
+}
+
+void IcingaDB::OnConnectedHandler()
+{
+ AssertOnWorkQueue();
+
+ if (m_ConfigDumpInProgress || m_ConfigDumpDone)
+ return;
+
+ /* Config dump */
+ m_ConfigDumpInProgress = true;
+ PublishStats();
+
+ UpdateAllConfigObjects();
+
+ m_ConfigDumpDone = true;
+
+ m_ConfigDumpInProgress = false;
+}
+
+void IcingaDB::PublishStatsTimerHandler(void)
+{
+ PublishStats();
+}
+
+void IcingaDB::PublishStats()
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ Dictionary::Ptr status = GetStats();
+ status->Set("config_dump_in_progress", m_ConfigDumpInProgress);
+ status->Set("timestamp", TimestampToMilliseconds(Utility::GetTime()));
+ status->Set("icingadb_environment", m_EnvironmentId);
+
+ std::vector<String> query {"XADD", "icinga:stats", "MAXLEN", "1", "*"};
+
+ {
+ ObjectLock statusLock (status);
+ for (auto& kv : status) {
+ query.emplace_back(kv.first);
+ query.emplace_back(JsonEncode(kv.second));
+ }
+ }
+
+ m_Rcon->FireAndForgetQuery(std::move(query), Prio::Heartbeat);
+}
+
+void IcingaDB::Stop(bool runtimeRemoved)
+{
+ Log(LogInformation, "IcingaDB")
+ << "Flushing history data buffer to Redis.";
+
+ if (m_HistoryThread.wait_for(std::chrono::minutes(1)) == std::future_status::timeout) {
+ Log(LogCritical, "IcingaDB")
+ << "Flushing takes more than one minute (while we're about to shut down). Giving up and discarding "
+ << m_HistoryBulker.Size() << " queued history queries.";
+ }
+
+ m_StatsTimer->Stop(true);
+
+ Log(LogInformation, "IcingaDB")
+ << "'" << GetName() << "' stopped.";
+
+ ObjectImpl<IcingaDB>::Stop(runtimeRemoved);
+}
+
+void IcingaDB::ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<IcingaDB>::ValidateTlsProtocolmin(lvalue, utils);
+
+ try {
+ ResolveTlsProtocolVersion(lvalue());
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "tls_protocolmin" }, ex.what()));
+ }
+}
+
+void IcingaDB::ValidateConnectTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<IcingaDB>::ValidateConnectTimeout(lvalue, utils);
+
+ if (lvalue() <= 0) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "connect_timeout" }, "Value must be greater than 0."));
+ }
+}
+
+void IcingaDB::AssertOnWorkQueue()
+{
+ ASSERT(m_WorkQueue.IsWorkerThread());
+}
+
+void IcingaDB::DumpedGlobals::Reset()
+{
+ std::lock_guard<std::mutex> l (m_Mutex);
+ m_Ids.clear();
+}
+
+String IcingaDB::GetEnvironmentId() const {
+ return m_EnvironmentId;
+}
+
+bool IcingaDB::DumpedGlobals::IsNew(const String& id)
+{
+ std::lock_guard<std::mutex> l (m_Mutex);
+ return m_Ids.emplace(id).second;
+}
+
+/**
+ * Initializes the m_EnvironmentId attribute or throws an exception on failure to do so. Can be called concurrently.
+ */
+void IcingaDB::InitEnvironmentId()
+{
+ // Initialize m_EnvironmentId once across all IcingaDB objects. In theory, this could be done using
+ // std::call_once, however, due to a bug in libstdc++ (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66146),
+ // this can result in a deadlock when an exception is thrown (which is explicitly allowed by the standard).
+ std::unique_lock<std::mutex> lock (m_EnvironmentIdInitMutex);
+
+ if (m_EnvironmentId.IsEmpty()) {
+ String path = Configuration::DataDir + "/icingadb.env";
+ String envId;
+
+ if (Utility::PathExists(path)) {
+ envId = Utility::LoadJsonFile(path);
+
+ if (envId.GetLength() != 2*SHA_DIGEST_LENGTH) {
+ throw std::runtime_error("environment ID stored at " + path + " is corrupt: wrong length.");
+ }
+
+ for (unsigned char c : envId) {
+ if (!std::isxdigit(c)) {
+ throw std::runtime_error("environment ID stored at " + path + " is corrupt: invalid hex string.");
+ }
+ }
+ } else {
+ String caPath = ApiListener::GetDefaultCaPath();
+
+ if (!Utility::PathExists(caPath)) {
+ throw std::runtime_error("Cannot find the CA certificate at '" + caPath + "'. "
+ "Please ensure the ApiListener is enabled first using 'icinga2 api setup'.");
+ }
+
+ std::shared_ptr<X509> cert = GetX509Certificate(caPath);
+
+ unsigned int n;
+ unsigned char digest[EVP_MAX_MD_SIZE];
+ if (X509_pubkey_digest(cert.get(), EVP_sha1(), digest, &n) != 1) {
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("X509_pubkey_digest")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ envId = BinaryToHex(digest, n);
+ }
+
+ m_EnvironmentId = envId.ToLower();
+ }
+}
+
+/**
+ * Ensures that the environment ID is persisted on disk or throws an exception on failure to do so.
+ * Can be called concurrently.
+ */
+void IcingaDB::PersistEnvironmentId()
+{
+ String path = Configuration::DataDir + "/icingadb.env";
+
+ std::unique_lock<std::mutex> lock (m_EnvironmentIdInitMutex);
+
+ if (!Utility::PathExists(path)) {
+ Utility::SaveJsonFile(path, 0600, m_EnvironmentId);
+ }
+}
diff --git a/lib/icingadb/icingadb.hpp b/lib/icingadb/icingadb.hpp
new file mode 100644
index 0000000..6652d9c
--- /dev/null
+++ b/lib/icingadb/icingadb.hpp
@@ -0,0 +1,241 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ICINGADB_H
+#define ICINGADB_H
+
+#include "icingadb/icingadb-ti.hpp"
+#include "icingadb/redisconnection.hpp"
+#include "base/atomic.hpp"
+#include "base/bulker.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/service.hpp"
+#include "icinga/downtime.hpp"
+#include "remote/messageorigin.hpp"
+#include <atomic>
+#include <chrono>
+#include <future>
+#include <memory>
+#include <mutex>
+#include <set>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+
+namespace icinga
+{
+
+/**
+ * @ingroup icingadb
+ */
+class IcingaDB : public ObjectImpl<IcingaDB>
+{
+public:
+ DECLARE_OBJECT(IcingaDB);
+ DECLARE_OBJECTNAME(IcingaDB);
+
+ IcingaDB();
+
+ static void ConfigStaticInitialize();
+
+ void Validate(int types, const ValidationUtils& utils) override;
+ virtual void Start(bool runtimeCreated) override;
+ virtual void Stop(bool runtimeRemoved) override;
+
+ String GetEnvironmentId() const override;
+
+ inline RedisConnection::Ptr GetConnection()
+ {
+ return m_RconLocked.load();
+ }
+
+ template<class T>
+ static void AddKvsToMap(const Array::Ptr& kvs, T& map)
+ {
+ Value* key = nullptr;
+ ObjectLock oLock (kvs);
+
+ for (auto& kv : kvs) {
+ if (key) {
+ map.emplace(std::move(*key), std::move(kv));
+ key = nullptr;
+ } else {
+ key = &kv;
+ }
+ }
+ }
+
+protected:
+ void ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+ void ValidateConnectTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ class DumpedGlobals
+ {
+ public:
+ void Reset();
+ bool IsNew(const String& id);
+
+ private:
+ std::set<String> m_Ids;
+ std::mutex m_Mutex;
+ };
+
+ enum StateUpdate
+ {
+ Volatile = 1ull << 0,
+ RuntimeOnly = 1ull << 1,
+ Full = Volatile | RuntimeOnly,
+ };
+
+ void OnConnectedHandler();
+
+ void PublishStatsTimerHandler();
+ void PublishStats();
+
+ /* config & status dump */
+ void UpdateAllConfigObjects();
+ std::vector<std::vector<intrusive_ptr<ConfigObject>>> ChunkObjects(std::vector<intrusive_ptr<ConfigObject>> objects, size_t chunkSize);
+ void DeleteKeys(const RedisConnection::Ptr& conn, const std::vector<String>& keys, RedisConnection::QueryPriority priority);
+ std::vector<String> GetTypeOverwriteKeys(const String& type);
+ std::vector<String> GetTypeDumpSignalKeys(const Type::Ptr& type);
+ void InsertObjectDependencies(const ConfigObject::Ptr& object, const String typeName, std::map<String, std::vector<String>>& hMSets,
+ std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate);
+ void UpdateState(const Checkable::Ptr& checkable, StateUpdate mode);
+ void SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpdate);
+ void CreateConfigUpdate(const ConfigObject::Ptr& object, const String type, std::map<String, std::vector<String>>& hMSets,
+ std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate);
+ void SendConfigDelete(const ConfigObject::Ptr& object);
+ void SendStateChange(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type);
+ void AddObjectDataToRuntimeUpdates(std::vector<Dictionary::Ptr>& runtimeUpdates, const String& objectKey,
+ const String& redisKey, const Dictionary::Ptr& data);
+ void DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum = false);
+
+ void SendSentNotification(
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text, double sendTime
+ );
+
+ void SendStartedDowntime(const Downtime::Ptr& downtime);
+ void SendRemovedDowntime(const Downtime::Ptr& downtime);
+ void SendAddedComment(const Comment::Ptr& comment);
+ void SendRemovedComment(const Comment::Ptr& comment);
+ void SendFlappingChange(const Checkable::Ptr& checkable, double changeTime, double flappingLastChange);
+ void SendNextUpdate(const Checkable::Ptr& checkable);
+ void SendAcknowledgementSet(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry);
+ void SendAcknowledgementCleared(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, double ackLastChange);
+ void SendNotificationUsersChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ void SendNotificationUserGroupsChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ void SendTimePeriodRangesChanged(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ void SendTimePeriodIncludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ void SendTimePeriodExcludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ template<class T>
+ void SendGroupsChanged(const ConfigObject::Ptr& command, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ void SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ void SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ void SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+
+ void ForwardHistoryEntries();
+
+ std::vector<String> UpdateObjectAttrs(const ConfigObject::Ptr& object, int fieldType, const String& typeNameOverride);
+ Dictionary::Ptr SerializeState(const Checkable::Ptr& checkable);
+
+ /* Stats */
+ Dictionary::Ptr GetStats();
+
+ /* utilities */
+ static String FormatCheckSumBinary(const String& str);
+ static String FormatCommandLine(const Value& commandLine);
+ static long long TimestampToMilliseconds(double timestamp);
+ static String IcingaToStreamValue(const Value& value);
+ static std::vector<Value> GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew);
+ static std::vector<String> GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew);
+
+ static String GetObjectIdentifier(const ConfigObject::Ptr& object);
+ static String CalcEventID(const char* eventType, const ConfigObject::Ptr& object, double eventTime = 0, NotificationType nt = NotificationType(0));
+ static const char* GetNotificationTypeByEnum(NotificationType type);
+ static Dictionary::Ptr SerializeVars(const Dictionary::Ptr& vars);
+
+ static String HashValue(const Value& value);
+ static String HashValue(const Value& value, const std::set<String>& propertiesBlacklist, bool propertiesWhitelist = false);
+
+ static String GetLowerCaseTypeNameDB(const ConfigObject::Ptr& obj);
+ static bool PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& attributes, Dictionary::Ptr& checkSums);
+
+ static void ReachabilityChangeHandler(const std::set<Checkable::Ptr>& children);
+ static void StateChangeHandler(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type);
+ static void VersionChangedHandler(const ConfigObject::Ptr& object);
+ static void DowntimeStartedHandler(const Downtime::Ptr& downtime);
+ static void DowntimeRemovedHandler(const Downtime::Ptr& downtime);
+
+ static void NotificationSentToAllUsersHandler(
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text
+ );
+
+ static void CommentAddedHandler(const Comment::Ptr& comment);
+ static void CommentRemovedHandler(const Comment::Ptr& comment);
+ static void FlappingChangeHandler(const Checkable::Ptr& checkable, double changeTime);
+ static void NewCheckResultHandler(const Checkable::Ptr& checkable);
+ static void NextCheckUpdatedHandler(const Checkable::Ptr& checkable);
+ static void HostProblemChangedHandler(const Service::Ptr& service);
+ static void AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry);
+ static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime);
+ static void NotificationUsersChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void NotificationUserGroupsChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void TimePeriodRangesChangedHandler(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ static void TimePeriodIncludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void TimePeriodExcludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void UserGroupsChangedHandler(const User::Ptr& user, const Array::Ptr&, const Array::Ptr& newValues);
+ static void HostGroupsChangedHandler(const Host::Ptr& host, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void ServiceGroupsChangedHandler(const Service::Ptr& service, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void CommandEnvChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ static void CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ static void CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+
+ void AssertOnWorkQueue();
+
+ void ExceptionHandler(boost::exception_ptr exp);
+
+ static std::vector<Type::Ptr> GetTypes();
+
+ static void InitEnvironmentId();
+ static void PersistEnvironmentId();
+
+ Timer::Ptr m_StatsTimer;
+ WorkQueue m_WorkQueue{0, 1, LogNotice};
+
+ std::future<void> m_HistoryThread;
+ Bulker<RedisConnection::Query> m_HistoryBulker {4096, std::chrono::milliseconds(250)};
+
+ String m_PrefixConfigObject;
+ String m_PrefixConfigCheckSum;
+
+ bool m_ConfigDumpInProgress;
+ bool m_ConfigDumpDone;
+
+ RedisConnection::Ptr m_Rcon;
+ // m_RconLocked containes a copy of the value in m_Rcon where all accesses are guarded by a mutex to allow safe
+ // concurrent access like from the icingadb check command. It's a copy to still allow fast access without additional
+ // syncronization to m_Rcon within the IcingaDB feature itself.
+ Locked<RedisConnection::Ptr> m_RconLocked;
+ std::unordered_map<ConfigType*, RedisConnection::Ptr> m_Rcons;
+ std::atomic_size_t m_PendingRcons;
+
+ struct {
+ DumpedGlobals CustomVar, ActionUrl, NotesUrl, IconImage;
+ } m_DumpedGlobals;
+
+ // m_EnvironmentId is shared across all IcingaDB objects (typically there is at most one, but it is perfectly fine
+ // to have multiple ones). It is initialized once (synchronized using m_EnvironmentIdInitMutex). After successful
+ // initialization, the value is read-only and can be accessed without further synchronization.
+ static String m_EnvironmentId;
+ static std::mutex m_EnvironmentIdInitMutex;
+
+ static std::unordered_set<Type*> m_IndexedTypes;
+};
+}
+
+#endif /* ICINGADB_H */
diff --git a/lib/icingadb/icingadb.ti b/lib/icingadb/icingadb.ti
new file mode 100644
index 0000000..1c649c8
--- /dev/null
+++ b/lib/icingadb/icingadb.ti
@@ -0,0 +1,63 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/tlsutility.hpp"
+
+library icingadb;
+
+namespace icinga
+{
+
+class IcingaDB : ConfigObject
+{
+ activation_priority 100;
+
+ [config] String host {
+ default {{{ return "127.0.0.1"; }}}
+ };
+ [config] int port {
+ default {{{ return 6380; }}}
+ };
+ [config] String path;
+ [config, no_user_view, no_user_modify] String password;
+ [config] int db_index;
+
+ [config] bool enable_tls {
+ default {{{ return false; }}}
+ };
+
+ [config] bool insecure_noverify {
+ default {{{ return false; }}}
+ };
+
+ [config] String cert_path;
+ [config] String key_path;
+ [config] String ca_path;
+ [config] String crl_path;
+ [config] String cipher_list {
+ default {{{ return DEFAULT_TLS_CIPHERS; }}}
+ };
+ [config] String tls_protocolmin {
+ default {{{ return DEFAULT_TLS_PROTOCOLMIN; }}}
+ };
+
+ [config] double connect_timeout {
+ default {{{ return DEFAULT_CONNECT_TIMEOUT; }}}
+ };
+
+ [no_storage] String environment_id {
+ get;
+ };
+
+ [set_protected] double ongoing_dump_start {
+ default {{{ return 0; }}}
+ };
+ [state, set_protected] double lastdump_end {
+ default {{{ return 0; }}}
+ };
+ [state, set_protected] double lastdump_took {
+ default {{{ return 0; }}}
+ };
+};
+
+}
diff --git a/lib/icingadb/icingadbchecktask.cpp b/lib/icingadb/icingadbchecktask.cpp
new file mode 100644
index 0000000..f7c5964
--- /dev/null
+++ b/lib/icingadb/icingadbchecktask.cpp
@@ -0,0 +1,513 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadbchecktask.hpp"
+#include "icinga/host.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/function.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/convert.hpp"
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, IcingadbCheck, &IcingadbCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+static void ReportIcingadbCheck(
+ const Checkable::Ptr& checkable, const CheckCommand::Ptr& commandObj,
+ const CheckResult::Ptr& cr, String output, ServiceState state)
+{
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = std::move(output);
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandObj->GetName(), pr);
+ } else {
+ cr->SetState(state);
+ cr->SetOutput(output);
+ checkable->ProcessCheckResult(cr);
+ }
+}
+
+static inline
+double GetXMessageTs(const Array::Ptr& xMessage)
+{
+ return Convert::ToLong(String(xMessage->Get(0)).Split("-")[0]) / 1000.0;
+}
+
+void IcingadbCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ CheckCommand::Ptr commandObj = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+ String silenceMissingMacroWarning;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", commandObj);
+
+ auto resolve ([&](const String& macro) {
+ return MacroProcessor::ResolveMacros(macro, resolvers, checkable->GetLastCheckResult(),
+ &silenceMissingMacroWarning, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+ });
+
+ struct Thresholds
+ {
+ Value Warning, Critical;
+ };
+
+ auto resolveThresholds ([&resolve](const String& wmacro, const String& cmacro) {
+ return Thresholds{resolve(wmacro), resolve(cmacro)};
+ });
+
+ String icingadbName = resolve("$icingadb_name$");
+
+ auto dumpTakesThresholds (resolveThresholds("$icingadb_full_dump_duration_warning$", "$icingadb_full_dump_duration_critical$"));
+ auto syncTakesThresholds (resolveThresholds("$icingadb_full_sync_duration_warning$", "$icingadb_full_sync_duration_critical$"));
+ auto icingaBacklogThresholds (resolveThresholds("$icingadb_redis_backlog_warning$", "$icingadb_redis_backlog_critical$"));
+ auto icingadbBacklogThresholds (resolveThresholds("$icingadb_database_backlog_warning$", "$icingadb_database_backlog_critical$"));
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ if (icingadbName.IsEmpty()) {
+ ReportIcingadbCheck(checkable, commandObj, cr, "Icinga DB UNKNOWN: Attribute 'icingadb_name' must be set.", ServiceUnknown);
+ return;
+ }
+
+ auto conn (IcingaDB::GetByName(icingadbName));
+
+ if (!conn) {
+ ReportIcingadbCheck(checkable, commandObj, cr, "Icinga DB UNKNOWN: Icinga DB connection '" + icingadbName + "' does not exist.", ServiceUnknown);
+ return;
+ }
+
+ auto redis (conn->GetConnection());
+
+ if (!redis || !redis->GetConnected()) {
+ ReportIcingadbCheck(checkable, commandObj, cr, "Icinga DB CRITICAL: Not connected to Redis.", ServiceCritical);
+ return;
+ }
+
+ auto now (Utility::GetTime());
+ Array::Ptr redisTime, xReadHeartbeat, xReadStats, xReadRuntimeBacklog, xReadHistoryBacklog;
+
+ try {
+ auto replies (redis->GetResultsOfQueries(
+ {
+ {"TIME"},
+ {"XREAD", "STREAMS", "icingadb:telemetry:heartbeat", "0-0"},
+ {"XREAD", "STREAMS", "icingadb:telemetry:stats", "0-0"},
+ {"XREAD", "COUNT", "1", "STREAMS", "icinga:runtime", "icinga:runtime:state", "0-0", "0-0"},
+ {
+ "XREAD", "COUNT", "1", "STREAMS",
+ "icinga:history:stream:acknowledgement",
+ "icinga:history:stream:comment",
+ "icinga:history:stream:downtime",
+ "icinga:history:stream:flapping",
+ "icinga:history:stream:notification",
+ "icinga:history:stream:state",
+ "0-0", "0-0", "0-0", "0-0", "0-0", "0-0",
+ }
+ },
+ RedisConnection::QueryPriority::Heartbeat
+ ));
+
+ redisTime = std::move(replies.at(0));
+ xReadHeartbeat = std::move(replies.at(1));
+ xReadStats = std::move(replies.at(2));
+ xReadRuntimeBacklog = std::move(replies.at(3));
+ xReadHistoryBacklog = std::move(replies.at(4));
+ } catch (const std::exception& ex) {
+ ReportIcingadbCheck(
+ checkable, commandObj, cr,
+ String("Icinga DB CRITICAL: Could not query Redis: ") + ex.what(), ServiceCritical
+ );
+ return;
+ }
+
+ if (!xReadHeartbeat) {
+ ReportIcingadbCheck(
+ checkable, commandObj, cr,
+ "Icinga DB CRITICAL: The Icinga DB daemon seems to have never run. (Missing heartbeat)",
+ ServiceCritical
+ );
+
+ return;
+ }
+
+ auto redisOldestPending (redis->GetOldestPendingQueryTs());
+ auto ongoingDumpStart (conn->GetOngoingDumpStart());
+ auto dumpWhen (conn->GetLastdumpEnd());
+ auto dumpTook (conn->GetLastdumpTook());
+
+ auto redisNow (Convert::ToLong(redisTime->Get(0)) + Convert::ToLong(redisTime->Get(1)) / 1000000.0);
+ Array::Ptr heartbeatMessage = Array::Ptr(Array::Ptr(xReadHeartbeat->Get(0))->Get(1))->Get(0);
+ auto heartbeatTime (GetXMessageTs(heartbeatMessage));
+ std::map<String, String> heartbeatData;
+
+ IcingaDB::AddKvsToMap(heartbeatMessage->Get(1), heartbeatData);
+
+ String version = heartbeatData.at("version");
+ auto icingadbNow (Convert::ToLong(heartbeatData.at("time")) / 1000.0 + (redisNow - heartbeatTime));
+ auto icingadbStartTime (Convert::ToLong(heartbeatData.at("start-time")) / 1000.0);
+ String errMsg (heartbeatData.at("error"));
+ auto errSince (Convert::ToLong(heartbeatData.at("error-since")) / 1000.0);
+ String perfdataFromRedis = heartbeatData.at("performance-data");
+ auto heartbeatLastReceived (Convert::ToLong(heartbeatData.at("last-heartbeat-received")) / 1000.0);
+ bool weResponsible = Convert::ToLong(heartbeatData.at("ha-responsible"));
+ auto weResponsibleTs (Convert::ToLong(heartbeatData.at("ha-responsible-ts")) / 1000.0);
+ bool otherResponsible = Convert::ToLong(heartbeatData.at("ha-other-responsible"));
+ auto syncOngoingSince (Convert::ToLong(heartbeatData.at("sync-ongoing-since")) / 1000.0);
+ auto syncSuccessWhen (Convert::ToLong(heartbeatData.at("sync-success-finish")) / 1000.0);
+ auto syncSuccessTook (Convert::ToLong(heartbeatData.at("sync-success-duration")) / 1000.0);
+
+ std::ostringstream i2okmsgs, idbokmsgs, warnmsgs, critmsgs;
+ Array::Ptr perfdata = new Array();
+
+ i2okmsgs << std::fixed << std::setprecision(3);
+ idbokmsgs << std::fixed << std::setprecision(3);
+ warnmsgs << std::fixed << std::setprecision(3);
+ critmsgs << std::fixed << std::setprecision(3);
+
+ const auto downForCritical (10);
+ auto downFor (redisNow - heartbeatTime);
+ bool down = false;
+
+ if (downFor > downForCritical) {
+ down = true;
+
+ critmsgs << " Last seen " << Utility::FormatDuration(downFor)
+ << " ago, greater than CRITICAL threshold (" << Utility::FormatDuration(downForCritical) << ")!";
+ } else {
+ idbokmsgs << "\n* Last seen: " << Utility::FormatDuration(downFor) << " ago";
+ }
+
+ perfdata->Add(new PerfdataValue("icingadb_heartbeat_age", downFor, false, "seconds", Empty, downForCritical, 0));
+
+ const auto errForCritical (10);
+ auto err (!errMsg.IsEmpty());
+ auto errFor (icingadbNow - errSince);
+
+ if (err) {
+ if (errFor > errForCritical) {
+ critmsgs << " ERROR: " << errMsg << "!";
+ }
+
+ perfdata->Add(new PerfdataValue("error_for", errFor * (err ? 1 : -1), false, "seconds", Empty, errForCritical, 0));
+ }
+
+ if (!down) {
+ const auto heartbeatLagWarning (3/* Icinga DB read freq. */ + 1/* Icinga DB write freq. */ + 2/* threshold */);
+ auto heartbeatLag (fmin(icingadbNow - heartbeatLastReceived, 10 * 60));
+
+ if (!heartbeatLastReceived) {
+ critmsgs << " Lost Icinga 2 heartbeat!";
+ } else if (heartbeatLag > heartbeatLagWarning) {
+ warnmsgs << " Icinga 2 heartbeat lag: " << Utility::FormatDuration(heartbeatLag)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(heartbeatLagWarning) << ").";
+ }
+
+ perfdata->Add(new PerfdataValue("icinga2_heartbeat_age", heartbeatLag, false, "seconds", heartbeatLagWarning, Empty, 0));
+ }
+
+ if (weResponsible) {
+ idbokmsgs << "\n* Responsible";
+ } else if (otherResponsible) {
+ idbokmsgs << "\n* Not responsible, but another instance is";
+ } else {
+ critmsgs << " No instance is responsible!";
+ }
+
+ perfdata->Add(new PerfdataValue("icingadb_responsible_instances", int(weResponsible || otherResponsible), false, "", Empty, Empty, 0, 1));
+
+ const auto clockDriftWarning (5);
+ const auto clockDriftCritical (30);
+ auto clockDrift (std::max({
+ fabs(now - redisNow),
+ fabs(redisNow - icingadbNow),
+ fabs(icingadbNow - now),
+ }));
+
+ if (clockDrift > clockDriftCritical) {
+ critmsgs << " Icinga 2/Redis/Icinga DB clock drift: " << Utility::FormatDuration(clockDrift)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(clockDriftCritical) << ")!";
+ } else if (clockDrift > clockDriftWarning) {
+ warnmsgs << " Icinga 2/Redis/Icinga DB clock drift: " << Utility::FormatDuration(clockDrift)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(clockDriftWarning) << ").";
+ }
+
+ perfdata->Add(new PerfdataValue("clock_drift", clockDrift, false, "seconds", clockDriftWarning, clockDriftCritical, 0));
+
+ if (ongoingDumpStart) {
+ auto ongoingDumpTakes (now - ongoingDumpStart);
+
+ if (!dumpTakesThresholds.Critical.IsEmpty() && ongoingDumpTakes > dumpTakesThresholds.Critical) {
+ critmsgs << " Current Icinga 2 full dump already takes " << Utility::FormatDuration(ongoingDumpTakes)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(dumpTakesThresholds.Critical) << ")!";
+ } else if (!dumpTakesThresholds.Warning.IsEmpty() && ongoingDumpTakes > dumpTakesThresholds.Warning) {
+ warnmsgs << " Current Icinga 2 full dump already takes " << Utility::FormatDuration(ongoingDumpTakes)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(dumpTakesThresholds.Warning) << ").";
+ } else {
+ i2okmsgs << "\n* Current full dump running for " << Utility::FormatDuration(ongoingDumpTakes);
+ }
+
+ perfdata->Add(new PerfdataValue("icinga2_current_full_dump_duration", ongoingDumpTakes, false, "seconds",
+ dumpTakesThresholds.Warning, dumpTakesThresholds.Critical, 0));
+ }
+
+ if (!down && syncOngoingSince) {
+ auto ongoingSyncTakes (icingadbNow - syncOngoingSince);
+
+ if (!syncTakesThresholds.Critical.IsEmpty() && ongoingSyncTakes > syncTakesThresholds.Critical) {
+ critmsgs << " Current full sync already takes " << Utility::FormatDuration(ongoingSyncTakes)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(syncTakesThresholds.Critical) << ")!";
+ } else if (!syncTakesThresholds.Warning.IsEmpty() && ongoingSyncTakes > syncTakesThresholds.Warning) {
+ warnmsgs << " Current full sync already takes " << Utility::FormatDuration(ongoingSyncTakes)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(syncTakesThresholds.Warning) << ").";
+ } else {
+ idbokmsgs << "\n* Current full sync running for " << Utility::FormatDuration(ongoingSyncTakes);
+ }
+
+ perfdata->Add(new PerfdataValue("icingadb_current_full_sync_duration", ongoingSyncTakes, false, "seconds",
+ syncTakesThresholds.Warning, syncTakesThresholds.Critical, 0));
+ }
+
+ auto redisBacklog (now - redisOldestPending);
+
+ if (!redisOldestPending) {
+ redisBacklog = 0;
+ }
+
+ if (!icingaBacklogThresholds.Critical.IsEmpty() && redisBacklog > icingaBacklogThresholds.Critical) {
+ critmsgs << " Icinga 2 Redis query backlog: " << Utility::FormatDuration(redisBacklog)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(icingaBacklogThresholds.Critical) << ")!";
+ } else if (!icingaBacklogThresholds.Warning.IsEmpty() && redisBacklog > icingaBacklogThresholds.Warning) {
+ warnmsgs << " Icinga 2 Redis query backlog: " << Utility::FormatDuration(redisBacklog)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(icingaBacklogThresholds.Warning) << ").";
+ }
+
+ perfdata->Add(new PerfdataValue("icinga2_redis_query_backlog", redisBacklog, false, "seconds",
+ icingaBacklogThresholds.Warning, icingaBacklogThresholds.Critical, 0));
+
+ if (!down) {
+ auto getBacklog = [redisNow](const Array::Ptr& streams) -> double {
+ if (!streams) {
+ return 0;
+ }
+
+ double minTs = 0;
+ ObjectLock lock (streams);
+
+ for (Array::Ptr stream : streams) {
+ auto ts (GetXMessageTs(Array::Ptr(stream->Get(1))->Get(0)));
+
+ if (minTs == 0 || ts < minTs) {
+ minTs = ts;
+ }
+ }
+
+ if (minTs > 0) {
+ return redisNow - minTs;
+ } else {
+ return 0;
+ }
+ };
+
+ double historyBacklog = getBacklog(xReadHistoryBacklog);
+
+ if (!icingadbBacklogThresholds.Critical.IsEmpty() && historyBacklog > icingadbBacklogThresholds.Critical) {
+ critmsgs << " History backlog: " << Utility::FormatDuration(historyBacklog)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(icingadbBacklogThresholds.Critical) << ")!";
+ } else if (!icingadbBacklogThresholds.Warning.IsEmpty() && historyBacklog > icingadbBacklogThresholds.Warning) {
+ warnmsgs << " History backlog: " << Utility::FormatDuration(historyBacklog)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(icingadbBacklogThresholds.Warning) << ").";
+ }
+
+ perfdata->Add(new PerfdataValue("icingadb_history_backlog", historyBacklog, false, "seconds",
+ icingadbBacklogThresholds.Warning, icingadbBacklogThresholds.Critical, 0));
+
+ double runtimeBacklog = 0;
+
+ if (weResponsible && !syncOngoingSince) {
+ // These streams are only processed by the responsible instance after the full sync finished,
+ // it's fine for some backlog to exist otherwise.
+ runtimeBacklog = getBacklog(xReadRuntimeBacklog);
+
+ if (!icingadbBacklogThresholds.Critical.IsEmpty() && runtimeBacklog > icingadbBacklogThresholds.Critical) {
+ critmsgs << " Runtime update backlog: " << Utility::FormatDuration(runtimeBacklog)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(icingadbBacklogThresholds.Critical) << ")!";
+ } else if (!icingadbBacklogThresholds.Warning.IsEmpty() && runtimeBacklog > icingadbBacklogThresholds.Warning) {
+ warnmsgs << " Runtime update backlog: " << Utility::FormatDuration(runtimeBacklog)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(icingadbBacklogThresholds.Warning) << ").";
+ }
+ }
+
+ // Also report the perfdata value on the standby instance or during a full sync (as 0 in this case).
+ perfdata->Add(new PerfdataValue("icingadb_runtime_update_backlog", runtimeBacklog, false, "seconds",
+ icingadbBacklogThresholds.Warning, icingadbBacklogThresholds.Critical, 0));
+ }
+
+ auto dumpAgo (now - dumpWhen);
+
+ if (dumpWhen) {
+ perfdata->Add(new PerfdataValue("icinga2_last_full_dump_ago", dumpAgo, false, "seconds", Empty, Empty, 0));
+ }
+
+ if (dumpTook) {
+ perfdata->Add(new PerfdataValue("icinga2_last_full_dump_duration", dumpTook, false, "seconds", Empty, Empty, 0));
+ }
+
+ if (dumpWhen && dumpTook) {
+ i2okmsgs << "\n* Last full dump: " << Utility::FormatDuration(dumpAgo)
+ << " ago, took " << Utility::FormatDuration(dumpTook);
+ }
+
+ auto icingadbUptime (icingadbNow - icingadbStartTime);
+
+ if (!down) {
+ perfdata->Add(new PerfdataValue("icingadb_uptime", icingadbUptime, false, "seconds", Empty, Empty, 0));
+ }
+
+ {
+ Array::Ptr values = PluginUtility::SplitPerfdata(perfdataFromRedis);
+ ObjectLock lock (values);
+
+ for (auto& v : values) {
+ perfdata->Add(PerfdataValue::Parse(v));
+ }
+ }
+
+ if (weResponsibleTs) {
+ perfdata->Add(new PerfdataValue("icingadb_responsible_for",
+ (weResponsible ? 1 : -1) * (icingadbNow - weResponsibleTs), false, "seconds"));
+ }
+
+ auto syncAgo (icingadbNow - syncSuccessWhen);
+
+ if (syncSuccessWhen) {
+ perfdata->Add(new PerfdataValue("icingadb_last_full_sync_ago", syncAgo, false, "seconds", Empty, Empty, 0));
+ }
+
+ if (syncSuccessTook) {
+ perfdata->Add(new PerfdataValue("icingadb_last_full_sync_duration", syncSuccessTook, false, "seconds", Empty, Empty, 0));
+ }
+
+ if (syncSuccessWhen && syncSuccessTook) {
+ idbokmsgs << "\n* Last full sync: " << Utility::FormatDuration(syncAgo)
+ << " ago, took " << Utility::FormatDuration(syncSuccessTook);
+ }
+
+ std::map<String, RingBuffer> statsPerOp;
+
+ const char * const icingadbKnownStats[] = {
+ "config_sync", "state_sync", "history_sync", "overdue_sync", "history_cleanup"
+ };
+
+ for (auto metric : icingadbKnownStats) {
+ statsPerOp.emplace(std::piecewise_construct, std::forward_as_tuple(metric), std::forward_as_tuple(15 * 60));
+ }
+
+ if (xReadStats) {
+ Array::Ptr messages = Array::Ptr(xReadStats->Get(0))->Get(1);
+ ObjectLock lock (messages);
+
+ for (Array::Ptr message : messages) {
+ auto ts (GetXMessageTs(message));
+ std::map<String, String> opsPerSec;
+
+ IcingaDB::AddKvsToMap(message->Get(1), opsPerSec);
+
+ for (auto& kv : opsPerSec) {
+ auto buf (statsPerOp.find(kv.first));
+
+ if (buf == statsPerOp.end()) {
+ buf = statsPerOp.emplace(
+ std::piecewise_construct,
+ std::forward_as_tuple(kv.first), std::forward_as_tuple(15 * 60)
+ ).first;
+ }
+
+ buf->second.InsertValue(ts, Convert::ToLong(kv.second));
+ }
+ }
+ }
+
+ for (auto& kv : statsPerOp) {
+ perfdata->Add(new PerfdataValue("icingadb_" + kv.first + "_items_1min", kv.second.UpdateAndGetValues(now, 60), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue("icingadb_" + kv.first + "_items_5mins", kv.second.UpdateAndGetValues(now, 5 * 60), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue("icingadb_" + kv.first + "_items_15mins", kv.second.UpdateAndGetValues(now, 15 * 60), false, "", Empty, Empty, 0));
+ }
+
+ perfdata->Add(new PerfdataValue("icinga2_redis_queries_1min", redis->GetQueryCount(60), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue("icinga2_redis_queries_5mins", redis->GetQueryCount(5 * 60), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue("icinga2_redis_queries_15mins", redis->GetQueryCount(15 * 60), false, "", Empty, Empty, 0));
+
+ perfdata->Add(new PerfdataValue("icinga2_redis_pending_queries", redis->GetPendingQueryCount(), false, "", Empty, Empty, 0));
+
+ struct {
+ const char * Name;
+ int (RedisConnection::* Getter)(RingBuffer::SizeType span, RingBuffer::SizeType tv);
+ } const icingaWriteSubjects[] = {
+ {"config_dump", &RedisConnection::GetWrittenConfigFor},
+ {"state_dump", &RedisConnection::GetWrittenStateFor},
+ {"history_dump", &RedisConnection::GetWrittenHistoryFor}
+ };
+
+ for (auto subject : icingaWriteSubjects) {
+ perfdata->Add(new PerfdataValue(String("icinga2_") + subject.Name + "_items_1min", (redis.get()->*subject.Getter)(60, now), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue(String("icinga2_") + subject.Name + "_items_5mins", (redis.get()->*subject.Getter)(5 * 60, now), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue(String("icinga2_") + subject.Name + "_items_15mins", (redis.get()->*subject.Getter)(15 * 60, now), false, "", Empty, Empty, 0));
+ }
+
+ ServiceState state;
+ std::ostringstream msgbuf;
+ auto i2okmsg (i2okmsgs.str());
+ auto idbokmsg (idbokmsgs.str());
+ auto warnmsg (warnmsgs.str());
+ auto critmsg (critmsgs.str());
+
+ msgbuf << "Icinga DB ";
+
+ if (!critmsg.empty()) {
+ state = ServiceCritical;
+ msgbuf << "CRITICAL:" << critmsg;
+
+ if (!warnmsg.empty()) {
+ msgbuf << "\n\nWARNING:" << warnmsg;
+ }
+ } else if (!warnmsg.empty()) {
+ state = ServiceWarning;
+ msgbuf << "WARNING:" << warnmsg;
+ } else {
+ state = ServiceOK;
+ msgbuf << "OK: Uptime: " << Utility::FormatDuration(icingadbUptime) << ". Version: " << version << ".";
+ }
+
+ if (!i2okmsg.empty()) {
+ msgbuf << "\n\nIcinga 2:\n" << i2okmsg;
+ }
+
+ if (!idbokmsg.empty()) {
+ msgbuf << "\n\nIcinga DB:\n" << idbokmsg;
+ }
+
+ cr->SetPerformanceData(perfdata);
+ ReportIcingadbCheck(checkable, commandObj, cr, msgbuf.str(), state);
+}
diff --git a/lib/icingadb/icingadbchecktask.hpp b/lib/icingadb/icingadbchecktask.hpp
new file mode 100644
index 0000000..ba7d61b
--- /dev/null
+++ b/lib/icingadb/icingadbchecktask.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#ifndef ICINGADBCHECKTASK_H
+#define ICINGADBCHECKTASK_H
+
+#include "icingadb/icingadb.hpp"
+#include "icinga/checkable.hpp"
+
+namespace icinga
+{
+
+/**
+ * Icinga DB check.
+ *
+ * @ingroup icingadb
+ */
+class IcingadbCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ IcingadbCheckTask();
+};
+
+}
+
+#endif /* ICINGADBCHECKTASK_H */
diff --git a/lib/icingadb/redisconnection.cpp b/lib/icingadb/redisconnection.cpp
new file mode 100644
index 0000000..798a827
--- /dev/null
+++ b/lib/icingadb/redisconnection.cpp
@@ -0,0 +1,773 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/redisconnection.hpp"
+#include "base/array.hpp"
+#include "base/convert.hpp"
+#include "base/defer.hpp"
+#include "base/exception.hpp"
+#include "base/io-engine.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/string.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
+#include <boost/asio.hpp>
+#include <boost/coroutine/exceptions.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/utility/string_view.hpp>
+#include <boost/variant/get.hpp>
+#include <exception>
+#include <future>
+#include <iterator>
+#include <memory>
+#include <openssl/ssl.h>
+#include <openssl/x509_vfy.h>
+#include <utility>
+
+using namespace icinga;
+namespace asio = boost::asio;
+
+boost::regex RedisConnection::m_ErrAuth ("\\AERR AUTH ");
+
+RedisConnection::RedisConnection(const String& host, int port, const String& path, const String& password, int db,
+ bool useTls, bool insecure, const String& certPath, const String& keyPath, const String& caPath, const String& crlPath,
+ const String& tlsProtocolmin, const String& cipherList, double connectTimeout, DebugInfo di, const RedisConnection::Ptr& parent)
+ : RedisConnection(IoEngine::Get().GetIoContext(), host, port, path, password, db,
+ useTls, insecure, certPath, keyPath, caPath, crlPath, tlsProtocolmin, cipherList, connectTimeout, std::move(di), parent)
+{
+}
+
+RedisConnection::RedisConnection(boost::asio::io_context& io, String host, int port, String path, String password,
+ int db, bool useTls, bool insecure, String certPath, String keyPath, String caPath, String crlPath,
+ String tlsProtocolmin, String cipherList, double connectTimeout, DebugInfo di, const RedisConnection::Ptr& parent)
+ : m_Host(std::move(host)), m_Port(port), m_Path(std::move(path)), m_Password(std::move(password)),
+ m_DbIndex(db), m_CertPath(std::move(certPath)), m_KeyPath(std::move(keyPath)), m_Insecure(insecure),
+ m_CaPath(std::move(caPath)), m_CrlPath(std::move(crlPath)), m_TlsProtocolmin(std::move(tlsProtocolmin)),
+ m_CipherList(std::move(cipherList)), m_ConnectTimeout(connectTimeout), m_DebugInfo(std::move(di)), m_Connecting(false), m_Connected(false),
+ m_Started(false), m_Strand(io), m_QueuedWrites(io), m_QueuedReads(io), m_LogStatsTimer(io), m_Parent(parent)
+{
+ if (useTls && m_Path.IsEmpty()) {
+ UpdateTLSContext();
+ }
+}
+
+void RedisConnection::UpdateTLSContext()
+{
+ m_TLSContext = SetupSslContext(m_CertPath, m_KeyPath, m_CaPath,
+ m_CrlPath, m_CipherList, m_TlsProtocolmin, m_DebugInfo);
+}
+
+void RedisConnection::Start()
+{
+ if (!m_Started.exchange(true)) {
+ Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { ReadLoop(yc); });
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { WriteLoop(yc); });
+
+ if (!m_Parent) {
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { LogStats(yc); });
+ }
+ }
+
+ if (!m_Connecting.exchange(true)) {
+ Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { Connect(yc); });
+ }
+}
+
+bool RedisConnection::IsConnected() {
+ return m_Connected.load();
+}
+
+/**
+ * Append a Redis query to a log message
+ *
+ * @param query Redis query
+ * @param msg Log message
+ */
+static inline
+void LogQuery(RedisConnection::Query& query, Log& msg)
+{
+ int i = 0;
+
+ for (auto& arg : query) {
+ if (++i == 8) {
+ msg << " ...";
+ break;
+ }
+
+ if (arg.GetLength() > 64) {
+ msg << " '" << arg.SubStr(0, 61) << "...'";
+ } else {
+ msg << " '" << arg << '\'';
+ }
+ }
+}
+
+/**
+ * Queue a Redis query for sending
+ *
+ * @param query Redis query
+ * @param priority The query's priority
+ */
+void RedisConnection::FireAndForgetQuery(RedisConnection::Query query, RedisConnection::QueryPriority priority, QueryAffects affects)
+{
+ if (LogDebug >= Logger::GetMinLogSeverity()) {
+ Log msg (LogDebug, "IcingaDB", "Firing and forgetting query:");
+ LogQuery(query, msg);
+ }
+
+ auto item (Shared<Query>::Make(std::move(query)));
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, item, priority, ctime, affects]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{item, nullptr, nullptr, nullptr, nullptr, ctime, affects});
+ m_QueuedWrites.Set();
+ IncreasePendingQueries(1);
+ });
+}
+
+/**
+ * Queue Redis queries for sending
+ *
+ * @param queries Redis queries
+ * @param priority The queries' priority
+ */
+void RedisConnection::FireAndForgetQueries(RedisConnection::Queries queries, RedisConnection::QueryPriority priority, QueryAffects affects)
+{
+ if (LogDebug >= Logger::GetMinLogSeverity()) {
+ for (auto& query : queries) {
+ Log msg(LogDebug, "IcingaDB", "Firing and forgetting query:");
+ LogQuery(query, msg);
+ }
+ }
+
+ auto item (Shared<Queries>::Make(std::move(queries)));
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, item, priority, ctime, affects]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{nullptr, item, nullptr, nullptr, nullptr, ctime, affects});
+ m_QueuedWrites.Set();
+ IncreasePendingQueries(item->size());
+ });
+}
+
+/**
+ * Queue a Redis query for sending, wait for the response and return (or throw) it
+ *
+ * @param query Redis query
+ * @param priority The query's priority
+ *
+ * @return The response
+ */
+RedisConnection::Reply RedisConnection::GetResultOfQuery(RedisConnection::Query query, RedisConnection::QueryPriority priority, QueryAffects affects)
+{
+ if (LogDebug >= Logger::GetMinLogSeverity()) {
+ Log msg (LogDebug, "IcingaDB", "Executing query:");
+ LogQuery(query, msg);
+ }
+
+ std::promise<Reply> promise;
+ auto future (promise.get_future());
+ auto item (Shared<std::pair<Query, std::promise<Reply>>>::Make(std::move(query), std::move(promise)));
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, item, priority, ctime, affects]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{nullptr, nullptr, item, nullptr, nullptr, ctime, affects});
+ m_QueuedWrites.Set();
+ IncreasePendingQueries(1);
+ });
+
+ item = nullptr;
+ future.wait();
+ return future.get();
+}
+
+/**
+ * Queue Redis queries for sending, wait for the responses and return (or throw) them
+ *
+ * @param queries Redis queries
+ * @param priority The queries' priority
+ *
+ * @return The responses
+ */
+RedisConnection::Replies RedisConnection::GetResultsOfQueries(RedisConnection::Queries queries, RedisConnection::QueryPriority priority, QueryAffects affects)
+{
+ if (LogDebug >= Logger::GetMinLogSeverity()) {
+ for (auto& query : queries) {
+ Log msg(LogDebug, "IcingaDB", "Executing query:");
+ LogQuery(query, msg);
+ }
+ }
+
+ std::promise<Replies> promise;
+ auto future (promise.get_future());
+ auto item (Shared<std::pair<Queries, std::promise<Replies>>>::Make(std::move(queries), std::move(promise)));
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, item, priority, ctime, affects]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{nullptr, nullptr, nullptr, item, nullptr, ctime, affects});
+ m_QueuedWrites.Set();
+ IncreasePendingQueries(item->first.size());
+ });
+
+ item = nullptr;
+ future.wait();
+ return future.get();
+}
+
+void RedisConnection::EnqueueCallback(const std::function<void(boost::asio::yield_context&)>& callback, RedisConnection::QueryPriority priority)
+{
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, callback, priority, ctime]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{nullptr, nullptr, nullptr, nullptr, callback, ctime});
+ m_QueuedWrites.Set();
+ });
+}
+
+/**
+ * Puts a no-op command with a result at the end of the queue and wait for the result,
+ * i.e. for everything enqueued to be processed by the server.
+ *
+ * @ingroup icingadb
+ */
+void RedisConnection::Sync()
+{
+ GetResultOfQuery({"PING"}, RedisConnection::QueryPriority::SyncConnection);
+}
+
+/**
+ * Get the enqueue time of the oldest still queued Redis query
+ *
+ * @return *nix timestamp or 0
+ */
+double RedisConnection::GetOldestPendingQueryTs()
+{
+ auto promise (Shared<std::promise<double>>::Make());
+ auto future (promise->get_future());
+
+ asio::post(m_Strand, [this, promise]() {
+ double oldest = 0;
+
+ for (auto& queue : m_Queues.Writes) {
+ if (m_SuppressedQueryKinds.find(queue.first) == m_SuppressedQueryKinds.end() && !queue.second.empty()) {
+ auto ctime (queue.second.front().CTime);
+
+ if (ctime < oldest || oldest == 0) {
+ oldest = ctime;
+ }
+ }
+ }
+
+ promise->set_value(oldest);
+ });
+
+ future.wait();
+ return future.get();
+}
+
+/**
+ * Mark kind as kind of queries not to actually send yet
+ *
+ * @param kind Query kind
+ */
+void RedisConnection::SuppressQueryKind(RedisConnection::QueryPriority kind)
+{
+ asio::post(m_Strand, [this, kind]() { m_SuppressedQueryKinds.emplace(kind); });
+}
+
+/**
+ * Unmark kind as kind of queries not to actually send yet
+ *
+ * @param kind Query kind
+ */
+void RedisConnection::UnsuppressQueryKind(RedisConnection::QueryPriority kind)
+{
+ asio::post(m_Strand, [this, kind]() {
+ m_SuppressedQueryKinds.erase(kind);
+ m_QueuedWrites.Set();
+ });
+}
+
+/**
+ * Try to connect to Redis
+ */
+void RedisConnection::Connect(asio::yield_context& yc)
+{
+ Defer notConnecting ([this]() { m_Connecting.store(m_Connected.load()); });
+
+ boost::asio::deadline_timer timer (m_Strand.context());
+
+ auto waitForReadLoop ([this, &yc]() {
+ while (!m_Queues.FutureResponseActions.empty()) {
+ IoEngine::YieldCurrentCoroutine(yc);
+ }
+ });
+
+ for (;;) {
+ try {
+ if (m_Path.IsEmpty()) {
+ if (m_TLSContext) {
+ Log(m_Parent ? LogNotice : LogInformation, "IcingaDB")
+ << "Trying to connect to Redis server (async, TLS) on host '" << m_Host << ":" << m_Port << "'";
+
+ auto conn (Shared<AsioTlsStream>::Make(m_Strand.context(), *m_TLSContext, m_Host));
+ auto& tlsConn (conn->next_layer());
+ auto connectTimeout (MakeTimeout(conn));
+ Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); });
+
+ icinga::Connect(conn->lowest_layer(), m_Host, Convert::ToString(m_Port), yc);
+ tlsConn.async_handshake(tlsConn.client, yc);
+
+ if (!m_Insecure) {
+ std::shared_ptr<X509> cert (tlsConn.GetPeerCertificate());
+
+ if (!cert) {
+ BOOST_THROW_EXCEPTION(std::runtime_error(
+ "Redis didn't present any TLS certificate."
+ ));
+ }
+
+ if (!tlsConn.IsVerifyOK()) {
+ BOOST_THROW_EXCEPTION(std::runtime_error(
+ "TLS certificate validation failed: " + std::string(tlsConn.GetVerifyError())
+ ));
+ }
+ }
+
+ Handshake(conn, yc);
+ waitForReadLoop();
+ m_TlsConn = std::move(conn);
+ } else {
+ Log(m_Parent ? LogNotice : LogInformation, "IcingaDB")
+ << "Trying to connect to Redis server (async) on host '" << m_Host << ":" << m_Port << "'";
+
+ auto conn (Shared<TcpConn>::Make(m_Strand.context()));
+ auto connectTimeout (MakeTimeout(conn));
+ Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); });
+
+ icinga::Connect(conn->next_layer(), m_Host, Convert::ToString(m_Port), yc);
+ Handshake(conn, yc);
+ waitForReadLoop();
+ m_TcpConn = std::move(conn);
+ }
+ } else {
+ Log(LogInformation, "IcingaDB")
+ << "Trying to connect to Redis server (async) on unix socket path '" << m_Path << "'";
+
+ auto conn (Shared<UnixConn>::Make(m_Strand.context()));
+ auto connectTimeout (MakeTimeout(conn));
+ Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); });
+
+ conn->next_layer().async_connect(Unix::endpoint(m_Path.CStr()), yc);
+ Handshake(conn, yc);
+ waitForReadLoop();
+ m_UnixConn = std::move(conn);
+ }
+
+ m_Connected.store(true);
+
+ Log(m_Parent ? LogNotice : LogInformation, "IcingaDB", "Connected to Redis server");
+
+ // Operate on a copy so that the callback can set a new callback without destroying itself while running.
+ auto callback (m_ConnectedCallback);
+ if (callback) {
+ callback(yc);
+ }
+
+ break;
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "IcingaDB")
+ << "Cannot connect to " << m_Host << ":" << m_Port << ": " << ex.what();
+ }
+
+ timer.expires_from_now(boost::posix_time::seconds(5));
+ timer.async_wait(yc);
+ }
+
+}
+
+/**
+ * Actually receive the responses to the Redis queries send by WriteItem() and handle them
+ */
+void RedisConnection::ReadLoop(asio::yield_context& yc)
+{
+ for (;;) {
+ m_QueuedReads.Wait(yc);
+
+ while (!m_Queues.FutureResponseActions.empty()) {
+ auto item (std::move(m_Queues.FutureResponseActions.front()));
+ m_Queues.FutureResponseActions.pop();
+
+ switch (item.Action) {
+ case ResponseAction::Ignore:
+ try {
+ for (auto i (item.Amount); i; --i) {
+ ReadOne(yc);
+ }
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "IcingaDB")
+ << "Error during receiving the response to a query which has been fired and forgotten: " << ex.what();
+
+ continue;
+ } catch (...) {
+ Log(LogCritical, "IcingaDB")
+ << "Error during receiving the response to a query which has been fired and forgotten";
+
+ continue;
+ }
+
+ break;
+ case ResponseAction::Deliver:
+ for (auto i (item.Amount); i; --i) {
+ auto promise (std::move(m_Queues.ReplyPromises.front()));
+ m_Queues.ReplyPromises.pop();
+
+ Reply reply;
+
+ try {
+ reply = ReadOne(yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ promise.set_exception(std::current_exception());
+
+ continue;
+ }
+
+ promise.set_value(std::move(reply));
+ }
+
+ break;
+ case ResponseAction::DeliverBulk:
+ {
+ auto promise (std::move(m_Queues.RepliesPromises.front()));
+ m_Queues.RepliesPromises.pop();
+
+ Replies replies;
+ replies.reserve(item.Amount);
+
+ for (auto i (item.Amount); i; --i) {
+ try {
+ replies.emplace_back(ReadOne(yc));
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ promise.set_exception(std::current_exception());
+ break;
+ }
+ }
+
+ try {
+ promise.set_value(std::move(replies));
+ } catch (const std::future_error&) {
+ // Complaint about the above op is not allowed
+ // due to promise.set_exception() was already called
+ }
+ }
+ }
+ }
+
+ m_QueuedReads.Clear();
+ }
+}
+
+/**
+ * Actually send the Redis queries queued by {FireAndForget,GetResultsOf}{Query,Queries}()
+ */
+void RedisConnection::WriteLoop(asio::yield_context& yc)
+{
+ for (;;) {
+ m_QueuedWrites.Wait(yc);
+
+ WriteFirstOfHighestPrio:
+ for (auto& queue : m_Queues.Writes) {
+ if (m_SuppressedQueryKinds.find(queue.first) != m_SuppressedQueryKinds.end() || queue.second.empty()) {
+ continue;
+ }
+
+ auto next (std::move(queue.second.front()));
+ queue.second.pop();
+
+ WriteItem(yc, std::move(next));
+
+ goto WriteFirstOfHighestPrio;
+ }
+
+ m_QueuedWrites.Clear();
+ }
+}
+
+/**
+ * Periodically log current query performance
+ */
+void RedisConnection::LogStats(asio::yield_context& yc)
+{
+ double lastMessage = 0;
+
+ m_LogStatsTimer.expires_from_now(boost::posix_time::seconds(10));
+
+ for (;;) {
+ m_LogStatsTimer.async_wait(yc);
+ m_LogStatsTimer.expires_from_now(boost::posix_time::seconds(10));
+
+ if (!IsConnected())
+ continue;
+
+ auto now (Utility::GetTime());
+ bool timeoutReached = now - lastMessage >= 5 * 60;
+
+ if (m_PendingQueries < 1 && !timeoutReached)
+ continue;
+
+ auto output (round(m_OutputQueries.CalculateRate(now, 10)));
+
+ if (m_PendingQueries < output * 5 && !timeoutReached)
+ continue;
+
+ Log(LogInformation, "IcingaDB")
+ << "Pending queries: " << m_PendingQueries << " (Input: "
+ << round(m_InputQueries.CalculateRate(now, 10)) << "/s; Output: " << output << "/s)";
+
+ lastMessage = now;
+ }
+}
+
+/**
+ * Send next and schedule receiving the response
+ *
+ * @param next Redis queries
+ */
+void RedisConnection::WriteItem(boost::asio::yield_context& yc, RedisConnection::WriteQueueItem next)
+{
+ if (next.FireAndForgetQuery) {
+ auto& item (*next.FireAndForgetQuery);
+ DecreasePendingQueries(1);
+
+ try {
+ WriteOne(item, yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (const std::exception& ex) {
+ Log msg (LogCritical, "IcingaDB", "Error during sending query");
+ LogQuery(item, msg);
+ msg << " which has been fired and forgotten: " << ex.what();
+
+ return;
+ } catch (...) {
+ Log msg (LogCritical, "IcingaDB", "Error during sending query");
+ LogQuery(item, msg);
+ msg << " which has been fired and forgotten";
+
+ return;
+ }
+
+ if (m_Queues.FutureResponseActions.empty() || m_Queues.FutureResponseActions.back().Action != ResponseAction::Ignore) {
+ m_Queues.FutureResponseActions.emplace(FutureResponseAction{1, ResponseAction::Ignore});
+ } else {
+ ++m_Queues.FutureResponseActions.back().Amount;
+ }
+
+ m_QueuedReads.Set();
+ }
+
+ if (next.FireAndForgetQueries) {
+ auto& item (*next.FireAndForgetQueries);
+ size_t i = 0;
+
+ DecreasePendingQueries(item.size());
+
+ try {
+ for (auto& query : item) {
+ WriteOne(query, yc);
+ ++i;
+ }
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (const std::exception& ex) {
+ Log msg (LogCritical, "IcingaDB", "Error during sending query");
+ LogQuery(item[i], msg);
+ msg << " which has been fired and forgotten: " << ex.what();
+
+ return;
+ } catch (...) {
+ Log msg (LogCritical, "IcingaDB", "Error during sending query");
+ LogQuery(item[i], msg);
+ msg << " which has been fired and forgotten";
+
+ return;
+ }
+
+ if (m_Queues.FutureResponseActions.empty() || m_Queues.FutureResponseActions.back().Action != ResponseAction::Ignore) {
+ m_Queues.FutureResponseActions.emplace(FutureResponseAction{item.size(), ResponseAction::Ignore});
+ } else {
+ m_Queues.FutureResponseActions.back().Amount += item.size();
+ }
+
+ m_QueuedReads.Set();
+ }
+
+ if (next.GetResultOfQuery) {
+ auto& item (*next.GetResultOfQuery);
+ DecreasePendingQueries(1);
+
+ try {
+ WriteOne(item.first, yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ item.second.set_exception(std::current_exception());
+
+ return;
+ }
+
+ m_Queues.ReplyPromises.emplace(std::move(item.second));
+
+ if (m_Queues.FutureResponseActions.empty() || m_Queues.FutureResponseActions.back().Action != ResponseAction::Deliver) {
+ m_Queues.FutureResponseActions.emplace(FutureResponseAction{1, ResponseAction::Deliver});
+ } else {
+ ++m_Queues.FutureResponseActions.back().Amount;
+ }
+
+ m_QueuedReads.Set();
+ }
+
+ if (next.GetResultsOfQueries) {
+ auto& item (*next.GetResultsOfQueries);
+ DecreasePendingQueries(item.first.size());
+
+ try {
+ for (auto& query : item.first) {
+ WriteOne(query, yc);
+ }
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ item.second.set_exception(std::current_exception());
+
+ return;
+ }
+
+ m_Queues.RepliesPromises.emplace(std::move(item.second));
+ m_Queues.FutureResponseActions.emplace(FutureResponseAction{item.first.size(), ResponseAction::DeliverBulk});
+
+ m_QueuedReads.Set();
+ }
+
+ if (next.Callback) {
+ next.Callback(yc);
+ }
+
+ RecordAffected(next.Affects, Utility::GetTime());
+}
+
+/**
+ * Receive the response to a Redis query
+ *
+ * @return The response
+ */
+RedisConnection::Reply RedisConnection::ReadOne(boost::asio::yield_context& yc)
+{
+ if (m_Path.IsEmpty()) {
+ if (m_TLSContext) {
+ return ReadOne(m_TlsConn, yc);
+ } else {
+ return ReadOne(m_TcpConn, yc);
+ }
+ } else {
+ return ReadOne(m_UnixConn, yc);
+ }
+}
+
+/**
+ * Send query
+ *
+ * @param query Redis query
+ */
+void RedisConnection::WriteOne(RedisConnection::Query& query, asio::yield_context& yc)
+{
+ if (m_Path.IsEmpty()) {
+ if (m_TLSContext) {
+ WriteOne(m_TlsConn, query, yc);
+ } else {
+ WriteOne(m_TcpConn, query, yc);
+ }
+ } else {
+ WriteOne(m_UnixConn, query, yc);
+ }
+}
+
+/**
+ * Specify a callback that is run each time a connection is successfully established
+ *
+ * The callback is executed from a Boost.Asio coroutine and should therefore not perform blocking operations.
+ *
+ * @param callback Callback to execute
+ */
+void RedisConnection::SetConnectedCallback(std::function<void(asio::yield_context& yc)> callback) {
+ m_ConnectedCallback = std::move(callback);
+}
+
+int RedisConnection::GetQueryCount(RingBuffer::SizeType span)
+{
+ return m_OutputQueries.UpdateAndGetValues(Utility::GetTime(), span);
+}
+
+void RedisConnection::IncreasePendingQueries(int count)
+{
+ if (m_Parent) {
+ auto parent (m_Parent);
+
+ asio::post(parent->m_Strand, [parent, count]() {
+ parent->IncreasePendingQueries(count);
+ });
+ } else {
+ m_PendingQueries += count;
+ m_InputQueries.InsertValue(Utility::GetTime(), count);
+ }
+}
+
+void RedisConnection::DecreasePendingQueries(int count)
+{
+ if (m_Parent) {
+ auto parent (m_Parent);
+
+ asio::post(parent->m_Strand, [parent, count]() {
+ parent->DecreasePendingQueries(count);
+ });
+ } else {
+ m_PendingQueries -= count;
+ m_OutputQueries.InsertValue(Utility::GetTime(), count);
+ }
+}
+
+void RedisConnection::RecordAffected(RedisConnection::QueryAffects affected, double when)
+{
+ if (m_Parent) {
+ auto parent (m_Parent);
+
+ asio::post(parent->m_Strand, [parent, affected, when]() {
+ parent->RecordAffected(affected, when);
+ });
+ } else {
+ if (affected.Config) {
+ m_WrittenConfig.InsertValue(when, affected.Config);
+ }
+
+ if (affected.State) {
+ m_WrittenState.InsertValue(when, affected.State);
+ }
+
+ if (affected.History) {
+ m_WrittenHistory.InsertValue(when, affected.History);
+ }
+ }
+}
diff --git a/lib/icingadb/redisconnection.hpp b/lib/icingadb/redisconnection.hpp
new file mode 100644
index 0000000..f346ba2
--- /dev/null
+++ b/lib/icingadb/redisconnection.hpp
@@ -0,0 +1,678 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef REDISCONNECTION_H
+#define REDISCONNECTION_H
+
+#include "base/array.hpp"
+#include "base/atomic.hpp"
+#include "base/convert.hpp"
+#include "base/io-engine.hpp"
+#include "base/object.hpp"
+#include "base/ringbuffer.hpp"
+#include "base/shared.hpp"
+#include "base/string.hpp"
+#include "base/tlsstream.hpp"
+#include "base/value.hpp"
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/buffered_stream.hpp>
+#include <boost/asio/deadline_timer.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/io_context_strand.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/local/stream_protocol.hpp>
+#include <boost/asio/read.hpp>
+#include <boost/asio/read_until.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/asio/streambuf.hpp>
+#include <boost/asio/write.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/regex.hpp>
+#include <boost/utility/string_view.hpp>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <future>
+#include <map>
+#include <memory>
+#include <queue>
+#include <set>
+#include <stdexcept>
+#include <utility>
+#include <vector>
+
+namespace icinga
+{
+/**
+ * An Async Redis connection.
+ *
+ * @ingroup icingadb
+ */
+ class RedisConnection final : public Object
+ {
+ public:
+ DECLARE_PTR_TYPEDEFS(RedisConnection);
+
+ typedef std::vector<String> Query;
+ typedef std::vector<Query> Queries;
+ typedef Value Reply;
+ typedef std::vector<Reply> Replies;
+
+ /**
+ * Redis query priorities, highest first.
+ *
+ * @ingroup icingadb
+ */
+ enum class QueryPriority : unsigned char
+ {
+ Heartbeat,
+ RuntimeStateStream, // runtime state updates, doesn't affect initially synced states
+ Config, // includes initially synced states
+ RuntimeStateSync, // updates initially synced states at runtime, in parallel to config dump, therefore must be < Config
+ History,
+ CheckResult,
+ SyncConnection = 255
+ };
+
+ struct QueryAffects
+ {
+ size_t Config;
+ size_t State;
+ size_t History;
+
+ QueryAffects(size_t config = 0, size_t state = 0, size_t history = 0)
+ : Config(config), State(state), History(history) { }
+ };
+
+ RedisConnection(const String& host, int port, const String& path, const String& password, int db,
+ bool useTls, bool insecure, const String& certPath, const String& keyPath, const String& caPath, const String& crlPath,
+ const String& tlsProtocolmin, const String& cipherList, double connectTimeout, DebugInfo di, const Ptr& parent = nullptr);
+
+ void UpdateTLSContext();
+
+ void Start();
+
+ bool IsConnected();
+
+ void FireAndForgetQuery(Query query, QueryPriority priority, QueryAffects affects = {});
+ void FireAndForgetQueries(Queries queries, QueryPriority priority, QueryAffects affects = {});
+
+ Reply GetResultOfQuery(Query query, QueryPriority priority, QueryAffects affects = {});
+ Replies GetResultsOfQueries(Queries queries, QueryPriority priority, QueryAffects affects = {});
+
+ void EnqueueCallback(const std::function<void(boost::asio::yield_context&)>& callback, QueryPriority priority);
+ void Sync();
+ double GetOldestPendingQueryTs();
+
+ void SuppressQueryKind(QueryPriority kind);
+ void UnsuppressQueryKind(QueryPriority kind);
+
+ void SetConnectedCallback(std::function<void(boost::asio::yield_context& yc)> callback);
+
+ inline bool GetConnected()
+ {
+ return m_Connected.load();
+ }
+
+ int GetQueryCount(RingBuffer::SizeType span);
+
+ inline int GetPendingQueryCount()
+ {
+ return m_PendingQueries;
+ }
+
+ inline int GetWrittenConfigFor(RingBuffer::SizeType span, RingBuffer::SizeType tv = Utility::GetTime())
+ {
+ return m_WrittenConfig.UpdateAndGetValues(tv, span);
+ }
+
+ inline int GetWrittenStateFor(RingBuffer::SizeType span, RingBuffer::SizeType tv = Utility::GetTime())
+ {
+ return m_WrittenState.UpdateAndGetValues(tv, span);
+ }
+
+ inline int GetWrittenHistoryFor(RingBuffer::SizeType span, RingBuffer::SizeType tv = Utility::GetTime())
+ {
+ return m_WrittenHistory.UpdateAndGetValues(tv, span);
+ }
+
+ private:
+ /**
+ * What to do with the responses to Redis queries.
+ *
+ * @ingroup icingadb
+ */
+ enum class ResponseAction : unsigned char
+ {
+ Ignore, // discard
+ Deliver, // submit to the requestor
+ DeliverBulk // submit multiple responses to the requestor at once
+ };
+
+ /**
+ * What to do with how many responses to Redis queries.
+ *
+ * @ingroup icingadb
+ */
+ struct FutureResponseAction
+ {
+ size_t Amount;
+ ResponseAction Action;
+ };
+
+ /**
+ * Something to be send to Redis.
+ *
+ * @ingroup icingadb
+ */
+ struct WriteQueueItem
+ {
+ Shared<Query>::Ptr FireAndForgetQuery;
+ Shared<Queries>::Ptr FireAndForgetQueries;
+ Shared<std::pair<Query, std::promise<Reply>>>::Ptr GetResultOfQuery;
+ Shared<std::pair<Queries, std::promise<Replies>>>::Ptr GetResultsOfQueries;
+ std::function<void(boost::asio::yield_context&)> Callback;
+
+ double CTime;
+ QueryAffects Affects;
+ };
+
+ typedef boost::asio::ip::tcp Tcp;
+ typedef boost::asio::local::stream_protocol Unix;
+
+ typedef boost::asio::buffered_stream<Tcp::socket> TcpConn;
+ typedef boost::asio::buffered_stream<Unix::socket> UnixConn;
+
+ Shared<boost::asio::ssl::context>::Ptr m_TLSContext;
+
+ template<class AsyncReadStream>
+ static Value ReadRESP(AsyncReadStream& stream, boost::asio::yield_context& yc);
+
+ template<class AsyncReadStream>
+ static std::vector<char> ReadLine(AsyncReadStream& stream, boost::asio::yield_context& yc, size_t hint = 0);
+
+ template<class AsyncWriteStream>
+ static void WriteRESP(AsyncWriteStream& stream, const Query& query, boost::asio::yield_context& yc);
+
+ static boost::regex m_ErrAuth;
+
+ RedisConnection(boost::asio::io_context& io, String host, int port, String path, String password,
+ int db, bool useTls, bool insecure, String certPath, String keyPath, String caPath, String crlPath,
+ String tlsProtocolmin, String cipherList, double connectTimeout, DebugInfo di, const Ptr& parent);
+
+ void Connect(boost::asio::yield_context& yc);
+ void ReadLoop(boost::asio::yield_context& yc);
+ void WriteLoop(boost::asio::yield_context& yc);
+ void LogStats(boost::asio::yield_context& yc);
+ void WriteItem(boost::asio::yield_context& yc, WriteQueueItem item);
+ Reply ReadOne(boost::asio::yield_context& yc);
+ void WriteOne(Query& query, boost::asio::yield_context& yc);
+
+ template<class StreamPtr>
+ Reply ReadOne(StreamPtr& stream, boost::asio::yield_context& yc);
+
+ template<class StreamPtr>
+ void WriteOne(StreamPtr& stream, Query& query, boost::asio::yield_context& yc);
+
+ void IncreasePendingQueries(int count);
+ void DecreasePendingQueries(int count);
+ void RecordAffected(QueryAffects affected, double when);
+
+ template<class StreamPtr>
+ void Handshake(StreamPtr& stream, boost::asio::yield_context& yc);
+
+ template<class StreamPtr>
+ Timeout::Ptr MakeTimeout(StreamPtr& stream);
+
+ String m_Path;
+ String m_Host;
+ int m_Port;
+ String m_Password;
+ int m_DbIndex;
+
+ String m_CertPath;
+ String m_KeyPath;
+ bool m_Insecure;
+ String m_CaPath;
+ String m_CrlPath;
+ String m_TlsProtocolmin;
+ String m_CipherList;
+ double m_ConnectTimeout;
+ DebugInfo m_DebugInfo;
+
+ boost::asio::io_context::strand m_Strand;
+ Shared<TcpConn>::Ptr m_TcpConn;
+ Shared<UnixConn>::Ptr m_UnixConn;
+ Shared<AsioTlsStream>::Ptr m_TlsConn;
+ Atomic<bool> m_Connecting, m_Connected, m_Started;
+
+ struct {
+ // Items to be send to Redis
+ std::map<QueryPriority, std::queue<WriteQueueItem>> Writes;
+ // Requestors, each waiting for a single response
+ std::queue<std::promise<Reply>> ReplyPromises;
+ // Requestors, each waiting for multiple responses at once
+ std::queue<std::promise<Replies>> RepliesPromises;
+ // Metadata about all of the above
+ std::queue<FutureResponseAction> FutureResponseActions;
+ } m_Queues;
+
+ // Kinds of queries not to actually send yet
+ std::set<QueryPriority> m_SuppressedQueryKinds;
+
+ // Indicate that there's something to send/receive
+ AsioConditionVariable m_QueuedWrites, m_QueuedReads;
+
+ std::function<void(boost::asio::yield_context& yc)> m_ConnectedCallback;
+
+ // Stats
+ RingBuffer m_InputQueries{10};
+ RingBuffer m_OutputQueries{15 * 60};
+ RingBuffer m_WrittenConfig{15 * 60};
+ RingBuffer m_WrittenState{15 * 60};
+ RingBuffer m_WrittenHistory{15 * 60};
+ int m_PendingQueries{0};
+ boost::asio::deadline_timer m_LogStatsTimer;
+ Ptr m_Parent;
+ };
+
+/**
+ * An error response from the Redis server.
+ *
+ * @ingroup icingadb
+ */
+class RedisError final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(RedisError);
+
+ inline RedisError(String message) : m_Message(std::move(message))
+ {
+ }
+
+ inline const String& GetMessage()
+ {
+ return m_Message;
+ }
+
+private:
+ String m_Message;
+};
+
+/**
+ * Thrown if the connection to the Redis server has already been lost.
+ *
+ * @ingroup icingadb
+ */
+class RedisDisconnected : public std::runtime_error
+{
+public:
+ inline RedisDisconnected() : runtime_error("")
+ {
+ }
+};
+
+/**
+ * Thrown on malformed Redis server responses.
+ *
+ * @ingroup icingadb
+ */
+class RedisProtocolError : public std::runtime_error
+{
+protected:
+ inline RedisProtocolError() : runtime_error("")
+ {
+ }
+};
+
+/**
+ * Thrown on malformed types in Redis server responses.
+ *
+ * @ingroup icingadb
+ */
+class BadRedisType : public RedisProtocolError
+{
+public:
+ inline BadRedisType(char type) : m_What{type, 0}
+ {
+ }
+
+ virtual const char * what() const noexcept override
+ {
+ return m_What;
+ }
+
+private:
+ char m_What[2];
+};
+
+/**
+ * Thrown on malformed ints in Redis server responses.
+ *
+ * @ingroup icingadb
+ */
+class BadRedisInt : public RedisProtocolError
+{
+public:
+ inline BadRedisInt(std::vector<char> intStr) : m_What(std::move(intStr))
+ {
+ m_What.emplace_back(0);
+ }
+
+ virtual const char * what() const noexcept override
+ {
+ return m_What.data();
+ }
+
+private:
+ std::vector<char> m_What;
+};
+
+/**
+ * Read a Redis server response from stream
+ *
+ * @param stream Redis server connection
+ *
+ * @return The response
+ */
+template<class StreamPtr>
+RedisConnection::Reply RedisConnection::ReadOne(StreamPtr& stream, boost::asio::yield_context& yc)
+{
+ namespace asio = boost::asio;
+
+ if (!stream) {
+ throw RedisDisconnected();
+ }
+
+ auto strm (stream);
+
+ try {
+ return ReadRESP(*strm, yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ if (m_Connecting.exchange(false)) {
+ m_Connected.store(false);
+ stream = nullptr;
+
+ if (!m_Connecting.exchange(true)) {
+ Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { Connect(yc); });
+ }
+ }
+
+ throw;
+ }
+}
+
+/**
+ * Write a Redis query to stream
+ *
+ * @param stream Redis server connection
+ * @param query Redis query
+ */
+template<class StreamPtr>
+void RedisConnection::WriteOne(StreamPtr& stream, RedisConnection::Query& query, boost::asio::yield_context& yc)
+{
+ namespace asio = boost::asio;
+
+ if (!stream) {
+ throw RedisDisconnected();
+ }
+
+ auto strm (stream);
+
+ try {
+ WriteRESP(*strm, query, yc);
+ strm->async_flush(yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ if (m_Connecting.exchange(false)) {
+ m_Connected.store(false);
+ stream = nullptr;
+
+ if (!m_Connecting.exchange(true)) {
+ Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { Connect(yc); });
+ }
+ }
+
+ throw;
+ }
+}
+
+/**
+ * Initialize a Redis stream
+ *
+ * @param stream Redis server connection
+ * @param query Redis query
+ */
+template<class StreamPtr>
+void RedisConnection::Handshake(StreamPtr& strm, boost::asio::yield_context& yc)
+{
+ if (m_Password.IsEmpty() && !m_DbIndex) {
+ // Trigger NOAUTH
+ WriteRESP(*strm, {"PING"}, yc);
+ } else {
+ if (!m_Password.IsEmpty()) {
+ WriteRESP(*strm, {"AUTH", m_Password}, yc);
+ }
+
+ if (m_DbIndex) {
+ WriteRESP(*strm, {"SELECT", Convert::ToString(m_DbIndex)}, yc);
+ }
+ }
+
+ strm->async_flush(yc);
+
+ if (m_Password.IsEmpty() && !m_DbIndex) {
+ Reply pong (ReadRESP(*strm, yc));
+
+ if (pong.IsObjectType<RedisError>()) {
+ // Likely NOAUTH
+ BOOST_THROW_EXCEPTION(std::runtime_error(RedisError::Ptr(pong)->GetMessage()));
+ }
+ } else {
+ if (!m_Password.IsEmpty()) {
+ Reply auth (ReadRESP(*strm, yc));
+
+ if (auth.IsObjectType<RedisError>()) {
+ auto& authErr (RedisError::Ptr(auth)->GetMessage().GetData());
+ boost::smatch what;
+
+ if (boost::regex_search(authErr, what, m_ErrAuth)) {
+ Log(LogWarning, "IcingaDB") << authErr;
+ } else {
+ // Likely WRONGPASS
+ BOOST_THROW_EXCEPTION(std::runtime_error(authErr));
+ }
+ }
+ }
+
+ if (m_DbIndex) {
+ Reply select (ReadRESP(*strm, yc));
+
+ if (select.IsObjectType<RedisError>()) {
+ // Likely NOAUTH or ERR DB
+ BOOST_THROW_EXCEPTION(std::runtime_error(RedisError::Ptr(select)->GetMessage()));
+ }
+ }
+ }
+}
+
+/**
+ * Creates a Timeout which cancels stream's I/O after m_ConnectTimeout
+ *
+ * @param stream Redis server connection
+ */
+template<class StreamPtr>
+Timeout::Ptr RedisConnection::MakeTimeout(StreamPtr& stream)
+{
+ Ptr keepAlive (this);
+
+ return new Timeout(
+ m_Strand.context(),
+ m_Strand,
+ boost::posix_time::microseconds(intmax_t(m_ConnectTimeout * 1000000)),
+ [keepAlive, stream](boost::asio::yield_context yc) {
+ boost::system::error_code ec;
+ stream->lowest_layer().cancel(ec);
+ }
+ );
+}
+
+/**
+ * Read a Redis protocol value from stream
+ *
+ * @param stream Redis server connection
+ *
+ * @return The value
+ */
+template<class AsyncReadStream>
+Value RedisConnection::ReadRESP(AsyncReadStream& stream, boost::asio::yield_context& yc)
+{
+ namespace asio = boost::asio;
+
+ char type = 0;
+ asio::async_read(stream, asio::mutable_buffer(&type, 1), yc);
+
+ switch (type) {
+ case '+':
+ {
+ auto buf (ReadLine(stream, yc));
+ return String(buf.begin(), buf.end());
+ }
+ case '-':
+ {
+ auto buf (ReadLine(stream, yc));
+ return new RedisError(String(buf.begin(), buf.end()));
+ }
+ case ':':
+ {
+ auto buf (ReadLine(stream, yc, 21));
+ intmax_t i = 0;
+
+ try {
+ i = boost::lexical_cast<intmax_t>(boost::string_view(buf.data(), buf.size()));
+ } catch (...) {
+ throw BadRedisInt(std::move(buf));
+ }
+
+ return (double)i;
+ }
+ case '$':
+ {
+ auto buf (ReadLine(stream, yc, 21));
+ intmax_t i = 0;
+
+ try {
+ i = boost::lexical_cast<intmax_t>(boost::string_view(buf.data(), buf.size()));
+ } catch (...) {
+ throw BadRedisInt(std::move(buf));
+ }
+
+ if (i < 0) {
+ return Value();
+ }
+
+ buf.clear();
+ buf.insert(buf.end(), i, 0);
+ asio::async_read(stream, asio::mutable_buffer(buf.data(), buf.size()), yc);
+
+ {
+ char crlf[2];
+ asio::async_read(stream, asio::mutable_buffer(crlf, 2), yc);
+ }
+
+ return String(buf.begin(), buf.end());
+ }
+ case '*':
+ {
+ auto buf (ReadLine(stream, yc, 21));
+ intmax_t i = 0;
+
+ try {
+ i = boost::lexical_cast<intmax_t>(boost::string_view(buf.data(), buf.size()));
+ } catch (...) {
+ throw BadRedisInt(std::move(buf));
+ }
+
+ if (i < 0) {
+ return Empty;
+ }
+
+ Array::Ptr arr = new Array();
+
+ arr->Reserve(i);
+
+ for (; i; --i) {
+ arr->Add(ReadRESP(stream, yc));
+ }
+
+ return arr;
+ }
+ default:
+ throw BadRedisType(type);
+ }
+}
+
+/**
+ * Read from stream until \r\n
+ *
+ * @param stream Redis server connection
+ * @param hint Expected amount of data
+ *
+ * @return Read data ex. \r\n
+ */
+template<class AsyncReadStream>
+std::vector<char> RedisConnection::ReadLine(AsyncReadStream& stream, boost::asio::yield_context& yc, size_t hint)
+{
+ namespace asio = boost::asio;
+
+ std::vector<char> line;
+ line.reserve(hint);
+
+ char next = 0;
+ asio::mutable_buffer buf (&next, 1);
+
+ for (;;) {
+ asio::async_read(stream, buf, yc);
+
+ if (next == '\r') {
+ asio::async_read(stream, buf, yc);
+ return line;
+ }
+
+ line.emplace_back(next);
+ }
+}
+
+/**
+ * Write a Redis protocol value to stream
+ *
+ * @param stream Redis server connection
+ * @param query Redis protocol value
+ */
+template<class AsyncWriteStream>
+void RedisConnection::WriteRESP(AsyncWriteStream& stream, const Query& query, boost::asio::yield_context& yc)
+{
+ namespace asio = boost::asio;
+
+ asio::streambuf writeBuffer;
+ std::ostream msg(&writeBuffer);
+
+ msg << "*" << query.size() << "\r\n";
+
+ for (auto& arg : query) {
+ msg << "$" << arg.GetLength() << "\r\n" << arg << "\r\n";
+ }
+
+ asio::async_write(stream, writeBuffer, yc);
+}
+
+}
+
+#endif //REDISCONNECTION_H
diff --git a/lib/livestatus/CMakeLists.txt b/lib/livestatus/CMakeLists.txt
new file mode 100644
index 0000000..d49f9f5
--- /dev/null
+++ b/lib/livestatus/CMakeLists.txt
@@ -0,0 +1,65 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(livestatuslistener.ti livestatuslistener-ti.cpp livestatuslistener-ti.hpp)
+
+set(livestatus_SOURCES
+ i2-livestatus.hpp
+ aggregator.cpp aggregator.hpp
+ andfilter.cpp andfilter.hpp
+ attributefilter.cpp attributefilter.hpp
+ avgaggregator.cpp avgaggregator.hpp
+ column.cpp column.hpp
+ combinerfilter.cpp combinerfilter.hpp
+ commandstable.cpp commandstable.hpp
+ commentstable.cpp commentstable.hpp
+ contactgroupstable.cpp contactgroupstable.hpp
+ contactstable.cpp contactstable.hpp
+ countaggregator.cpp countaggregator.hpp
+ downtimestable.cpp downtimestable.hpp
+ endpointstable.cpp endpointstable.hpp
+ filter.hpp
+ historytable.hpp
+ hostgroupstable.cpp hostgroupstable.hpp
+ hoststable.cpp hoststable.hpp
+ invavgaggregator.cpp invavgaggregator.hpp
+ invsumaggregator.cpp invsumaggregator.hpp
+ livestatuslistener.cpp livestatuslistener.hpp livestatuslistener-ti.hpp
+ livestatuslogutility.cpp livestatuslogutility.hpp
+ livestatusquery.cpp livestatusquery.hpp
+ logtable.cpp logtable.hpp
+ maxaggregator.cpp maxaggregator.hpp
+ minaggregator.cpp minaggregator.hpp
+ negatefilter.cpp negatefilter.hpp
+ orfilter.cpp orfilter.hpp
+ servicegroupstable.cpp servicegroupstable.hpp
+ servicestable.cpp servicestable.hpp
+ statehisttable.cpp statehisttable.hpp
+ statustable.cpp statustable.hpp
+ stdaggregator.cpp stdaggregator.hpp
+ sumaggregator.cpp sumaggregator.hpp
+ table.cpp table.hpp
+ timeperiodstable.cpp timeperiodstable.hpp
+ zonestable.cpp zonestable.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(livestatus livestatus livestatus_SOURCES)
+endif()
+
+add_library(livestatus OBJECT ${livestatus_SOURCES})
+
+add_dependencies(livestatus base config icinga remote)
+
+set_target_properties (
+ livestatus PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/livestatus.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_INITRUNDIR}/cmd\")")
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/livestatus/aggregator.cpp b/lib/livestatus/aggregator.cpp
new file mode 100644
index 0000000..a809b07
--- /dev/null
+++ b/lib/livestatus/aggregator.cpp
@@ -0,0 +1,18 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/aggregator.hpp"
+
+using namespace icinga;
+
+void Aggregator::SetFilter(const Filter::Ptr& filter)
+{
+ m_Filter = filter;
+}
+
+Filter::Ptr Aggregator::GetFilter() const
+{
+ return m_Filter;
+}
+
+AggregatorState::~AggregatorState()
+{ }
diff --git a/lib/livestatus/aggregator.hpp b/lib/livestatus/aggregator.hpp
new file mode 100644
index 0000000..1c0f778
--- /dev/null
+++ b/lib/livestatus/aggregator.hpp
@@ -0,0 +1,44 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef AGGREGATOR_H
+#define AGGREGATOR_H
+
+#include "livestatus/i2-livestatus.hpp"
+#include "livestatus/table.hpp"
+#include "livestatus/filter.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct AggregatorState
+{
+ virtual ~AggregatorState();
+};
+
+/**
+ * @ingroup livestatus
+ */
+class Aggregator : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Aggregator);
+
+ virtual void Apply(const Table::Ptr& table, const Value& row, AggregatorState **state) = 0;
+ virtual double GetResultAndFreeState(AggregatorState *state) const = 0;
+ void SetFilter(const Filter::Ptr& filter);
+
+protected:
+ Aggregator() = default;
+
+ Filter::Ptr GetFilter() const;
+
+private:
+ Filter::Ptr m_Filter;
+};
+
+}
+
+#endif /* AGGREGATOR_H */
diff --git a/lib/livestatus/andfilter.cpp b/lib/livestatus/andfilter.cpp
new file mode 100644
index 0000000..9852580
--- /dev/null
+++ b/lib/livestatus/andfilter.cpp
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/andfilter.hpp"
+
+using namespace icinga;
+
+bool AndFilter::Apply(const Table::Ptr& table, const Value& row)
+{
+ for (const Filter::Ptr& filter : m_Filters) {
+ if (!filter->Apply(table, row))
+ return false;
+ }
+
+ return true;
+}
diff --git a/lib/livestatus/andfilter.hpp b/lib/livestatus/andfilter.hpp
new file mode 100644
index 0000000..8192bf7
--- /dev/null
+++ b/lib/livestatus/andfilter.hpp
@@ -0,0 +1,26 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ANDFILTER_H
+#define ANDFILTER_H
+
+#include "livestatus/combinerfilter.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class AndFilter final : public CombinerFilter
+{
+public:
+ DECLARE_PTR_TYPEDEFS(AndFilter);
+
+ bool Apply(const Table::Ptr& table, const Value& row) override;
+};
+
+}
+
+#endif /* ANDFILTER_H */
diff --git a/lib/livestatus/attributefilter.cpp b/lib/livestatus/attributefilter.cpp
new file mode 100644
index 0000000..50d7244
--- /dev/null
+++ b/lib/livestatus/attributefilter.cpp
@@ -0,0 +1,121 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/attributefilter.hpp"
+#include "base/convert.hpp"
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include <boost/regex.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+using namespace icinga;
+
+AttributeFilter::AttributeFilter(String column, String op, String operand)
+ : m_Column(std::move(column)), m_Operator(std::move(op)), m_Operand(std::move(operand))
+{ }
+
+bool AttributeFilter::Apply(const Table::Ptr& table, const Value& row)
+{
+ Column column = table->GetColumn(m_Column);
+
+ Value value = column.ExtractValue(row);
+
+ if (value.IsObjectType<Array>()) {
+ Array::Ptr array = value;
+
+ if (m_Operator == ">=" || m_Operator == "<") {
+ bool negate = (m_Operator == "<");
+
+ ObjectLock olock(array);
+ for (const String& item : array) {
+ if (item == m_Operand)
+ return !negate; /* Item found in list. */
+ }
+
+ return negate; /* Item not found in list. */
+ } else if (m_Operator == "=") {
+ return (array->GetLength() == 0);
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid operator for column '" + m_Column + "': " + m_Operator + " (expected '>=' or '=')."));
+ }
+ } else {
+ if (m_Operator == "=") {
+ if (value.GetType() == ValueNumber || value.GetType() == ValueBoolean)
+ return (static_cast<double>(value) == Convert::ToDouble(m_Operand));
+ else
+ return (static_cast<String>(value) == m_Operand);
+ } else if (m_Operator == "~") {
+ bool ret;
+ try {
+ boost::regex expr(m_Operand.GetData());
+ String operand = value;
+ boost::smatch what;
+ ret = boost::regex_search(operand.GetData(), what, expr);
+ } catch (boost::exception&) {
+ Log(LogWarning, "AttributeFilter")
+ << "Regex '" << m_Operand << " " << m_Operator << " " << value << "' error.";
+ ret = false;
+ }
+
+ //Log(LogDebug, "LivestatusListener/AttributeFilter")
+ // << "Attribute filter '" << m_Operand + " " << m_Operator << " "
+ // << value << "' " << (ret ? "matches" : "doesn't match") << ".";
+
+ return ret;
+ } else if (m_Operator == "=~") {
+ bool ret;
+ try {
+ String operand = value;
+ ret = boost::iequals(operand, m_Operand.GetData());
+ } catch (boost::exception&) {
+ Log(LogWarning, "AttributeFilter")
+ << "Case-insensitive equality '" << m_Operand << " " << m_Operator << " " << value << "' error.";
+ ret = false;
+ }
+
+ return ret;
+ } else if (m_Operator == "~~") {
+ bool ret;
+ try {
+ boost::regex expr(m_Operand.GetData(), boost::regex::icase);
+ String operand = value;
+ boost::smatch what;
+ ret = boost::regex_search(operand.GetData(), what, expr);
+ } catch (boost::exception&) {
+ Log(LogWarning, "AttributeFilter")
+ << "Regex '" << m_Operand << " " << m_Operator << " " << value << "' error.";
+ ret = false;
+ }
+
+ //Log(LogDebug, "LivestatusListener/AttributeFilter")
+ // << "Attribute filter '" << m_Operand << " " << m_Operator << " "
+ // << value << "' " << (ret ? "matches" : "doesn't match") << ".";
+
+ return ret;
+ } else if (m_Operator == "<") {
+ if (value.GetType() == ValueNumber)
+ return (static_cast<double>(value) < Convert::ToDouble(m_Operand));
+ else
+ return (static_cast<String>(value) < m_Operand);
+ } else if (m_Operator == ">") {
+ if (value.GetType() == ValueNumber)
+ return (static_cast<double>(value) > Convert::ToDouble(m_Operand));
+ else
+ return (static_cast<String>(value) > m_Operand);
+ } else if (m_Operator == "<=") {
+ if (value.GetType() == ValueNumber)
+ return (static_cast<double>(value) <= Convert::ToDouble(m_Operand));
+ else
+ return (static_cast<String>(value) <= m_Operand);
+ } else if (m_Operator == ">=") {
+ if (value.GetType() == ValueNumber)
+ return (static_cast<double>(value) >= Convert::ToDouble(m_Operand));
+ else
+ return (static_cast<String>(value) >= m_Operand);
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Unknown operator for column '" + m_Column + "': " + m_Operator));
+ }
+ }
+
+ return false;
+}
diff --git a/lib/livestatus/attributefilter.hpp b/lib/livestatus/attributefilter.hpp
new file mode 100644
index 0000000..18bd843
--- /dev/null
+++ b/lib/livestatus/attributefilter.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ATTRIBUTEFILTER_H
+#define ATTRIBUTEFILTER_H
+
+#include "livestatus/filter.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class AttributeFilter final : public Filter
+{
+public:
+ DECLARE_PTR_TYPEDEFS(AttributeFilter);
+
+ AttributeFilter(String column, String op, String operand);
+
+ bool Apply(const Table::Ptr& table, const Value& row) override;
+
+protected:
+ String m_Column;
+ String m_Operator;
+ String m_Operand;
+};
+
+}
+
+#endif /* FILTER_H */
diff --git a/lib/livestatus/avgaggregator.cpp b/lib/livestatus/avgaggregator.cpp
new file mode 100644
index 0000000..35701f3
--- /dev/null
+++ b/lib/livestatus/avgaggregator.cpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/avgaggregator.hpp"
+
+using namespace icinga;
+
+AvgAggregator::AvgAggregator(String attr)
+ : m_AvgAttr(std::move(attr))
+{ }
+
+AvgAggregatorState *AvgAggregator::EnsureState(AggregatorState **state)
+{
+ if (!*state)
+ *state = new AvgAggregatorState();
+
+ return static_cast<AvgAggregatorState *>(*state);
+}
+
+void AvgAggregator::Apply(const Table::Ptr& table, const Value& row, AggregatorState **state)
+{
+ Column column = table->GetColumn(m_AvgAttr);
+
+ Value value = column.ExtractValue(row);
+
+ AvgAggregatorState *pstate = EnsureState(state);
+
+ pstate->Avg += value;
+ pstate->AvgCount++;
+}
+
+double AvgAggregator::GetResultAndFreeState(AggregatorState *state) const
+{
+ AvgAggregatorState *pstate = EnsureState(&state);
+ double result = pstate->Avg / pstate->AvgCount;
+ delete pstate;
+
+ return result;
+}
diff --git a/lib/livestatus/avgaggregator.hpp b/lib/livestatus/avgaggregator.hpp
new file mode 100644
index 0000000..11bd9f3
--- /dev/null
+++ b/lib/livestatus/avgaggregator.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef AVGAGGREGATOR_H
+#define AVGAGGREGATOR_H
+
+#include "livestatus/table.hpp"
+#include "livestatus/aggregator.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct AvgAggregatorState final : public AggregatorState
+{
+ double Avg{0};
+ double AvgCount{0};
+};
+
+/**
+ * @ingroup livestatus
+ */
+class AvgAggregator final : public Aggregator
+{
+public:
+ DECLARE_PTR_TYPEDEFS(AvgAggregator);
+
+ AvgAggregator(String attr);
+
+ void Apply(const Table::Ptr& table, const Value& row, AggregatorState **state) override;
+ double GetResultAndFreeState(AggregatorState *state) const override;
+
+private:
+ String m_AvgAttr;
+
+ static AvgAggregatorState *EnsureState(AggregatorState **state);
+};
+
+}
+
+#endif /* AVGAGGREGATOR_H */
diff --git a/lib/livestatus/column.cpp b/lib/livestatus/column.cpp
new file mode 100644
index 0000000..c915b3d
--- /dev/null
+++ b/lib/livestatus/column.cpp
@@ -0,0 +1,21 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/column.hpp"
+
+using namespace icinga;
+
+Column::Column(ValueAccessor valueAccessor, ObjectAccessor objectAccessor)
+ : m_ValueAccessor(std::move(valueAccessor)), m_ObjectAccessor(std::move(objectAccessor))
+{ }
+
+Value Column::ExtractValue(const Value& urow, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject) const
+{
+ Value row;
+
+ if (m_ObjectAccessor)
+ row = m_ObjectAccessor(urow, groupByType, groupByObject);
+ else
+ row = urow;
+
+ return m_ValueAccessor(row);
+}
diff --git a/lib/livestatus/column.hpp b/lib/livestatus/column.hpp
new file mode 100644
index 0000000..264cca7
--- /dev/null
+++ b/lib/livestatus/column.hpp
@@ -0,0 +1,37 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COLUMN_H
+#define COLUMN_H
+
+#include "livestatus/i2-livestatus.hpp"
+#include "base/value.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+enum LivestatusGroupByType {
+ LivestatusGroupByNone,
+ LivestatusGroupByHostGroup,
+ LivestatusGroupByServiceGroup
+};
+
+class Column
+{
+public:
+ typedef std::function<Value (const Value&)> ValueAccessor;
+ typedef std::function<Value (const Value&, LivestatusGroupByType, const Object::Ptr&)> ObjectAccessor;
+
+ Column(ValueAccessor valueAccessor, ObjectAccessor objectAccessor);
+
+ Value ExtractValue(const Value& urow, LivestatusGroupByType groupByType = LivestatusGroupByNone, const Object::Ptr& groupByObject = Empty) const;
+
+private:
+ ValueAccessor m_ValueAccessor;
+ ObjectAccessor m_ObjectAccessor;
+};
+
+}
+
+#endif /* COLUMN_H */
diff --git a/lib/livestatus/combinerfilter.cpp b/lib/livestatus/combinerfilter.cpp
new file mode 100644
index 0000000..36a8328
--- /dev/null
+++ b/lib/livestatus/combinerfilter.cpp
@@ -0,0 +1,10 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/combinerfilter.hpp"
+
+using namespace icinga;
+
+void CombinerFilter::AddSubFilter(const Filter::Ptr& filter)
+{
+ m_Filters.push_back(filter);
+}
diff --git a/lib/livestatus/combinerfilter.hpp b/lib/livestatus/combinerfilter.hpp
new file mode 100644
index 0000000..49b8b61
--- /dev/null
+++ b/lib/livestatus/combinerfilter.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMBINERFILTER_H
+#define COMBINERFILTER_H
+
+#include "livestatus/filter.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class CombinerFilter : public Filter
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CombinerFilter);
+
+ void AddSubFilter(const Filter::Ptr& filter);
+
+protected:
+ std::vector<Filter::Ptr> m_Filters;
+
+ CombinerFilter() = default;
+};
+
+}
+
+#endif /* COMBINERFILTER_H */
diff --git a/lib/livestatus/commandstable.cpp b/lib/livestatus/commandstable.cpp
new file mode 100644
index 0000000..3a777d2
--- /dev/null
+++ b/lib/livestatus/commandstable.cpp
@@ -0,0 +1,142 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/commandstable.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/compatutility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include <boost/algorithm/string/replace.hpp>
+
+using namespace icinga;
+
+CommandsTable::CommandsTable()
+{
+ AddColumns(this);
+}
+
+void CommandsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&CommandsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "line", Column(&CommandsTable::LineAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variable_names", Column(&CommandsTable::CustomVariableNamesAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variable_values", Column(&CommandsTable::CustomVariableValuesAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variables", Column(&CommandsTable::CustomVariablesAccessor, objectAccessor));
+ table->AddColumn(prefix + "modified_attributes", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "modified_attributes_list", Column(&Table::ZeroAccessor, objectAccessor));
+}
+
+String CommandsTable::GetName() const
+{
+ return "commands";
+}
+
+String CommandsTable::GetPrefix() const
+{
+ return "command";
+}
+
+void CommandsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const ConfigObject::Ptr& object : ConfigType::GetObjectsByType<CheckCommand>()) {
+ if (!addRowFn(object, LivestatusGroupByNone, Empty))
+ return;
+ }
+
+ for (const ConfigObject::Ptr& object : ConfigType::GetObjectsByType<EventCommand>()) {
+ if (!addRowFn(object, LivestatusGroupByNone, Empty))
+ return;
+ }
+
+ for (const ConfigObject::Ptr& object : ConfigType::GetObjectsByType<NotificationCommand>()) {
+ if (!addRowFn(object, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Value CommandsTable::NameAccessor(const Value& row)
+{
+ Command::Ptr command = static_cast<Command::Ptr>(row);
+
+ return CompatUtility::GetCommandName(command);
+}
+
+Value CommandsTable::LineAccessor(const Value& row)
+{
+ Command::Ptr command = static_cast<Command::Ptr>(row);
+
+ if (!command)
+ return Empty;
+
+ return CompatUtility::GetCommandLine(command);
+}
+
+Value CommandsTable::CustomVariableNamesAccessor(const Value& row)
+{
+ Command::Ptr command = static_cast<Command::Ptr>(row);
+
+ if (!command)
+ return Empty;
+
+ Dictionary::Ptr vars = command->GetVars();
+
+ ArrayData keys;
+
+ if (vars) {
+ ObjectLock xlock(vars);
+ for (const auto& kv : vars) {
+ keys.push_back(kv.first);
+ }
+ }
+
+ return new Array(std::move(keys));
+}
+
+Value CommandsTable::CustomVariableValuesAccessor(const Value& row)
+{
+ Command::Ptr command = static_cast<Command::Ptr>(row);
+
+ if (!command)
+ return Empty;
+
+ Dictionary::Ptr vars = command->GetVars();
+
+ ArrayData keys;
+
+ if (vars) {
+ ObjectLock xlock(vars);
+ for (const auto& kv : vars) {
+ keys.push_back(kv.second);
+ }
+ }
+
+ return new Array(std::move(keys));
+}
+
+Value CommandsTable::CustomVariablesAccessor(const Value& row)
+{
+ Command::Ptr command = static_cast<Command::Ptr>(row);
+
+ if (!command)
+ return Empty;
+
+ Dictionary::Ptr vars = command->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock xlock(vars);
+ for (const auto& kv : vars) {
+ result.push_back(new Array({
+ kv.first,
+ kv.second
+ }));
+ }
+ }
+
+ return new Array(std::move(result));
+}
diff --git a/lib/livestatus/commandstable.hpp b/lib/livestatus/commandstable.hpp
new file mode 100644
index 0000000..cd2d915
--- /dev/null
+++ b/lib/livestatus/commandstable.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMMANDSTABLE_H
+#define COMMANDSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class CommandsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CommandsTable);
+
+ CommandsTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value NameAccessor(const Value& row);
+ static Value LineAccessor(const Value& row);
+ static Value CustomVariableNamesAccessor(const Value& row);
+ static Value CustomVariableValuesAccessor(const Value& row);
+ static Value CustomVariablesAccessor(const Value& row);
+};
+
+}
+
+#endif /* COMMANDSTABLE_H */
diff --git a/lib/livestatus/commentstable.cpp b/lib/livestatus/commentstable.cpp
new file mode 100644
index 0000000..40bffad
--- /dev/null
+++ b/lib/livestatus/commentstable.cpp
@@ -0,0 +1,178 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/commentstable.hpp"
+#include "livestatus/hoststable.hpp"
+#include "livestatus/servicestable.hpp"
+#include "icinga/service.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+CommentsTable::CommentsTable()
+{
+ AddColumns(this);
+}
+
+void CommentsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "author", Column(&CommentsTable::AuthorAccessor, objectAccessor));
+ table->AddColumn(prefix + "comment", Column(&CommentsTable::CommentAccessor, objectAccessor));
+ table->AddColumn(prefix + "id", Column(&CommentsTable::IdAccessor, objectAccessor));
+ table->AddColumn(prefix + "entry_time", Column(&CommentsTable::EntryTimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "type", Column(&CommentsTable::TypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_service", Column(&CommentsTable::IsServiceAccessor, objectAccessor));
+ table->AddColumn(prefix + "persistent", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "source", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "entry_type", Column(&CommentsTable::EntryTypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "expires", Column(&CommentsTable::ExpiresAccessor, objectAccessor));
+ table->AddColumn(prefix + "expire_time", Column(&CommentsTable::ExpireTimeAccessor, objectAccessor));
+
+ /* order is important - host w/o services must not be empty */
+ ServicesTable::AddColumns(table, "service_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return ServiceAccessor(row, objectAccessor);
+ });
+ HostsTable::AddColumns(table, "host_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return HostAccessor(row, objectAccessor);
+ });
+}
+
+String CommentsTable::GetName() const
+{
+ return "comments";
+}
+
+String CommentsTable::GetPrefix() const
+{
+ return "comment";
+}
+
+void CommentsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const Comment::Ptr& comment : ConfigType::GetObjectsByType<Comment>()) {
+ if (!addRowFn(comment, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Object::Ptr CommentsTable::HostAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ Checkable::Ptr checkable = comment->GetCheckable();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ return host;
+}
+
+Object::Ptr CommentsTable::ServiceAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ Checkable::Ptr checkable = comment->GetCheckable();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ return service;
+}
+
+Value CommentsTable::AuthorAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ if (!comment)
+ return Empty;
+
+ return comment->GetAuthor();
+}
+
+Value CommentsTable::CommentAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ if (!comment)
+ return Empty;
+
+ return comment->GetText();
+}
+
+Value CommentsTable::IdAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ if (!comment)
+ return Empty;
+
+ return comment->GetLegacyId();
+}
+
+Value CommentsTable::EntryTimeAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ if (!comment)
+ return Empty;
+
+ return static_cast<int>(comment->GetEntryTime());
+}
+
+Value CommentsTable::TypeAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+ Checkable::Ptr checkable = comment->GetCheckable();
+
+ if (!checkable)
+ return Empty;
+
+ if (dynamic_pointer_cast<Host>(checkable))
+ return 1;
+ else
+ return 2;
+}
+
+Value CommentsTable::IsServiceAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+ Checkable::Ptr checkable = comment->GetCheckable();
+
+ if (!checkable)
+ return Empty;
+
+ return (dynamic_pointer_cast<Host>(checkable) ? 0 : 1);
+}
+
+Value CommentsTable::EntryTypeAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ if (!comment)
+ return Empty;
+
+ return comment->GetEntryType();
+}
+
+Value CommentsTable::ExpiresAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ if (!comment)
+ return Empty;
+
+ return comment->GetExpireTime() != 0;
+}
+
+Value CommentsTable::ExpireTimeAccessor(const Value& row)
+{
+ Comment::Ptr comment = static_cast<Comment::Ptr>(row);
+
+ if (!comment)
+ return Empty;
+
+ return static_cast<int>(comment->GetExpireTime());
+}
diff --git a/lib/livestatus/commentstable.hpp b/lib/livestatus/commentstable.hpp
new file mode 100644
index 0000000..b46e155
--- /dev/null
+++ b/lib/livestatus/commentstable.hpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMMENTSTABLE_H
+#define COMMENTSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class CommentsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CommentsTable);
+
+ CommentsTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+private:
+ static Object::Ptr HostAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+ static Object::Ptr ServiceAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+
+ static Value AuthorAccessor(const Value& row);
+ static Value CommentAccessor(const Value& row);
+ static Value IdAccessor(const Value& row);
+ static Value EntryTimeAccessor(const Value& row);
+ static Value TypeAccessor(const Value& row);
+ static Value IsServiceAccessor(const Value& row);
+ static Value EntryTypeAccessor(const Value& row);
+ static Value ExpiresAccessor(const Value& row);
+ static Value ExpireTimeAccessor(const Value& row);
+};
+
+}
+
+#endif /* COMMENTSTABLE_H */
diff --git a/lib/livestatus/contactgroupstable.cpp b/lib/livestatus/contactgroupstable.cpp
new file mode 100644
index 0000000..b4d6853
--- /dev/null
+++ b/lib/livestatus/contactgroupstable.cpp
@@ -0,0 +1,74 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/contactgroupstable.hpp"
+#include "icinga/usergroup.hpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+ContactGroupsTable::ContactGroupsTable()
+{
+ AddColumns(this);
+}
+
+void ContactGroupsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&ContactGroupsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "alias", Column(&ContactGroupsTable::AliasAccessor, objectAccessor));
+ table->AddColumn(prefix + "members", Column(&ContactGroupsTable::MembersAccessor, objectAccessor));
+}
+
+String ContactGroupsTable::GetName() const
+{
+ return "contactgroups";
+}
+
+String ContactGroupsTable::GetPrefix() const
+{
+ return "contactgroup";
+}
+
+void ContactGroupsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const UserGroup::Ptr& ug : ConfigType::GetObjectsByType<UserGroup>()) {
+ if (!addRowFn(ug, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Value ContactGroupsTable::NameAccessor(const Value& row)
+{
+ UserGroup::Ptr user_group = static_cast<UserGroup::Ptr>(row);
+
+ if (!user_group)
+ return Empty;
+
+ return user_group->GetName();
+}
+
+Value ContactGroupsTable::AliasAccessor(const Value& row)
+{
+ UserGroup::Ptr user_group = static_cast<UserGroup::Ptr>(row);
+
+ if (!user_group)
+ return Empty;
+
+ return user_group->GetName();
+}
+
+Value ContactGroupsTable::MembersAccessor(const Value& row)
+{
+ UserGroup::Ptr user_group = static_cast<UserGroup::Ptr>(row);
+
+ if (!user_group)
+ return Empty;
+
+ ArrayData result;
+
+ for (const User::Ptr& user : user_group->GetMembers()) {
+ result.push_back(user->GetName());
+ }
+
+ return new Array(std::move(result));
+}
diff --git a/lib/livestatus/contactgroupstable.hpp b/lib/livestatus/contactgroupstable.hpp
new file mode 100644
index 0000000..a57f5c3
--- /dev/null
+++ b/lib/livestatus/contactgroupstable.hpp
@@ -0,0 +1,39 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONTACTGROUPSTABLE_H
+#define CONTACTGROUPSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class ContactGroupsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ContactGroupsTable);
+
+ ContactGroupsTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value NameAccessor(const Value& row);
+ static Value AliasAccessor(const Value& row);
+ static Value MembersAccessor(const Value& row);
+};
+
+}
+
+#endif /* CONTACTGROUPSTABLE_H */
diff --git a/lib/livestatus/contactstable.cpp b/lib/livestatus/contactstable.cpp
new file mode 100644
index 0000000..d6a04c4
--- /dev/null
+++ b/lib/livestatus/contactstable.cpp
@@ -0,0 +1,278 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/contactstable.hpp"
+#include "icinga/user.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/compatutility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/json.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+ContactsTable::ContactsTable()
+{
+ AddColumns(this);
+}
+
+void ContactsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&ContactsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "alias", Column(&ContactsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "email", Column(&ContactsTable::EmailAccessor, objectAccessor));
+ table->AddColumn(prefix + "pager", Column(&ContactsTable::PagerAccessor, objectAccessor));
+ table->AddColumn(prefix + "host_notification_period", Column(&ContactsTable::HostNotificationPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "service_notification_period", Column(&ContactsTable::ServiceNotificationPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "can_submit_commands", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "host_notifications_enabled", Column(&ContactsTable::HostNotificationsEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "service_notifications_enabled", Column(&ContactsTable::ServiceNotificationsEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_host_notification_period", Column(&ContactsTable::InHostNotificationPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_service_notification_period", Column(&ContactsTable::InServiceNotificationPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "vars_variable_names", Column(&ContactsTable::CustomVariableNamesAccessor, objectAccessor));
+ table->AddColumn(prefix + "vars_variable_values", Column(&ContactsTable::CustomVariableValuesAccessor, objectAccessor));
+ table->AddColumn(prefix + "vars_variables", Column(&ContactsTable::CustomVariablesAccessor, objectAccessor));
+ table->AddColumn(prefix + "modified_attributes", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "modified_attributes_list", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "cv_is_json", Column(&ContactsTable::CVIsJsonAccessor, objectAccessor));
+
+}
+
+String ContactsTable::GetName() const
+{
+ return "contacts";
+}
+
+String ContactsTable::GetPrefix() const
+{
+ return "contact";
+}
+
+void ContactsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const User::Ptr& user : ConfigType::GetObjectsByType<User>()) {
+ if (!addRowFn(user, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Value ContactsTable::NameAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ return user->GetName();
+}
+
+Value ContactsTable::AliasAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ return user->GetDisplayName();
+}
+
+Value ContactsTable::EmailAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ return user->GetEmail();
+}
+
+Value ContactsTable::PagerAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ return user->GetPager();
+}
+
+Value ContactsTable::HostNotificationPeriodAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ /* same as service */
+ TimePeriod::Ptr timeperiod = user->GetPeriod();
+
+ if (!timeperiod)
+ return Empty;
+
+ return timeperiod->GetName();
+}
+
+Value ContactsTable::ServiceNotificationPeriodAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ TimePeriod::Ptr timeperiod = user->GetPeriod();
+
+ if (!timeperiod)
+ return Empty;
+
+ return timeperiod->GetName();
+}
+
+Value ContactsTable::HostNotificationsEnabledAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ return (user->GetEnableNotifications() ? 1 : 0);
+}
+
+Value ContactsTable::ServiceNotificationsEnabledAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ return (user->GetEnableNotifications() ? 1 : 0);
+}
+
+Value ContactsTable::InHostNotificationPeriodAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ TimePeriod::Ptr timeperiod = user->GetPeriod();
+
+ if (!timeperiod)
+ return Empty;
+
+ return (timeperiod->IsInside(Utility::GetTime()) ? 1 : 0);
+}
+
+Value ContactsTable::InServiceNotificationPeriodAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ TimePeriod::Ptr timeperiod = user->GetPeriod();
+
+ if (!timeperiod)
+ return Empty;
+
+ return (timeperiod->IsInside(Utility::GetTime()) ? 1 : 0);
+}
+
+Value ContactsTable::CustomVariableNamesAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ Dictionary::Ptr vars = user->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ result.push_back(kv.first);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ContactsTable::CustomVariableValuesAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ Dictionary::Ptr vars = user->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ result.push_back(JsonEncode(kv.second));
+ else
+ result.push_back(kv.second);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ContactsTable::CustomVariablesAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ Dictionary::Ptr vars = user->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ Value val;
+
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ val = JsonEncode(kv.second);
+ else
+ val = kv.second;
+
+ result.push_back(new Array({
+ kv.first,
+ val
+ }));
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ContactsTable::CVIsJsonAccessor(const Value& row)
+{
+ User::Ptr user = static_cast<User::Ptr>(row);
+
+ if (!user)
+ return Empty;
+
+ Dictionary::Ptr vars = user->GetVars();
+
+ if (!vars)
+ return Empty;
+
+ bool cv_is_json = false;
+
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ cv_is_json = true;
+ }
+
+ return cv_is_json;
+}
diff --git a/lib/livestatus/contactstable.hpp b/lib/livestatus/contactstable.hpp
new file mode 100644
index 0000000..0bd2679
--- /dev/null
+++ b/lib/livestatus/contactstable.hpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONTACTSTABLE_H
+#define CONTACTSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class ContactsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ContactsTable);
+
+ ContactsTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value NameAccessor(const Value& row);
+ static Value AliasAccessor(const Value& row);
+ static Value EmailAccessor(const Value& row);
+ static Value PagerAccessor(const Value& row);
+ static Value HostNotificationPeriodAccessor(const Value& row);
+ static Value ServiceNotificationPeriodAccessor(const Value& row);
+ static Value HostNotificationsEnabledAccessor(const Value& row);
+ static Value ServiceNotificationsEnabledAccessor(const Value& row);
+ static Value InHostNotificationPeriodAccessor(const Value& row);
+ static Value InServiceNotificationPeriodAccessor(const Value& row);
+ static Value CustomVariableNamesAccessor(const Value& row);
+ static Value CustomVariableValuesAccessor(const Value& row);
+ static Value CustomVariablesAccessor(const Value& row);
+ static Value CVIsJsonAccessor(const Value& row);
+};
+
+}
+
+#endif /* CONTACTSTABLE_H */
diff --git a/lib/livestatus/countaggregator.cpp b/lib/livestatus/countaggregator.cpp
new file mode 100644
index 0000000..b8a7238
--- /dev/null
+++ b/lib/livestatus/countaggregator.cpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/countaggregator.hpp"
+
+using namespace icinga;
+
+CountAggregatorState *CountAggregator::EnsureState(AggregatorState **state)
+{
+ if (!*state)
+ *state = new CountAggregatorState();
+
+ return static_cast<CountAggregatorState *>(*state);
+}
+
+void CountAggregator::Apply(const Table::Ptr& table, const Value& row, AggregatorState **state)
+{
+ CountAggregatorState *pstate = EnsureState(state);
+
+ if (GetFilter()->Apply(table, row))
+ pstate->Count++;
+}
+
+double CountAggregator::GetResultAndFreeState(AggregatorState *state) const
+{
+ CountAggregatorState *pstate = EnsureState(&state);
+ double result = pstate->Count;
+ delete pstate;
+
+ return result;
+}
diff --git a/lib/livestatus/countaggregator.hpp b/lib/livestatus/countaggregator.hpp
new file mode 100644
index 0000000..22d4983
--- /dev/null
+++ b/lib/livestatus/countaggregator.hpp
@@ -0,0 +1,37 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COUNTAGGREGATOR_H
+#define COUNTAGGREGATOR_H
+
+#include "livestatus/table.hpp"
+#include "livestatus/aggregator.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct CountAggregatorState final : public AggregatorState
+{
+ int Count{0};
+};
+
+/**
+ * @ingroup livestatus
+ */
+class CountAggregator final : public Aggregator
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CountAggregator);
+
+ void Apply(const Table::Ptr& table, const Value& row, AggregatorState **) override;
+ double GetResultAndFreeState(AggregatorState *state) const override;
+
+private:
+ static CountAggregatorState *EnsureState(AggregatorState **state);
+};
+
+}
+
+#endif /* COUNTAGGREGATOR_H */
diff --git a/lib/livestatus/downtimestable.cpp b/lib/livestatus/downtimestable.cpp
new file mode 100644
index 0000000..09c111e
--- /dev/null
+++ b/lib/livestatus/downtimestable.cpp
@@ -0,0 +1,168 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/downtimestable.hpp"
+#include "livestatus/hoststable.hpp"
+#include "livestatus/servicestable.hpp"
+#include "icinga/service.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+DowntimesTable::DowntimesTable()
+{
+ AddColumns(this);
+}
+
+void DowntimesTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "author", Column(&DowntimesTable::AuthorAccessor, objectAccessor));
+ table->AddColumn(prefix + "comment", Column(&DowntimesTable::CommentAccessor, objectAccessor));
+ table->AddColumn(prefix + "id", Column(&DowntimesTable::IdAccessor, objectAccessor));
+ table->AddColumn(prefix + "entry_time", Column(&DowntimesTable::EntryTimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "type", Column(&DowntimesTable::TypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_service", Column(&DowntimesTable::IsServiceAccessor, objectAccessor));
+ table->AddColumn(prefix + "start_time", Column(&DowntimesTable::StartTimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "end_time", Column(&DowntimesTable::EndTimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "fixed", Column(&DowntimesTable::FixedAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration", Column(&DowntimesTable::DurationAccessor, objectAccessor));
+ table->AddColumn(prefix + "triggered_by", Column(&DowntimesTable::TriggeredByAccessor, objectAccessor));
+
+ /* order is important - host w/o services must not be empty */
+ ServicesTable::AddColumns(table, "service_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return ServiceAccessor(row, objectAccessor);
+ });
+ HostsTable::AddColumns(table, "host_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return HostAccessor(row, objectAccessor);
+ });
+}
+
+String DowntimesTable::GetName() const
+{
+ return "downtimes";
+}
+
+String DowntimesTable::GetPrefix() const
+{
+ return "downtime";
+}
+
+void DowntimesTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const Downtime::Ptr& downtime : ConfigType::GetObjectsByType<Downtime>()) {
+ if (!addRowFn(downtime, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Object::Ptr DowntimesTable::HostAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ return host;
+}
+
+Object::Ptr DowntimesTable::ServiceAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ return service;
+}
+
+Value DowntimesTable::AuthorAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ return downtime->GetAuthor();
+}
+
+Value DowntimesTable::CommentAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ return downtime->GetComment();
+}
+
+Value DowntimesTable::IdAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ return downtime->GetLegacyId();
+}
+
+Value DowntimesTable::EntryTimeAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ return static_cast<int>(downtime->GetEntryTime());
+}
+
+Value DowntimesTable::TypeAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+ // 1 .. active, 0 .. pending
+ return (downtime->IsInEffect() ? 1 : 0);
+}
+
+Value DowntimesTable::IsServiceAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ return (dynamic_pointer_cast<Host>(checkable) ? 0 : 1);
+}
+
+Value DowntimesTable::StartTimeAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ return static_cast<int>(downtime->GetStartTime());
+}
+
+Value DowntimesTable::EndTimeAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ return static_cast<int>(downtime->GetEndTime());
+}
+
+Value DowntimesTable::FixedAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ return downtime->GetFixed();
+}
+
+Value DowntimesTable::DurationAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ return downtime->GetDuration();
+}
+
+Value DowntimesTable::TriggeredByAccessor(const Value& row)
+{
+ Downtime::Ptr downtime = static_cast<Downtime::Ptr>(row);
+
+ String triggerDowntimeName = downtime->GetTriggeredBy();
+
+ Downtime::Ptr triggerDowntime = Downtime::GetByName(triggerDowntimeName);
+
+ if (triggerDowntime)
+ return triggerDowntime->GetLegacyId();
+
+ return Empty;
+}
diff --git a/lib/livestatus/downtimestable.hpp b/lib/livestatus/downtimestable.hpp
new file mode 100644
index 0000000..4b5c909
--- /dev/null
+++ b/lib/livestatus/downtimestable.hpp
@@ -0,0 +1,51 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DOWNTIMESTABLE_H
+#define DOWNTIMESTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class DowntimesTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(DowntimesTable);
+
+ DowntimesTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+private:
+ static Object::Ptr HostAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+ static Object::Ptr ServiceAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+
+ static Value AuthorAccessor(const Value& row);
+ static Value CommentAccessor(const Value& row);
+ static Value IdAccessor(const Value& row);
+ static Value EntryTimeAccessor(const Value& row);
+ static Value TypeAccessor(const Value& row);
+ static Value IsServiceAccessor(const Value& row);
+ static Value StartTimeAccessor(const Value& row);
+ static Value EndTimeAccessor(const Value& row);
+ static Value FixedAccessor(const Value& row);
+ static Value DurationAccessor(const Value& row);
+ static Value TriggeredByAccessor(const Value& row);
+};
+
+}
+
+#endif /* DOWNTIMESTABLE_H */
diff --git a/lib/livestatus/endpointstable.cpp b/lib/livestatus/endpointstable.cpp
new file mode 100644
index 0000000..3d407eb
--- /dev/null
+++ b/lib/livestatus/endpointstable.cpp
@@ -0,0 +1,109 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/endpointstable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/zone.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/replace.hpp>
+
+using namespace icinga;
+
+EndpointsTable::EndpointsTable()
+{
+ AddColumns(this);
+}
+
+void EndpointsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&EndpointsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "identity", Column(&EndpointsTable::IdentityAccessor, objectAccessor));
+ table->AddColumn(prefix + "node", Column(&EndpointsTable::NodeAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_connected", Column(&EndpointsTable::IsConnectedAccessor, objectAccessor));
+ table->AddColumn(prefix + "zone", Column(&EndpointsTable::ZoneAccessor, objectAccessor));
+}
+
+String EndpointsTable::GetName() const
+{
+ return "endpoints";
+}
+
+String EndpointsTable::GetPrefix() const
+{
+ return "endpoint";
+}
+
+void EndpointsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
+ if (!addRowFn(endpoint, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Value EndpointsTable::NameAccessor(const Value& row)
+{
+ Endpoint::Ptr endpoint = static_cast<Endpoint::Ptr>(row);
+
+ if (!endpoint)
+ return Empty;
+
+ return endpoint->GetName();
+}
+
+Value EndpointsTable::IdentityAccessor(const Value& row)
+{
+ Endpoint::Ptr endpoint = static_cast<Endpoint::Ptr>(row);
+
+ if (!endpoint)
+ return Empty;
+
+ return endpoint->GetName();
+}
+
+Value EndpointsTable::NodeAccessor(const Value& row)
+{
+ Endpoint::Ptr endpoint = static_cast<Endpoint::Ptr>(row);
+
+ if (!endpoint)
+ return Empty;
+
+ return IcingaApplication::GetInstance()->GetNodeName();
+}
+
+Value EndpointsTable::IsConnectedAccessor(const Value& row)
+{
+ Endpoint::Ptr endpoint = static_cast<Endpoint::Ptr>(row);
+
+ if (!endpoint)
+ return Empty;
+
+ unsigned int is_connected = endpoint->GetConnected() ? 1 : 0;
+
+ /* if identity is equal to node, fake is_connected */
+ if (endpoint->GetName() == IcingaApplication::GetInstance()->GetNodeName())
+ is_connected = 1;
+
+ return is_connected;
+}
+
+Value EndpointsTable::ZoneAccessor(const Value& row)
+{
+ Endpoint::Ptr endpoint = static_cast<Endpoint::Ptr>(row);
+
+ if (!endpoint)
+ return Empty;
+
+ Zone::Ptr zone = endpoint->GetZone();
+
+ if (!zone)
+ return Empty;
+
+ return zone->GetName();
+}
diff --git a/lib/livestatus/endpointstable.hpp b/lib/livestatus/endpointstable.hpp
new file mode 100644
index 0000000..7d011ef
--- /dev/null
+++ b/lib/livestatus/endpointstable.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ENDPOINTSTABLE_H
+#define ENDPOINTSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class EndpointsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EndpointsTable);
+
+ EndpointsTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value NameAccessor(const Value& row);
+ static Value IdentityAccessor(const Value& row);
+ static Value NodeAccessor(const Value& row);
+ static Value IsConnectedAccessor(const Value& row);
+ static Value ZoneAccessor(const Value& row);
+};
+
+}
+
+#endif /* ENDPOINTSTABLE_H */
diff --git a/lib/livestatus/filter.hpp b/lib/livestatus/filter.hpp
new file mode 100644
index 0000000..b9a01c8
--- /dev/null
+++ b/lib/livestatus/filter.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FILTER_H
+#define FILTER_H
+
+#include "livestatus/i2-livestatus.hpp"
+#include "livestatus/table.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class Filter : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Filter);
+
+ virtual bool Apply(const Table::Ptr& table, const Value& row) = 0;
+
+protected:
+ Filter() = default;
+};
+
+}
+
+#endif /* FILTER_H */
diff --git a/lib/livestatus/historytable.hpp b/lib/livestatus/historytable.hpp
new file mode 100644
index 0000000..f117857
--- /dev/null
+++ b/lib/livestatus/historytable.hpp
@@ -0,0 +1,24 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HISTORYTABLE_H
+#define HISTORYTABLE_H
+
+#include "livestatus/table.hpp"
+#include "base/dictionary.hpp"
+
+namespace icinga
+{
+
+
+/**
+ * @ingroup livestatus
+ */
+class HistoryTable : public Table
+{
+public:
+ virtual void UpdateLogEntries(const Dictionary::Ptr& bag, int line_count, int lineno, const AddRowFunction& addRowFn) = 0;
+};
+
+}
+
+#endif /* HISTORYTABLE_H */
diff --git a/lib/livestatus/hostgroupstable.cpp b/lib/livestatus/hostgroupstable.cpp
new file mode 100644
index 0000000..984eddb
--- /dev/null
+++ b/lib/livestatus/hostgroupstable.cpp
@@ -0,0 +1,473 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/hostgroupstable.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+HostGroupsTable::HostGroupsTable()
+{
+ AddColumns(this);
+}
+
+void HostGroupsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&HostGroupsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "alias", Column(&HostGroupsTable::AliasAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes", Column(&HostGroupsTable::NotesAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes_url", Column(&HostGroupsTable::NotesUrlAccessor, objectAccessor));
+ table->AddColumn(prefix + "action_url", Column(&HostGroupsTable::ActionUrlAccessor, objectAccessor));
+ table->AddColumn(prefix + "members", Column(&HostGroupsTable::MembersAccessor, objectAccessor));
+ table->AddColumn(prefix + "members_with_state", Column(&HostGroupsTable::MembersWithStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "worst_host_state", Column(&HostGroupsTable::WorstHostStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_hosts", Column(&HostGroupsTable::NumHostsAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_hosts_pending", Column(&HostGroupsTable::NumHostsPendingAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_hosts_up", Column(&HostGroupsTable::NumHostsUpAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_hosts_down", Column(&HostGroupsTable::NumHostsDownAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_hosts_unreach", Column(&HostGroupsTable::NumHostsUnreachAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services", Column(&HostGroupsTable::NumServicesAccessor, objectAccessor));
+ table->AddColumn(prefix + "worst_service_state", Column(&HostGroupsTable::WorstServiceStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_pending", Column(&HostGroupsTable::NumServicesPendingAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_ok", Column(&HostGroupsTable::NumServicesOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_warn", Column(&HostGroupsTable::NumServicesWarnAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_crit", Column(&HostGroupsTable::NumServicesCritAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_unknown", Column(&HostGroupsTable::NumServicesUnknownAccessor, objectAccessor));
+ table->AddColumn(prefix + "worst_service_hard_state", Column(&HostGroupsTable::WorstServiceHardStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_ok", Column(&HostGroupsTable::NumServicesHardOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_warn", Column(&HostGroupsTable::NumServicesHardWarnAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_crit", Column(&HostGroupsTable::NumServicesHardCritAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_unknown", Column(&HostGroupsTable::NumServicesHardUnknownAccessor, objectAccessor));
+}
+
+String HostGroupsTable::GetName() const
+{
+ return "hostgroups";
+}
+
+String HostGroupsTable::GetPrefix() const
+{
+ return "hostgroup";
+}
+
+void HostGroupsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const HostGroup::Ptr& hg : ConfigType::GetObjectsByType<HostGroup>()) {
+ if (!addRowFn(hg, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Value HostGroupsTable::NameAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ return hg->GetName();
+}
+
+Value HostGroupsTable::AliasAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ return hg->GetDisplayName();
+}
+
+Value HostGroupsTable::NotesAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ return hg->GetNotes();
+}
+
+Value HostGroupsTable::NotesUrlAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ return hg->GetNotesUrl();
+}
+
+Value HostGroupsTable::ActionUrlAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ return hg->GetActionUrl();
+}
+
+Value HostGroupsTable::MembersAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ ArrayData members;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ members.push_back(host->GetName());
+ }
+
+ return new Array(std::move(members));
+}
+
+Value HostGroupsTable::MembersWithStateAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ ArrayData members;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ members.push_back(new Array({
+ host->GetName(),
+ host->GetState()
+ }));
+ }
+
+ return new Array(std::move(members));
+}
+
+Value HostGroupsTable::WorstHostStateAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int worst_host = HostUp;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ if (host->GetState() > worst_host)
+ worst_host = host->GetState();
+ }
+
+ return worst_host;
+}
+
+Value HostGroupsTable::NumHostsAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ return hg->GetMembers().size();
+}
+
+Value HostGroupsTable::NumHostsPendingAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_hosts = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ /* no checkresult */
+ if (!host->GetLastCheckResult())
+ num_hosts++;
+ }
+
+ return num_hosts;
+}
+
+Value HostGroupsTable::NumHostsUpAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_hosts = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ if (host->GetState() == HostUp)
+ num_hosts++;
+ }
+
+ return num_hosts;
+}
+
+Value HostGroupsTable::NumHostsDownAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_hosts = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ if (host->GetState() == HostDown)
+ num_hosts++;
+ }
+
+ return num_hosts;
+}
+
+Value HostGroupsTable::NumHostsUnreachAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_hosts = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ if (!host->IsReachable())
+ num_hosts++;
+ }
+
+ return num_hosts;
+}
+
+Value HostGroupsTable::NumServicesAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ if (hg->GetMembers().size() == 0)
+ return 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ num_services += host->GetServices().size();
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::WorstServiceStateAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ Value worst_service = ServiceOK;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() > worst_service)
+ worst_service = service->GetState();
+ }
+ }
+
+ return worst_service;
+}
+
+Value HostGroupsTable::NumServicesPendingAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (!service->GetLastCheckResult())
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::NumServicesOkAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() == ServiceOK)
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::NumServicesWarnAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() == ServiceWarning)
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::NumServicesCritAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() == ServiceCritical)
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::NumServicesUnknownAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() == ServiceUnknown)
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::WorstServiceHardStateAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ Value worst_service = ServiceOK;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard) {
+ if (service->GetState() > worst_service)
+ worst_service = service->GetState();
+ }
+ }
+ }
+
+ return worst_service;
+}
+
+Value HostGroupsTable::NumServicesHardOkAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceOK)
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::NumServicesHardWarnAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceWarning)
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::NumServicesHardCritAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceCritical)
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
+
+Value HostGroupsTable::NumServicesHardUnknownAccessor(const Value& row)
+{
+ HostGroup::Ptr hg = static_cast<HostGroup::Ptr>(row);
+
+ if (!hg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceUnknown)
+ num_services++;
+ }
+ }
+
+ return num_services;
+}
diff --git a/lib/livestatus/hostgroupstable.hpp b/lib/livestatus/hostgroupstable.hpp
new file mode 100644
index 0000000..cc5039f
--- /dev/null
+++ b/lib/livestatus/hostgroupstable.hpp
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HOSTGROUPSTABLE_H
+#define HOSTGROUPSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class HostGroupsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(HostGroupsTable);
+
+ HostGroupsTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value NameAccessor(const Value& row);
+ static Value AliasAccessor(const Value& row);
+ static Value NotesAccessor(const Value& row);
+ static Value NotesUrlAccessor(const Value& row);
+ static Value ActionUrlAccessor(const Value& row);
+ static Value MembersAccessor(const Value& row);
+ static Value MembersWithStateAccessor(const Value& row);
+ static Value WorstHostStateAccessor(const Value& row);
+ static Value NumHostsAccessor(const Value& row);
+ static Value NumHostsPendingAccessor(const Value& row);
+ static Value NumHostsUpAccessor(const Value& row);
+ static Value NumHostsDownAccessor(const Value& row);
+ static Value NumHostsUnreachAccessor(const Value& row);
+ static Value NumServicesAccessor(const Value& row);
+ static Value WorstServiceStateAccessor(const Value& row);
+ static Value NumServicesPendingAccessor(const Value& row);
+ static Value NumServicesOkAccessor(const Value& row);
+ static Value NumServicesWarnAccessor(const Value& row);
+ static Value NumServicesCritAccessor(const Value& row);
+ static Value NumServicesUnknownAccessor(const Value& row);
+ static Value WorstServiceHardStateAccessor(const Value& row);
+ static Value NumServicesHardOkAccessor(const Value& row);
+ static Value NumServicesHardWarnAccessor(const Value& row);
+ static Value NumServicesHardCritAccessor(const Value& row);
+ static Value NumServicesHardUnknownAccessor(const Value& row);
+};
+
+}
+
+#endif /* HOSTGROUPSTABLE_H */
diff --git a/lib/livestatus/hoststable.cpp b/lib/livestatus/hoststable.cpp
new file mode 100644
index 0000000..d90f4a5
--- /dev/null
+++ b/lib/livestatus/hoststable.cpp
@@ -0,0 +1,1517 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/hoststable.hpp"
+#include "livestatus/hostgroupstable.hpp"
+#include "livestatus/endpointstable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/json.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/replace.hpp>
+
+using namespace icinga;
+
+HostsTable::HostsTable(LivestatusGroupByType type)
+ :Table(type)
+{
+ AddColumns(this);
+}
+
+void HostsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&HostsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "host_name", Column(&HostsTable::NameAccessor, objectAccessor)); //ugly compatibility hack
+ table->AddColumn(prefix + "display_name", Column(&HostsTable::DisplayNameAccessor, objectAccessor));
+ table->AddColumn(prefix + "alias", Column(&HostsTable::DisplayNameAccessor, objectAccessor));
+ table->AddColumn(prefix + "address", Column(&HostsTable::AddressAccessor, objectAccessor));
+ table->AddColumn(prefix + "address6", Column(&HostsTable::Address6Accessor, objectAccessor));
+ table->AddColumn(prefix + "check_command", Column(&HostsTable::CheckCommandAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_command_expanded", Column(&HostsTable::CheckCommandExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "event_handler", Column(&HostsTable::EventHandlerAccessor, objectAccessor));
+ table->AddColumn(prefix + "notification_period", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_period", Column(&HostsTable::CheckPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes", Column(&HostsTable::NotesAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes_expanded", Column(&HostsTable::NotesExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes_url", Column(&HostsTable::NotesUrlAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes_url_expanded", Column(&HostsTable::NotesUrlExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "action_url", Column(&HostsTable::ActionUrlAccessor, objectAccessor));
+ table->AddColumn(prefix + "action_url_expanded", Column(&HostsTable::ActionUrlExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "plugin_output", Column(&HostsTable::PluginOutputAccessor, objectAccessor));
+ table->AddColumn(prefix + "perf_data", Column(&HostsTable::PerfDataAccessor, objectAccessor));
+ table->AddColumn(prefix + "icon_image", Column(&HostsTable::IconImageAccessor, objectAccessor));
+ table->AddColumn(prefix + "icon_image_expanded", Column(&HostsTable::IconImageExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "icon_image_alt", Column(&HostsTable::IconImageAltAccessor, objectAccessor));
+ table->AddColumn(prefix + "statusmap_image", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "long_plugin_output", Column(&HostsTable::LongPluginOutputAccessor, objectAccessor));
+ table->AddColumn(prefix + "initial_state", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "max_check_attempts", Column(&HostsTable::MaxCheckAttemptsAccessor, objectAccessor));
+ table->AddColumn(prefix + "flap_detection_enabled", Column(&HostsTable::FlapDetectionEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_freshness", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "process_performance_data", Column(&HostsTable::ProcessPerformanceDataAccessor, objectAccessor));
+ table->AddColumn(prefix + "accept_passive_checks", Column(&HostsTable::AcceptPassiveChecksAccessor, objectAccessor));
+ table->AddColumn(prefix + "event_handler_enabled", Column(&HostsTable::EventHandlerEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "acknowledgement_type", Column(&HostsTable::AcknowledgementTypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_type", Column(&HostsTable::CheckTypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_state", Column(&HostsTable::LastStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_hard_state", Column(&HostsTable::LastHardStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "current_attempt", Column(&HostsTable::CurrentAttemptAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_notification", Column(&HostsTable::LastNotificationAccessor, objectAccessor));
+ table->AddColumn(prefix + "next_notification", Column(&HostsTable::NextNotificationAccessor, objectAccessor));
+ table->AddColumn(prefix + "next_check", Column(&HostsTable::NextCheckAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_hard_state_change", Column(&HostsTable::LastHardStateChangeAccessor, objectAccessor));
+ table->AddColumn(prefix + "has_been_checked", Column(&HostsTable::HasBeenCheckedAccessor, objectAccessor));
+ table->AddColumn(prefix + "current_notification_number", Column(&HostsTable::CurrentNotificationNumberAccessor, objectAccessor));
+ table->AddColumn(prefix + "pending_flex_downtime", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "total_services", Column(&HostsTable::TotalServicesAccessor, objectAccessor));
+ table->AddColumn(prefix + "checks_enabled", Column(&HostsTable::ChecksEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "notifications_enabled", Column(&HostsTable::NotificationsEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "acknowledged", Column(&HostsTable::AcknowledgedAccessor, objectAccessor));
+ table->AddColumn(prefix + "state", Column(&HostsTable::StateAccessor, objectAccessor));
+ table->AddColumn(prefix + "state_type", Column(&HostsTable::StateTypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "no_more_notifications", Column(&HostsTable::NoMoreNotificationsAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_flapping_recovery_notification", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_check", Column(&HostsTable::LastCheckAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_state_change", Column(&HostsTable::LastStateChangeAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_time_up", Column(&HostsTable::LastTimeUpAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_time_down", Column(&HostsTable::LastTimeDownAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_time_unreachable", Column(&HostsTable::LastTimeUnreachableAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_flapping", Column(&HostsTable::IsFlappingAccessor, objectAccessor));
+ table->AddColumn(prefix + "scheduled_downtime_depth", Column(&HostsTable::ScheduledDowntimeDepthAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_executing", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "active_checks_enabled", Column(&HostsTable::ActiveChecksEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_options", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "obsess_over_host", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "modified_attributes", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "modified_attributes_list", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_interval", Column(&HostsTable::CheckIntervalAccessor, objectAccessor));
+ table->AddColumn(prefix + "retry_interval", Column(&HostsTable::RetryIntervalAccessor, objectAccessor));
+ table->AddColumn(prefix + "notification_interval", Column(&HostsTable::NotificationIntervalAccessor, objectAccessor));
+ table->AddColumn(prefix + "first_notification_delay", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "low_flap_threshold", Column(&HostsTable::LowFlapThresholdAccessor, objectAccessor));
+ table->AddColumn(prefix + "high_flap_threshold", Column(&HostsTable::HighFlapThresholdAccessor, objectAccessor));
+ table->AddColumn(prefix + "x_3d", Column(&EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "y_3d", Column(&EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "z_3d", Column(&EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "x_2d", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "y_2d", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "latency", Column(&HostsTable::LatencyAccessor, objectAccessor));
+ table->AddColumn(prefix + "execution_time", Column(&HostsTable::ExecutionTimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "percent_state_change", Column(&HostsTable::PercentStateChangeAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_notification_period", Column(&HostsTable::InNotificationPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_check_period", Column(&HostsTable::InCheckPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "contacts", Column(&HostsTable::ContactsAccessor, objectAccessor));
+ table->AddColumn(prefix + "downtimes", Column(&HostsTable::DowntimesAccessor, objectAccessor));
+ table->AddColumn(prefix + "downtimes_with_info", Column(&HostsTable::DowntimesWithInfoAccessor, objectAccessor));
+ table->AddColumn(prefix + "comments", Column(&HostsTable::CommentsAccessor, objectAccessor));
+ table->AddColumn(prefix + "comments_with_info", Column(&HostsTable::CommentsWithInfoAccessor, objectAccessor));
+ table->AddColumn(prefix + "comments_with_extra_info", Column(&HostsTable::CommentsWithExtraInfoAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variable_names", Column(&HostsTable::CustomVariableNamesAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variable_values", Column(&HostsTable::CustomVariableValuesAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variables", Column(&HostsTable::CustomVariablesAccessor, objectAccessor));
+ table->AddColumn(prefix + "filename", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "parents", Column(&HostsTable::ParentsAccessor, objectAccessor));
+ table->AddColumn(prefix + "childs", Column(&HostsTable::ChildsAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services", Column(&HostsTable::NumServicesAccessor, objectAccessor));
+ table->AddColumn(prefix + "worst_service_state", Column(&HostsTable::WorstServiceStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_ok", Column(&HostsTable::NumServicesOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_warn", Column(&HostsTable::NumServicesWarnAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_crit", Column(&HostsTable::NumServicesCritAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_unknown", Column(&HostsTable::NumServicesUnknownAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_pending", Column(&HostsTable::NumServicesPendingAccessor, objectAccessor));
+ table->AddColumn(prefix + "worst_service_hard_state", Column(&HostsTable::WorstServiceHardStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_ok", Column(&HostsTable::NumServicesHardOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_warn", Column(&HostsTable::NumServicesHardWarnAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_crit", Column(&HostsTable::NumServicesHardCritAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_unknown", Column(&HostsTable::NumServicesHardUnknownAccessor, objectAccessor));
+ table->AddColumn(prefix + "hard_state", Column(&HostsTable::HardStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "pnpgraph_present", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "staleness", Column(&HostsTable::StalenessAccessor, objectAccessor));
+ table->AddColumn(prefix + "groups", Column(&HostsTable::GroupsAccessor, objectAccessor));
+ table->AddColumn(prefix + "contact_groups", Column(&HostsTable::ContactGroupsAccessor, objectAccessor));
+ table->AddColumn(prefix + "services", Column(&HostsTable::ServicesAccessor, objectAccessor));
+ table->AddColumn(prefix + "services_with_state", Column(&HostsTable::ServicesWithStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "services_with_info", Column(&HostsTable::ServicesWithInfoAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_source", Column(&HostsTable::CheckSourceAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_reachable", Column(&HostsTable::IsReachableAccessor, objectAccessor));
+ table->AddColumn(prefix + "cv_is_json", Column(&HostsTable::CVIsJsonAccessor, objectAccessor));
+ table->AddColumn(prefix + "original_attributes", Column(&HostsTable::OriginalAttributesAccessor, objectAccessor));
+
+ /* add additional group by values received through the object accessor */
+ if (table->GetGroupByType() == LivestatusGroupByHostGroup) {
+ /* _1 = row, _2 = groupByType, _3 = groupByObject */
+ Log(LogDebug, "Livestatus")
+ << "Processing hosts group by hostgroup table.";
+ HostGroupsTable::AddColumns(table, "hostgroup_", [](const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject) -> Value {
+ return HostGroupAccessor(row, groupByType, groupByObject);
+ });
+ }
+}
+
+String HostsTable::GetName() const
+{
+ return "hosts";
+}
+
+String HostsTable::GetPrefix() const
+{
+ return "host";
+}
+
+void HostsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ if (GetGroupByType() == LivestatusGroupByHostGroup) {
+ for (const HostGroup::Ptr& hg : ConfigType::GetObjectsByType<HostGroup>()) {
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ /* the caller must know which groupby type and value are set for this row */
+ if (!addRowFn(host, LivestatusGroupByHostGroup, hg))
+ return;
+ }
+ }
+ } else {
+ for (const Host::Ptr& host : ConfigType::GetObjectsByType<Host>()) {
+ if (!addRowFn(host, LivestatusGroupByNone, Empty))
+ return;
+ }
+ }
+}
+
+Object::Ptr HostsTable::HostGroupAccessor(const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject)
+{
+ /* return the current group by value set from within FetchRows()
+ * this is the hostgrouo object used for the table join inside
+ * in AddColumns()
+ */
+ if (groupByType == LivestatusGroupByHostGroup)
+ return groupByObject;
+
+ return nullptr;
+}
+
+Value HostsTable::NameAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetName();
+}
+
+Value HostsTable::DisplayNameAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetDisplayName();
+}
+
+Value HostsTable::AddressAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetAddress();
+}
+
+Value HostsTable::Address6Accessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetAddress6();
+}
+
+Value HostsTable::CheckCommandAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ CheckCommand::Ptr checkcommand = host->GetCheckCommand();
+ if (checkcommand)
+ return CompatUtility::GetCommandName(checkcommand) + "!" + CompatUtility::GetCheckableCommandArgs(host);
+
+ return Empty;
+}
+
+Value HostsTable::CheckCommandExpandedAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ CheckCommand::Ptr checkcommand = host->GetCheckCommand();
+ if (checkcommand)
+ return CompatUtility::GetCommandName(checkcommand) + "!" + CompatUtility::GetCheckableCommandArgs(host);
+
+ return Empty;
+}
+
+Value HostsTable::EventHandlerAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ EventCommand::Ptr eventcommand = host->GetEventCommand();
+ if (eventcommand)
+ return CompatUtility::GetCommandName(eventcommand);
+
+ return Empty;
+}
+
+Value HostsTable::CheckPeriodAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ TimePeriod::Ptr checkPeriod = host->GetCheckPeriod();
+
+ if (!checkPeriod)
+ return Empty;
+
+ return checkPeriod->GetName();
+}
+
+Value HostsTable::NotesAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetNotes();
+}
+
+Value HostsTable::NotesExpandedAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ MacroProcessor::ResolverList resolvers {
+ { "host", host },
+ };
+
+ return MacroProcessor::ResolveMacros(host->GetNotes(), resolvers);
+}
+
+Value HostsTable::NotesUrlAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetNotesUrl();
+}
+
+Value HostsTable::NotesUrlExpandedAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ MacroProcessor::ResolverList resolvers {
+ { "host", host },
+ };
+
+ return MacroProcessor::ResolveMacros(host->GetNotesUrl(), resolvers);
+}
+
+Value HostsTable::ActionUrlAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetActionUrl();
+}
+
+Value HostsTable::ActionUrlExpandedAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ MacroProcessor::ResolverList resolvers {
+ { "host", host },
+ };
+
+ return MacroProcessor::ResolveMacros(host->GetActionUrl(), resolvers);
+}
+
+Value HostsTable::PluginOutputAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ String output;
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (cr)
+ output = CompatUtility::GetCheckResultOutput(cr);
+
+ return output;
+}
+
+Value HostsTable::PerfDataAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ String perfdata;
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (!cr)
+ return Empty;
+
+ return PluginUtility::FormatPerfdata(cr->GetPerformanceData());
+}
+
+Value HostsTable::IconImageAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetIconImage();
+}
+
+Value HostsTable::IconImageExpandedAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ MacroProcessor::ResolverList resolvers {
+ { "host", host },
+ };
+
+ return MacroProcessor::ResolveMacros(host->GetIconImage(), resolvers);
+}
+
+Value HostsTable::IconImageAltAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetIconImageAlt();
+}
+
+Value HostsTable::LongPluginOutputAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ String long_output;
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (cr)
+ long_output = CompatUtility::GetCheckResultLongOutput(cr);
+
+ return long_output;
+}
+
+Value HostsTable::MaxCheckAttemptsAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetMaxCheckAttempts();
+}
+
+Value HostsTable::FlapDetectionEnabledAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return Convert::ToLong(host->GetEnableFlapping());
+}
+
+Value HostsTable::AcceptPassiveChecksAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return Convert::ToLong(host->GetEnablePassiveChecks());
+}
+
+Value HostsTable::EventHandlerEnabledAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return Convert::ToLong(host->GetEnableEventHandler());
+}
+
+Value HostsTable::AcknowledgementTypeAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ObjectLock olock(host);
+ return host->GetAcknowledgement();
+}
+
+Value HostsTable::CheckTypeAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return (host->GetEnableActiveChecks() ? 0 : 1); /* 0 .. active, 1 .. passive */
+}
+
+Value HostsTable::LastStateAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetLastState();
+}
+
+Value HostsTable::LastHardStateAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetLastHardState();
+}
+
+Value HostsTable::CurrentAttemptAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetCheckAttempt();
+}
+
+Value HostsTable::LastNotificationAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return CompatUtility::GetCheckableNotificationLastNotification(host);
+}
+
+Value HostsTable::NextNotificationAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return CompatUtility::GetCheckableNotificationNextNotification(host);
+}
+
+Value HostsTable::NextCheckAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return static_cast<int>(host->GetNextCheck());
+}
+
+Value HostsTable::LastHardStateChangeAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return static_cast<int>(host->GetLastHardStateChange());
+}
+
+Value HostsTable::HasBeenCheckedAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return Convert::ToLong(host->HasBeenChecked());
+}
+
+Value HostsTable::CurrentNotificationNumberAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return CompatUtility::GetCheckableNotificationNotificationNumber(host);
+}
+
+Value HostsTable::TotalServicesAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetTotalServices();
+}
+
+Value HostsTable::ChecksEnabledAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return Convert::ToLong(host->GetEnableActiveChecks());
+}
+
+Value HostsTable::NotificationsEnabledAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return Convert::ToLong(host->GetEnableNotifications());
+}
+
+Value HostsTable::ProcessPerformanceDataAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return Convert::ToLong(host->GetEnablePerfdata());
+}
+
+Value HostsTable::AcknowledgedAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ObjectLock olock(host);
+ return host->IsAcknowledged();
+}
+
+Value HostsTable::StateAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->IsReachable() ? host->GetState() : 2;
+}
+
+Value HostsTable::StateTypeAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetStateType();
+}
+
+Value HostsTable::NoMoreNotificationsAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return (CompatUtility::GetCheckableNotificationNotificationInterval(host) == 0 && !host->GetVolatile()) ? 1 : 0;
+}
+
+Value HostsTable::LastCheckAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return static_cast<int>(host->GetLastCheck());
+}
+
+Value HostsTable::LastStateChangeAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return static_cast<int>(host->GetLastStateChange());
+}
+
+Value HostsTable::LastTimeUpAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return static_cast<int>(host->GetLastStateUp());
+}
+
+Value HostsTable::LastTimeDownAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return static_cast<int>(host->GetLastStateDown());
+}
+
+Value HostsTable::LastTimeUnreachableAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return static_cast<int>(host->GetLastStateUnreachable());
+}
+
+Value HostsTable::IsFlappingAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->IsFlapping();
+}
+
+Value HostsTable::ScheduledDowntimeDepthAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetDowntimeDepth();
+}
+
+Value HostsTable::ActiveChecksEnabledAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return Convert::ToLong(host->GetEnableActiveChecks());
+}
+
+Value HostsTable::CheckIntervalAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetCheckInterval() / LIVESTATUS_INTERVAL_LENGTH;
+}
+
+Value HostsTable::RetryIntervalAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetRetryInterval() / LIVESTATUS_INTERVAL_LENGTH;
+}
+
+Value HostsTable::NotificationIntervalAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return CompatUtility::GetCheckableNotificationNotificationInterval(host);
+}
+
+Value HostsTable::LowFlapThresholdAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetFlappingThresholdLow();
+}
+
+Value HostsTable::HighFlapThresholdAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetFlappingThresholdHigh();
+}
+
+Value HostsTable::LatencyAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (!cr)
+ return Empty;
+
+ return cr->CalculateLatency();
+}
+
+Value HostsTable::ExecutionTimeAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (!cr)
+ return Empty;
+
+ return cr->CalculateExecutionTime();
+}
+
+Value HostsTable::PercentStateChangeAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetFlappingCurrent();
+}
+
+Value HostsTable::InNotificationPeriodAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ for (const Notification::Ptr& notification : host->GetNotifications()) {
+ TimePeriod::Ptr timeperiod = notification->GetPeriod();
+
+ if (!timeperiod || timeperiod->IsInside(Utility::GetTime()))
+ return 1;
+ }
+
+ return 0;
+}
+
+Value HostsTable::InCheckPeriodAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ TimePeriod::Ptr timeperiod = host->GetCheckPeriod();
+
+ /* none set means always checked */
+ if (!timeperiod)
+ return 1;
+
+ return Convert::ToLong(timeperiod->IsInside(Utility::GetTime()));
+}
+
+Value HostsTable::ContactsAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const User::Ptr& user : CompatUtility::GetCheckableNotificationUsers(host)) {
+ result.push_back(user->GetName());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::DowntimesAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Downtime::Ptr& downtime : host->GetDowntimes()) {
+ if (downtime->IsExpired())
+ continue;
+
+ result.push_back(downtime->GetLegacyId());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::DowntimesWithInfoAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Downtime::Ptr& downtime : host->GetDowntimes()) {
+ if (downtime->IsExpired())
+ continue;
+
+ result.push_back(new Array({
+ downtime->GetLegacyId(),
+ downtime->GetAuthor(),
+ downtime->GetComment()
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::CommentsAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Comment::Ptr& comment : host->GetComments()) {
+ if (comment->IsExpired())
+ continue;
+
+ result.push_back(comment->GetLegacyId());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::CommentsWithInfoAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Comment::Ptr& comment : host->GetComments()) {
+ if (comment->IsExpired())
+ continue;
+
+ result.push_back(new Array({
+ comment->GetLegacyId(),
+ comment->GetAuthor(),
+ comment->GetText()
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::CommentsWithExtraInfoAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Comment::Ptr& comment : host->GetComments()) {
+ if (comment->IsExpired())
+ continue;
+
+ result.push_back(new Array({
+ comment->GetLegacyId(),
+ comment->GetAuthor(),
+ comment->GetText(),
+ comment->GetEntryType(),
+ static_cast<int>(comment->GetEntryTime())
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::CustomVariableNamesAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ Dictionary::Ptr vars = host->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ result.push_back(kv.first);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::CustomVariableValuesAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ Dictionary::Ptr vars = host->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ result.push_back(JsonEncode(kv.second));
+ else
+ result.push_back(kv.second);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::CustomVariablesAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ Dictionary::Ptr vars = host->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ Value val;
+
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ val = JsonEncode(kv.second);
+ else
+ val = kv.second;
+
+ result.push_back(new Array({
+ kv.first,
+ val
+ }));
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::CVIsJsonAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ Dictionary::Ptr vars = host->GetVars();
+
+ if (!vars)
+ return Empty;
+
+ bool cv_is_json = false;
+
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ cv_is_json = true;
+ }
+
+ return cv_is_json;
+}
+
+Value HostsTable::ParentsAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Checkable::Ptr& parent : host->GetParents()) {
+ Host::Ptr parent_host = dynamic_pointer_cast<Host>(parent);
+
+ if (!parent_host)
+ continue;
+
+ result.push_back(parent_host->GetName());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::ChildsAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Checkable::Ptr& child : host->GetChildren()) {
+ Host::Ptr child_host = dynamic_pointer_cast<Host>(child);
+
+ if (!child_host)
+ continue;
+
+ result.push_back(child_host->GetName());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::NumServicesAccessor(const Value& row)
+{
+ /* duplicate of TotalServices */
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->GetTotalServices();
+}
+
+Value HostsTable::WorstServiceStateAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ Value worst_service = ServiceOK;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() > worst_service)
+ worst_service = service->GetState();
+ }
+
+ return worst_service;
+}
+
+Value HostsTable::NumServicesOkAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() == ServiceOK)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::NumServicesWarnAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() == ServiceWarning)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::NumServicesCritAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() == ServiceCritical)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::NumServicesUnknownAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetState() == ServiceUnknown)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::NumServicesPendingAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (!service->GetLastCheckResult())
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::WorstServiceHardStateAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ Value worst_service = ServiceOK;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard) {
+ if (service->GetState() > worst_service)
+ worst_service = service->GetState();
+ }
+ }
+
+ return worst_service;
+}
+
+Value HostsTable::NumServicesHardOkAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceOK)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::NumServicesHardWarnAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceWarning)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::NumServicesHardCritAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceCritical)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::NumServicesHardUnknownAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceUnknown)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value HostsTable::HardStateAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ if (host->GetState() == HostUp)
+ return HostUp;
+ else if (host->GetStateType() == StateTypeHard)
+ return host->GetState();
+
+ return host->GetLastHardState();
+}
+
+Value HostsTable::StalenessAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ if (host->HasBeenChecked() && host->GetLastCheck() > 0)
+ return (Utility::GetTime() - host->GetLastCheck()) / (host->GetCheckInterval() * 3600);
+
+ return 0.0;
+}
+
+Value HostsTable::GroupsAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ Array::Ptr groups = host->GetGroups();
+
+ if (!groups)
+ return Empty;
+
+ return groups;
+}
+
+Value HostsTable::ContactGroupsAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ ArrayData result;
+
+ for (const UserGroup::Ptr& usergroup : CompatUtility::GetCheckableNotificationUserGroups(host)) {
+ result.push_back(usergroup->GetName());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::ServicesAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ std::vector<Service::Ptr> rservices = host->GetServices();
+
+ ArrayData result;
+ result.reserve(rservices.size());
+
+ for (const Service::Ptr& service : rservices) {
+ result.push_back(service->GetShortName());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::ServicesWithStateAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ std::vector<Service::Ptr> rservices = host->GetServices();
+
+ ArrayData result;
+ result.reserve(rservices.size());
+
+ for (const Service::Ptr& service : rservices) {
+ result.push_back(new Array({
+ service->GetShortName(),
+ service->GetState(),
+ service->HasBeenChecked() ? 1 : 0
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::ServicesWithInfoAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ std::vector<Service::Ptr> rservices = host->GetServices();
+
+ ArrayData result;
+ result.reserve(rservices.size());
+
+ for (const Service::Ptr& service : rservices) {
+ String output;
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (cr)
+ output = CompatUtility::GetCheckResultOutput(cr);
+
+ result.push_back(new Array({
+ service->GetShortName(),
+ service->GetState(),
+ service->HasBeenChecked() ? 1 : 0,
+ output
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value HostsTable::CheckSourceAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (cr)
+ return cr->GetCheckSource();
+
+ return Empty;
+}
+
+Value HostsTable::IsReachableAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return host->IsReachable();
+}
+
+Value HostsTable::OriginalAttributesAccessor(const Value& row)
+{
+ Host::Ptr host = static_cast<Host::Ptr>(row);
+
+ if (!host)
+ return Empty;
+
+ return JsonEncode(host->GetOriginalAttributes());
+}
diff --git a/lib/livestatus/hoststable.hpp b/lib/livestatus/hoststable.hpp
new file mode 100644
index 0000000..9386183
--- /dev/null
+++ b/lib/livestatus/hoststable.hpp
@@ -0,0 +1,133 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HOSTSTABLE_H
+#define HOSTSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class HostsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(HostsTable);
+
+ HostsTable(LivestatusGroupByType type = LivestatusGroupByNone);
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Object::Ptr HostGroupAccessor(const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject);
+
+ static Value NameAccessor(const Value& row);
+ static Value DisplayNameAccessor(const Value& row);
+ static Value AddressAccessor(const Value& row);
+ static Value Address6Accessor(const Value& row);
+ static Value CheckCommandAccessor(const Value& row);
+ static Value CheckCommandExpandedAccessor(const Value& row);
+ static Value EventHandlerAccessor(const Value& row);
+ static Value CheckPeriodAccessor(const Value& row);
+ static Value NotesAccessor(const Value& row);
+ static Value NotesExpandedAccessor(const Value& row);
+ static Value NotesUrlAccessor(const Value& row);
+ static Value NotesUrlExpandedAccessor(const Value& row);
+ static Value ActionUrlAccessor(const Value& row);
+ static Value ActionUrlExpandedAccessor(const Value& row);
+ static Value PluginOutputAccessor(const Value& row);
+ static Value PerfDataAccessor(const Value& row);
+ static Value IconImageAccessor(const Value& row);
+ static Value IconImageExpandedAccessor(const Value& row);
+ static Value IconImageAltAccessor(const Value& row);
+ static Value LongPluginOutputAccessor(const Value& row);
+ static Value MaxCheckAttemptsAccessor(const Value& row);
+ static Value FlapDetectionEnabledAccessor(const Value& row);
+ static Value ProcessPerformanceDataAccessor(const Value& row);
+ static Value AcceptPassiveChecksAccessor(const Value& row);
+ static Value EventHandlerEnabledAccessor(const Value& row);
+ static Value AcknowledgementTypeAccessor(const Value& row);
+ static Value CheckTypeAccessor(const Value& row);
+ static Value LastStateAccessor(const Value& row);
+ static Value LastHardStateAccessor(const Value& row);
+ static Value CurrentAttemptAccessor(const Value& row);
+ static Value LastNotificationAccessor(const Value& row);
+ static Value NextNotificationAccessor(const Value& row);
+ static Value NextCheckAccessor(const Value& row);
+ static Value LastHardStateChangeAccessor(const Value& row);
+ static Value HasBeenCheckedAccessor(const Value& row);
+ static Value CurrentNotificationNumberAccessor(const Value& row);
+ static Value TotalServicesAccessor(const Value& row);
+ static Value ChecksEnabledAccessor(const Value& row);
+ static Value NotificationsEnabledAccessor(const Value& row);
+ static Value AcknowledgedAccessor(const Value& row);
+ static Value StateAccessor(const Value& row);
+ static Value StateTypeAccessor(const Value& row);
+ static Value NoMoreNotificationsAccessor(const Value& row);
+ static Value LastCheckAccessor(const Value& row);
+ static Value LastStateChangeAccessor(const Value& row);
+ static Value LastTimeUpAccessor(const Value& row);
+ static Value LastTimeDownAccessor(const Value& row);
+ static Value LastTimeUnreachableAccessor(const Value& row);
+ static Value IsFlappingAccessor(const Value& row);
+ static Value ScheduledDowntimeDepthAccessor(const Value& row);
+ static Value ActiveChecksEnabledAccessor(const Value& row);
+ static Value CheckIntervalAccessor(const Value& row);
+ static Value RetryIntervalAccessor(const Value& row);
+ static Value NotificationIntervalAccessor(const Value& row);
+ static Value LowFlapThresholdAccessor(const Value& row);
+ static Value HighFlapThresholdAccessor(const Value& row);
+ static Value LatencyAccessor(const Value& row);
+ static Value ExecutionTimeAccessor(const Value& row);
+ static Value PercentStateChangeAccessor(const Value& row);
+ static Value InNotificationPeriodAccessor(const Value& row);
+ static Value InCheckPeriodAccessor(const Value& row);
+ static Value ContactsAccessor(const Value& row);
+ static Value DowntimesAccessor(const Value& row);
+ static Value DowntimesWithInfoAccessor(const Value& row);
+ static Value CommentsAccessor(const Value& row);
+ static Value CommentsWithInfoAccessor(const Value& row);
+ static Value CommentsWithExtraInfoAccessor(const Value& row);
+ static Value CustomVariableNamesAccessor(const Value& row);
+ static Value CustomVariableValuesAccessor(const Value& row);
+ static Value CustomVariablesAccessor(const Value& row);
+ static Value ParentsAccessor(const Value& row);
+ static Value ChildsAccessor(const Value& row);
+ static Value NumServicesAccessor(const Value& row);
+ static Value WorstServiceStateAccessor(const Value& row);
+ static Value NumServicesOkAccessor(const Value& row);
+ static Value NumServicesWarnAccessor(const Value& row);
+ static Value NumServicesCritAccessor(const Value& row);
+ static Value NumServicesUnknownAccessor(const Value& row);
+ static Value NumServicesPendingAccessor(const Value& row);
+ static Value WorstServiceHardStateAccessor(const Value& row);
+ static Value NumServicesHardOkAccessor(const Value& row);
+ static Value NumServicesHardWarnAccessor(const Value& row);
+ static Value NumServicesHardCritAccessor(const Value& row);
+ static Value NumServicesHardUnknownAccessor(const Value& row);
+ static Value HardStateAccessor(const Value& row);
+ static Value StalenessAccessor(const Value& row);
+ static Value GroupsAccessor(const Value& row);
+ static Value ContactGroupsAccessor(const Value& row);
+ static Value ServicesAccessor(const Value& row);
+ static Value ServicesWithStateAccessor(const Value& row);
+ static Value ServicesWithInfoAccessor(const Value& row);
+ static Value CheckSourceAccessor(const Value& row);
+ static Value IsReachableAccessor(const Value& row);
+ static Value CVIsJsonAccessor(const Value& row);
+ static Value OriginalAttributesAccessor(const Value& row);
+};
+
+}
+
+#endif /* HOSTSTABLE_H */
diff --git a/lib/livestatus/i2-livestatus.hpp b/lib/livestatus/i2-livestatus.hpp
new file mode 100644
index 0000000..3375d97
--- /dev/null
+++ b/lib/livestatus/i2-livestatus.hpp
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2LIVESTATUS_H
+#define I2LIVESTATUS_H
+
+/**
+ * @defgroup icinga Livestatus
+ *
+ * The Livestatus library implements the Livestatus protocol for Icinga.
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2LIVESTATUS_H */
diff --git a/lib/livestatus/invavgaggregator.cpp b/lib/livestatus/invavgaggregator.cpp
new file mode 100644
index 0000000..33cf85c
--- /dev/null
+++ b/lib/livestatus/invavgaggregator.cpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/invavgaggregator.hpp"
+
+using namespace icinga;
+
+InvAvgAggregator::InvAvgAggregator(String attr)
+ : m_InvAvgAttr(std::move(attr))
+{ }
+
+InvAvgAggregatorState *InvAvgAggregator::EnsureState(AggregatorState **state)
+{
+ if (!*state)
+ *state = new InvAvgAggregatorState();
+
+ return static_cast<InvAvgAggregatorState *>(*state);
+}
+
+void InvAvgAggregator::Apply(const Table::Ptr& table, const Value& row, AggregatorState **state)
+{
+ Column column = table->GetColumn(m_InvAvgAttr);
+
+ Value value = column.ExtractValue(row);
+
+ InvAvgAggregatorState *pstate = EnsureState(state);
+
+ pstate->InvAvg += (1.0 / value);
+ pstate->InvAvgCount++;
+}
+
+double InvAvgAggregator::GetResultAndFreeState(AggregatorState *state) const
+{
+ InvAvgAggregatorState *pstate = EnsureState(&state);
+ double result = pstate->InvAvg / pstate->InvAvgCount;
+ delete pstate;
+
+ return result;
+}
diff --git a/lib/livestatus/invavgaggregator.hpp b/lib/livestatus/invavgaggregator.hpp
new file mode 100644
index 0000000..9282b37
--- /dev/null
+++ b/lib/livestatus/invavgaggregator.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef INVAVGAGGREGATOR_H
+#define INVAVGAGGREGATOR_H
+
+#include "livestatus/table.hpp"
+#include "livestatus/aggregator.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct InvAvgAggregatorState final : public AggregatorState
+{
+ double InvAvg{0};
+ double InvAvgCount{0};
+};
+
+/**
+ * @ingroup livestatus
+ */
+class InvAvgAggregator final : public Aggregator
+{
+public:
+ DECLARE_PTR_TYPEDEFS(InvAvgAggregator);
+
+ InvAvgAggregator(String attr);
+
+ void Apply(const Table::Ptr& table, const Value& row, AggregatorState **state) override;
+ double GetResultAndFreeState(AggregatorState *state) const override;
+
+private:
+ String m_InvAvgAttr;
+
+ static InvAvgAggregatorState *EnsureState(AggregatorState **state);
+};
+
+}
+
+#endif /* INVAVGAGGREGATOR_H */
diff --git a/lib/livestatus/invsumaggregator.cpp b/lib/livestatus/invsumaggregator.cpp
new file mode 100644
index 0000000..c955667
--- /dev/null
+++ b/lib/livestatus/invsumaggregator.cpp
@@ -0,0 +1,37 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/invsumaggregator.hpp"
+
+using namespace icinga;
+
+InvSumAggregator::InvSumAggregator(String attr)
+ : m_InvSumAttr(std::move(attr))
+{ }
+
+InvSumAggregatorState *InvSumAggregator::EnsureState(AggregatorState **state)
+{
+ if (!*state)
+ *state = new InvSumAggregatorState();
+
+ return static_cast<InvSumAggregatorState *>(*state);
+}
+
+void InvSumAggregator::Apply(const Table::Ptr& table, const Value& row, AggregatorState **state)
+{
+ Column column = table->GetColumn(m_InvSumAttr);
+
+ Value value = column.ExtractValue(row);
+
+ InvSumAggregatorState *pstate = EnsureState(state);
+
+ pstate->InvSum += (1.0 / value);
+}
+
+double InvSumAggregator::GetResultAndFreeState(AggregatorState *state) const
+{
+ InvSumAggregatorState *pstate = EnsureState(&state);
+ double result = pstate->InvSum;
+ delete pstate;
+
+ return result;
+}
diff --git a/lib/livestatus/invsumaggregator.hpp b/lib/livestatus/invsumaggregator.hpp
new file mode 100644
index 0000000..f7de7be
--- /dev/null
+++ b/lib/livestatus/invsumaggregator.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef INVSUMAGGREGATOR_H
+#define INVSUMAGGREGATOR_H
+
+#include "livestatus/table.hpp"
+#include "livestatus/aggregator.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct InvSumAggregatorState final : public AggregatorState
+{
+ double InvSum{0};
+};
+
+/**
+ * @ingroup livestatus
+ */
+class InvSumAggregator final : public Aggregator
+{
+public:
+ DECLARE_PTR_TYPEDEFS(InvSumAggregator);
+
+ InvSumAggregator(String attr);
+
+ void Apply(const Table::Ptr& table, const Value& row, AggregatorState **state) override;
+ double GetResultAndFreeState(AggregatorState *state) const override;
+
+private:
+ String m_InvSumAttr;
+
+ static InvSumAggregatorState *EnsureState(AggregatorState **state);
+};
+
+}
+
+#endif /* INVSUMAGGREGATOR_H */
diff --git a/lib/livestatus/livestatuslistener.cpp b/lib/livestatus/livestatuslistener.cpp
new file mode 100644
index 0000000..e44650b
--- /dev/null
+++ b/lib/livestatus/livestatuslistener.cpp
@@ -0,0 +1,211 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/livestatuslistener.hpp"
+#include "livestatus/livestatuslistener-ti.cpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/objectlock.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/unixsocket.hpp"
+#include "base/networkstream.hpp"
+#include "base/application.hpp"
+#include "base/function.hpp"
+#include "base/statsfunction.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(LivestatusListener);
+
+static int l_ClientsConnected = 0;
+static int l_Connections = 0;
+static std::mutex l_ComponentMutex;
+
+REGISTER_STATSFUNCTION(LivestatusListener, &LivestatusListener::StatsFunc);
+
+void LivestatusListener::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const LivestatusListener::Ptr& livestatuslistener : ConfigType::GetObjectsByType<LivestatusListener>()) {
+ nodes.emplace_back(livestatuslistener->GetName(), new Dictionary({
+ { "connections", l_Connections }
+ }));
+
+ perfdata->Add(new PerfdataValue("livestatuslistener_" + livestatuslistener->GetName() + "_connections", l_Connections));
+ }
+
+ status->Set("livestatuslistener", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * Starts the component.
+ */
+void LivestatusListener::Start(bool runtimeCreated)
+{
+ ObjectImpl<LivestatusListener>::Start(runtimeCreated);
+
+ Log(LogInformation, "LivestatusListener")
+ << "'" << GetName() << "' started.";
+
+ if (GetSocketType() == "tcp") {
+ TcpSocket::Ptr socket = new TcpSocket();
+
+ try {
+ socket->Bind(GetBindHost(), GetBindPort(), AF_UNSPEC);
+ } catch (std::exception&) {
+ Log(LogCritical, "LivestatusListener")
+ << "Cannot bind TCP socket on host '" << GetBindHost() << "' port '" << GetBindPort() << "'.";
+ return;
+ }
+
+ m_Listener = socket;
+
+ m_Thread = std::thread([this]() { ServerThreadProc(); });
+
+ Log(LogInformation, "LivestatusListener")
+ << "Created TCP socket listening on host '" << GetBindHost() << "' port '" << GetBindPort() << "'.";
+ }
+ else if (GetSocketType() == "unix") {
+#ifndef _WIN32
+ UnixSocket::Ptr socket = new UnixSocket();
+
+ try {
+ socket->Bind(GetSocketPath());
+ } catch (std::exception&) {
+ Log(LogCritical, "LivestatusListener")
+ << "Cannot bind UNIX socket to '" << GetSocketPath() << "'.";
+ return;
+ }
+
+ /* group must be able to write */
+ mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
+
+ if (chmod(GetSocketPath().CStr(), mode) < 0) {
+ Log(LogCritical, "LivestatusListener")
+ << "chmod() on unix socket '" << GetSocketPath() << "' failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ return;
+ }
+
+ m_Listener = socket;
+
+ m_Thread = std::thread([this]() { ServerThreadProc(); });
+
+ Log(LogInformation, "LivestatusListener")
+ << "Created UNIX socket in '" << GetSocketPath() << "'.";
+#else
+ /* no UNIX sockets on windows */
+ Log(LogCritical, "LivestatusListener", "Unix sockets are not supported on Windows.");
+ return;
+#endif
+ }
+}
+
+void LivestatusListener::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<LivestatusListener>::Stop(runtimeRemoved);
+
+ Log(LogInformation, "LivestatusListener")
+ << "'" << GetName() << "' stopped.";
+
+ m_Listener->Close();
+
+ if (m_Thread.joinable())
+ m_Thread.join();
+}
+
+int LivestatusListener::GetClientsConnected()
+{
+ std::unique_lock<std::mutex> lock(l_ComponentMutex);
+
+ return l_ClientsConnected;
+}
+
+int LivestatusListener::GetConnections()
+{
+ std::unique_lock<std::mutex> lock(l_ComponentMutex);
+
+ return l_Connections;
+}
+
+void LivestatusListener::ServerThreadProc()
+{
+ m_Listener->Listen();
+
+ try {
+ for (;;) {
+ timeval tv = { 0, 500000 };
+
+ if (m_Listener->Poll(true, false, &tv)) {
+ Socket::Ptr client = m_Listener->Accept();
+ Log(LogNotice, "LivestatusListener", "Client connected");
+ Utility::QueueAsyncCallback([this, client]() { ClientHandler(client); }, LowLatencyScheduler);
+ }
+
+ if (!IsActive())
+ break;
+ }
+ } catch (std::exception&) {
+ Log(LogCritical, "LivestatusListener", "Cannot accept new connection.");
+ }
+
+ m_Listener->Close();
+}
+
+void LivestatusListener::ClientHandler(const Socket::Ptr& client)
+{
+ {
+ std::unique_lock<std::mutex> lock(l_ComponentMutex);
+ l_ClientsConnected++;
+ l_Connections++;
+ }
+
+ Stream::Ptr stream = new NetworkStream(client);
+
+ StreamReadContext context;
+
+ for (;;) {
+ String line;
+
+ std::vector<String> lines;
+
+ for (;;) {
+ StreamReadStatus srs = stream->ReadLine(&line, context);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ if (line.GetLength() > 0)
+ lines.push_back(line);
+ else
+ break;
+ }
+
+ if (lines.empty())
+ break;
+
+ LivestatusQuery::Ptr query = new LivestatusQuery(lines, GetCompatLogPath());
+ if (!query->Execute(stream))
+ break;
+ }
+
+ {
+ std::unique_lock<std::mutex> lock(l_ComponentMutex);
+ l_ClientsConnected--;
+ }
+}
+
+
+void LivestatusListener::ValidateSocketType(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<LivestatusListener>::ValidateSocketType(lvalue, utils);
+
+ if (lvalue() != "unix" && lvalue() != "tcp")
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "socket_type" }, "Socket type '" + lvalue() + "' is invalid."));
+}
diff --git a/lib/livestatus/livestatuslistener.hpp b/lib/livestatus/livestatuslistener.hpp
new file mode 100644
index 0000000..dc739f6
--- /dev/null
+++ b/lib/livestatus/livestatuslistener.hpp
@@ -0,0 +1,47 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LIVESTATUSLISTENER_H
+#define LIVESTATUSLISTENER_H
+
+#include "livestatus/i2-livestatus.hpp"
+#include "livestatus/livestatuslistener-ti.hpp"
+#include "livestatus/livestatusquery.hpp"
+#include "base/socket.hpp"
+#include <thread>
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class LivestatusListener final : public ObjectImpl<LivestatusListener>
+{
+public:
+ DECLARE_OBJECT(LivestatusListener);
+ DECLARE_OBJECTNAME(LivestatusListener);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ static int GetClientsConnected();
+ static int GetConnections();
+
+ void ValidateSocketType(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ void ServerThreadProc();
+ void ClientHandler(const Socket::Ptr& client);
+
+ Socket::Ptr m_Listener;
+ std::thread m_Thread;
+};
+
+}
+
+#endif /* LIVESTATUSLISTENER_H */
diff --git a/lib/livestatus/livestatuslistener.ti b/lib/livestatus/livestatuslistener.ti
new file mode 100644
index 0000000..31482cf
--- /dev/null
+++ b/lib/livestatus/livestatuslistener.ti
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/application.hpp"
+
+library livestatus;
+
+namespace icinga
+{
+
+class LivestatusListener : ConfigObject {
+ activation_priority 100;
+
+ [config] String socket_type {
+ default {{{ return "unix"; }}}
+ };
+ [config] String socket_path {
+ default {{{ return Configuration::InitRunDir + "/cmd/livestatus"; }}}
+ };
+ [config] String bind_host {
+ default {{{ return "127.0.0.1"; }}}
+ };
+ [config] String bind_port {
+ default {{{ return "6558"; }}}
+ };
+ [config] String compat_log_path {
+ default {{{ return Configuration::LogDir + "/compat"; }}}
+ };
+};
+
+}
diff --git a/lib/livestatus/livestatuslogutility.cpp b/lib/livestatus/livestatuslogutility.cpp
new file mode 100644
index 0000000..565c2ca
--- /dev/null
+++ b/lib/livestatus/livestatuslogutility.cpp
@@ -0,0 +1,321 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/livestatuslogutility.hpp"
+#include "icinga/service.hpp"
+#include "icinga/host.hpp"
+#include "icinga/user.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/logger.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <fstream>
+
+using namespace icinga;
+
+void LivestatusLogUtility::CreateLogIndex(const String& path, std::map<time_t, String>& index)
+{
+ Utility::Glob(path + "/icinga.log", [&index](const String& newPath) { CreateLogIndexFileHandler(newPath, index); }, GlobFile);
+ Utility::Glob(path + "/archives/*.log", [&index](const String& newPath) { CreateLogIndexFileHandler(newPath, index); }, GlobFile);
+}
+
+void LivestatusLogUtility::CreateLogIndexFileHandler(const String& path, std::map<time_t, String>& index)
+{
+ std::ifstream stream;
+ stream.open(path.CStr(), std::ifstream::in);
+
+ if (!stream)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not open log file: " + path));
+
+ /* read the first bytes to get the timestamp: [123456789] */
+ char buffer[12];
+
+ stream.read(buffer, 12);
+
+ if (buffer[0] != '[' || buffer[11] != ']') {
+ /* this can happen for directories too, silently ignore them */
+ return;
+ }
+
+ /* extract timestamp */
+ buffer[11] = 0;
+ time_t ts_start = atoi(buffer+1);
+
+ stream.close();
+
+ Log(LogDebug, "LivestatusLogUtility")
+ << "Indexing log file: '" << path << "' with timestamp start: '" << ts_start << "'.";
+
+ index[ts_start] = path;
+}
+
+void LivestatusLogUtility::CreateLogCache(std::map<time_t, String> index, HistoryTable *table,
+ time_t from, time_t until, const AddRowFunction& addRowFn)
+{
+ ASSERT(table);
+
+ /* m_LogFileIndex map tells which log files are involved ordered by their start timestamp */
+ unsigned long line_count = 0;
+ for (const auto& kv : index) {
+ unsigned int ts = kv.first;
+
+ /* skip log files not in range (performance optimization) */
+ if (ts < from || ts > until)
+ continue;
+
+ String log_file = index[ts];
+ int lineno = 0;
+
+ std::ifstream fp;
+ fp.exceptions(std::ifstream::badbit);
+ fp.open(log_file.CStr(), std::ifstream::in);
+
+ while (fp.good()) {
+ std::string line;
+ std::getline(fp, line);
+
+ if (line.empty())
+ continue; /* Ignore empty lines */
+
+ Dictionary::Ptr log_entry_attrs = LivestatusLogUtility::GetAttributes(line);
+
+ /* no attributes available - invalid log line */
+ if (!log_entry_attrs) {
+ Log(LogDebug, "LivestatusLogUtility")
+ << "Skipping invalid log line: '" << line << "'.";
+ continue;
+ }
+
+ table->UpdateLogEntries(log_entry_attrs, line_count, lineno, addRowFn);
+
+ line_count++;
+ lineno++;
+ }
+
+ fp.close();
+ }
+}
+
+Dictionary::Ptr LivestatusLogUtility::GetAttributes(const String& text)
+{
+ Dictionary::Ptr bag = new Dictionary();
+
+ /*
+ * [1379025342] SERVICE NOTIFICATION: contactname;hostname;servicedesc;WARNING;true;foo output
+ */
+ unsigned long time = atoi(text.SubStr(1, 11).CStr());
+
+ Log(LogDebug, "LivestatusLogUtility")
+ << "Processing log line: '" << text << "'.";
+ bag->Set("time", time);
+
+ size_t colon = text.FindFirstOf(':');
+ size_t colon_offset = colon - 13;
+
+ String type = String(text.SubStr(13, colon_offset)).Trim();
+ String options = String(text.SubStr(colon + 1)).Trim();
+
+ bag->Set("type", type);
+ bag->Set("options", options);
+
+ std::vector<String> tokens = options.Split(";");
+
+ /* set default values */
+ bag->Set("class", LogEntryClassInfo);
+ bag->Set("log_type", 0);
+ bag->Set("state", 0);
+ bag->Set("attempt", 0);
+ bag->Set("message", text); /* used as 'message' in log table, and 'log_output' in statehist table */
+
+ if (type.Contains("INITIAL HOST STATE") ||
+ type.Contains("CURRENT HOST STATE") ||
+ type.Contains("HOST ALERT")) {
+ if (tokens.size() < 5)
+ return bag;
+
+ bag->Set("host_name", tokens[0]);
+ bag->Set("state", Host::StateFromString(tokens[1]));
+ bag->Set("state_type", tokens[2]);
+ bag->Set("attempt", atoi(tokens[3].CStr()));
+ bag->Set("plugin_output", tokens[4]);
+
+ if (type.Contains("INITIAL HOST STATE")) {
+ bag->Set("class", LogEntryClassState);
+ bag->Set("log_type", LogEntryTypeHostInitialState);
+ }
+ else if (type.Contains("CURRENT HOST STATE")) {
+ bag->Set("class", LogEntryClassState);
+ bag->Set("log_type", LogEntryTypeHostCurrentState);
+ }
+ else {
+ bag->Set("class", LogEntryClassAlert);
+ bag->Set("log_type", LogEntryTypeHostAlert);
+ }
+
+ return bag;
+ } else if (type.Contains("HOST DOWNTIME ALERT") || type.Contains("HOST FLAPPING ALERT")) {
+ if (tokens.size() < 3)
+ return bag;
+
+ bag->Set("host_name", tokens[0]);
+ bag->Set("state_type", tokens[1]);
+ bag->Set("comment", tokens[2]);
+
+ if (type.Contains("HOST FLAPPING ALERT")) {
+ bag->Set("class", LogEntryClassAlert);
+ bag->Set("log_type", LogEntryTypeHostFlapping);
+ } else {
+ bag->Set("class", LogEntryClassAlert);
+ bag->Set("log_type", LogEntryTypeHostDowntimeAlert);
+ }
+
+ return bag;
+ } else if (type.Contains("INITIAL SERVICE STATE") ||
+ type.Contains("CURRENT SERVICE STATE") ||
+ type.Contains("SERVICE ALERT")) {
+ if (tokens.size() < 6)
+ return bag;
+
+ bag->Set("host_name", tokens[0]);
+ bag->Set("service_description", tokens[1]);
+ bag->Set("state", Service::StateFromString(tokens[2]));
+ bag->Set("state_type", tokens[3]);
+ bag->Set("attempt", atoi(tokens[4].CStr()));
+ bag->Set("plugin_output", tokens[5]);
+
+ if (type.Contains("INITIAL SERVICE STATE")) {
+ bag->Set("class", LogEntryClassState);
+ bag->Set("log_type", LogEntryTypeServiceInitialState);
+ }
+ else if (type.Contains("CURRENT SERVICE STATE")) {
+ bag->Set("class", LogEntryClassState);
+ bag->Set("log_type", LogEntryTypeServiceCurrentState);
+ }
+ else {
+ bag->Set("class", LogEntryClassAlert);
+ bag->Set("log_type", LogEntryTypeServiceAlert);
+ }
+
+ return bag;
+ } else if (type.Contains("SERVICE DOWNTIME ALERT") ||
+ type.Contains("SERVICE FLAPPING ALERT")) {
+ if (tokens.size() < 4)
+ return bag;
+
+ bag->Set("host_name", tokens[0]);
+ bag->Set("service_description", tokens[1]);
+ bag->Set("state_type", tokens[2]);
+ bag->Set("comment", tokens[3]);
+
+ if (type.Contains("SERVICE FLAPPING ALERT")) {
+ bag->Set("class", LogEntryClassAlert);
+ bag->Set("log_type", LogEntryTypeServiceFlapping);
+ } else {
+ bag->Set("class", LogEntryClassAlert);
+ bag->Set("log_type", LogEntryTypeServiceDowntimeAlert);
+ }
+
+ return bag;
+ } else if (type.Contains("TIMEPERIOD TRANSITION")) {
+ if (tokens.size() < 4)
+ return bag;
+
+ bag->Set("class", LogEntryClassState);
+ bag->Set("log_type", LogEntryTypeTimeperiodTransition);
+
+ bag->Set("host_name", tokens[0]);
+ bag->Set("service_description", tokens[1]);
+ bag->Set("state_type", tokens[2]);
+ bag->Set("comment", tokens[3]);
+ } else if (type.Contains("HOST NOTIFICATION")) {
+ if (tokens.size() < 6)
+ return bag;
+
+ bag->Set("contact_name", tokens[0]);
+ bag->Set("host_name", tokens[1]);
+ bag->Set("state_type", tokens[2].CStr());
+ bag->Set("state", Service::StateFromString(tokens[3]));
+ bag->Set("command_name", tokens[4]);
+ bag->Set("plugin_output", tokens[5]);
+
+ bag->Set("class", LogEntryClassNotification);
+ bag->Set("log_type", LogEntryTypeHostNotification);
+
+ return bag;
+ } else if (type.Contains("SERVICE NOTIFICATION")) {
+ if (tokens.size() < 7)
+ return bag;
+
+ bag->Set("contact_name", tokens[0]);
+ bag->Set("host_name", tokens[1]);
+ bag->Set("service_description", tokens[2]);
+ bag->Set("state_type", tokens[3].CStr());
+ bag->Set("state", Service::StateFromString(tokens[4]));
+ bag->Set("command_name", tokens[5]);
+ bag->Set("plugin_output", tokens[6]);
+
+ bag->Set("class", LogEntryClassNotification);
+ bag->Set("log_type", LogEntryTypeServiceNotification);
+
+ return bag;
+ } else if (type.Contains("PASSIVE HOST CHECK")) {
+ if (tokens.size() < 3)
+ return bag;
+
+ bag->Set("host_name", tokens[0]);
+ bag->Set("state", Host::StateFromString(tokens[1]));
+ bag->Set("plugin_output", tokens[2]);
+
+ bag->Set("class", LogEntryClassPassive);
+
+ return bag;
+ } else if (type.Contains("PASSIVE SERVICE CHECK")) {
+ if (tokens.size() < 4)
+ return bag;
+
+ bag->Set("host_name", tokens[0]);
+ bag->Set("service_description", tokens[1]);
+ bag->Set("state", Host::StateFromString(tokens[2]));
+ bag->Set("plugin_output", tokens[3]);
+
+ bag->Set("class", LogEntryClassPassive);
+
+ return bag;
+ } else if (type.Contains("EXTERNAL COMMAND")) {
+ bag->Set("class", LogEntryClassCommand);
+ /* string processing not implemented in 1.x */
+
+ return bag;
+ } else if (type.Contains("LOG VERSION")) {
+ bag->Set("class", LogEntryClassProgram);
+ bag->Set("log_type", LogEntryTypeVersion);
+
+ return bag;
+ } else if (type.Contains("logging initial states")) {
+ bag->Set("class", LogEntryClassProgram);
+ bag->Set("log_type", LogEntryTypeInitialStates);
+
+ return bag;
+ } else if (type.Contains("starting... (PID=")) {
+ bag->Set("class", LogEntryClassProgram);
+ bag->Set("log_type", LogEntryTypeProgramStarting);
+
+ return bag;
+ }
+ /* program */
+ else if (type.Contains("restarting...") ||
+ type.Contains("shutting down...") ||
+ type.Contains("Bailing out") ||
+ type.Contains("active mode...") ||
+ type.Contains("standby mode...")) {
+ bag->Set("class", LogEntryClassProgram);
+
+ return bag;
+ }
+
+ return bag;
+}
diff --git a/lib/livestatus/livestatuslogutility.hpp b/lib/livestatus/livestatuslogutility.hpp
new file mode 100644
index 0000000..66d1154
--- /dev/null
+++ b/lib/livestatus/livestatuslogutility.hpp
@@ -0,0 +1,60 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LIVESTATUSLOGUTILITY_H
+#define LIVESTATUSLOGUTILITY_H
+
+#include "livestatus/historytable.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+enum LogEntryType {
+ LogEntryTypeHostAlert,
+ LogEntryTypeHostDowntimeAlert,
+ LogEntryTypeHostFlapping,
+ LogEntryTypeHostNotification,
+ LogEntryTypeHostInitialState,
+ LogEntryTypeHostCurrentState,
+ LogEntryTypeServiceAlert,
+ LogEntryTypeServiceDowntimeAlert,
+ LogEntryTypeServiceFlapping,
+ LogEntryTypeServiceNotification,
+ LogEntryTypeServiceInitialState,
+ LogEntryTypeServiceCurrentState,
+ LogEntryTypeTimeperiodTransition,
+ LogEntryTypeVersion,
+ LogEntryTypeInitialStates,
+ LogEntryTypeProgramStarting
+};
+
+enum LogEntryClass {
+ LogEntryClassInfo = 0,
+ LogEntryClassAlert = 1,
+ LogEntryClassProgram = 2,
+ LogEntryClassNotification = 3,
+ LogEntryClassPassive = 4,
+ LogEntryClassCommand = 5,
+ LogEntryClassState = 6,
+ LogEntryClassText = 7
+};
+
+/**
+ * @ingroup livestatus
+ */
+class LivestatusLogUtility
+{
+public:
+ static void CreateLogIndex(const String& path, std::map<time_t, String>& index);
+ static void CreateLogIndexFileHandler(const String& path, std::map<time_t, String>& index);
+ static void CreateLogCache(std::map<time_t, String> index, HistoryTable *table, time_t from, time_t until, const AddRowFunction& addRowFn);
+ static Dictionary::Ptr GetAttributes(const String& text);
+
+private:
+ LivestatusLogUtility();
+};
+
+}
+
+#endif /* LIVESTATUSLOGUTILITY_H */
diff --git a/lib/livestatus/livestatusquery.cpp b/lib/livestatus/livestatusquery.cpp
new file mode 100644
index 0000000..0f9b3da
--- /dev/null
+++ b/lib/livestatus/livestatusquery.cpp
@@ -0,0 +1,648 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/livestatusquery.hpp"
+#include "livestatus/countaggregator.hpp"
+#include "livestatus/sumaggregator.hpp"
+#include "livestatus/minaggregator.hpp"
+#include "livestatus/maxaggregator.hpp"
+#include "livestatus/avgaggregator.hpp"
+#include "livestatus/stdaggregator.hpp"
+#include "livestatus/invsumaggregator.hpp"
+#include "livestatus/invavgaggregator.hpp"
+#include "livestatus/attributefilter.hpp"
+#include "livestatus/negatefilter.hpp"
+#include "livestatus/orfilter.hpp"
+#include "livestatus/andfilter.hpp"
+#include "icinga/externalcommandprocessor.hpp"
+#include "base/debug.hpp"
+#include "base/convert.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include "base/json.hpp"
+#include "base/serializer.hpp"
+#include "base/timer.hpp"
+#include "base/initialize.hpp"
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/join.hpp>
+
+using namespace icinga;
+
+static int l_ExternalCommands = 0;
+static std::mutex l_QueryMutex;
+
+LivestatusQuery::LivestatusQuery(const std::vector<String>& lines, const String& compat_log_path)
+ : m_KeepAlive(false), m_OutputFormat("csv"), m_ColumnHeaders(true), m_Limit(-1), m_ErrorCode(0),
+ m_LogTimeFrom(0), m_LogTimeUntil(static_cast<long>(Utility::GetTime()))
+{
+ if (lines.size() == 0) {
+ m_Verb = "ERROR";
+ m_ErrorCode = LivestatusErrorQuery;
+ m_ErrorMessage = "Empty Query. Aborting.";
+ return;
+ }
+
+ String msg;
+ for (const String& line : lines) {
+ msg += line + "\n";
+ }
+ Log(LogDebug, "LivestatusQuery", msg);
+
+ m_CompatLogPath = compat_log_path;
+
+ /* default separators */
+ m_Separators.emplace_back("\n");
+ m_Separators.emplace_back(";");
+ m_Separators.emplace_back(",");
+ m_Separators.emplace_back("|");
+
+ String line = lines[0];
+
+ size_t sp_index = line.FindFirstOf(" ");
+
+ if (sp_index == String::NPos)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Livestatus header must contain a verb."));
+
+ String verb = line.SubStr(0, sp_index);
+ String target = line.SubStr(sp_index + 1);
+
+ m_Verb = verb;
+
+ if (m_Verb == "COMMAND") {
+ m_KeepAlive = true;
+ m_Command = target;
+ } else if (m_Verb == "GET") {
+ m_Table = target;
+ } else {
+ m_Verb = "ERROR";
+ m_ErrorCode = LivestatusErrorQuery;
+ m_ErrorMessage = "Unknown livestatus verb: " + m_Verb;
+ return;
+ }
+
+ std::deque<Filter::Ptr> filters, stats;
+ std::deque<Aggregator::Ptr> aggregators;
+
+ for (unsigned int i = 1; i < lines.size(); i++) {
+ line = lines[i];
+
+ size_t col_index = line.FindFirstOf(":");
+ String header = line.SubStr(0, col_index);
+ String params;
+
+ //OutputFormat:json or OutputFormat: json
+ if (line.GetLength() > col_index + 1)
+ params = line.SubStr(col_index + 1).Trim();
+
+ if (header == "ResponseHeader")
+ m_ResponseHeader = params;
+ else if (header == "OutputFormat")
+ m_OutputFormat = params;
+ else if (header == "KeepAlive")
+ m_KeepAlive = (params == "on");
+ else if (header == "Columns") {
+ m_ColumnHeaders = false; // Might be explicitly re-enabled later on
+ m_Columns = params.Split(" ");
+ } else if (header == "Separators") {
+ std::vector<String> separators = params.Split(" ");
+
+ /* ugly ascii long to char conversion, but works */
+ if (separators.size() > 0)
+ m_Separators[0] = String(1, static_cast<char>(Convert::ToLong(separators[0])));
+ if (separators.size() > 1)
+ m_Separators[1] = String(1, static_cast<char>(Convert::ToLong(separators[1])));
+ if (separators.size() > 2)
+ m_Separators[2] = String(1, static_cast<char>(Convert::ToLong(separators[2])));
+ if (separators.size() > 3)
+ m_Separators[3] = String(1, static_cast<char>(Convert::ToLong(separators[3])));
+ } else if (header == "ColumnHeaders")
+ m_ColumnHeaders = (params == "on");
+ else if (header == "Limit")
+ m_Limit = Convert::ToLong(params);
+ else if (header == "Filter") {
+ Filter::Ptr filter = ParseFilter(params, m_LogTimeFrom, m_LogTimeUntil);
+
+ if (!filter) {
+ m_Verb = "ERROR";
+ m_ErrorCode = LivestatusErrorQuery;
+ m_ErrorMessage = "Invalid filter specification: " + line;
+ return;
+ }
+
+ filters.push_back(filter);
+ } else if (header == "Stats") {
+ m_ColumnHeaders = false; // Might be explicitly re-enabled later on
+
+ std::vector<String> tokens = params.Split(" ");
+
+ if (tokens.size() < 2) {
+ m_Verb = "ERROR";
+ m_ErrorCode = LivestatusErrorQuery;
+ m_ErrorMessage = "Missing aggregator column name: " + line;
+ return;
+ }
+
+ String aggregate_arg = tokens[0];
+ String aggregate_attr = tokens[1];
+
+ Aggregator::Ptr aggregator;
+ Filter::Ptr filter;
+
+ if (aggregate_arg == "sum") {
+ aggregator = new SumAggregator(aggregate_attr);
+ } else if (aggregate_arg == "min") {
+ aggregator = new MinAggregator(aggregate_attr);
+ } else if (aggregate_arg == "max") {
+ aggregator = new MaxAggregator(aggregate_attr);
+ } else if (aggregate_arg == "avg") {
+ aggregator = new AvgAggregator(aggregate_attr);
+ } else if (aggregate_arg == "std") {
+ aggregator = new StdAggregator(aggregate_attr);
+ } else if (aggregate_arg == "suminv") {
+ aggregator = new InvSumAggregator(aggregate_attr);
+ } else if (aggregate_arg == "avginv") {
+ aggregator = new InvAvgAggregator(aggregate_attr);
+ } else {
+ filter = ParseFilter(params, m_LogTimeFrom, m_LogTimeUntil);
+
+ if (!filter) {
+ m_Verb = "ERROR";
+ m_ErrorCode = LivestatusErrorQuery;
+ m_ErrorMessage = "Invalid filter specification: " + line;
+ return;
+ }
+
+ aggregator = new CountAggregator();
+ }
+
+ aggregator->SetFilter(filter);
+ aggregators.push_back(aggregator);
+
+ stats.push_back(filter);
+ } else if (header == "Or" || header == "And" || header == "StatsOr" || header == "StatsAnd") {
+ std::deque<Filter::Ptr>& deq = (header == "Or" || header == "And") ? filters : stats;
+
+ unsigned int num = Convert::ToLong(params);
+ CombinerFilter::Ptr filter;
+
+ if (header == "Or" || header == "StatsOr") {
+ filter = new OrFilter();
+ Log(LogDebug, "LivestatusQuery")
+ << "Add OR filter for " << params << " column(s). " << deq.size() << " filters available.";
+ } else {
+ filter = new AndFilter();
+ Log(LogDebug, "LivestatusQuery")
+ << "Add AND filter for " << params << " column(s). " << deq.size() << " filters available.";
+ }
+
+ if (num > deq.size()) {
+ m_Verb = "ERROR";
+ m_ErrorCode = 451;
+ m_ErrorMessage = "Or/StatsOr is referencing " + Convert::ToString(num) + " filters; stack only contains " + Convert::ToString(static_cast<long>(deq.size())) + " filters";
+ return;
+ }
+
+ while (num > 0 && num--) {
+ filter->AddSubFilter(deq.back());
+ Log(LogDebug, "LivestatusQuery")
+ << "Add " << num << " filter.";
+ deq.pop_back();
+ if (&deq == &stats)
+ aggregators.pop_back();
+ }
+
+ deq.emplace_back(filter);
+ if (&deq == &stats) {
+ Aggregator::Ptr aggregator = new CountAggregator();
+ aggregator->SetFilter(filter);
+ aggregators.push_back(aggregator);
+ }
+ } else if (header == "Negate" || header == "StatsNegate") {
+ std::deque<Filter::Ptr>& deq = (header == "Negate") ? filters : stats;
+
+ if (deq.empty()) {
+ m_Verb = "ERROR";
+ m_ErrorCode = 451;
+ m_ErrorMessage = "Negate/StatsNegate used, however the filter stack is empty";
+ return;
+ }
+
+ Filter::Ptr filter = deq.back();
+ deq.pop_back();
+
+ if (!filter) {
+ m_Verb = "ERROR";
+ m_ErrorCode = 451;
+ m_ErrorMessage = "Negate/StatsNegate used, however last stats doesn't have a filter";
+ return;
+ }
+
+ deq.push_back(new NegateFilter(filter));
+
+ if (deq == stats) {
+ Aggregator::Ptr aggregator = aggregators.back();
+ aggregator->SetFilter(filter);
+ }
+ }
+ }
+
+ /* Combine all top-level filters into a single filter. */
+ AndFilter::Ptr top_filter = new AndFilter();
+
+ for (const Filter::Ptr& filter : filters) {
+ top_filter->AddSubFilter(filter);
+ }
+
+ m_Filter = top_filter;
+ m_Aggregators.swap(aggregators);
+}
+
+int LivestatusQuery::GetExternalCommands()
+{
+ std::unique_lock<std::mutex> lock(l_QueryMutex);
+
+ return l_ExternalCommands;
+}
+
+Filter::Ptr LivestatusQuery::ParseFilter(const String& params, unsigned long& from, unsigned long& until)
+{
+ /*
+ * time >= 1382696656
+ * type = SERVICE FLAPPING ALERT
+ */
+ std::vector<String> tokens;
+ size_t sp_index;
+ String temp_buffer = params;
+
+ /* extract attr and op */
+ for (int i = 0; i < 2; i++) {
+ sp_index = temp_buffer.FindFirstOf(" ");
+
+ /* check if this is the last argument */
+ if (sp_index == String::NPos) {
+ /* 'attr op' or 'attr op val' is valid */
+ if (i < 1)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Livestatus filter '" + params + "' does not contain all required fields."));
+
+ break;
+ }
+
+ tokens.emplace_back(temp_buffer.SubStr(0, sp_index));
+ temp_buffer = temp_buffer.SubStr(sp_index + 1);
+ }
+
+ /* add the rest as value */
+ tokens.emplace_back(std::move(temp_buffer));
+
+ if (tokens.size() == 2)
+ tokens.emplace_back("");
+
+ if (tokens.size() < 3)
+ return nullptr;
+
+ bool negate = false;
+ String attr = tokens[0];
+ String op = tokens[1];
+ String val = tokens[2];
+
+ if (op == "!=") {
+ op = "=";
+ negate = true;
+ } else if (op == "!~") {
+ op = "~";
+ negate = true;
+ } else if (op == "!=~") {
+ op = "=~";
+ negate = true;
+ } else if (op == "!~~") {
+ op = "~~";
+ negate = true;
+ }
+
+ Filter::Ptr filter = new AttributeFilter(attr, op, val);
+
+ if (negate)
+ filter = new NegateFilter(filter);
+
+ /* pre-filter log time duration */
+ if (attr == "time") {
+ if (op == "<" || op == "<=") {
+ until = Convert::ToLong(val);
+ } else if (op == ">" || op == ">=") {
+ from = Convert::ToLong(val);
+ }
+ }
+
+ Log(LogDebug, "LivestatusQuery")
+ << "Parsed filter with attr: '" << attr << "' op: '" << op << "' val: '" << val << "'.";
+
+ return filter;
+}
+
+void LivestatusQuery::BeginResultSet(std::ostream& fp) const
+{
+ if (m_OutputFormat == "json" || m_OutputFormat == "python")
+ fp << "[";
+}
+
+void LivestatusQuery::EndResultSet(std::ostream& fp) const
+{
+ if (m_OutputFormat == "json" || m_OutputFormat == "python")
+ fp << "]";
+}
+
+void LivestatusQuery::AppendResultRow(std::ostream& fp, const Array::Ptr& row, bool& first_row) const
+{
+ if (m_OutputFormat == "csv") {
+ bool first = true;
+
+ ObjectLock rlock(row);
+ for (const Value& value : row) {
+ if (first)
+ first = false;
+ else
+ fp << m_Separators[1];
+
+ if (value.IsObjectType<Array>())
+ PrintCsvArray(fp, value, 0);
+ else
+ fp << value;
+ }
+
+ fp << m_Separators[0];
+ } else if (m_OutputFormat == "json") {
+ if (!first_row)
+ fp << ", ";
+
+ fp << JsonEncode(row);
+ } else if (m_OutputFormat == "python") {
+ if (!first_row)
+ fp << ", ";
+
+ PrintPythonArray(fp, row);
+ }
+
+ first_row = false;
+}
+
+void LivestatusQuery::PrintCsvArray(std::ostream& fp, const Array::Ptr& array, int level) const
+{
+ bool first = true;
+
+ ObjectLock olock(array);
+ for (const Value& value : array) {
+ if (first)
+ first = false;
+ else
+ fp << ((level == 0) ? m_Separators[2] : m_Separators[3]);
+
+ if (value.IsObjectType<Array>())
+ PrintCsvArray(fp, value, level + 1);
+ else if (value.IsBoolean())
+ fp << Convert::ToLong(value);
+ else
+ fp << value;
+ }
+}
+
+void LivestatusQuery::PrintPythonArray(std::ostream& fp, const Array::Ptr& rs) const
+{
+ fp << "[ ";
+
+ bool first = true;
+
+ for (const Value& value : rs) {
+ if (first)
+ first = false;
+ else
+ fp << ", ";
+
+ if (value.IsObjectType<Array>())
+ PrintPythonArray(fp, value);
+ else if (value.IsNumber())
+ fp << value;
+ else
+ fp << QuoteStringPython(value);
+ }
+
+ fp << " ]";
+}
+
+String LivestatusQuery::QuoteStringPython(const String& str) {
+ String result = str;
+ boost::algorithm::replace_all(result, "\"", "\\\"");
+ return "r\"" + result + "\"";
+}
+
+void LivestatusQuery::ExecuteGetHelper(const Stream::Ptr& stream)
+{
+ Log(LogNotice, "LivestatusQuery")
+ << "Table: " << m_Table;
+
+ Table::Ptr table = Table::GetByName(m_Table, m_CompatLogPath, m_LogTimeFrom, m_LogTimeUntil);
+
+ if (!table) {
+ SendResponse(stream, LivestatusErrorNotFound, "Table '" + m_Table + "' does not exist.");
+
+ return;
+ }
+
+ std::vector<LivestatusRowValue> objects = table->FilterRows(m_Filter, m_Limit);
+ std::vector<String> columns;
+
+ if (m_Columns.size() > 0)
+ columns = m_Columns;
+ else
+ columns = table->GetColumnNames();
+
+ std::ostringstream result;
+ bool first_row = true;
+ BeginResultSet(result);
+
+ if (m_Aggregators.empty()) {
+ typedef std::pair<String, Column> ColumnPair;
+
+ std::vector<ColumnPair> column_objs;
+ column_objs.reserve(columns.size());
+
+ for (const String& columnName : columns)
+ column_objs.emplace_back(columnName, table->GetColumn(columnName));
+
+ ArrayData header;
+
+ for (const LivestatusRowValue& object : objects) {
+ ArrayData row;
+
+ row.reserve(column_objs.size());
+
+ for (const ColumnPair& cv : column_objs) {
+ if (m_ColumnHeaders)
+ header.push_back(cv.first);
+
+ row.push_back(cv.second.ExtractValue(object.Row, object.GroupByType, object.GroupByObject));
+ }
+
+ if (m_ColumnHeaders) {
+ AppendResultRow(result, new Array(std::move(header)), first_row);
+ m_ColumnHeaders = false;
+ }
+
+ AppendResultRow(result, new Array(std::move(row)), first_row);
+ }
+ } else {
+ std::map<std::vector<Value>, std::vector<AggregatorState *> > allStats;
+
+ /* add aggregated stats */
+ for (const LivestatusRowValue& object : objects) {
+ std::vector<Value> statsKey;
+
+ for (const String& columnName : m_Columns) {
+ Column column = table->GetColumn(columnName);
+ statsKey.emplace_back(column.ExtractValue(object.Row, object.GroupByType, object.GroupByObject));
+ }
+
+ auto it = allStats.find(statsKey);
+
+ if (it == allStats.end()) {
+ std::vector<AggregatorState *> newStats(m_Aggregators.size(), nullptr);
+ it = allStats.insert(std::make_pair(statsKey, newStats)).first;
+ }
+
+ auto& stats = it->second;
+
+ int index = 0;
+
+ for (const Aggregator::Ptr& aggregator : m_Aggregators) {
+ aggregator->Apply(table, object.Row, &stats[index]);
+ index++;
+ }
+ }
+
+ /* add column headers both for raw and aggregated data */
+ if (m_ColumnHeaders) {
+ ArrayData header;
+
+ for (const String& columnName : m_Columns) {
+ header.push_back(columnName);
+ }
+
+ for (size_t i = 1; i <= m_Aggregators.size(); i++) {
+ header.push_back("stats_" + Convert::ToString(i));
+ }
+
+ AppendResultRow(result, new Array(std::move(header)), first_row);
+ }
+
+ for (const auto& kv : allStats) {
+ ArrayData row;
+
+ row.reserve(m_Columns.size() + m_Aggregators.size());
+
+ for (const Value& keyPart : kv.first) {
+ row.push_back(keyPart);
+ }
+
+ auto& stats = kv.second;
+
+ for (size_t i = 0; i < m_Aggregators.size(); i++)
+ row.push_back(m_Aggregators[i]->GetResultAndFreeState(stats[i]));
+
+ AppendResultRow(result, new Array(std::move(row)), first_row);
+ }
+
+ /* add a bogus zero value if aggregated is empty*/
+ if (allStats.empty()) {
+ ArrayData row;
+
+ row.reserve(m_Aggregators.size());
+
+ for (size_t i = 1; i <= m_Aggregators.size(); i++) {
+ row.push_back(0);
+ }
+
+ AppendResultRow(result, new Array(std::move(row)), first_row);
+ }
+ }
+
+ EndResultSet(result);
+
+ SendResponse(stream, LivestatusErrorOK, result.str());
+}
+
+void LivestatusQuery::ExecuteCommandHelper(const Stream::Ptr& stream)
+{
+ {
+ std::unique_lock<std::mutex> lock(l_QueryMutex);
+
+ l_ExternalCommands++;
+ }
+
+ Log(LogNotice, "LivestatusQuery")
+ << "Executing command: " << m_Command;
+ ExternalCommandProcessor::Execute(m_Command);
+ SendResponse(stream, LivestatusErrorOK, "");
+}
+
+void LivestatusQuery::ExecuteErrorHelper(const Stream::Ptr& stream)
+{
+ Log(LogDebug, "LivestatusQuery")
+ << "ERROR: Code: '" << m_ErrorCode << "' Message: '" << m_ErrorMessage << "'.";
+ SendResponse(stream, m_ErrorCode, m_ErrorMessage);
+}
+
+void LivestatusQuery::SendResponse(const Stream::Ptr& stream, int code, const String& data)
+{
+ if (m_ResponseHeader == "fixed16")
+ PrintFixed16(stream, code, data);
+
+ if (m_ResponseHeader == "fixed16" || code == LivestatusErrorOK) {
+ try {
+ stream->Write(data.CStr(), data.GetLength());
+ } catch (const std::exception&) {
+ Log(LogCritical, "LivestatusQuery", "Cannot write query response to socket.");
+ }
+ }
+}
+
+void LivestatusQuery::PrintFixed16(const Stream::Ptr& stream, int code, const String& data)
+{
+ ASSERT(code >= 100 && code <= 999);
+
+ String sCode = Convert::ToString(code);
+ String sLength = Convert::ToString(static_cast<long>(data.GetLength()));
+
+ String header = sCode + String(16 - 3 - sLength.GetLength() - 1, ' ') + sLength + m_Separators[0];
+
+ try {
+ stream->Write(header.CStr(), header.GetLength());
+ } catch (const std::exception&) {
+ Log(LogCritical, "LivestatusQuery", "Cannot write to TCP socket.");
+ }
+}
+
+bool LivestatusQuery::Execute(const Stream::Ptr& stream)
+{
+ try {
+ Log(LogNotice, "LivestatusQuery")
+ << "Executing livestatus query: " << m_Verb;
+
+ if (m_Verb == "GET")
+ ExecuteGetHelper(stream);
+ else if (m_Verb == "COMMAND")
+ ExecuteCommandHelper(stream);
+ else if (m_Verb == "ERROR")
+ ExecuteErrorHelper(stream);
+ else
+ BOOST_THROW_EXCEPTION(std::runtime_error("Invalid livestatus query verb."));
+ } catch (const std::exception& ex) {
+ SendResponse(stream, LivestatusErrorQuery, DiagnosticInformation(ex));
+ }
+
+ if (!m_KeepAlive) {
+ stream->Close();
+ return false;
+ }
+
+ return true;
+}
diff --git a/lib/livestatus/livestatusquery.hpp b/lib/livestatus/livestatusquery.hpp
new file mode 100644
index 0000000..910cc16
--- /dev/null
+++ b/lib/livestatus/livestatusquery.hpp
@@ -0,0 +1,90 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LIVESTATUSQUERY_H
+#define LIVESTATUSQUERY_H
+
+#include "livestatus/filter.hpp"
+#include "livestatus/aggregator.hpp"
+#include "base/object.hpp"
+#include "base/array.hpp"
+#include "base/stream.hpp"
+#include "base/scriptframe.hpp"
+#include <deque>
+
+using namespace icinga;
+
+namespace icinga
+{
+
+enum LivestatusError
+{
+ LivestatusErrorOK = 200,
+ LivestatusErrorNotFound = 404,
+ LivestatusErrorQuery = 452
+};
+
+/**
+ * @ingroup livestatus
+ */
+class LivestatusQuery final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(LivestatusQuery);
+
+ LivestatusQuery(const std::vector<String>& lines, const String& compat_log_path);
+
+ bool Execute(const Stream::Ptr& stream);
+
+ static int GetExternalCommands();
+
+private:
+ String m_Verb;
+
+ bool m_KeepAlive;
+
+ /* Parameters for GET queries. */
+ String m_Table;
+ std::vector<String> m_Columns;
+ std::vector<String> m_Separators;
+
+ Filter::Ptr m_Filter;
+ std::deque<Aggregator::Ptr> m_Aggregators;
+
+ String m_OutputFormat;
+ bool m_ColumnHeaders;
+ int m_Limit;
+
+ String m_ResponseHeader;
+
+ /* Parameters for COMMAND/SCRIPT queries. */
+ String m_Command;
+ String m_Session;
+
+ /* Parameters for invalid queries. */
+ int m_ErrorCode;
+ String m_ErrorMessage;
+
+ unsigned long m_LogTimeFrom;
+ unsigned long m_LogTimeUntil;
+ String m_CompatLogPath;
+
+ void BeginResultSet(std::ostream& fp) const;
+ void EndResultSet(std::ostream& fp) const;
+ void AppendResultRow(std::ostream& fp, const Array::Ptr& row, bool& first_row) const;
+ void PrintCsvArray(std::ostream& fp, const Array::Ptr& array, int level) const;
+ void PrintPythonArray(std::ostream& fp, const Array::Ptr& array) const;
+ static String QuoteStringPython(const String& str);
+
+ void ExecuteGetHelper(const Stream::Ptr& stream);
+ void ExecuteCommandHelper(const Stream::Ptr& stream);
+ void ExecuteErrorHelper(const Stream::Ptr& stream);
+
+ void SendResponse(const Stream::Ptr& stream, int code, const String& data);
+ void PrintFixed16(const Stream::Ptr& stream, int code, const String& data);
+
+ static Filter::Ptr ParseFilter(const String& params, unsigned long& from, unsigned long& until);
+};
+
+}
+
+#endif /* LIVESTATUSQUERY_H */
diff --git a/lib/livestatus/logtable.cpp b/lib/livestatus/logtable.cpp
new file mode 100644
index 0000000..c1358dd
--- /dev/null
+++ b/lib/livestatus/logtable.cpp
@@ -0,0 +1,229 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/logtable.hpp"
+#include "livestatus/livestatuslogutility.hpp"
+#include "livestatus/hoststable.hpp"
+#include "livestatus/servicestable.hpp"
+#include "livestatus/contactstable.hpp"
+#include "livestatus/commandstable.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/cib.hpp"
+#include "icinga/service.hpp"
+#include "icinga/host.hpp"
+#include "icinga/user.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <fstream>
+
+using namespace icinga;
+
+LogTable::LogTable(const String& compat_log_path, time_t from, time_t until)
+{
+ /* store attributes for FetchRows */
+ m_TimeFrom = from;
+ m_TimeUntil = until;
+ m_CompatLogPath = compat_log_path;
+
+ AddColumns(this);
+}
+
+void LogTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "time", Column(&LogTable::TimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "lineno", Column(&LogTable::LinenoAccessor, objectAccessor));
+ table->AddColumn(prefix + "class", Column(&LogTable::ClassAccessor, objectAccessor));
+ table->AddColumn(prefix + "message", Column(&LogTable::MessageAccessor, objectAccessor));
+ table->AddColumn(prefix + "type", Column(&LogTable::TypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "options", Column(&LogTable::OptionsAccessor, objectAccessor));
+ table->AddColumn(prefix + "comment", Column(&LogTable::CommentAccessor, objectAccessor));
+ table->AddColumn(prefix + "plugin_output", Column(&LogTable::PluginOutputAccessor, objectAccessor));
+ table->AddColumn(prefix + "state", Column(&LogTable::StateAccessor, objectAccessor));
+ table->AddColumn(prefix + "state_type", Column(&LogTable::StateTypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "attempt", Column(&LogTable::AttemptAccessor, objectAccessor));
+ table->AddColumn(prefix + "service_description", Column(&LogTable::ServiceDescriptionAccessor, objectAccessor));
+ table->AddColumn(prefix + "host_name", Column(&LogTable::HostNameAccessor, objectAccessor));
+ table->AddColumn(prefix + "contact_name", Column(&LogTable::ContactNameAccessor, objectAccessor));
+ table->AddColumn(prefix + "command_name", Column(&LogTable::CommandNameAccessor, objectAccessor));
+
+ HostsTable::AddColumns(table, "current_host_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return HostAccessor(row, objectAccessor);
+ });
+ ServicesTable::AddColumns(table, "current_service_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return ServiceAccessor(row, objectAccessor);
+ });
+ ContactsTable::AddColumns(table, "current_contact_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return ContactAccessor(row, objectAccessor);
+ });
+ CommandsTable::AddColumns(table, "current_command_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return CommandAccessor(row, objectAccessor);
+ });
+}
+
+String LogTable::GetName() const
+{
+ return "log";
+}
+
+String LogTable::GetPrefix() const
+{
+ return "log";
+}
+
+void LogTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ Log(LogDebug, "LogTable")
+ << "Pre-selecting log file from " << m_TimeFrom << " until " << m_TimeUntil;
+
+ /* create log file index */
+ LivestatusLogUtility::CreateLogIndex(m_CompatLogPath, m_LogFileIndex);
+
+ /* generate log cache */
+ LivestatusLogUtility::CreateLogCache(m_LogFileIndex, this, m_TimeFrom, m_TimeUntil, addRowFn);
+}
+
+/* gets called in LivestatusLogUtility::CreateLogCache */
+void LogTable::UpdateLogEntries(const Dictionary::Ptr& log_entry_attrs, int line_count, int lineno, const AddRowFunction& addRowFn)
+{
+ /* additional attributes only for log table */
+ log_entry_attrs->Set("lineno", lineno);
+
+ addRowFn(log_entry_attrs, LivestatusGroupByNone, Empty);
+}
+
+Object::Ptr LogTable::HostAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ String host_name = static_cast<Dictionary::Ptr>(row)->Get("host_name");
+
+ if (host_name.IsEmpty())
+ return nullptr;
+
+ return Host::GetByName(host_name);
+}
+
+Object::Ptr LogTable::ServiceAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ String host_name = static_cast<Dictionary::Ptr>(row)->Get("host_name");
+ String service_description = static_cast<Dictionary::Ptr>(row)->Get("service_description");
+
+ if (service_description.IsEmpty() || host_name.IsEmpty())
+ return nullptr;
+
+ return Service::GetByNamePair(host_name, service_description);
+}
+
+Object::Ptr LogTable::ContactAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ String contact_name = static_cast<Dictionary::Ptr>(row)->Get("contact_name");
+
+ if (contact_name.IsEmpty())
+ return nullptr;
+
+ return User::GetByName(contact_name);
+}
+
+Object::Ptr LogTable::CommandAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ String command_name = static_cast<Dictionary::Ptr>(row)->Get("command_name");
+
+ if (command_name.IsEmpty())
+ return nullptr;
+
+ CheckCommand::Ptr check_command = CheckCommand::GetByName(command_name);
+ if (!check_command) {
+ EventCommand::Ptr event_command = EventCommand::GetByName(command_name);
+ if (!event_command) {
+ NotificationCommand::Ptr notification_command = NotificationCommand::GetByName(command_name);
+ if (!notification_command)
+ return nullptr;
+ else
+ return notification_command;
+ } else
+ return event_command;
+ } else
+ return check_command;
+}
+
+Value LogTable::TimeAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("time");
+}
+
+Value LogTable::LinenoAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("lineno");
+}
+
+Value LogTable::ClassAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("class");
+}
+
+Value LogTable::MessageAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("message");
+}
+
+Value LogTable::TypeAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("type");
+}
+
+Value LogTable::OptionsAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("options");
+}
+
+Value LogTable::CommentAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("comment");
+}
+
+Value LogTable::PluginOutputAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("plugin_output");
+}
+
+Value LogTable::StateAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("state");
+}
+
+Value LogTable::StateTypeAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("state_type");
+}
+
+Value LogTable::AttemptAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("attempt");
+}
+
+Value LogTable::ServiceDescriptionAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("service_description");
+}
+
+Value LogTable::HostNameAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("host_name");
+}
+
+Value LogTable::ContactNameAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("contact_name");
+}
+
+Value LogTable::CommandNameAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("command_name");
+}
diff --git a/lib/livestatus/logtable.hpp b/lib/livestatus/logtable.hpp
new file mode 100644
index 0000000..7a89310
--- /dev/null
+++ b/lib/livestatus/logtable.hpp
@@ -0,0 +1,65 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LOGTABLE_H
+#define LOGTABLE_H
+
+#include "livestatus/historytable.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class LogTable final : public HistoryTable
+{
+public:
+ DECLARE_PTR_TYPEDEFS(LogTable);
+
+ LogTable(const String& compat_log_path, time_t from, time_t until);
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+ void UpdateLogEntries(const Dictionary::Ptr& log_entry_attrs, int line_count, int lineno, const AddRowFunction& addRowFn) override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Object::Ptr HostAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+ static Object::Ptr ServiceAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+ static Object::Ptr ContactAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+ static Object::Ptr CommandAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+
+ static Value TimeAccessor(const Value& row);
+ static Value LinenoAccessor(const Value& row);
+ static Value ClassAccessor(const Value& row);
+ static Value MessageAccessor(const Value& row);
+ static Value TypeAccessor(const Value& row);
+ static Value OptionsAccessor(const Value& row);
+ static Value CommentAccessor(const Value& row);
+ static Value PluginOutputAccessor(const Value& row);
+ static Value StateAccessor(const Value& row);
+ static Value StateTypeAccessor(const Value& row);
+ static Value AttemptAccessor(const Value& row);
+ static Value ServiceDescriptionAccessor(const Value& row);
+ static Value HostNameAccessor(const Value& row);
+ static Value ContactNameAccessor(const Value& row);
+ static Value CommandNameAccessor(const Value& row);
+
+private:
+ std::map<time_t, String> m_LogFileIndex;
+ std::map<time_t, Dictionary::Ptr> m_RowsCache;
+ time_t m_TimeFrom;
+ time_t m_TimeUntil;
+ String m_CompatLogPath;
+};
+
+}
+
+#endif /* LOGTABLE_H */
diff --git a/lib/livestatus/maxaggregator.cpp b/lib/livestatus/maxaggregator.cpp
new file mode 100644
index 0000000..375d24b
--- /dev/null
+++ b/lib/livestatus/maxaggregator.cpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/maxaggregator.hpp"
+
+using namespace icinga;
+
+MaxAggregator::MaxAggregator(String attr)
+ : m_MaxAttr(std::move(attr))
+{ }
+
+MaxAggregatorState *MaxAggregator::EnsureState(AggregatorState **state)
+{
+ if (!*state)
+ *state = new MaxAggregatorState();
+
+ return static_cast<MaxAggregatorState *>(*state);
+}
+
+void MaxAggregator::Apply(const Table::Ptr& table, const Value& row, AggregatorState **state)
+{
+ Column column = table->GetColumn(m_MaxAttr);
+
+ Value value = column.ExtractValue(row);
+
+ MaxAggregatorState *pstate = EnsureState(state);
+
+ if (value > pstate->Max)
+ pstate->Max = value;
+}
+
+double MaxAggregator::GetResultAndFreeState(AggregatorState *state) const
+{
+ MaxAggregatorState *pstate = EnsureState(&state);
+ double result = pstate->Max;
+ delete pstate;
+
+ return result;
+}
diff --git a/lib/livestatus/maxaggregator.hpp b/lib/livestatus/maxaggregator.hpp
new file mode 100644
index 0000000..5bff5f9
--- /dev/null
+++ b/lib/livestatus/maxaggregator.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MAXAGGREGATOR_H
+#define MAXAGGREGATOR_H
+
+#include "livestatus/table.hpp"
+#include "livestatus/aggregator.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct MaxAggregatorState final : public AggregatorState
+{
+ double Max{0};
+};
+
+/**
+ * @ingroup livestatus
+ */
+class MaxAggregator final : public Aggregator
+{
+public:
+ DECLARE_PTR_TYPEDEFS(MaxAggregator);
+
+ MaxAggregator(String attr);
+
+ void Apply(const Table::Ptr& table, const Value& row, AggregatorState **state) override;
+ double GetResultAndFreeState(AggregatorState *state) const override;
+
+private:
+ String m_MaxAttr;
+
+ static MaxAggregatorState *EnsureState(AggregatorState **state);
+};
+
+}
+
+#endif /* MAXAGGREGATOR_H */
diff --git a/lib/livestatus/minaggregator.cpp b/lib/livestatus/minaggregator.cpp
new file mode 100644
index 0000000..06cb76e
--- /dev/null
+++ b/lib/livestatus/minaggregator.cpp
@@ -0,0 +1,45 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/minaggregator.hpp"
+
+using namespace icinga;
+
+MinAggregator::MinAggregator(String attr)
+ : m_MinAttr(std::move(attr))
+{ }
+
+MinAggregatorState *MinAggregator::EnsureState(AggregatorState **state)
+{
+ if (!*state)
+ *state = new MinAggregatorState();
+
+ return static_cast<MinAggregatorState *>(*state);
+}
+
+void MinAggregator::Apply(const Table::Ptr& table, const Value& row, AggregatorState **state)
+{
+ Column column = table->GetColumn(m_MinAttr);
+
+ Value value = column.ExtractValue(row);
+
+ MinAggregatorState *pstate = EnsureState(state);
+
+ if (value < pstate->Min)
+ pstate->Min = value;
+}
+
+double MinAggregator::GetResultAndFreeState(AggregatorState *state) const
+{
+ MinAggregatorState *pstate = EnsureState(&state);
+
+ double result;
+
+ if (pstate->Min == DBL_MAX)
+ result = 0;
+ else
+ result = pstate->Min;
+
+ delete pstate;
+
+ return result;
+}
diff --git a/lib/livestatus/minaggregator.hpp b/lib/livestatus/minaggregator.hpp
new file mode 100644
index 0000000..71a9d89
--- /dev/null
+++ b/lib/livestatus/minaggregator.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MINAGGREGATOR_H
+#define MINAGGREGATOR_H
+
+#include "livestatus/table.hpp"
+#include "livestatus/aggregator.hpp"
+#include <cfloat>
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct MinAggregatorState final : public AggregatorState
+{
+ double Min{DBL_MAX};
+};
+
+/**
+ * @ingroup livestatus
+ */
+class MinAggregator final : public Aggregator
+{
+public:
+ DECLARE_PTR_TYPEDEFS(MinAggregator);
+
+ MinAggregator(String attr);
+
+ void Apply(const Table::Ptr& table, const Value& row, AggregatorState **state) override;
+ double GetResultAndFreeState(AggregatorState *state) const override;
+
+private:
+ String m_MinAttr;
+
+ static MinAggregatorState *EnsureState(AggregatorState **state);
+};
+
+}
+
+#endif /* MINAGGREGATOR_H */
diff --git a/lib/livestatus/negatefilter.cpp b/lib/livestatus/negatefilter.cpp
new file mode 100644
index 0000000..60202b4
--- /dev/null
+++ b/lib/livestatus/negatefilter.cpp
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/negatefilter.hpp"
+
+using namespace icinga;
+
+NegateFilter::NegateFilter(Filter::Ptr inner)
+ : m_Inner(std::move(inner))
+{ }
+
+bool NegateFilter::Apply(const Table::Ptr& table, const Value& row)
+{
+ return !m_Inner->Apply(table, row);
+}
diff --git a/lib/livestatus/negatefilter.hpp b/lib/livestatus/negatefilter.hpp
new file mode 100644
index 0000000..c08943c
--- /dev/null
+++ b/lib/livestatus/negatefilter.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NEGATEFILTER_H
+#define NEGATEFILTER_H
+
+#include "livestatus/filter.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class NegateFilter final : public Filter
+{
+public:
+ DECLARE_PTR_TYPEDEFS(NegateFilter);
+
+ NegateFilter(Filter::Ptr inner);
+
+ bool Apply(const Table::Ptr& table, const Value& row) override;
+
+private:
+ Filter::Ptr m_Inner;
+};
+
+}
+
+#endif /* NEGATEFILTER_H */
diff --git a/lib/livestatus/orfilter.cpp b/lib/livestatus/orfilter.cpp
new file mode 100644
index 0000000..6cc446c
--- /dev/null
+++ b/lib/livestatus/orfilter.cpp
@@ -0,0 +1,18 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/orfilter.hpp"
+
+using namespace icinga;
+
+bool OrFilter::Apply(const Table::Ptr& table, const Value& row)
+{
+ if (m_Filters.empty())
+ return true;
+
+ for (const Filter::Ptr& filter : m_Filters) {
+ if (filter->Apply(table, row))
+ return true;
+ }
+
+ return false;
+}
diff --git a/lib/livestatus/orfilter.hpp b/lib/livestatus/orfilter.hpp
new file mode 100644
index 0000000..df855c1
--- /dev/null
+++ b/lib/livestatus/orfilter.hpp
@@ -0,0 +1,26 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ORFILTER_H
+#define ORFILTER_H
+
+#include "livestatus/combinerfilter.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class OrFilter final : public CombinerFilter
+{
+public:
+ DECLARE_PTR_TYPEDEFS(OrFilter);
+
+ bool Apply(const Table::Ptr& table, const Value& row) override;
+};
+
+}
+
+#endif /* ORFILTER_H */
diff --git a/lib/livestatus/servicegroupstable.cpp b/lib/livestatus/servicegroupstable.cpp
new file mode 100644
index 0000000..38d6d05
--- /dev/null
+++ b/lib/livestatus/servicegroupstable.cpp
@@ -0,0 +1,323 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/servicegroupstable.hpp"
+#include "icinga/servicegroup.hpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+ServiceGroupsTable::ServiceGroupsTable()
+{
+ AddColumns(this);
+}
+
+void ServiceGroupsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&ServiceGroupsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "alias", Column(&ServiceGroupsTable::AliasAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes", Column(&ServiceGroupsTable::NotesAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes_url", Column(&ServiceGroupsTable::NotesUrlAccessor, objectAccessor));
+ table->AddColumn(prefix + "action_url", Column(&ServiceGroupsTable::ActionUrlAccessor, objectAccessor));
+ table->AddColumn(prefix + "members", Column(&ServiceGroupsTable::MembersAccessor, objectAccessor));
+ table->AddColumn(prefix + "members_with_state", Column(&ServiceGroupsTable::MembersWithStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "worst_service_state", Column(&ServiceGroupsTable::WorstServiceStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services", Column(&ServiceGroupsTable::NumServicesAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_ok", Column(&ServiceGroupsTable::NumServicesOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_warn", Column(&ServiceGroupsTable::NumServicesWarnAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_crit", Column(&ServiceGroupsTable::NumServicesCritAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_unknown", Column(&ServiceGroupsTable::NumServicesUnknownAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_pending", Column(&ServiceGroupsTable::NumServicesPendingAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_ok", Column(&ServiceGroupsTable::NumServicesHardOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_warn", Column(&ServiceGroupsTable::NumServicesHardWarnAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_crit", Column(&ServiceGroupsTable::NumServicesHardCritAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services_hard_unknown", Column(&ServiceGroupsTable::NumServicesHardUnknownAccessor, objectAccessor));
+}
+
+String ServiceGroupsTable::GetName() const
+{
+ return "servicegroups";
+}
+
+String ServiceGroupsTable::GetPrefix() const
+{
+ return "servicegroup";
+}
+
+void ServiceGroupsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const ServiceGroup::Ptr& sg : ConfigType::GetObjectsByType<ServiceGroup>()) {
+ if (!addRowFn(sg, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Value ServiceGroupsTable::NameAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ return sg->GetName();
+}
+
+Value ServiceGroupsTable::AliasAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ return sg->GetDisplayName();
+}
+
+Value ServiceGroupsTable::NotesAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ return sg->GetNotes();
+}
+
+Value ServiceGroupsTable::NotesUrlAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ return sg->GetNotesUrl();
+}
+
+Value ServiceGroupsTable::ActionUrlAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ return sg->GetActionUrl();
+}
+
+Value ServiceGroupsTable::MembersAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ result.push_back(new Array({
+ service->GetHost()->GetName(),
+ service->GetShortName()
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServiceGroupsTable::MembersWithStateAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ result.push_back(new Array({
+ service->GetHost()->GetName(),
+ service->GetShortName(),
+ service->GetHost()->GetState(),
+ service->GetState()
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServiceGroupsTable::WorstServiceStateAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ Value worst_service = ServiceOK;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetState() > worst_service)
+ worst_service = service->GetState();
+ }
+
+ return worst_service;
+}
+
+Value ServiceGroupsTable::NumServicesAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ return sg->GetMembers().size();
+}
+
+Value ServiceGroupsTable::NumServicesOkAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetState() == ServiceOK)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value ServiceGroupsTable::NumServicesWarnAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetState() == ServiceWarning)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value ServiceGroupsTable::NumServicesCritAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetState() == ServiceCritical)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value ServiceGroupsTable::NumServicesUnknownAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetState() == ServiceUnknown)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value ServiceGroupsTable::NumServicesPendingAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (!service->GetLastCheckResult())
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value ServiceGroupsTable::NumServicesHardOkAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceOK)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value ServiceGroupsTable::NumServicesHardWarnAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceWarning)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value ServiceGroupsTable::NumServicesHardCritAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceCritical)
+ num_services++;
+ }
+
+ return num_services;
+}
+
+Value ServiceGroupsTable::NumServicesHardUnknownAccessor(const Value& row)
+{
+ ServiceGroup::Ptr sg = static_cast<ServiceGroup::Ptr>(row);
+
+ if (!sg)
+ return Empty;
+
+ int num_services = 0;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ if (service->GetStateType() == StateTypeHard && service->GetState() == ServiceUnknown)
+ num_services++;
+ }
+
+ return num_services;
+}
diff --git a/lib/livestatus/servicegroupstable.hpp b/lib/livestatus/servicegroupstable.hpp
new file mode 100644
index 0000000..b3c60c4
--- /dev/null
+++ b/lib/livestatus/servicegroupstable.hpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERVICEGROUPSTABLE_H
+#define SERVICEGROUPSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class ServiceGroupsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ServiceGroupsTable);
+
+ ServiceGroupsTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value NameAccessor(const Value& row);
+ static Value AliasAccessor(const Value& row);
+ static Value NotesAccessor(const Value& row);
+ static Value NotesUrlAccessor(const Value& row);
+ static Value ActionUrlAccessor(const Value& row);
+ static Value MembersAccessor(const Value& row);
+ static Value MembersWithStateAccessor(const Value& row);
+ static Value WorstServiceStateAccessor(const Value& row);
+ static Value NumServicesAccessor(const Value& row);
+ static Value NumServicesOkAccessor(const Value& row);
+ static Value NumServicesWarnAccessor(const Value& row);
+ static Value NumServicesCritAccessor(const Value& row);
+ static Value NumServicesUnknownAccessor(const Value& row);
+ static Value NumServicesPendingAccessor(const Value& row);
+ static Value NumServicesHardOkAccessor(const Value& row);
+ static Value NumServicesHardWarnAccessor(const Value& row);
+ static Value NumServicesHardCritAccessor(const Value& row);
+ static Value NumServicesHardUnknownAccessor(const Value& row);
+};
+
+}
+
+#endif /* SERVICEGROUPSTABLE_H */
diff --git a/lib/livestatus/servicestable.cpp b/lib/livestatus/servicestable.cpp
new file mode 100644
index 0000000..681445a
--- /dev/null
+++ b/lib/livestatus/servicestable.cpp
@@ -0,0 +1,1200 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/servicestable.hpp"
+#include "livestatus/hoststable.hpp"
+#include "livestatus/servicegroupstable.hpp"
+#include "livestatus/hostgroupstable.hpp"
+#include "livestatus/endpointstable.hpp"
+#include "icinga/service.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/json.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/replace.hpp>
+
+using namespace icinga;
+
+ServicesTable::ServicesTable(LivestatusGroupByType type)
+ : Table(type)
+{
+ AddColumns(this);
+}
+
+
+void ServicesTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "description", Column(&ServicesTable::ShortNameAccessor, objectAccessor));
+ table->AddColumn(prefix + "service_description", Column(&ServicesTable::ShortNameAccessor, objectAccessor)); //ugly compatibility hack
+ table->AddColumn(prefix + "display_name", Column(&ServicesTable::DisplayNameAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_command", Column(&ServicesTable::CheckCommandAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_command_expanded", Column(&ServicesTable::CheckCommandExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "event_handler", Column(&ServicesTable::EventHandlerAccessor, objectAccessor));
+ table->AddColumn(prefix + "plugin_output", Column(&ServicesTable::PluginOutputAccessor, objectAccessor));
+ table->AddColumn(prefix + "long_plugin_output", Column(&ServicesTable::LongPluginOutputAccessor, objectAccessor));
+ table->AddColumn(prefix + "perf_data", Column(&ServicesTable::PerfDataAccessor, objectAccessor));
+ table->AddColumn(prefix + "notification_period", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_period", Column(&ServicesTable::CheckPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes", Column(&ServicesTable::NotesAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes_expanded", Column(&ServicesTable::NotesExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes_url", Column(&ServicesTable::NotesUrlAccessor, objectAccessor));
+ table->AddColumn(prefix + "notes_url_expanded", Column(&ServicesTable::NotesUrlExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "action_url", Column(&ServicesTable::ActionUrlAccessor, objectAccessor));
+ table->AddColumn(prefix + "action_url_expanded", Column(&ServicesTable::ActionUrlExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "icon_image", Column(&ServicesTable::IconImageAccessor, objectAccessor));
+ table->AddColumn(prefix + "icon_image_expanded", Column(&ServicesTable::IconImageExpandedAccessor, objectAccessor));
+ table->AddColumn(prefix + "icon_image_alt", Column(&ServicesTable::IconImageAltAccessor, objectAccessor));
+ table->AddColumn(prefix + "initial_state", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "max_check_attempts", Column(&ServicesTable::MaxCheckAttemptsAccessor, objectAccessor));
+ table->AddColumn(prefix + "current_attempt", Column(&ServicesTable::CurrentAttemptAccessor, objectAccessor));
+ table->AddColumn(prefix + "state", Column(&ServicesTable::StateAccessor, objectAccessor));
+ table->AddColumn(prefix + "has_been_checked", Column(&ServicesTable::HasBeenCheckedAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_state", Column(&ServicesTable::LastStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_hard_state", Column(&ServicesTable::LastHardStateAccessor, objectAccessor));
+ table->AddColumn(prefix + "state_type", Column(&ServicesTable::StateTypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_type", Column(&ServicesTable::CheckTypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "acknowledged", Column(&ServicesTable::AcknowledgedAccessor, objectAccessor));
+ table->AddColumn(prefix + "acknowledgement_type", Column(&ServicesTable::AcknowledgementTypeAccessor, objectAccessor));
+ table->AddColumn(prefix + "no_more_notifications", Column(&ServicesTable::NoMoreNotificationsAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_time_ok", Column(&ServicesTable::LastTimeOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_time_warning", Column(&ServicesTable::LastTimeWarningAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_time_critical", Column(&ServicesTable::LastTimeCriticalAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_time_unknown", Column(&ServicesTable::LastTimeUnknownAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_check", Column(&ServicesTable::LastCheckAccessor, objectAccessor));
+ table->AddColumn(prefix + "next_check", Column(&ServicesTable::NextCheckAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_notification", Column(&ServicesTable::LastNotificationAccessor, objectAccessor));
+ table->AddColumn(prefix + "next_notification", Column(&ServicesTable::NextNotificationAccessor, objectAccessor));
+ table->AddColumn(prefix + "current_notification_number", Column(&ServicesTable::CurrentNotificationNumberAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_state_change", Column(&ServicesTable::LastStateChangeAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_hard_state_change", Column(&ServicesTable::LastHardStateChangeAccessor, objectAccessor));
+ table->AddColumn(prefix + "scheduled_downtime_depth", Column(&ServicesTable::ScheduledDowntimeDepthAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_flapping", Column(&ServicesTable::IsFlappingAccessor, objectAccessor));
+ table->AddColumn(prefix + "checks_enabled", Column(&ServicesTable::ChecksEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "accept_passive_checks", Column(&ServicesTable::AcceptPassiveChecksAccessor, objectAccessor));
+ table->AddColumn(prefix + "event_handler_enabled", Column(&ServicesTable::EventHandlerEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "notifications_enabled", Column(&ServicesTable::NotificationsEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "process_performance_data", Column(&ServicesTable::ProcessPerformanceDataAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_executing", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "active_checks_enabled", Column(&ServicesTable::ActiveChecksEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_options", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "flap_detection_enabled", Column(&ServicesTable::FlapDetectionEnabledAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_freshness", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "obsess_over_service", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "modified_attributes", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "modified_attributes_list", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "pnpgraph_present", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "staleness", Column(&ServicesTable::StalenessAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_interval", Column(&ServicesTable::CheckIntervalAccessor, objectAccessor));
+ table->AddColumn(prefix + "retry_interval", Column(&ServicesTable::RetryIntervalAccessor, objectAccessor));
+ table->AddColumn(prefix + "notification_interval", Column(&ServicesTable::NotificationIntervalAccessor, objectAccessor));
+ table->AddColumn(prefix + "first_notification_delay", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "low_flap_threshold", Column(&ServicesTable::LowFlapThresholdAccessor, objectAccessor));
+ table->AddColumn(prefix + "high_flap_threshold", Column(&ServicesTable::HighFlapThresholdAccessor, objectAccessor));
+ table->AddColumn(prefix + "latency", Column(&ServicesTable::LatencyAccessor, objectAccessor));
+ table->AddColumn(prefix + "execution_time", Column(&ServicesTable::ExecutionTimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "percent_state_change", Column(&ServicesTable::PercentStateChangeAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_check_period", Column(&ServicesTable::InCheckPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_notification_period", Column(&ServicesTable::InNotificationPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "contacts", Column(&ServicesTable::ContactsAccessor, objectAccessor));
+ table->AddColumn(prefix + "downtimes", Column(&ServicesTable::DowntimesAccessor, objectAccessor));
+ table->AddColumn(prefix + "downtimes_with_info", Column(&ServicesTable::DowntimesWithInfoAccessor, objectAccessor));
+ table->AddColumn(prefix + "comments", Column(&ServicesTable::CommentsAccessor, objectAccessor));
+ table->AddColumn(prefix + "comments_with_info", Column(&ServicesTable::CommentsWithInfoAccessor, objectAccessor));
+ table->AddColumn(prefix + "comments_with_extra_info", Column(&ServicesTable::CommentsWithExtraInfoAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variable_names", Column(&ServicesTable::CustomVariableNamesAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variable_values", Column(&ServicesTable::CustomVariableValuesAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variables", Column(&ServicesTable::CustomVariablesAccessor, objectAccessor));
+ table->AddColumn(prefix + "groups", Column(&ServicesTable::GroupsAccessor, objectAccessor));
+ table->AddColumn(prefix + "contact_groups", Column(&ServicesTable::ContactGroupsAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_source", Column(&ServicesTable::CheckSourceAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_reachable", Column(&ServicesTable::IsReachableAccessor, objectAccessor));
+ table->AddColumn(prefix + "cv_is_json", Column(&ServicesTable::CVIsJsonAccessor, objectAccessor));
+ table->AddColumn(prefix + "original_attributes", Column(&ServicesTable::OriginalAttributesAccessor, objectAccessor));
+
+ HostsTable::AddColumns(table, "host_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return HostAccessor(row, objectAccessor);
+ });
+
+ /* add additional group by values received through the object accessor */
+ if (table->GetGroupByType() == LivestatusGroupByServiceGroup) {
+ /* _1 = row, _2 = groupByType, _3 = groupByObject */
+ Log(LogDebug, "Livestatus")
+ << "Processing services group by servicegroup table.";
+ ServiceGroupsTable::AddColumns(table, "servicegroup_", [](const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject) -> Value {
+ return ServiceGroupAccessor(row, groupByType, groupByObject);
+ });
+ } else if (table->GetGroupByType() == LivestatusGroupByHostGroup) {
+ /* _1 = row, _2 = groupByType, _3 = groupByObject */
+ Log(LogDebug, "Livestatus")
+ << "Processing services group by hostgroup table.";
+ HostGroupsTable::AddColumns(table, "hostgroup_", [](const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject) -> Value {
+ return HostGroupAccessor(row, groupByType, groupByObject);
+ });
+ }
+}
+
+String ServicesTable::GetName() const
+{
+ return "services";
+}
+
+String ServicesTable::GetPrefix() const
+{
+ return "service";
+}
+
+void ServicesTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ if (GetGroupByType() == LivestatusGroupByServiceGroup) {
+ for (const ServiceGroup::Ptr& sg : ConfigType::GetObjectsByType<ServiceGroup>()) {
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ /* the caller must know which groupby type and value are set for this row */
+ if (!addRowFn(service, LivestatusGroupByServiceGroup, sg))
+ return;
+ }
+ }
+ } else if (GetGroupByType() == LivestatusGroupByHostGroup) {
+ for (const HostGroup::Ptr& hg : ConfigType::GetObjectsByType<HostGroup>()) {
+ ObjectLock ylock(hg);
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ ObjectLock ylock(host);
+ for (const Service::Ptr& service : host->GetServices()) {
+ /* the caller must know which groupby type and value are set for this row */
+ if (!addRowFn(service, LivestatusGroupByHostGroup, hg))
+ return;
+ }
+ }
+ }
+ } else {
+ for (const Service::Ptr& service : ConfigType::GetObjectsByType<Service>()) {
+ if (!addRowFn(service, LivestatusGroupByNone, Empty))
+ return;
+ }
+ }
+}
+
+Object::Ptr ServicesTable::HostAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor)
+{
+ Value service;
+
+ if (parentObjectAccessor)
+ service = parentObjectAccessor(row, LivestatusGroupByNone, Empty);
+ else
+ service = row;
+
+ Service::Ptr svc = static_cast<Service::Ptr>(service);
+
+ if (!svc)
+ return nullptr;
+
+ return svc->GetHost();
+}
+
+Object::Ptr ServicesTable::ServiceGroupAccessor(const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject)
+{
+ /* return the current group by value set from within FetchRows()
+ * this is the servicegroup object used for the table join inside
+ * in AddColumns()
+ */
+ if (groupByType == LivestatusGroupByServiceGroup)
+ return groupByObject;
+
+ return nullptr;
+}
+
+Object::Ptr ServicesTable::HostGroupAccessor(const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject)
+{
+ /* return the current group by value set from within FetchRows()
+ * this is the servicegroup object used for the table join inside
+ * in AddColumns()
+ */
+ if (groupByType == LivestatusGroupByHostGroup)
+ return groupByObject;
+
+ return nullptr;
+}
+
+Value ServicesTable::ShortNameAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetShortName();
+}
+
+Value ServicesTable::DisplayNameAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetDisplayName();
+}
+
+Value ServicesTable::CheckCommandAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ CheckCommand::Ptr checkcommand = service->GetCheckCommand();
+
+ if (checkcommand)
+ return CompatUtility::GetCommandName(checkcommand) + "!" + CompatUtility::GetCheckableCommandArgs(service);
+
+ return Empty;
+}
+
+Value ServicesTable::CheckCommandExpandedAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ CheckCommand::Ptr checkcommand = service->GetCheckCommand();
+
+ if (checkcommand)
+ return CompatUtility::GetCommandName(checkcommand) + "!" + CompatUtility::GetCheckableCommandArgs(service);
+
+ return Empty;
+}
+
+Value ServicesTable::EventHandlerAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ EventCommand::Ptr eventcommand = service->GetEventCommand();
+
+ if (eventcommand)
+ return CompatUtility::GetCommandName(eventcommand);
+
+ return Empty;
+}
+
+Value ServicesTable::PluginOutputAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ String output;
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (cr)
+ output = CompatUtility::GetCheckResultOutput(cr);
+
+ return output;
+}
+
+Value ServicesTable::LongPluginOutputAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ String long_output;
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (cr)
+ long_output = CompatUtility::GetCheckResultLongOutput(cr);
+
+ return long_output;
+}
+
+Value ServicesTable::PerfDataAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ String perfdata;
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (!cr)
+ return Empty;
+
+ return PluginUtility::FormatPerfdata(cr->GetPerformanceData());
+}
+
+Value ServicesTable::CheckPeriodAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ TimePeriod::Ptr checkPeriod = service->GetCheckPeriod();
+
+ if (!checkPeriod)
+ return Empty;
+
+ return checkPeriod->GetName();
+}
+
+Value ServicesTable::NotesAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetNotes();
+}
+
+Value ServicesTable::NotesExpandedAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ MacroProcessor::ResolverList resolvers {
+ { "service", service },
+ { "host", service->GetHost() },
+ };
+
+ return MacroProcessor::ResolveMacros(service->GetNotes(), resolvers);
+}
+
+Value ServicesTable::NotesUrlAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetNotesUrl();
+}
+
+Value ServicesTable::NotesUrlExpandedAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ MacroProcessor::ResolverList resolvers {
+ { "service", service },
+ { "host", service->GetHost() },
+ };
+
+ return MacroProcessor::ResolveMacros(service->GetNotesUrl(), resolvers);
+}
+
+Value ServicesTable::ActionUrlAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetActionUrl();
+}
+
+Value ServicesTable::ActionUrlExpandedAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ MacroProcessor::ResolverList resolvers {
+ { "service", service },
+ { "host", service->GetHost() },
+ };
+
+ return MacroProcessor::ResolveMacros(service->GetActionUrl(), resolvers);
+}
+
+Value ServicesTable::IconImageAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetIconImage();
+}
+
+Value ServicesTable::IconImageExpandedAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ MacroProcessor::ResolverList resolvers {
+ { "service", service },
+ { "host", service->GetHost() },
+ };
+
+ return MacroProcessor::ResolveMacros(service->GetIconImage(), resolvers);
+}
+
+Value ServicesTable::IconImageAltAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetIconImageAlt();
+}
+
+Value ServicesTable::MaxCheckAttemptsAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetMaxCheckAttempts();
+}
+
+Value ServicesTable::CurrentAttemptAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetCheckAttempt();
+}
+
+Value ServicesTable::StateAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetState();
+}
+
+Value ServicesTable::HasBeenCheckedAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return Convert::ToLong(service->HasBeenChecked());
+}
+
+Value ServicesTable::LastStateAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetLastState();
+}
+
+Value ServicesTable::LastHardStateAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetLastHardState();
+}
+
+Value ServicesTable::StateTypeAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetStateType();
+}
+
+Value ServicesTable::CheckTypeAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return (service->GetEnableActiveChecks() ? 0 : 1); /* 0 .. active, 1 .. passive */
+}
+
+Value ServicesTable::AcknowledgedAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ObjectLock olock(service);
+ return service->IsAcknowledged();
+}
+
+Value ServicesTable::AcknowledgementTypeAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ObjectLock olock(service);
+ return service->GetAcknowledgement();
+}
+
+Value ServicesTable::NoMoreNotificationsAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return (CompatUtility::GetCheckableNotificationNotificationInterval(service) == 0 && !service->GetVolatile()) ? 1 : 0;
+}
+
+Value ServicesTable::LastTimeOkAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return static_cast<int>(service->GetLastStateOK());
+}
+
+Value ServicesTable::LastTimeWarningAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return static_cast<int>(service->GetLastStateWarning());
+}
+
+Value ServicesTable::LastTimeCriticalAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return static_cast<int>(service->GetLastStateCritical());
+}
+
+Value ServicesTable::LastTimeUnknownAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return static_cast<int>(service->GetLastStateUnknown());
+}
+
+Value ServicesTable::LastCheckAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return static_cast<int>(service->GetLastCheck());
+}
+
+Value ServicesTable::NextCheckAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return static_cast<int>(service->GetNextCheck());
+}
+
+Value ServicesTable::LastNotificationAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return CompatUtility::GetCheckableNotificationLastNotification(service);
+}
+
+Value ServicesTable::NextNotificationAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return CompatUtility::GetCheckableNotificationNextNotification(service);
+}
+
+Value ServicesTable::CurrentNotificationNumberAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return CompatUtility::GetCheckableNotificationNotificationNumber(service);
+}
+
+Value ServicesTable::LastStateChangeAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return static_cast<int>(service->GetLastStateChange());
+}
+
+Value ServicesTable::LastHardStateChangeAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return static_cast<int>(service->GetLastHardStateChange());
+}
+
+Value ServicesTable::ScheduledDowntimeDepthAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetDowntimeDepth();
+}
+
+Value ServicesTable::IsFlappingAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->IsFlapping();
+}
+
+Value ServicesTable::ChecksEnabledAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return Convert::ToLong(service->GetEnableActiveChecks());
+}
+
+Value ServicesTable::AcceptPassiveChecksAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return Convert::ToLong(service->GetEnablePassiveChecks());
+}
+
+Value ServicesTable::EventHandlerEnabledAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return Convert::ToLong(service->GetEnableEventHandler());
+}
+
+Value ServicesTable::NotificationsEnabledAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return Convert::ToLong(service->GetEnableNotifications());
+}
+
+Value ServicesTable::ProcessPerformanceDataAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return Convert::ToLong(service->GetEnablePerfdata());
+}
+
+Value ServicesTable::ActiveChecksEnabledAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return Convert::ToLong(service->GetEnableActiveChecks());
+}
+
+Value ServicesTable::FlapDetectionEnabledAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return Convert::ToLong(service->GetEnableFlapping());
+}
+
+Value ServicesTable::StalenessAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ if (service->HasBeenChecked() && service->GetLastCheck() > 0)
+ return (Utility::GetTime() - service->GetLastCheck()) / (service->GetCheckInterval() * 3600);
+
+ return 0.0;
+}
+
+Value ServicesTable::CheckIntervalAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetCheckInterval() / LIVESTATUS_INTERVAL_LENGTH;
+}
+
+Value ServicesTable::RetryIntervalAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetRetryInterval() / LIVESTATUS_INTERVAL_LENGTH;
+}
+
+Value ServicesTable::NotificationIntervalAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return CompatUtility::GetCheckableNotificationNotificationInterval(service);
+}
+
+Value ServicesTable::LowFlapThresholdAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetFlappingThresholdLow();
+}
+
+Value ServicesTable::HighFlapThresholdAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetFlappingThresholdHigh();
+}
+
+Value ServicesTable::LatencyAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (!cr)
+ return Empty;
+
+ return cr->CalculateLatency();
+}
+
+Value ServicesTable::ExecutionTimeAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (!cr)
+ return Empty;
+
+ return cr->CalculateExecutionTime();
+}
+
+Value ServicesTable::PercentStateChangeAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->GetFlappingCurrent();
+}
+
+Value ServicesTable::InCheckPeriodAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ TimePeriod::Ptr timeperiod = service->GetCheckPeriod();
+
+ /* none set means always checked */
+ if (!timeperiod)
+ return 1;
+
+ return Convert::ToLong(timeperiod->IsInside(Utility::GetTime()));
+}
+
+Value ServicesTable::InNotificationPeriodAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ for (const Notification::Ptr& notification : service->GetNotifications()) {
+ TimePeriod::Ptr timeperiod = notification->GetPeriod();
+
+ if (!timeperiod || timeperiod->IsInside(Utility::GetTime()))
+ return 1;
+ }
+
+ return 0;
+}
+
+Value ServicesTable::ContactsAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ArrayData result;
+
+ for (const User::Ptr& user : CompatUtility::GetCheckableNotificationUsers(service)) {
+ result.push_back(user->GetName());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::DowntimesAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Downtime::Ptr& downtime : service->GetDowntimes()) {
+ if (downtime->IsExpired())
+ continue;
+
+ result.push_back(downtime->GetLegacyId());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::DowntimesWithInfoAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Downtime::Ptr& downtime : service->GetDowntimes()) {
+ if (downtime->IsExpired())
+ continue;
+
+ result.push_back(new Array({
+ downtime->GetLegacyId(),
+ downtime->GetAuthor(),
+ downtime->GetComment()
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::CommentsAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Comment::Ptr& comment : service->GetComments()) {
+ if (comment->IsExpired())
+ continue;
+
+ result.push_back(comment->GetLegacyId());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::CommentsWithInfoAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Comment::Ptr& comment : service->GetComments()) {
+ if (comment->IsExpired())
+ continue;
+
+ result.push_back(new Array({
+ comment->GetLegacyId(),
+ comment->GetAuthor(),
+ comment->GetText()
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::CommentsWithExtraInfoAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ArrayData result;
+
+ for (const Comment::Ptr& comment : service->GetComments()) {
+ if (comment->IsExpired())
+ continue;
+
+ result.push_back(new Array({
+ comment->GetLegacyId(),
+ comment->GetAuthor(),
+ comment->GetText(),
+ comment->GetEntryType(),
+ static_cast<int>(comment->GetEntryTime())
+ }));
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::CustomVariableNamesAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ Dictionary::Ptr vars = service->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ result.push_back(kv.first);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::CustomVariableValuesAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ Dictionary::Ptr vars = service->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ result.push_back(JsonEncode(kv.second));
+ else
+ result.push_back(kv.second);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::CustomVariablesAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ Dictionary::Ptr vars = service->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ Value val;
+
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ val = JsonEncode(kv.second);
+ else
+ val = kv.second;
+
+ result.push_back(new Array({
+ kv.first,
+ val
+ }));
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::CVIsJsonAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ Dictionary::Ptr vars = service->GetVars();
+
+ if (!vars)
+ return Empty;
+
+ bool cv_is_json = false;
+
+ ObjectLock olock(vars);
+ for (const Dictionary::Pair& kv : vars) {
+ if (kv.second.IsObjectType<Array>() || kv.second.IsObjectType<Dictionary>())
+ cv_is_json = true;
+ }
+
+ return cv_is_json;
+}
+
+Value ServicesTable::GroupsAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ Array::Ptr groups = service->GetGroups();
+
+ if (!groups)
+ return Empty;
+
+ return groups;
+}
+
+Value ServicesTable::ContactGroupsAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ ArrayData result;
+
+ for (const UserGroup::Ptr& usergroup : CompatUtility::GetCheckableNotificationUserGroups(service)) {
+ result.push_back(usergroup->GetName());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ServicesTable::CheckSourceAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (cr)
+ return cr->GetCheckSource();
+
+ return Empty;
+}
+
+Value ServicesTable::IsReachableAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return service->IsReachable();
+}
+
+Value ServicesTable::OriginalAttributesAccessor(const Value& row)
+{
+ Service::Ptr service = static_cast<Service::Ptr>(row);
+
+ if (!service)
+ return Empty;
+
+ return JsonEncode(service->GetOriginalAttributes());
+}
diff --git a/lib/livestatus/servicestable.hpp b/lib/livestatus/servicestable.hpp
new file mode 100644
index 0000000..56d5ae5
--- /dev/null
+++ b/lib/livestatus/servicestable.hpp
@@ -0,0 +1,115 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERVICESTABLE_H
+#define SERVICESTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class ServicesTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ServicesTable);
+
+ ServicesTable(LivestatusGroupByType type = LivestatusGroupByNone);
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Object::Ptr HostAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+ static Object::Ptr ServiceGroupAccessor(const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject);
+ static Object::Ptr HostGroupAccessor(const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject);
+
+ static Value ShortNameAccessor(const Value& row);
+ static Value DisplayNameAccessor(const Value& row);
+ static Value CheckCommandAccessor(const Value& row);
+ static Value CheckCommandExpandedAccessor(const Value& row);
+ static Value EventHandlerAccessor(const Value& row);
+ static Value PluginOutputAccessor(const Value& row);
+ static Value LongPluginOutputAccessor(const Value& row);
+ static Value PerfDataAccessor(const Value& row);
+ static Value CheckPeriodAccessor(const Value& row);
+ static Value NotesAccessor(const Value& row);
+ static Value NotesExpandedAccessor(const Value& row);
+ static Value NotesUrlAccessor(const Value& row);
+ static Value NotesUrlExpandedAccessor(const Value& row);
+ static Value ActionUrlAccessor(const Value& row);
+ static Value ActionUrlExpandedAccessor(const Value& row);
+ static Value IconImageAccessor(const Value& row);
+ static Value IconImageExpandedAccessor(const Value& row);
+ static Value IconImageAltAccessor(const Value& row);
+ static Value MaxCheckAttemptsAccessor(const Value& row);
+ static Value CurrentAttemptAccessor(const Value& row);
+ static Value StateAccessor(const Value& row);
+ static Value HasBeenCheckedAccessor(const Value& row);
+ static Value LastStateAccessor(const Value& row);
+ static Value LastHardStateAccessor(const Value& row);
+ static Value StateTypeAccessor(const Value& row);
+ static Value CheckTypeAccessor(const Value& row);
+ static Value AcknowledgedAccessor(const Value& row);
+ static Value AcknowledgementTypeAccessor(const Value& row);
+ static Value NoMoreNotificationsAccessor(const Value& row);
+ static Value LastTimeOkAccessor(const Value& row);
+ static Value LastTimeWarningAccessor(const Value& row);
+ static Value LastTimeCriticalAccessor(const Value& row);
+ static Value LastTimeUnknownAccessor(const Value& row);
+ static Value LastCheckAccessor(const Value& row);
+ static Value NextCheckAccessor(const Value& row);
+ static Value LastNotificationAccessor(const Value& row);
+ static Value NextNotificationAccessor(const Value& row);
+ static Value CurrentNotificationNumberAccessor(const Value& row);
+ static Value LastStateChangeAccessor(const Value& row);
+ static Value LastHardStateChangeAccessor(const Value& row);
+ static Value ScheduledDowntimeDepthAccessor(const Value& row);
+ static Value IsFlappingAccessor(const Value& row);
+ static Value ChecksEnabledAccessor(const Value& row);
+ static Value AcceptPassiveChecksAccessor(const Value& row);
+ static Value EventHandlerEnabledAccessor(const Value& row);
+ static Value NotificationsEnabledAccessor(const Value& row);
+ static Value ProcessPerformanceDataAccessor(const Value& row);
+ static Value ActiveChecksEnabledAccessor(const Value& row);
+ static Value FlapDetectionEnabledAccessor(const Value& row);
+ static Value StalenessAccessor(const Value& row);
+ static Value CheckIntervalAccessor(const Value& row);
+ static Value RetryIntervalAccessor(const Value& row);
+ static Value NotificationIntervalAccessor(const Value& row);
+ static Value LowFlapThresholdAccessor(const Value& row);
+ static Value HighFlapThresholdAccessor(const Value& row);
+ static Value LatencyAccessor(const Value& row);
+ static Value ExecutionTimeAccessor(const Value& row);
+ static Value PercentStateChangeAccessor(const Value& row);
+ static Value InCheckPeriodAccessor(const Value& row);
+ static Value InNotificationPeriodAccessor(const Value& row);
+ static Value ContactsAccessor(const Value& row);
+ static Value DowntimesAccessor(const Value& row);
+ static Value DowntimesWithInfoAccessor(const Value& row);
+ static Value CommentsAccessor(const Value& row);
+ static Value CommentsWithInfoAccessor(const Value& row);
+ static Value CommentsWithExtraInfoAccessor(const Value& row);
+ static Value CustomVariableNamesAccessor(const Value& row);
+ static Value CustomVariableValuesAccessor(const Value& row);
+ static Value CustomVariablesAccessor(const Value& row);
+ static Value GroupsAccessor(const Value& row);
+ static Value ContactGroupsAccessor(const Value& row);
+ static Value CheckSourceAccessor(const Value& row);
+ static Value IsReachableAccessor(const Value& row);
+ static Value CVIsJsonAccessor(const Value& row);
+ static Value OriginalAttributesAccessor(const Value& row);
+};
+
+}
+
+#endif /* SERVICESTABLE_H */
diff --git a/lib/livestatus/statehisttable.cpp b/lib/livestatus/statehisttable.cpp
new file mode 100644
index 0000000..2d7e49b
--- /dev/null
+++ b/lib/livestatus/statehisttable.cpp
@@ -0,0 +1,466 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/statehisttable.hpp"
+#include "livestatus/livestatuslogutility.hpp"
+#include "livestatus/hoststable.hpp"
+#include "livestatus/servicestable.hpp"
+#include "livestatus/contactstable.hpp"
+#include "livestatus/commandstable.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/cib.hpp"
+#include "icinga/service.hpp"
+#include "icinga/host.hpp"
+#include "icinga/user.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <fstream>
+
+using namespace icinga;
+
+StateHistTable::StateHistTable(const String& compat_log_path, time_t from, time_t until)
+{
+ /* store attributes for FetchRows */
+ m_TimeFrom = from;
+ m_TimeUntil = until;
+ m_CompatLogPath = compat_log_path;
+
+ AddColumns(this);
+}
+
+void StateHistTable::UpdateLogEntries(const Dictionary::Ptr& log_entry_attrs, int line_count, int lineno, const AddRowFunction& addRowFn)
+{
+ unsigned int time = log_entry_attrs->Get("time");
+ String host_name = log_entry_attrs->Get("host_name");
+ String service_description = log_entry_attrs->Get("service_description");
+ unsigned long state = log_entry_attrs->Get("state");
+ int log_type = log_entry_attrs->Get("log_type");
+ String state_type = log_entry_attrs->Get("state_type"); //SOFT, HARD, STARTED, STOPPED, ...
+ String log_line = log_entry_attrs->Get("message"); /* use message from log table */
+
+ Checkable::Ptr checkable;
+
+ if (service_description.IsEmpty())
+ checkable = Host::GetByName(host_name);
+ else
+ checkable = Service::GetByNamePair(host_name, service_description);
+
+ /* invalid log line for state history */
+ if (!checkable)
+ return;
+
+ Array::Ptr state_hist_service_states;
+ Dictionary::Ptr state_hist_bag;
+ unsigned long query_part = m_TimeUntil - m_TimeFrom;
+
+ /* insert new service states array with values if not existing */
+ if (m_CheckablesCache.find(checkable) == m_CheckablesCache.end()) {
+
+ /* create new values */
+ state_hist_service_states = new Array();
+ state_hist_bag = new Dictionary();
+
+ Service::Ptr service = dynamic_pointer_cast<Service>(checkable);
+ Host::Ptr host;
+
+ if (service)
+ host = service->GetHost();
+ else
+ host = static_pointer_cast<Host>(checkable);
+
+ state_hist_bag->Set("host_name", host->GetName());
+
+ if (service)
+ state_hist_bag->Set("service_description", service->GetShortName());
+
+ state_hist_bag->Set("state", state);
+ state_hist_bag->Set("in_downtime", 0);
+ state_hist_bag->Set("in_host_downtime", 0);
+ state_hist_bag->Set("in_notification_period", 1); // assume "always"
+ state_hist_bag->Set("is_flapping", 0);
+ state_hist_bag->Set("time", time);
+ state_hist_bag->Set("lineno", lineno);
+ state_hist_bag->Set("log_output", log_line); /* complete line */
+ state_hist_bag->Set("from", time); /* starting at current timestamp */
+ state_hist_bag->Set("until", time); /* will be updated later on state change */
+ state_hist_bag->Set("query_part", query_part); /* required for _part calculations */
+
+ state_hist_service_states->Add(state_hist_bag);
+
+ Log(LogDebug, "StateHistTable")
+ << "statehist: Adding new object '" << checkable->GetName() << "' to services cache.";
+ } else {
+ state_hist_service_states = m_CheckablesCache[checkable];
+ state_hist_bag = state_hist_service_states->Get(state_hist_service_states->GetLength()-1); /* fetch latest state from history */
+
+ /* state duration */
+
+ /* determine service notifications notification_period and compare against current timestamp */
+ bool in_notification_period = true;
+ String notification_period_name;
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ TimePeriod::Ptr notification_period = notification->GetPeriod();
+
+ if (notification_period) {
+ if (notification_period->IsInside(static_cast<double>(time)))
+ in_notification_period = true;
+ else
+ in_notification_period = false;
+
+ notification_period_name = notification_period->GetName(); // last one wins
+ } else
+ in_notification_period = true; // assume "always"
+ }
+
+ /* check for state changes, flapping & downtime start/end */
+ switch (log_type) {
+ case LogEntryTypeHostAlert:
+ case LogEntryTypeHostInitialState:
+ case LogEntryTypeHostCurrentState:
+ case LogEntryTypeServiceAlert:
+ case LogEntryTypeServiceInitialState:
+ case LogEntryTypeServiceCurrentState:
+ if (state != state_hist_bag->Get("state")) {
+ /* 1. seal old state_hist_bag */
+ state_hist_bag->Set("until", time); /* add until record for duration calculation */
+
+ /* 2. add new state_hist_bag */
+ Dictionary::Ptr state_hist_bag_new = new Dictionary();
+
+ state_hist_bag_new->Set("host_name", state_hist_bag->Get("host_name"));
+ state_hist_bag_new->Set("service_description", state_hist_bag->Get("service_description"));
+ state_hist_bag_new->Set("state", state);
+ state_hist_bag_new->Set("in_downtime", state_hist_bag->Get("in_downtime")); // keep value from previous state!
+ state_hist_bag_new->Set("in_host_downtime", state_hist_bag->Get("in_host_downtime")); // keep value from previous state!
+ state_hist_bag_new->Set("in_notification_period", (in_notification_period ? 1 : 0));
+ state_hist_bag_new->Set("notification_period", notification_period_name);
+ state_hist_bag_new->Set("is_flapping", state_hist_bag->Get("is_flapping")); // keep value from previous state!
+ state_hist_bag_new->Set("time", time);
+ state_hist_bag_new->Set("lineno", lineno);
+ state_hist_bag_new->Set("log_output", log_line); /* complete line */
+ state_hist_bag_new->Set("from", time); /* starting at current timestamp */
+ state_hist_bag_new->Set("until", time + 1); /* will be updated later */
+ state_hist_bag_new->Set("query_part", query_part);
+
+ state_hist_service_states->Add(state_hist_bag_new);
+
+ Log(LogDebug, "StateHistTable")
+ << "statehist: State change detected for object '" << checkable->GetName() << "' in '" << log_line << "'.";
+ }
+ break;
+ case LogEntryTypeHostFlapping:
+ case LogEntryTypeServiceFlapping:
+ if (state_type == "STARTED")
+ state_hist_bag->Set("is_flapping", 1);
+ else if (state_type == "STOPPED" || state_type == "DISABLED")
+ state_hist_bag->Set("is_flapping", 0);
+ break;
+ break;
+ case LogEntryTypeHostDowntimeAlert:
+ case LogEntryTypeServiceDowntimeAlert:
+ if (state_type == "STARTED") {
+ state_hist_bag->Set("in_downtime", 1);
+ if (log_type == LogEntryTypeHostDowntimeAlert)
+ state_hist_bag->Set("in_host_downtime", 1);
+ }
+ else if (state_type == "STOPPED" || state_type == "CANCELLED") {
+ state_hist_bag->Set("in_downtime", 0);
+ if (log_type == LogEntryTypeHostDowntimeAlert)
+ state_hist_bag->Set("in_host_downtime", 0);
+ }
+ break;
+ default:
+ //nothing to update
+ break;
+ }
+
+ }
+
+ m_CheckablesCache[checkable] = state_hist_service_states;
+
+ /* TODO find a way to directly call addRowFn() - right now m_ServicesCache depends on historical lines ("already seen service") */
+}
+
+void StateHistTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "time", Column(&StateHistTable::TimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "lineno", Column(&StateHistTable::LinenoAccessor, objectAccessor));
+ table->AddColumn(prefix + "from", Column(&StateHistTable::FromAccessor, objectAccessor));
+ table->AddColumn(prefix + "until", Column(&StateHistTable::UntilAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration", Column(&StateHistTable::DurationAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_part", Column(&StateHistTable::DurationPartAccessor, objectAccessor));
+ table->AddColumn(prefix + "state", Column(&StateHistTable::StateAccessor, objectAccessor));
+ table->AddColumn(prefix + "host_down", Column(&StateHistTable::HostDownAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_downtime", Column(&StateHistTable::InDowntimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_host_downtime", Column(&StateHistTable::InHostDowntimeAccessor, objectAccessor));
+ table->AddColumn(prefix + "is_flapping", Column(&StateHistTable::IsFlappingAccessor, objectAccessor));
+ table->AddColumn(prefix + "in_notification_period", Column(&StateHistTable::InNotificationPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "notification_period", Column(&StateHistTable::NotificationPeriodAccessor, objectAccessor));
+ table->AddColumn(prefix + "debug_info", Column(&Table::EmptyStringAccessor, objectAccessor));
+ table->AddColumn(prefix + "host_name", Column(&StateHistTable::HostNameAccessor, objectAccessor));
+ table->AddColumn(prefix + "service_description", Column(&StateHistTable::ServiceDescriptionAccessor, objectAccessor));
+ table->AddColumn(prefix + "log_output", Column(&StateHistTable::LogOutputAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_ok", Column(&StateHistTable::DurationOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_part_ok", Column(&StateHistTable::DurationPartOkAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_warning", Column(&StateHistTable::DurationWarningAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_part_warning", Column(&StateHistTable::DurationPartWarningAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_critical", Column(&StateHistTable::DurationCriticalAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_part_critical", Column(&StateHistTable::DurationPartCriticalAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_unknown", Column(&StateHistTable::DurationUnknownAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_part_unknown", Column(&StateHistTable::DurationPartUnknownAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_unmonitored", Column(&StateHistTable::DurationUnmonitoredAccessor, objectAccessor));
+ table->AddColumn(prefix + "duration_part_unmonitored", Column(&StateHistTable::DurationPartUnmonitoredAccessor, objectAccessor));
+
+ HostsTable::AddColumns(table, "current_host_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return HostAccessor(row, objectAccessor);
+ });
+ ServicesTable::AddColumns(table, "current_service_", [objectAccessor](const Value& row, LivestatusGroupByType, const Object::Ptr&) -> Value {
+ return ServiceAccessor(row, objectAccessor);
+ });
+}
+
+String StateHistTable::GetName() const
+{
+ return "log";
+}
+
+String StateHistTable::GetPrefix() const
+{
+ return "log";
+}
+
+void StateHistTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ Log(LogDebug, "StateHistTable")
+ << "Pre-selecting log file from " << m_TimeFrom << " until " << m_TimeUntil;
+
+ /* create log file index */
+ LivestatusLogUtility::CreateLogIndex(m_CompatLogPath, m_LogFileIndex);
+
+ /* generate log cache */
+ LivestatusLogUtility::CreateLogCache(m_LogFileIndex, this, m_TimeFrom, m_TimeUntil, addRowFn);
+
+ Checkable::Ptr checkable;
+
+ for (const auto& kv : m_CheckablesCache) {
+ for (const Dictionary::Ptr& state_hist_bag : kv.second) {
+ /* pass a dictionary from state history array */
+ if (!addRowFn(state_hist_bag, LivestatusGroupByNone, Empty))
+ return;
+ }
+ }
+}
+
+Object::Ptr StateHistTable::HostAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ String host_name = static_cast<Dictionary::Ptr>(row)->Get("host_name");
+
+ if (host_name.IsEmpty())
+ return nullptr;
+
+ return Host::GetByName(host_name);
+}
+
+Object::Ptr StateHistTable::ServiceAccessor(const Value& row, const Column::ObjectAccessor&)
+{
+ String host_name = static_cast<Dictionary::Ptr>(row)->Get("host_name");
+ String service_description = static_cast<Dictionary::Ptr>(row)->Get("service_description");
+
+ if (service_description.IsEmpty() || host_name.IsEmpty())
+ return nullptr;
+
+ return Service::GetByNamePair(host_name, service_description);
+}
+
+Value StateHistTable::TimeAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("time");
+}
+
+Value StateHistTable::LinenoAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("lineno");
+}
+
+Value StateHistTable::FromAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("from");
+}
+
+Value StateHistTable::UntilAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("until");
+}
+
+Value StateHistTable::DurationAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from"));
+}
+
+Value StateHistTable::DurationPartAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from")) / state_hist_bag->Get("query_part");
+}
+
+Value StateHistTable::StateAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("state");
+}
+
+Value StateHistTable::HostDownAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("host_down");
+}
+
+Value StateHistTable::InDowntimeAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("in_downtime");
+}
+
+Value StateHistTable::InHostDowntimeAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("in_host_downtime");
+}
+
+Value StateHistTable::IsFlappingAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("is_flapping");
+}
+
+Value StateHistTable::InNotificationPeriodAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("in_notification_period");
+}
+
+Value StateHistTable::NotificationPeriodAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("notification_period");
+}
+
+Value StateHistTable::HostNameAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("host_name");
+}
+
+Value StateHistTable::ServiceDescriptionAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("service_description");
+}
+
+Value StateHistTable::LogOutputAccessor(const Value& row)
+{
+ return static_cast<Dictionary::Ptr>(row)->Get("log_output");
+}
+
+Value StateHistTable::DurationOkAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == ServiceOK)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from"));
+
+ return 0;
+}
+
+Value StateHistTable::DurationPartOkAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == ServiceOK)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from")) / state_hist_bag->Get("query_part");
+
+ return 0;
+}
+
+Value StateHistTable::DurationWarningAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == ServiceWarning)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from"));
+
+ return 0;
+}
+
+Value StateHistTable::DurationPartWarningAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == ServiceWarning)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from")) / state_hist_bag->Get("query_part");
+
+ return 0;
+}
+
+Value StateHistTable::DurationCriticalAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == ServiceCritical)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from"));
+
+ return 0;
+}
+
+Value StateHistTable::DurationPartCriticalAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == ServiceCritical)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from")) / state_hist_bag->Get("query_part");
+
+ return 0;
+}
+
+Value StateHistTable::DurationUnknownAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == ServiceUnknown)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from"));
+
+ return 0;
+}
+
+Value StateHistTable::DurationPartUnknownAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == ServiceUnknown)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from")) / state_hist_bag->Get("query_part");
+
+ return 0;
+}
+
+Value StateHistTable::DurationUnmonitoredAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == -1)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from"));
+
+ return 0;
+}
+
+Value StateHistTable::DurationPartUnmonitoredAccessor(const Value& row)
+{
+ Dictionary::Ptr state_hist_bag = static_cast<Dictionary::Ptr>(row);
+
+ if (state_hist_bag->Get("state") == -1)
+ return (state_hist_bag->Get("until") - state_hist_bag->Get("from")) / state_hist_bag->Get("query_part");
+
+ return 0;
+}
diff --git a/lib/livestatus/statehisttable.hpp b/lib/livestatus/statehisttable.hpp
new file mode 100644
index 0000000..db00615
--- /dev/null
+++ b/lib/livestatus/statehisttable.hpp
@@ -0,0 +1,75 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STATEHISTTABLE_H
+#define STATEHISTTABLE_H
+
+#include "icinga/service.hpp"
+#include "livestatus/historytable.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class StateHistTable final : public HistoryTable
+{
+public:
+ DECLARE_PTR_TYPEDEFS(StateHistTable);
+
+ StateHistTable(const String& compat_log_path, time_t from, time_t until);
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+ void UpdateLogEntries(const Dictionary::Ptr& log_entry_attrs, int line_count, int lineno, const AddRowFunction& addRowFn) override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Object::Ptr HostAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+ static Object::Ptr ServiceAccessor(const Value& row, const Column::ObjectAccessor& parentObjectAccessor);
+
+ static Value TimeAccessor(const Value& row);
+ static Value LinenoAccessor(const Value& row);
+ static Value FromAccessor(const Value& row);
+ static Value UntilAccessor(const Value& row);
+ static Value DurationAccessor(const Value& row);
+ static Value DurationPartAccessor(const Value& row);
+ static Value StateAccessor(const Value& row);
+ static Value HostDownAccessor(const Value& row);
+ static Value InDowntimeAccessor(const Value& row);
+ static Value InHostDowntimeAccessor(const Value& row);
+ static Value IsFlappingAccessor(const Value& row);
+ static Value InNotificationPeriodAccessor(const Value& row);
+ static Value NotificationPeriodAccessor(const Value& row);
+ static Value HostNameAccessor(const Value& row);
+ static Value ServiceDescriptionAccessor(const Value& row);
+ static Value LogOutputAccessor(const Value& row);
+ static Value DurationOkAccessor(const Value& row);
+ static Value DurationPartOkAccessor(const Value& row);
+ static Value DurationWarningAccessor(const Value& row);
+ static Value DurationPartWarningAccessor(const Value& row);
+ static Value DurationCriticalAccessor(const Value& row);
+ static Value DurationPartCriticalAccessor(const Value& row);
+ static Value DurationUnknownAccessor(const Value& row);
+ static Value DurationPartUnknownAccessor(const Value& row);
+ static Value DurationUnmonitoredAccessor(const Value& row);
+ static Value DurationPartUnmonitoredAccessor(const Value& row);
+
+private:
+ std::map<time_t, String> m_LogFileIndex;
+ std::map<Checkable::Ptr, Array::Ptr> m_CheckablesCache;
+ time_t m_TimeFrom;
+ time_t m_TimeUntil;
+ String m_CompatLogPath;
+};
+
+}
+
+#endif /* STATEHISTTABLE_H */
diff --git a/lib/livestatus/statustable.cpp b/lib/livestatus/statustable.cpp
new file mode 100644
index 0000000..ae0a736
--- /dev/null
+++ b/lib/livestatus/statustable.cpp
@@ -0,0 +1,269 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/statustable.hpp"
+#include "livestatus/livestatuslistener.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/cib.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/application.hpp"
+
+using namespace icinga;
+
+StatusTable::StatusTable()
+{
+ AddColumns(this);
+}
+
+void StatusTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "neb_callbacks", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "neb_callbacks_rate", Column(&Table::ZeroAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "requests", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "requests_rate", Column(&Table::ZeroAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "connections", Column(&StatusTable::ConnectionsAccessor, objectAccessor));
+ table->AddColumn(prefix + "connections_rate", Column(&StatusTable::ConnectionsRateAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "service_checks", Column(&StatusTable::ServiceChecksAccessor, objectAccessor));
+ table->AddColumn(prefix + "service_checks_rate", Column(&StatusTable::ServiceChecksRateAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "host_checks", Column(&StatusTable::HostChecksAccessor, objectAccessor));
+ table->AddColumn(prefix + "host_checks_rate", Column(&StatusTable::HostChecksRateAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "forks", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "forks_rate", Column(&Table::ZeroAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "log_messages", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "log_messages_rate", Column(&Table::ZeroAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "external_commands", Column(&StatusTable::ExternalCommandsAccessor, objectAccessor));
+ table->AddColumn(prefix + "external_commands_rate", Column(&StatusTable::ExternalCommandsRateAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "livechecks", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "livechecks_rate", Column(&Table::ZeroAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "livecheck_overflows", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "livecheck_overflows_rate", Column(&Table::ZeroAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "nagios_pid", Column(&StatusTable::NagiosPidAccessor, objectAccessor));
+ table->AddColumn(prefix + "enable_notifications", Column(&StatusTable::EnableNotificationsAccessor, objectAccessor));
+ table->AddColumn(prefix + "execute_service_checks", Column(&StatusTable::ExecuteServiceChecksAccessor, objectAccessor));
+ table->AddColumn(prefix + "accept_passive_service_checks", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "execute_host_checks", Column(&StatusTable::ExecuteHostChecksAccessor, objectAccessor));
+ table->AddColumn(prefix + "accept_passive_host_checks", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "enable_event_handlers", Column(&StatusTable::EnableEventHandlersAccessor, objectAccessor));
+ table->AddColumn(prefix + "obsess_over_services", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "obsess_over_hosts", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_service_freshness", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_host_freshness", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "enable_flap_detection", Column(&StatusTable::EnableFlapDetectionAccessor, objectAccessor));
+ table->AddColumn(prefix + "process_performance_data", Column(&StatusTable::ProcessPerformanceDataAccessor, objectAccessor));
+ table->AddColumn(prefix + "check_external_commands", Column(&Table::OneAccessor, objectAccessor));
+ table->AddColumn(prefix + "program_start", Column(&StatusTable::ProgramStartAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_command_check", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "last_log_rotation", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "interval_length", Column(&StatusTable::IntervalLengthAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_hosts", Column(&StatusTable::NumHostsAccessor, objectAccessor));
+ table->AddColumn(prefix + "num_services", Column(&StatusTable::NumServicesAccessor, objectAccessor));
+ table->AddColumn(prefix + "program_version", Column(&StatusTable::ProgramVersionAccessor, objectAccessor));
+ table->AddColumn(prefix + "external_command_buffer_slots", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "external_command_buffer_usage", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "external_command_buffer_max", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "cached_log_messages", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "livestatus_version", Column(&StatusTable::LivestatusVersionAccessor, objectAccessor));
+ table->AddColumn(prefix + "livestatus_active_connections", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "livestatus_queued_connections", Column(&Table::ZeroAccessor, objectAccessor));
+ table->AddColumn(prefix + "livestatus_threads", Column(&Table::ZeroAccessor, objectAccessor));
+
+ table->AddColumn(prefix + "custom_variable_names", Column(&StatusTable::CustomVariableNamesAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variable_values", Column(&StatusTable::CustomVariableValuesAccessor, objectAccessor));
+ table->AddColumn(prefix + "custom_variables", Column(&StatusTable::CustomVariablesAccessor, objectAccessor));
+}
+
+String StatusTable::GetName() const
+{
+ return "status";
+}
+
+String StatusTable::GetPrefix() const
+{
+ return "status";
+}
+
+void StatusTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ Object::Ptr obj = new Object();
+
+ /* Return a fake row. */
+ addRowFn(obj, LivestatusGroupByNone, Empty);
+}
+
+Value StatusTable::ConnectionsAccessor(const Value&)
+{
+ return LivestatusListener::GetConnections();
+}
+
+Value StatusTable::ConnectionsRateAccessor(const Value&)
+{
+ return (LivestatusListener::GetConnections() / (Utility::GetTime() - Application::GetStartTime()));
+}
+
+Value StatusTable::HostChecksAccessor(const Value&)
+{
+ auto timespan = static_cast<long>(Utility::GetTime() - Application::GetStartTime());
+ return CIB::GetActiveHostChecksStatistics(timespan);
+}
+
+Value StatusTable::HostChecksRateAccessor(const Value&)
+{
+ auto timespan = static_cast<long>(Utility::GetTime() - Application::GetStartTime());
+ return (CIB::GetActiveHostChecksStatistics(timespan) / (Utility::GetTime() - Application::GetStartTime()));
+}
+
+Value StatusTable::ServiceChecksAccessor(const Value&)
+{
+ auto timespan = static_cast<long>(Utility::GetTime() - Application::GetStartTime());
+ return CIB::GetActiveServiceChecksStatistics(timespan);
+}
+
+Value StatusTable::ServiceChecksRateAccessor(const Value&)
+{
+ auto timespan = static_cast<long>(Utility::GetTime() - Application::GetStartTime());
+ return (CIB::GetActiveServiceChecksStatistics(timespan) / (Utility::GetTime() - Application::GetStartTime()));
+}
+
+Value StatusTable::ExternalCommandsAccessor(const Value&)
+{
+ return LivestatusQuery::GetExternalCommands();
+}
+
+Value StatusTable::ExternalCommandsRateAccessor(const Value&)
+{
+ return (LivestatusQuery::GetExternalCommands() / (Utility::GetTime() - Application::GetStartTime()));
+}
+
+Value StatusTable::NagiosPidAccessor(const Value&)
+{
+ return Utility::GetPid();
+}
+
+Value StatusTable::EnableNotificationsAccessor(const Value&)
+{
+ return (IcingaApplication::GetInstance()->GetEnableNotifications() ? 1 : 0);
+}
+
+Value StatusTable::ExecuteServiceChecksAccessor(const Value&)
+{
+ return (IcingaApplication::GetInstance()->GetEnableServiceChecks() ? 1 : 0);
+}
+
+Value StatusTable::ExecuteHostChecksAccessor(const Value&)
+{
+ return (IcingaApplication::GetInstance()->GetEnableHostChecks() ? 1 : 0);
+}
+
+Value StatusTable::EnableEventHandlersAccessor(const Value&)
+{
+ return (IcingaApplication::GetInstance()->GetEnableEventHandlers() ? 1 : 0);
+}
+
+Value StatusTable::EnableFlapDetectionAccessor(const Value&)
+{
+ return (IcingaApplication::GetInstance()->GetEnableFlapping() ? 1 : 0);
+}
+
+Value StatusTable::ProcessPerformanceDataAccessor(const Value&)
+{
+ return (IcingaApplication::GetInstance()->GetEnablePerfdata() ? 1 : 0);
+}
+
+Value StatusTable::ProgramStartAccessor(const Value&)
+{
+ return static_cast<long>(Application::GetStartTime());
+}
+
+Value StatusTable::IntervalLengthAccessor(const Value&)
+{
+ return LIVESTATUS_INTERVAL_LENGTH;
+}
+
+Value StatusTable::NumHostsAccessor(const Value&)
+{
+ return ConfigType::Get<Host>()->GetObjectCount();
+}
+
+Value StatusTable::NumServicesAccessor(const Value&)
+{
+ return ConfigType::Get<Service>()->GetObjectCount();
+}
+
+Value StatusTable::ProgramVersionAccessor(const Value&)
+{
+ return Application::GetAppVersion() + "-icinga2";
+}
+
+Value StatusTable::LivestatusVersionAccessor(const Value&)
+{
+ return Application::GetAppVersion();
+}
+
+Value StatusTable::LivestatusActiveConnectionsAccessor(const Value&)
+{
+ return LivestatusListener::GetClientsConnected();
+}
+
+Value StatusTable::CustomVariableNamesAccessor(const Value&)
+{
+ Dictionary::Ptr vars = IcingaApplication::GetInstance()->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const auto& kv : vars) {
+ result.push_back(kv.first);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value StatusTable::CustomVariableValuesAccessor(const Value&)
+{
+ Dictionary::Ptr vars = IcingaApplication::GetInstance()->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const auto& kv : vars) {
+ result.push_back(kv.second);
+ }
+ }
+
+ return new Array(std::move(result));
+}
+
+Value StatusTable::CustomVariablesAccessor(const Value&)
+{
+ Dictionary::Ptr vars = IcingaApplication::GetInstance()->GetVars();
+
+ ArrayData result;
+
+ if (vars) {
+ ObjectLock olock(vars);
+ for (const auto& kv : vars) {
+ result.push_back(new Array({
+ kv.first,
+ kv.second
+ }));
+ }
+ }
+
+ return new Array(std::move(result));
+}
diff --git a/lib/livestatus/statustable.hpp b/lib/livestatus/statustable.hpp
new file mode 100644
index 0000000..2fba249
--- /dev/null
+++ b/lib/livestatus/statustable.hpp
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STATUSTABLE_H
+#define STATUSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class StatusTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(StatusTable);
+
+ StatusTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value ConnectionsAccessor(const Value& row);
+ static Value ConnectionsRateAccessor(const Value& row);
+ static Value ServiceChecksAccessor(const Value& row);
+ static Value ServiceChecksRateAccessor(const Value& row);
+ static Value HostChecksAccessor(const Value& row);
+ static Value HostChecksRateAccessor(const Value& row);
+ static Value ExternalCommandsAccessor(const Value& row);
+ static Value ExternalCommandsRateAccessor(const Value& row);
+ static Value NagiosPidAccessor(const Value& row);
+ static Value EnableNotificationsAccessor(const Value& row);
+ static Value ExecuteServiceChecksAccessor(const Value& row);
+ static Value ExecuteHostChecksAccessor(const Value& row);
+ static Value EnableEventHandlersAccessor(const Value& row);
+ static Value EnableFlapDetectionAccessor(const Value& row);
+ static Value ProcessPerformanceDataAccessor(const Value& row);
+ static Value ProgramStartAccessor(const Value& row);
+ static Value IntervalLengthAccessor(const Value& row);
+ static Value NumHostsAccessor(const Value& row);
+ static Value NumServicesAccessor(const Value& row);
+ static Value ProgramVersionAccessor(const Value& row);
+ static Value LivestatusVersionAccessor(const Value& row);
+ static Value LivestatusActiveConnectionsAccessor(const Value& row);
+ static Value CustomVariableNamesAccessor(const Value& row);
+ static Value CustomVariableValuesAccessor(const Value& row);
+ static Value CustomVariablesAccessor(const Value& row);
+};
+
+}
+
+#endif /* STATUSTABLE_H */
diff --git a/lib/livestatus/stdaggregator.cpp b/lib/livestatus/stdaggregator.cpp
new file mode 100644
index 0000000..99c3a8e
--- /dev/null
+++ b/lib/livestatus/stdaggregator.cpp
@@ -0,0 +1,40 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/stdaggregator.hpp"
+#include <math.h>
+
+using namespace icinga;
+
+StdAggregator::StdAggregator(String attr)
+ : m_StdAttr(std::move(attr))
+{ }
+
+StdAggregatorState *StdAggregator::EnsureState(AggregatorState **state)
+{
+ if (!*state)
+ *state = new StdAggregatorState();
+
+ return static_cast<StdAggregatorState *>(*state);
+}
+
+void StdAggregator::Apply(const Table::Ptr& table, const Value& row, AggregatorState **state)
+{
+ Column column = table->GetColumn(m_StdAttr);
+
+ Value value = column.ExtractValue(row);
+
+ StdAggregatorState *pstate = EnsureState(state);
+
+ pstate->StdSum += value;
+ pstate->StdQSum += pow(value, 2);
+ pstate->StdCount++;
+}
+
+double StdAggregator::GetResultAndFreeState(AggregatorState *state) const
+{
+ StdAggregatorState *pstate = EnsureState(&state);
+ double result = sqrt((pstate->StdQSum - (1 / pstate->StdCount) * pow(pstate->StdSum, 2)) / (pstate->StdCount - 1));
+ delete pstate;
+
+ return result;
+}
diff --git a/lib/livestatus/stdaggregator.hpp b/lib/livestatus/stdaggregator.hpp
new file mode 100644
index 0000000..3680fe7
--- /dev/null
+++ b/lib/livestatus/stdaggregator.hpp
@@ -0,0 +1,43 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STDAGGREGATOR_H
+#define STDAGGREGATOR_H
+
+#include "livestatus/table.hpp"
+#include "livestatus/aggregator.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct StdAggregatorState final : public AggregatorState
+{
+ double StdSum{0};
+ double StdQSum{0};
+ double StdCount{0};
+};
+
+/**
+ * @ingroup livestatus
+ */
+class StdAggregator final : public Aggregator
+{
+public:
+ DECLARE_PTR_TYPEDEFS(StdAggregator);
+
+ StdAggregator(String attr);
+
+ void Apply(const Table::Ptr& table, const Value& row, AggregatorState **state) override;
+ double GetResultAndFreeState(AggregatorState *state) const override;
+
+private:
+ String m_StdAttr;
+
+ static StdAggregatorState *EnsureState(AggregatorState **state);
+};
+
+}
+
+#endif /* STDAGGREGATOR_H */
diff --git a/lib/livestatus/sumaggregator.cpp b/lib/livestatus/sumaggregator.cpp
new file mode 100644
index 0000000..fc4b62e
--- /dev/null
+++ b/lib/livestatus/sumaggregator.cpp
@@ -0,0 +1,37 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/sumaggregator.hpp"
+
+using namespace icinga;
+
+SumAggregator::SumAggregator(String attr)
+ : m_SumAttr(std::move(attr))
+{ }
+
+SumAggregatorState *SumAggregator::EnsureState(AggregatorState **state)
+{
+ if (!*state)
+ *state = new SumAggregatorState();
+
+ return static_cast<SumAggregatorState *>(*state);
+}
+
+void SumAggregator::Apply(const Table::Ptr& table, const Value& row, AggregatorState **state)
+{
+ Column column = table->GetColumn(m_SumAttr);
+
+ Value value = column.ExtractValue(row);
+
+ SumAggregatorState *pstate = EnsureState(state);
+
+ pstate->Sum += value;
+}
+
+double SumAggregator::GetResultAndFreeState(AggregatorState *state) const
+{
+ SumAggregatorState *pstate = EnsureState(&state);
+ double result = pstate->Sum;
+ delete pstate;
+
+ return result;
+}
diff --git a/lib/livestatus/sumaggregator.hpp b/lib/livestatus/sumaggregator.hpp
new file mode 100644
index 0000000..23f22fb
--- /dev/null
+++ b/lib/livestatus/sumaggregator.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SUMAGGREGATOR_H
+#define SUMAGGREGATOR_H
+
+#include "livestatus/table.hpp"
+#include "livestatus/aggregator.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+struct SumAggregatorState final : public AggregatorState
+{
+ double Sum{0};
+};
+
+/**
+ * @ingroup livestatus
+ */
+class SumAggregator final : public Aggregator
+{
+public:
+ DECLARE_PTR_TYPEDEFS(SumAggregator);
+
+ SumAggregator(String attr);
+
+ void Apply(const Table::Ptr& table, const Value& row, AggregatorState **state) override;
+ double GetResultAndFreeState(AggregatorState *state) const override;
+
+private:
+ String m_SumAttr;
+
+ static SumAggregatorState *EnsureState(AggregatorState **state);
+};
+
+}
+
+#endif /* SUMAGGREGATOR_H */
diff --git a/lib/livestatus/table.cpp b/lib/livestatus/table.cpp
new file mode 100644
index 0000000..8e5d85a
--- /dev/null
+++ b/lib/livestatus/table.cpp
@@ -0,0 +1,165 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/table.hpp"
+#include "livestatus/statustable.hpp"
+#include "livestatus/contactgroupstable.hpp"
+#include "livestatus/contactstable.hpp"
+#include "livestatus/hostgroupstable.hpp"
+#include "livestatus/hoststable.hpp"
+#include "livestatus/servicegroupstable.hpp"
+#include "livestatus/servicestable.hpp"
+#include "livestatus/commandstable.hpp"
+#include "livestatus/commentstable.hpp"
+#include "livestatus/downtimestable.hpp"
+#include "livestatus/endpointstable.hpp"
+#include "livestatus/zonestable.hpp"
+#include "livestatus/timeperiodstable.hpp"
+#include "livestatus/logtable.hpp"
+#include "livestatus/statehisttable.hpp"
+#include "livestatus/filter.hpp"
+#include "base/array.hpp"
+#include "base/dictionary.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+
+using namespace icinga;
+
+Table::Table(LivestatusGroupByType type)
+ : m_GroupByType(type), m_GroupByObject(Empty)
+{ }
+
+Table::Ptr Table::GetByName(const String& name, const String& compat_log_path, const unsigned long& from, const unsigned long& until)
+{
+ if (name == "status")
+ return new StatusTable();
+ else if (name == "contactgroups")
+ return new ContactGroupsTable();
+ else if (name == "contacts")
+ return new ContactsTable();
+ else if (name == "hostgroups")
+ return new HostGroupsTable();
+ else if (name == "hosts")
+ return new HostsTable();
+ else if (name == "hostsbygroup")
+ return new HostsTable(LivestatusGroupByHostGroup);
+ else if (name == "servicegroups")
+ return new ServiceGroupsTable();
+ else if (name == "services")
+ return new ServicesTable();
+ else if (name == "servicesbygroup")
+ return new ServicesTable(LivestatusGroupByServiceGroup);
+ else if (name == "servicesbyhostgroup")
+ return new ServicesTable(LivestatusGroupByHostGroup);
+ else if (name == "commands")
+ return new CommandsTable();
+ else if (name == "comments")
+ return new CommentsTable();
+ else if (name == "downtimes")
+ return new DowntimesTable();
+ else if (name == "timeperiods")
+ return new TimePeriodsTable();
+ else if (name == "log")
+ return new LogTable(compat_log_path, from, until);
+ else if (name == "statehist")
+ return new StateHistTable(compat_log_path, from, until);
+ else if (name == "endpoints")
+ return new EndpointsTable();
+ else if (name == "zones")
+ return new ZonesTable();
+
+ return nullptr;
+}
+
+void Table::AddColumn(const String& name, const Column& column)
+{
+ std::pair<String, Column> item = std::make_pair(name, column);
+
+ auto ret = m_Columns.insert(item);
+
+ if (!ret.second)
+ ret.first->second = column;
+}
+
+Column Table::GetColumn(const String& name) const
+{
+ String dname = name;
+ String prefix = GetPrefix() + "_";
+
+ if (dname.Find(prefix) == 0)
+ dname = dname.SubStr(prefix.GetLength());
+
+ auto it = m_Columns.find(dname);
+
+ if (it == m_Columns.end())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Column '" + dname + "' does not exist in table '" + GetName() + "'."));
+
+ return it->second;
+}
+
+std::vector<String> Table::GetColumnNames() const
+{
+ std::vector<String> names;
+
+ for (const auto& kv : m_Columns) {
+ names.push_back(kv.first);
+ }
+
+ return names;
+}
+
+std::vector<LivestatusRowValue> Table::FilterRows(const Filter::Ptr& filter, int limit)
+{
+ std::vector<LivestatusRowValue> rs;
+
+ FetchRows([this, filter, limit, &rs](const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject) {
+ return FilteredAddRow(rs, filter, limit, row, groupByType, groupByObject);
+ });
+
+ return rs;
+}
+
+bool Table::FilteredAddRow(std::vector<LivestatusRowValue>& rs, const Filter::Ptr& filter, int limit, const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject)
+{
+ if (limit != -1 && static_cast<int>(rs.size()) == limit)
+ return false;
+
+ if (!filter || filter->Apply(this, row)) {
+ LivestatusRowValue rval;
+ rval.Row = row;
+ rval.GroupByType = groupByType;
+ rval.GroupByObject = groupByObject;
+
+ rs.emplace_back(std::move(rval));
+ }
+
+ return true;
+}
+
+Value Table::ZeroAccessor(const Value&)
+{
+ return 0;
+}
+
+Value Table::OneAccessor(const Value&)
+{
+ return 1;
+}
+
+Value Table::EmptyStringAccessor(const Value&)
+{
+ return "";
+}
+
+Value Table::EmptyArrayAccessor(const Value&)
+{
+ return new Array();
+}
+
+Value Table::EmptyDictionaryAccessor(const Value&)
+{
+ return new Dictionary();
+}
+
+LivestatusGroupByType Table::GetGroupByType() const
+{
+ return m_GroupByType;
+}
diff --git a/lib/livestatus/table.hpp b/lib/livestatus/table.hpp
new file mode 100644
index 0000000..fa3fc2a
--- /dev/null
+++ b/lib/livestatus/table.hpp
@@ -0,0 +1,73 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TABLE_H
+#define TABLE_H
+
+#include "livestatus/column.hpp"
+#include "base/object.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+// Well, don't ask.
+#define LIVESTATUS_INTERVAL_LENGTH 60.0
+
+struct LivestatusRowValue {
+ Value Row;
+ LivestatusGroupByType GroupByType;
+ Value GroupByObject;
+};
+
+typedef std::function<bool (const Value&, LivestatusGroupByType, const Object::Ptr&)> AddRowFunction;
+
+class Filter;
+
+/**
+ * @ingroup livestatus
+ */
+class Table : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Table);
+
+ static Table::Ptr GetByName(const String& name, const String& compat_log_path = "", const unsigned long& from = 0, const unsigned long& until = 0);
+
+ virtual String GetName() const = 0;
+ virtual String GetPrefix() const = 0;
+
+ std::vector<LivestatusRowValue> FilterRows(const intrusive_ptr<Filter>& filter, int limit = -1);
+
+ void AddColumn(const String& name, const Column& column);
+ Column GetColumn(const String& name) const;
+ std::vector<String> GetColumnNames() const;
+
+ LivestatusGroupByType GetGroupByType() const;
+
+protected:
+ Table(LivestatusGroupByType type = LivestatusGroupByNone);
+
+ virtual void FetchRows(const AddRowFunction& addRowFn) = 0;
+
+ static Value ZeroAccessor(const Value&);
+ static Value OneAccessor(const Value&);
+ static Value EmptyStringAccessor(const Value&);
+ static Value EmptyArrayAccessor(const Value&);
+ static Value EmptyDictionaryAccessor(const Value&);
+
+ LivestatusGroupByType m_GroupByType;
+ Value m_GroupByObject;
+
+private:
+ std::map<String, Column> m_Columns;
+
+ bool FilteredAddRow(std::vector<LivestatusRowValue>& rs, const intrusive_ptr<Filter>& filter, int limit, const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject);
+};
+
+}
+
+#endif /* TABLE_H */
+
+#include "livestatus/filter.hpp"
diff --git a/lib/livestatus/timeperiodstable.cpp b/lib/livestatus/timeperiodstable.cpp
new file mode 100644
index 0000000..5797d93
--- /dev/null
+++ b/lib/livestatus/timeperiodstable.cpp
@@ -0,0 +1,58 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/timeperiodstable.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/timeperiod.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/replace.hpp>
+
+using namespace icinga;
+
+TimePeriodsTable::TimePeriodsTable()
+{
+ AddColumns(this);
+}
+
+void TimePeriodsTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&TimePeriodsTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "alias", Column(&TimePeriodsTable::AliasAccessor, objectAccessor));
+ table->AddColumn(prefix + "in", Column(&TimePeriodsTable::InAccessor, objectAccessor));
+}
+
+String TimePeriodsTable::GetName() const
+{
+ return "timeperiod";
+}
+
+String TimePeriodsTable::GetPrefix() const
+{
+ return "timeperiod";
+}
+
+void TimePeriodsTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const TimePeriod::Ptr& tp : ConfigType::GetObjectsByType<TimePeriod>()) {
+ if (!addRowFn(tp, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Value TimePeriodsTable::NameAccessor(const Value& row)
+{
+ return static_cast<TimePeriod::Ptr>(row)->GetName();
+}
+
+Value TimePeriodsTable::AliasAccessor(const Value& row)
+{
+ return static_cast<TimePeriod::Ptr>(row)->GetDisplayName();
+}
+
+Value TimePeriodsTable::InAccessor(const Value& row)
+{
+ return (static_cast<TimePeriod::Ptr>(row)->IsInside(Utility::GetTime()) ? 1 : 0);
+}
diff --git a/lib/livestatus/timeperiodstable.hpp b/lib/livestatus/timeperiodstable.hpp
new file mode 100644
index 0000000..31cef93
--- /dev/null
+++ b/lib/livestatus/timeperiodstable.hpp
@@ -0,0 +1,39 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TIMEPERIODSTABLE_H
+#define TIMEPERIODSTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class TimePeriodsTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TimePeriodsTable);
+
+ TimePeriodsTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value NameAccessor(const Value& row);
+ static Value AliasAccessor(const Value& row);
+ static Value InAccessor(const Value& row);
+};
+
+}
+
+#endif /* TIMEPERIODSTABLE_H */
diff --git a/lib/livestatus/zonestable.cpp b/lib/livestatus/zonestable.cpp
new file mode 100644
index 0000000..d86cc72
--- /dev/null
+++ b/lib/livestatus/zonestable.cpp
@@ -0,0 +1,92 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "livestatus/zonestable.hpp"
+#include "remote/zone.hpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+ZonesTable::ZonesTable()
+{
+ AddColumns(this);
+}
+
+void ZonesTable::AddColumns(Table *table, const String& prefix,
+ const Column::ObjectAccessor& objectAccessor)
+{
+ table->AddColumn(prefix + "name", Column(&ZonesTable::NameAccessor, objectAccessor));
+ table->AddColumn(prefix + "parent", Column(&ZonesTable::ParentAccessor, objectAccessor));
+ table->AddColumn(prefix + "endpoints", Column(&ZonesTable::EndpointsAccessor, objectAccessor));
+ table->AddColumn(prefix + "global", Column(&ZonesTable::GlobalAccessor, objectAccessor));
+}
+
+String ZonesTable::GetName() const
+{
+ return "zones";
+}
+
+String ZonesTable::GetPrefix() const
+{
+ return "zone";
+}
+
+void ZonesTable::FetchRows(const AddRowFunction& addRowFn)
+{
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ if (!addRowFn(zone, LivestatusGroupByNone, Empty))
+ return;
+ }
+}
+
+Value ZonesTable::NameAccessor(const Value& row)
+{
+ Zone::Ptr zone = static_cast<Zone::Ptr>(row);
+
+ if (!zone)
+ return Empty;
+
+ return zone->GetName();
+}
+
+Value ZonesTable::ParentAccessor(const Value& row)
+{
+ Zone::Ptr zone = static_cast<Zone::Ptr>(row);
+
+ if (!zone)
+ return Empty;
+
+ Zone::Ptr parent_zone = zone->GetParent();
+
+ if (!parent_zone)
+ return Empty;
+
+ return parent_zone->GetName();
+}
+
+Value ZonesTable::EndpointsAccessor(const Value& row)
+{
+ Zone::Ptr zone = static_cast<Zone::Ptr>(row);
+
+ if (!zone)
+ return Empty;
+
+ std::set<Endpoint::Ptr> endpoints = zone->GetEndpoints();
+
+ ArrayData result;
+
+ for (const Endpoint::Ptr& endpoint : endpoints) {
+ result.push_back(endpoint->GetName());
+ }
+
+ return new Array(std::move(result));
+}
+
+Value ZonesTable::GlobalAccessor(const Value& row)
+{
+ Zone::Ptr zone = static_cast<Zone::Ptr>(row);
+
+ if (!zone)
+ return Empty;
+
+ return zone->GetGlobal() ? 1 : 0;
+}
diff --git a/lib/livestatus/zonestable.hpp b/lib/livestatus/zonestable.hpp
new file mode 100644
index 0000000..fb03488
--- /dev/null
+++ b/lib/livestatus/zonestable.hpp
@@ -0,0 +1,40 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ZONESTABLE_H
+#define ZONESTABLE_H
+
+#include "livestatus/table.hpp"
+
+using namespace icinga;
+
+namespace icinga
+{
+
+/**
+ * @ingroup livestatus
+ */
+class ZonesTable final : public Table
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ZonesTable);
+
+ ZonesTable();
+
+ static void AddColumns(Table *table, const String& prefix = String(),
+ const Column::ObjectAccessor& objectAccessor = Column::ObjectAccessor());
+
+ String GetName() const override;
+ String GetPrefix() const override;
+
+protected:
+ void FetchRows(const AddRowFunction& addRowFn) override;
+
+ static Value NameAccessor(const Value& row);
+ static Value ParentAccessor(const Value& row);
+ static Value EndpointsAccessor(const Value& row);
+ static Value GlobalAccessor(const Value& row);
+};
+
+}
+
+#endif /* ZONESTABLE_H */
diff --git a/lib/methods/CMakeLists.txt b/lib/methods/CMakeLists.txt
new file mode 100644
index 0000000..a7c3090
--- /dev/null
+++ b/lib/methods/CMakeLists.txt
@@ -0,0 +1,34 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkembedconfig_target(methods-itl.conf methods-itl.cpp)
+
+set(methods_SOURCES
+ i2-methods.hpp methods-itl.cpp
+ clusterchecktask.cpp clusterchecktask.hpp
+ clusterzonechecktask.cpp clusterzonechecktask.hpp
+ dummychecktask.cpp dummychecktask.hpp
+ exceptionchecktask.cpp exceptionchecktask.hpp
+ icingachecktask.cpp icingachecktask.hpp
+ ifwapichecktask.cpp ifwapichecktask.hpp
+ nullchecktask.cpp nullchecktask.hpp
+ nulleventtask.cpp nulleventtask.hpp
+ pluginchecktask.cpp pluginchecktask.hpp
+ plugineventtask.cpp plugineventtask.hpp
+ pluginnotificationtask.cpp pluginnotificationtask.hpp
+ randomchecktask.cpp randomchecktask.hpp
+ timeperiodtask.cpp timeperiodtask.hpp
+ sleepchecktask.cpp sleepchecktask.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(methods methods methods_SOURCES)
+endif()
+
+add_library(methods OBJECT ${methods_SOURCES})
+
+add_dependencies(methods base config icinga)
+
+set_target_properties (
+ methods PROPERTIES
+ FOLDER Lib
+)
diff --git a/lib/methods/clusterchecktask.cpp b/lib/methods/clusterchecktask.cpp
new file mode 100644
index 0000000..6ce28ca
--- /dev/null
+++ b/lib/methods/clusterchecktask.cpp
@@ -0,0 +1,117 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/clusterchecktask.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/endpoint.hpp"
+#include "icinga/cib.hpp"
+#include "icinga/service.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/checkcommand.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/function.hpp"
+#include "base/configtype.hpp"
+#include <boost/algorithm/string/join.hpp>
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, ClusterCheck, &ClusterCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void ClusterCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+ String commandName = command->GetName();
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+ if (!listener) {
+ String output = "No API listener is configured for this instance.";
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = 126;
+ pr.Output = output;
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ cr->SetOutput(output);
+ cr->SetState(ServiceUnknown);
+ checkable->ProcessCheckResult(cr);
+ }
+
+ return;
+ }
+
+ std::pair<Dictionary::Ptr, Dictionary::Ptr> stats = listener->GetStatus();
+ Dictionary::Ptr status = stats.first;
+ int numConnEndpoints = status->Get("num_conn_endpoints");
+ int numNotConnEndpoints = status->Get("num_not_conn_endpoints");
+
+ ServiceState state;
+ String output = "Icinga 2 Cluster";
+
+ if (numNotConnEndpoints > 0) {
+ output += " Problem: " + Convert::ToString(numNotConnEndpoints) + " endpoints are not connected.";
+ output += "\n(" + FormatArray(status->Get("not_conn_endpoints")) + ")";
+
+ state = ServiceCritical;
+ } else {
+ output += " OK: " + Convert::ToString(numConnEndpoints) + " endpoints are connected.";
+ output += "\n(" + FormatArray(status->Get("conn_endpoints")) + ")";
+
+ state = ServiceOK;
+ }
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ /* use feature stats perfdata */
+ std::pair<Dictionary::Ptr, Array::Ptr> feature_stats = CIB::GetFeatureStats();
+ cr->SetPerformanceData(feature_stats.second);
+
+ cr->SetCommand(commandName);
+ cr->SetState(state);
+ cr->SetOutput(output);
+
+ checkable->ProcessCheckResult(cr);
+ }
+}
+
+String ClusterCheckTask::FormatArray(const Array::Ptr& arr)
+{
+ bool first = true;
+ String str;
+
+ if (arr) {
+ ObjectLock olock(arr);
+ for (const Value& value : arr) {
+ if (first)
+ first = false;
+ else
+ str += ", ";
+
+ str += Convert::ToString(value);
+ }
+ }
+
+ return str;
+}
diff --git a/lib/methods/clusterchecktask.hpp b/lib/methods/clusterchecktask.hpp
new file mode 100644
index 0000000..16ee8a5
--- /dev/null
+++ b/lib/methods/clusterchecktask.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CLUSTERCHECKTASK_H
+#define CLUSTERCHECKTASK_H
+
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+/**
+ * Cluster check type.
+ *
+ * @ingroup methods
+ */
+class ClusterCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ ClusterCheckTask();
+ static String FormatArray(const Array::Ptr& arr);
+};
+
+}
+
+#endif /* CLUSTERCHECKTASK_H */
diff --git a/lib/methods/clusterzonechecktask.cpp b/lib/methods/clusterzonechecktask.cpp
new file mode 100644
index 0000000..fd52534
--- /dev/null
+++ b/lib/methods/clusterzonechecktask.cpp
@@ -0,0 +1,218 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/clusterzonechecktask.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/zone.hpp"
+#include "base/function.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, ClusterZoneCheck, &ClusterZoneCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void ClusterZoneCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+ CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+ String commandName = command->GetName();
+
+ if (!listener) {
+ String output = "No API listener is configured for this instance.";
+ ServiceState state = ServiceUnknown;
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ cr->SetCommand(commandName);
+ cr->SetOutput(output);
+ cr->SetState(state);
+ checkable->ProcessCheckResult(cr);
+ }
+
+ return;
+ }
+
+ Value raw_command = command->GetCommandLine();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", command);
+
+ String zoneName = MacroProcessor::ResolveMacros("$cluster_zone$", resolvers, checkable->GetLastCheckResult(),
+ nullptr, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ String missingLagWarning;
+ String missingLagCritical;
+
+ double lagWarning = MacroProcessor::ResolveMacros("$cluster_lag_warning$", resolvers, checkable->GetLastCheckResult(),
+ &missingLagWarning, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ double lagCritical = MacroProcessor::ResolveMacros("$cluster_lag_critical$", resolvers, checkable->GetLastCheckResult(),
+ &missingLagCritical, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ if (zoneName.IsEmpty()) {
+ String output = "Macro 'cluster_zone' must be set.";
+ ServiceState state = ServiceUnknown;
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ cr->SetCommand(commandName);
+ cr->SetOutput(output);
+ cr->SetState(state);
+ checkable->ProcessCheckResult(cr);
+ }
+
+ return;
+ }
+
+ Zone::Ptr zone = Zone::GetByName(zoneName);
+
+ if (!zone) {
+ String output = "Zone '" + zoneName + "' does not exist.";
+ ServiceState state = ServiceUnknown;
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ cr->SetCommand(commandName);
+ cr->SetOutput(output);
+ cr->SetState(state);
+ checkable->ProcessCheckResult(cr);
+ }
+ return;
+ }
+
+ bool connected = false;
+ double zoneLag = 0;
+
+ double lastMessageSent = 0;
+ double lastMessageReceived = 0;
+ double messagesSentPerSecond = 0;
+ double messagesReceivedPerSecond = 0;
+ double bytesSentPerSecond = 0;
+ double bytesReceivedPerSecond = 0;
+
+ {
+ auto endpoints (zone->GetEndpoints());
+
+ for (const Endpoint::Ptr& endpoint : endpoints) {
+ if (endpoint->GetConnected())
+ connected = true;
+
+ double eplag = ApiListener::CalculateZoneLag(endpoint);
+
+ if (eplag > 0 && eplag > zoneLag)
+ zoneLag = eplag;
+
+ if (endpoint->GetLastMessageSent() > lastMessageSent)
+ lastMessageSent = endpoint->GetLastMessageSent();
+
+ if (endpoint->GetLastMessageReceived() > lastMessageReceived)
+ lastMessageReceived = endpoint->GetLastMessageReceived();
+
+ messagesSentPerSecond += endpoint->GetMessagesSentPerSecond();
+ messagesReceivedPerSecond += endpoint->GetMessagesReceivedPerSecond();
+ bytesSentPerSecond += endpoint->GetBytesSentPerSecond();
+ bytesReceivedPerSecond += endpoint->GetBytesReceivedPerSecond();
+ }
+
+ if (!connected && endpoints.size() == 1u && *endpoints.begin() == Endpoint::GetLocalEndpoint()) {
+ connected = true;
+ }
+ }
+
+ ServiceState state;
+ String output;
+
+ if (connected) {
+ state = ServiceOK;
+ output = "Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag);
+
+ /* Check whether the thresholds have been resolved and compare them */
+ if (missingLagCritical.IsEmpty() && zoneLag > lagCritical) {
+ state = ServiceCritical;
+ output = "Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag)
+ + " greater than critical threshold: " + Utility::FormatDuration(lagCritical);
+ } else if (missingLagWarning.IsEmpty() && zoneLag > lagWarning) {
+ state = ServiceWarning;
+ output = "Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag)
+ + " greater than warning threshold: " + Utility::FormatDuration(lagWarning);
+ }
+ } else {
+ state = ServiceCritical;
+ output = "Zone '" + zoneName + "' is not connected. Log lag: " + Utility::FormatDuration(zoneLag);
+ }
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ cr->SetCommand(commandName);
+ cr->SetState(state);
+ cr->SetOutput(output);
+ cr->SetPerformanceData(new Array({
+ new PerfdataValue("slave_lag", zoneLag, false, "s", lagWarning, lagCritical),
+ new PerfdataValue("last_messages_sent", lastMessageSent),
+ new PerfdataValue("last_messages_received", lastMessageReceived),
+ new PerfdataValue("sum_messages_sent_per_second", messagesSentPerSecond),
+ new PerfdataValue("sum_messages_received_per_second", messagesReceivedPerSecond),
+ new PerfdataValue("sum_bytes_sent_per_second", bytesSentPerSecond),
+ new PerfdataValue("sum_bytes_received_per_second", bytesReceivedPerSecond)
+ }));
+
+ checkable->ProcessCheckResult(cr);
+ }
+}
diff --git a/lib/methods/clusterzonechecktask.hpp b/lib/methods/clusterzonechecktask.hpp
new file mode 100644
index 0000000..2af442c
--- /dev/null
+++ b/lib/methods/clusterzonechecktask.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CLUSTERZONECHECKTASK_H
+#define CLUSTERZONECHECKTASK_H
+
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+/**
+ * Cluster zone check type.
+ *
+ * @ingroup methods
+ */
+class ClusterZoneCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ ClusterZoneCheckTask();
+};
+
+}
+
+#endif /* CLUSTERZONECHECKTASK_H */
diff --git a/lib/methods/dummychecktask.cpp b/lib/methods/dummychecktask.cpp
new file mode 100644
index 0000000..905a022
--- /dev/null
+++ b/lib/methods/dummychecktask.cpp
@@ -0,0 +1,75 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+# include <stdlib.h>
+#endif /* _WIN32 */
+#include "methods/dummychecktask.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/convert.hpp"
+#include "base/function.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, DummyCheck, &DummyCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void DummyCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", command);
+
+ int dummyState = MacroProcessor::ResolveMacros("$dummy_state$", resolvers, checkable->GetLastCheckResult(),
+ nullptr, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ String dummyText = MacroProcessor::ResolveMacros("$dummy_text$", resolvers, checkable->GetLastCheckResult(),
+ nullptr, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ /* Parse output and performance data. */
+ std::pair<String, String> co = PluginUtility::ParseCheckOutput(dummyText);
+
+ double now = Utility::GetTime();
+ String commandName = command->GetName();
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = dummyText;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = dummyState;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ cr->SetOutput(co.first);
+ cr->SetPerformanceData(PluginUtility::SplitPerfdata(co.second));
+ cr->SetState(PluginUtility::ExitStatusToState(dummyState));
+ cr->SetExitStatus(dummyState);
+ cr->SetExecutionStart(now);
+ cr->SetExecutionEnd(now);
+ cr->SetCommand(commandName);
+
+ checkable->ProcessCheckResult(cr);
+ }
+}
diff --git a/lib/methods/dummychecktask.hpp b/lib/methods/dummychecktask.hpp
new file mode 100644
index 0000000..621bbfb
--- /dev/null
+++ b/lib/methods/dummychecktask.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DUMMYCHECKTASK_H
+#define DUMMYCHECKTASK_H
+
+#include "methods/i2-methods.hpp"
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+
+namespace icinga
+{
+
+/**
+ * Test class for additional check types. Implements the "dummy" check type.
+ *
+ * @ingroup methods
+ */
+class DummyCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ DummyCheckTask();
+};
+
+}
+
+#endif /* DUMMYCHECKTASK_H */
diff --git a/lib/methods/exceptionchecktask.cpp b/lib/methods/exceptionchecktask.cpp
new file mode 100644
index 0000000..47707f2
--- /dev/null
+++ b/lib/methods/exceptionchecktask.cpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+# include <stdlib.h>
+#endif /* _WIN32 */
+#include "methods/exceptionchecktask.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/function.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, ExceptionCheck, &ExceptionCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void ExceptionCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ ScriptError scriptError = ScriptError("Test") << boost::errinfo_api_function("Test");
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = scriptError.what();
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = 3;
+
+ Checkable::ExecuteCommandProcessFinishedHandler("", pr);
+ } else {
+ BOOST_THROW_EXCEPTION(ScriptError("Test") << boost::errinfo_api_function("Test"));
+ }
+}
diff --git a/lib/methods/exceptionchecktask.hpp b/lib/methods/exceptionchecktask.hpp
new file mode 100644
index 0000000..09db104
--- /dev/null
+++ b/lib/methods/exceptionchecktask.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EXCEPTIONCHECKTASK_H
+#define EXCEPTIONCHECKTASK_H
+
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+
+namespace icinga
+{
+
+/**
+ * Test class for additional check types. Implements the "exception" check type.
+ *
+ * @ingroup methods
+ */
+class ExceptionCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ ExceptionCheckTask();
+};
+
+}
+
+#endif /* EXCEPTIONCHECKTASK_H */
diff --git a/lib/methods/i2-methods.hpp b/lib/methods/i2-methods.hpp
new file mode 100644
index 0000000..ffd8002
--- /dev/null
+++ b/lib/methods/i2-methods.hpp
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2METHODS_H
+#define I2METHODS_H
+
+/**
+ * @defgroup methods Icinga methods
+ *
+ * The methods library implements methods for various task (e.g. checks, event
+ * handlers, etc.).
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2METHODS_H */
diff --git a/lib/methods/icingachecktask.cpp b/lib/methods/icingachecktask.cpp
new file mode 100644
index 0000000..d3eae1f
--- /dev/null
+++ b/lib/methods/icingachecktask.cpp
@@ -0,0 +1,209 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/icingachecktask.hpp"
+#include "icinga/cib.hpp"
+#include "icinga/service.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/clusterevents.hpp"
+#include "icinga/checkable.hpp"
+#include "remote/apilistener.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/function.hpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, IcingaCheck, &IcingaCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void IcingaCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", command);
+
+ String missingIcingaMinVersion;
+
+ String icingaMinVersion = MacroProcessor::ResolveMacros("$icinga_min_version$", resolvers, checkable->GetLastCheckResult(),
+ &missingIcingaMinVersion, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ double interval = Utility::GetTime() - Application::GetStartTime();
+
+ if (interval > 60)
+ interval = 60;
+
+ /* use feature stats perfdata */
+ std::pair<Dictionary::Ptr, Array::Ptr> feature_stats = CIB::GetFeatureStats();
+
+ Array::Ptr perfdata = feature_stats.second;
+
+ perfdata->Add(new PerfdataValue("active_host_checks", CIB::GetActiveHostChecksStatistics(interval) / interval));
+ perfdata->Add(new PerfdataValue("passive_host_checks", CIB::GetPassiveHostChecksStatistics(interval) / interval));
+ perfdata->Add(new PerfdataValue("active_host_checks_1min", CIB::GetActiveHostChecksStatistics(60)));
+ perfdata->Add(new PerfdataValue("passive_host_checks_1min", CIB::GetPassiveHostChecksStatistics(60)));
+ perfdata->Add(new PerfdataValue("active_host_checks_5min", CIB::GetActiveHostChecksStatistics(60 * 5)));
+ perfdata->Add(new PerfdataValue("passive_host_checks_5min", CIB::GetPassiveHostChecksStatistics(60 * 5)));
+ perfdata->Add(new PerfdataValue("active_host_checks_15min", CIB::GetActiveHostChecksStatistics(60 * 15)));
+ perfdata->Add(new PerfdataValue("passive_host_checks_15min", CIB::GetPassiveHostChecksStatistics(60 * 15)));
+
+ perfdata->Add(new PerfdataValue("active_service_checks", CIB::GetActiveServiceChecksStatistics(interval) / interval));
+ perfdata->Add(new PerfdataValue("passive_service_checks", CIB::GetPassiveServiceChecksStatistics(interval) / interval));
+ perfdata->Add(new PerfdataValue("active_service_checks_1min", CIB::GetActiveServiceChecksStatistics(60)));
+ perfdata->Add(new PerfdataValue("passive_service_checks_1min", CIB::GetPassiveServiceChecksStatistics(60)));
+ perfdata->Add(new PerfdataValue("active_service_checks_5min", CIB::GetActiveServiceChecksStatistics(60 * 5)));
+ perfdata->Add(new PerfdataValue("passive_service_checks_5min", CIB::GetPassiveServiceChecksStatistics(60 * 5)));
+ perfdata->Add(new PerfdataValue("active_service_checks_15min", CIB::GetActiveServiceChecksStatistics(60 * 15)));
+ perfdata->Add(new PerfdataValue("passive_service_checks_15min", CIB::GetPassiveServiceChecksStatistics(60 * 15)));
+
+ perfdata->Add(new PerfdataValue("current_pending_callbacks", Application::GetTP().GetPending()));
+ perfdata->Add(new PerfdataValue("current_concurrent_checks", Checkable::CurrentConcurrentChecks.load()));
+ perfdata->Add(new PerfdataValue("remote_check_queue", ClusterEvents::GetCheckRequestQueueSize()));
+
+ CheckableCheckStatistics scs = CIB::CalculateServiceCheckStats();
+
+ perfdata->Add(new PerfdataValue("min_latency", scs.min_latency));
+ perfdata->Add(new PerfdataValue("max_latency", scs.max_latency));
+ perfdata->Add(new PerfdataValue("avg_latency", scs.avg_latency));
+ perfdata->Add(new PerfdataValue("min_execution_time", scs.min_execution_time));
+ perfdata->Add(new PerfdataValue("max_execution_time", scs.max_execution_time));
+ perfdata->Add(new PerfdataValue("avg_execution_time", scs.avg_execution_time));
+
+ ServiceStatistics ss = CIB::CalculateServiceStats();
+
+ perfdata->Add(new PerfdataValue("num_services_ok", ss.services_ok));
+ perfdata->Add(new PerfdataValue("num_services_warning", ss.services_warning));
+ perfdata->Add(new PerfdataValue("num_services_critical", ss.services_critical));
+ perfdata->Add(new PerfdataValue("num_services_unknown", ss.services_unknown));
+ perfdata->Add(new PerfdataValue("num_services_pending", ss.services_pending));
+ perfdata->Add(new PerfdataValue("num_services_unreachable", ss.services_unreachable));
+ perfdata->Add(new PerfdataValue("num_services_flapping", ss.services_flapping));
+ perfdata->Add(new PerfdataValue("num_services_in_downtime", ss.services_in_downtime));
+ perfdata->Add(new PerfdataValue("num_services_acknowledged", ss.services_acknowledged));
+ perfdata->Add(new PerfdataValue("num_services_handled", ss.services_handled));
+ perfdata->Add(new PerfdataValue("num_services_problem", ss.services_problem));
+
+ double uptime = Application::GetUptime();
+ perfdata->Add(new PerfdataValue("uptime", uptime));
+
+ HostStatistics hs = CIB::CalculateHostStats();
+
+ perfdata->Add(new PerfdataValue("num_hosts_up", hs.hosts_up));
+ perfdata->Add(new PerfdataValue("num_hosts_down", hs.hosts_down));
+ perfdata->Add(new PerfdataValue("num_hosts_pending", hs.hosts_pending));
+ perfdata->Add(new PerfdataValue("num_hosts_unreachable", hs.hosts_unreachable));
+ perfdata->Add(new PerfdataValue("num_hosts_flapping", hs.hosts_flapping));
+ perfdata->Add(new PerfdataValue("num_hosts_in_downtime", hs.hosts_in_downtime));
+ perfdata->Add(new PerfdataValue("num_hosts_acknowledged", hs.hosts_acknowledged));
+ perfdata->Add(new PerfdataValue("num_hosts_handled", hs.hosts_handled));
+ perfdata->Add(new PerfdataValue("num_hosts_problem", hs.hosts_problem));
+
+ std::vector<Endpoint::Ptr> endpoints = ConfigType::GetObjectsByType<Endpoint>();
+
+ double lastMessageSent = 0;
+ double lastMessageReceived = 0;
+ double messagesSentPerSecond = 0;
+ double messagesReceivedPerSecond = 0;
+ double bytesSentPerSecond = 0;
+ double bytesReceivedPerSecond = 0;
+
+ for (const Endpoint::Ptr& endpoint : endpoints)
+ {
+ if (endpoint->GetLastMessageSent() > lastMessageSent)
+ lastMessageSent = endpoint->GetLastMessageSent();
+
+ if (endpoint->GetLastMessageReceived() > lastMessageReceived)
+ lastMessageReceived = endpoint->GetLastMessageReceived();
+
+ messagesSentPerSecond += endpoint->GetMessagesSentPerSecond();
+ messagesReceivedPerSecond += endpoint->GetMessagesReceivedPerSecond();
+ bytesSentPerSecond += endpoint->GetBytesSentPerSecond();
+ bytesReceivedPerSecond += endpoint->GetBytesReceivedPerSecond();
+ }
+
+ perfdata->Add(new PerfdataValue("last_messages_sent", lastMessageSent));
+ perfdata->Add(new PerfdataValue("last_messages_received", lastMessageReceived));
+ perfdata->Add(new PerfdataValue("sum_messages_sent_per_second", messagesSentPerSecond));
+ perfdata->Add(new PerfdataValue("sum_messages_received_per_second", messagesReceivedPerSecond));
+ perfdata->Add(new PerfdataValue("sum_bytes_sent_per_second", bytesSentPerSecond));
+ perfdata->Add(new PerfdataValue("sum_bytes_received_per_second", bytesReceivedPerSecond));
+
+ cr->SetPerformanceData(perfdata);
+ ServiceState state = ServiceOK;
+
+ String appVersion = Application::GetAppVersion();
+
+ String output = "Icinga 2 has been running for " + Utility::FormatDuration(uptime) +
+ ". Version: " + appVersion;
+
+ /* Indicate a warning if the last reload failed. */
+ double lastReloadFailed = Application::GetLastReloadFailed();
+
+ if (lastReloadFailed > 0) {
+ output += "; Last reload attempt failed at " + Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", lastReloadFailed);
+ state =ServiceWarning;
+ }
+
+ /* Indicate a warning when the last synced config caused a stage validation error. */
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener) {
+ Dictionary::Ptr validationResult = listener->GetLastFailedZonesStageValidation();
+
+ if (validationResult) {
+ output += "; Last zone sync stage validation failed at "
+ + Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", validationResult->Get("ts"));
+
+ state = ServiceWarning;
+ }
+ }
+
+ String parsedAppVersion = Utility::ParseVersion(appVersion);
+
+ /* Return an error if the version is less than specified (optional). */
+ if (missingIcingaMinVersion.IsEmpty() && !icingaMinVersion.IsEmpty() && Utility::CompareVersion(icingaMinVersion, parsedAppVersion) < 0) {
+ output += "; Minimum version " + icingaMinVersion + " is not installed.";
+ state = ServiceCritical;
+ }
+
+ String commandName = command->GetName();
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ cr->SetState(state);
+ cr->SetOutput(output);
+ cr->SetCommand(commandName);
+
+ checkable->ProcessCheckResult(cr);
+ }
+}
diff --git a/lib/methods/icingachecktask.hpp b/lib/methods/icingachecktask.hpp
new file mode 100644
index 0000000..93def62
--- /dev/null
+++ b/lib/methods/icingachecktask.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ICINGACHECKTASK_H
+#define ICINGACHECKTASK_H
+
+#include "methods/i2-methods.hpp"
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+/**
+ * Icinga check type.
+ *
+ * @ingroup methods
+ */
+class IcingaCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ IcingaCheckTask();
+};
+
+}
+
+#endif /* ICINGACHECKTASK_H */
diff --git a/lib/methods/ifwapichecktask.cpp b/lib/methods/ifwapichecktask.cpp
new file mode 100644
index 0000000..8516d70
--- /dev/null
+++ b/lib/methods/ifwapichecktask.cpp
@@ -0,0 +1,531 @@
+/* Icinga 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+# include <stdlib.h>
+#endif /* _WIN32 */
+#include "methods/ifwapichecktask.hpp"
+#include "methods/pluginchecktask.hpp"
+#include "icinga/checkresult-ti.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/base64.hpp"
+#include "base/defer.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/convert.hpp"
+#include "base/function.hpp"
+#include "base/io-engine.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include "base/shared.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/tlsstream.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/url.hpp"
+#include <boost/asio.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/system/system_error.hpp>
+#include <exception>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, IfwApiCheck, &IfwApiCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+static void ReportIfwCheckResult(
+ const Checkable::Ptr& checkable, const Value& cmdLine, const CheckResult::Ptr& cr,
+ const String& output, double start, double end, int exitcode = 3, const Array::Ptr& perfdata = nullptr
+)
+{
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = perfdata ? output + " |" + String(perfdata->Join(" ")) : output;
+ pr.ExecutionStart = start;
+ pr.ExecutionEnd = end;
+ pr.ExitStatus = exitcode;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(cmdLine, pr);
+ } else {
+ auto splittedPerfdata (perfdata);
+
+ if (perfdata) {
+ splittedPerfdata = new Array();
+ ObjectLock oLock (perfdata);
+
+ for (String pv : perfdata) {
+ PluginUtility::SplitPerfdata(pv)->CopyTo(splittedPerfdata);
+ }
+ }
+
+ cr->SetOutput(output);
+ cr->SetPerformanceData(splittedPerfdata);
+ cr->SetState((ServiceState)exitcode);
+ cr->SetExitStatus(exitcode);
+ cr->SetExecutionStart(start);
+ cr->SetExecutionEnd(end);
+ cr->SetCommand(cmdLine);
+
+ checkable->ProcessCheckResult(cr);
+ }
+}
+
+static void ReportIfwCheckResult(
+ boost::asio::yield_context yc, const Checkable::Ptr& checkable, const Value& cmdLine,
+ const CheckResult::Ptr& cr, const String& output, double start
+)
+{
+ double end = Utility::GetTime();
+ CpuBoundWork cbw (yc);
+
+ ReportIfwCheckResult(checkable, cmdLine, cr, output, start, end);
+}
+
+static const char* GetUnderstandableError(const std::exception& ex)
+{
+ auto se (dynamic_cast<const boost::system::system_error*>(&ex));
+
+ if (se && se->code() == boost::asio::error::operation_aborted) {
+ return "Timeout exceeded";
+ }
+
+ return ex.what();
+}
+
+static void DoIfwNetIo(
+ boost::asio::yield_context yc, const Checkable::Ptr& checkable, const Array::Ptr& cmdLine,
+ const CheckResult::Ptr& cr, const String& psCommand, const String& psHost, const String& san, const String& psPort,
+ AsioTlsStream& conn, boost::beast::http::request<boost::beast::http::string_body>& req, double start
+)
+{
+ namespace http = boost::beast::http;
+
+ boost::beast::flat_buffer buf;
+ http::response<http::string_body> resp;
+
+ try {
+ Connect(conn.lowest_layer(), psHost, psPort, yc);
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "Can't connect to IfW API on host '" + psHost + "' port '" + psPort + "': " + GetUnderstandableError(ex),
+ start
+ );
+ return;
+ }
+
+ auto& sslConn (conn.next_layer());
+
+ try {
+ sslConn.async_handshake(conn.next_layer().client, yc);
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "TLS handshake with IfW API on host '" + psHost + "' (SNI: '" + san
+ + "') port '" + psPort + "' failed: " + GetUnderstandableError(ex),
+ start
+ );
+ return;
+ }
+
+ if (!sslConn.IsVerifyOK()) {
+ auto cert (sslConn.GetPeerCertificate());
+ Value cn;
+
+ try {
+ cn = GetCertificateCN(cert);
+ } catch (const std::exception&) {
+ }
+
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "Certificate validation failed for IfW API on host '" + psHost + "' (SNI: '" + san + "'; CN: "
+ + (cn.IsString() ? "'" + cn + "'" : "N/A") + ") port '" + psPort + "': " + sslConn.GetVerifyError(),
+ start
+ );
+ return;
+ }
+
+ try {
+ http::async_write(conn, req, yc);
+ conn.async_flush(yc);
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "Can't send HTTP request to IfW API on host '" + psHost + "' port '" + psPort + "': " + GetUnderstandableError(ex),
+ start
+ );
+ return;
+ }
+
+ try {
+ http::async_read(conn, buf, resp, yc);
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "Can't read HTTP response from IfW API on host '" + psHost + "' port '" + psPort + "': " + GetUnderstandableError(ex),
+ start
+ );
+ return;
+ }
+
+ double end = Utility::GetTime();
+
+ {
+ boost::system::error_code ec;
+ sslConn.async_shutdown(yc[ec]);
+ }
+
+ CpuBoundWork cbw (yc);
+ Value jsonRoot;
+
+ try {
+ jsonRoot = JsonDecode(resp.body());
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got bad JSON from IfW API on host '" + psHost + "' port '" + psPort + "': " + ex.what(), start, end
+ );
+ return;
+ }
+
+ if (!jsonRoot.IsObjectType<Dictionary>()) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got JSON, but not an object, from IfW API on host '"
+ + psHost + "' port '" + psPort + "': " + JsonEncode(jsonRoot),
+ start, end
+ );
+ return;
+ }
+
+ Value jsonBranch;
+
+ if (!Dictionary::Ptr(jsonRoot)->Get(psCommand, &jsonBranch)) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Missing ." + psCommand + " in JSON object from IfW API on host '"
+ + psHost + "' port '" + psPort + "': " + JsonEncode(jsonRoot),
+ start, end
+ );
+ return;
+ }
+
+ if (!jsonBranch.IsObjectType<Dictionary>()) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "." + psCommand + " in JSON from IfW API on host '"
+ + psHost + "' port '" + psPort + "' is not an object: " + JsonEncode(jsonBranch),
+ start, end
+ );
+ return;
+ }
+
+ Dictionary::Ptr result = jsonBranch;
+
+ Value exitcode;
+
+ if (!result->Get("exitcode", &exitcode)) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Missing ." + psCommand + ".exitcode in JSON object from IfW API on host '"
+ + psHost + "' port '" + psPort + "': " + JsonEncode(result),
+ start, end
+ );
+ return;
+ }
+
+ static const std::set<double> exitcodes {ServiceOK, ServiceWarning, ServiceCritical, ServiceUnknown};
+ static const auto exitcodeList (Array::FromSet(exitcodes)->Join(", "));
+
+ if (!exitcode.IsNumber() || exitcodes.find(exitcode) == exitcodes.end()) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got bad exitcode " + JsonEncode(exitcode) + " from IfW API on host '" + psHost + "' port '" + psPort
+ + "', expected one of: " + exitcodeList,
+ start, end
+ );
+ return;
+ }
+
+ auto perfdataVal (result->Get("perfdata"));
+ Array::Ptr perfdata;
+
+ try {
+ perfdata = perfdataVal;
+ } catch (const std::exception&) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got bad perfdata " + JsonEncode(perfdataVal) + " from IfW API on host '"
+ + psHost + "' port '" + psPort + "', expected an array",
+ start, end
+ );
+ return;
+ }
+
+ if (perfdata) {
+ ObjectLock oLock (perfdata);
+
+ for (auto& pv : perfdata) {
+ if (!pv.IsString()) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got bad perfdata value " + JsonEncode(perfdata) + " from IfW API on host '"
+ + psHost + "' port '" + psPort + "', expected an array of strings",
+ start, end
+ );
+ return;
+ }
+ }
+ }
+
+ ReportIfwCheckResult(checkable, cmdLine, cr, result->Get("checkresult"), start, end, exitcode, perfdata);
+}
+
+void IfwApiCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ namespace asio = boost::asio;
+ namespace http = boost::beast::http;
+ using http::field;
+
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ // We're going to just resolve macros for the actual check execution happening elsewhere
+ if (resolvedMacros && !useResolvedMacros) {
+ auto commandEndpoint (checkable->GetCommandEndpoint());
+
+ // There's indeed a command endpoint, obviously for the actual check execution
+ if (commandEndpoint) {
+ // But it doesn't have this function, yet ("ifw-api-check-command")
+ if (!(commandEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::IfwApiCheckCommand)) {
+ // Assume "ifw-api-check-command" has been imported into a check command which can also work
+ // based on "plugin-check-command", delegate respectively and hope for the best
+ PluginCheckTask::ScriptFunc(checkable, cr, resolvedMacros, useResolvedMacros);
+ return;
+ }
+ }
+ }
+
+ CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+ auto lcr (checkable->GetLastCheckResult());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", command);
+
+ auto resolveMacros ([&resolvers, &lcr, &resolvedMacros, useResolvedMacros](const char* macros) -> Value {
+ return MacroProcessor::ResolveMacros(
+ macros, resolvers, lcr, nullptr, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros
+ );
+ });
+
+ String psCommand = resolveMacros("$ifw_api_command$");
+ Dictionary::Ptr arguments = resolveMacros("$ifw_api_arguments$");
+ String psHost = resolveMacros("$ifw_api_host$");
+ String psPort = resolveMacros("$ifw_api_port$");
+ String expectedSan = resolveMacros("$ifw_api_expected_san$");
+ String cert = resolveMacros("$ifw_api_cert$");
+ String key = resolveMacros("$ifw_api_key$");
+ String ca = resolveMacros("$ifw_api_ca$");
+ String crl = resolveMacros("$ifw_api_crl$");
+ String username = resolveMacros("$ifw_api_username$");
+ String password = resolveMacros("$ifw_api_password$");
+
+ Dictionary::Ptr params = new Dictionary();
+
+ if (arguments) {
+ ObjectLock oLock (arguments);
+ Array::Ptr emptyCmd = new Array();
+
+ for (auto& kv : arguments) {
+ Dictionary::Ptr argSpec;
+
+ if (kv.second.IsObjectType<Dictionary>()) {
+ argSpec = Dictionary::Ptr(kv.second)->ShallowClone();
+ } else {
+ argSpec = new Dictionary({{ "value", kv.second }});
+ }
+
+ // See default branch of below switch
+ argSpec->Set("repeat_key", false);
+
+ {
+ ObjectLock oLock (argSpec);
+
+ for (auto& kv : argSpec) {
+ if (kv.second.GetType() == ValueObject) {
+ auto now (Utility::GetTime());
+
+ ReportIfwCheckResult(
+ checkable, command->GetName(), cr,
+ "$ifw_api_arguments$ may not directly contain objects (especially functions).", now, now
+ );
+
+ return;
+ }
+ }
+ }
+
+ /* MacroProcessor::ResolveArguments() converts
+ *
+ * [ "check_example" ]
+ * and
+ * {
+ * "-f" = { set_if = "$example_flag$" }
+ * "-a" = "$example_arg$"
+ * }
+ *
+ * to
+ *
+ * [ "check_example", "-f", "-a", "X" ]
+ *
+ * but we need the args one-by-one like [ "-f" ] or [ "-a", "X" ].
+ */
+ Array::Ptr arg = MacroProcessor::ResolveArguments(
+ emptyCmd, new Dictionary({{kv.first, argSpec}}), resolvers, lcr, resolvedMacros, useResolvedMacros
+ );
+
+ switch (arg ? arg->GetLength() : 0) {
+ case 0:
+ break;
+ case 1: // [ "-f" ]
+ params->Set(arg->Get(0), true);
+ break;
+ case 2: // [ "-a", "X" ]
+ params->Set(arg->Get(0), arg->Get(1));
+ break;
+ default: { // [ "-a", "X", "Y" ]
+ auto k (arg->Get(0));
+
+ arg->Remove(0);
+ params->Set(k, arg);
+ }
+ }
+ }
+ }
+
+ auto checkTimeout (command->GetTimeout());
+ auto checkableTimeout (checkable->GetCheckTimeout());
+
+ if (!checkableTimeout.IsEmpty())
+ checkTimeout = checkableTimeout;
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ if (psHost.IsEmpty()) {
+ psHost = "localhost";
+ }
+
+ if (expectedSan.IsEmpty()) {
+ expectedSan = IcingaApplication::GetInstance()->GetNodeName();
+ }
+
+ if (cert.IsEmpty()) {
+ cert = ApiListener::GetDefaultCertPath();
+ }
+
+ if (key.IsEmpty()) {
+ key = ApiListener::GetDefaultKeyPath();
+ }
+
+ if (ca.IsEmpty()) {
+ ca = ApiListener::GetDefaultCaPath();
+ }
+
+ Url::Ptr uri = new Url();
+
+ uri->SetPath({ "v1", "checker" });
+ uri->SetQuery({{ "command", psCommand }});
+
+ static const auto userAgent ("Icinga/" + Application::GetAppVersion());
+ auto relative (uri->Format());
+ auto body (JsonEncode(params));
+ auto req (Shared<http::request<http::string_body>>::Make());
+
+ req->method(http::verb::post);
+ req->target(relative);
+ req->set(field::accept, "application/json");
+ req->set(field::content_type, "application/json");
+ req->set(field::host, expectedSan + ":" + psPort);
+ req->set(field::user_agent, userAgent);
+ req->body() = body;
+ req->content_length(req->body().size());
+
+ static const auto curlTlsMinVersion ((String("--") + DEFAULT_TLS_PROTOCOLMIN).ToLower());
+
+ Array::Ptr cmdLine = new Array({
+ "curl", "--verbose", curlTlsMinVersion, "--fail-with-body",
+ "--connect-to", expectedSan + ":" + psPort + ":" + psHost + ":" + psPort,
+ "--ciphers", DEFAULT_TLS_CIPHERS,
+ "--cert", cert,
+ "--key", key,
+ "--cacert", ca,
+ "--request", "POST",
+ "--url", "https://" + expectedSan + ":" + psPort + relative,
+ "--user-agent", userAgent,
+ "--header", "Accept: application/json",
+ "--header", "Content-Type: application/json",
+ "--data-raw", body
+ });
+
+ if (!crl.IsEmpty()) {
+ cmdLine->Add("--crlfile");
+ cmdLine->Add(crl);
+ }
+
+ if (!username.IsEmpty() && !password.IsEmpty()) {
+ auto authn (username + ":" + password);
+
+ req->set(field::authorization, "Basic " + Base64::Encode(authn));
+ cmdLine->Add("--user");
+ cmdLine->Add(authn);
+ }
+
+ auto& io (IoEngine::Get().GetIoContext());
+ auto strand (Shared<asio::io_context::strand>::Make(io));
+ Shared<asio::ssl::context>::Ptr ctx;
+ double start = Utility::GetTime();
+
+ try {
+ ctx = SetupSslContext(cert, key, ca, crl, DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(checkable, cmdLine, cr, ex.what(), start, Utility::GetTime());
+ return;
+ }
+
+ auto conn (Shared<AsioTlsStream>::Make(io, *ctx, expectedSan));
+
+ IoEngine::SpawnCoroutine(
+ *strand,
+ [strand, checkable, cmdLine, cr, psCommand, psHost, expectedSan, psPort, conn, req, start, checkTimeout](asio::yield_context yc) {
+ Timeout::Ptr timeout = new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(checkTimeout * 1e6)),
+ [&conn, &checkable](boost::asio::yield_context yc) {
+ Log(LogNotice, "IfwApiCheckTask")
+ << "Timeout while checking " << checkable->GetReflectionType()->GetName()
+ << " '" << checkable->GetName() << "', cancelling attempt";
+
+ boost::system::error_code ec;
+ conn->lowest_layer().cancel(ec);
+ }
+ );
+
+ Defer cancelTimeout ([&timeout]() { timeout->Cancel(); });
+
+ DoIfwNetIo(yc, checkable, cmdLine, cr, psCommand, psHost, expectedSan, psPort, *conn, *req, start);
+ }
+ );
+}
diff --git a/lib/methods/ifwapichecktask.hpp b/lib/methods/ifwapichecktask.hpp
new file mode 100644
index 0000000..3932733
--- /dev/null
+++ b/lib/methods/ifwapichecktask.hpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+#pragma once
+
+#include "methods/i2-methods.hpp"
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+
+namespace icinga
+{
+
+/**
+ * Executes checks via Icinga for Windows API.
+ *
+ * @ingroup methods
+ */
+class IfwApiCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ IfwApiCheckTask();
+};
+
+}
diff --git a/lib/methods/methods-itl.conf b/lib/methods/methods-itl.conf
new file mode 100644
index 0000000..6249692
--- /dev/null
+++ b/lib/methods/methods-itl.conf
@@ -0,0 +1,90 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+System.assert(Internal.run_with_activation_context(function() {
+ template CheckCommand "icinga-check-command" use (IcingaCheck = Internal.IcingaCheck) {
+ execute = IcingaCheck
+
+ vars.icinga_min_version = ""
+ }
+
+ template CheckCommand "cluster-check-command" use (ClusterCheck = Internal.ClusterCheck) {
+ execute = ClusterCheck
+ }
+
+ template CheckCommand "cluster-zone-check-command" use (ClusterZoneCheck = Internal.ClusterZoneCheck) {
+ execute = ClusterZoneCheck
+ }
+
+ template CheckCommand "plugin-check-command" use (PluginCheck = Internal.PluginCheck) default {
+ execute = PluginCheck
+ }
+
+ template NotificationCommand "plugin-notification-command" use (PluginNotification = Internal.PluginNotification) default {
+ execute = PluginNotification
+ }
+
+ template EventCommand "plugin-event-command" use (PluginEvent = Internal.PluginEvent) default {
+ execute = PluginEvent
+ }
+
+ template CheckCommand "dummy-check-command" use (DummyCheck = Internal.DummyCheck) {
+ execute = DummyCheck
+ }
+
+ template CheckCommand "random-check-command" use (RandomCheck = Internal.RandomCheck) {
+ execute = RandomCheck
+ }
+
+ template CheckCommand "exception-check-command" use (ExceptionCheck = Internal.ExceptionCheck) {
+ execute = ExceptionCheck
+ }
+
+ template CheckCommand "null-check-command" use (NullCheck = Internal.NullCheck) {
+ execute = NullCheck
+ }
+
+ template CheckCommand "ifw-api-check-command" use (IfwApiCheck = Internal.IfwApiCheck) {
+ execute = IfwApiCheck
+ }
+
+ template EventCommand "null-event-command" use (NullEvent = Internal.NullEvent) {
+ execute = NullEvent
+ }
+
+ template TimePeriod "empty-timeperiod" use (EmptyTimePeriod = Internal.EmptyTimePeriod) {
+ update = EmptyTimePeriod
+ }
+
+ template TimePeriod "even-minutes-timeperiod" use (EvenMinutesTimePeriod = Internal.EvenMinutesTimePeriod) {
+ update = EvenMinutesTimePeriod
+ }
+
+ template CheckCommand "sleep-check-command" use (SleepCheck = Internal.SleepCheck) {
+ execute = SleepCheck
+
+ vars.sleep_time = 1s
+ }
+}))
+
+var methods = [
+ "IcingaCheck",
+ "IfwApiCheck",
+ "ClusterCheck",
+ "ClusterZoneCheck",
+ "PluginCheck",
+ "ClrCheck",
+ "PluginNotification",
+ "PluginEvent",
+ "DummyCheck",
+ "RandomCheck",
+ "ExceptionCheck",
+ "NullCheck",
+ "NullEvent",
+ "EmptyTimePeriod",
+ "EvenMinutesTimePeriod",
+ "SleepCheck"
+]
+
+for (method in methods) {
+ Internal.remove(method)
+}
diff --git a/lib/methods/nullchecktask.cpp b/lib/methods/nullchecktask.cpp
new file mode 100644
index 0000000..ee66029
--- /dev/null
+++ b/lib/methods/nullchecktask.cpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+# include <stdlib.h>
+#endif /* _WIN32 */
+#include "methods/nullchecktask.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/convert.hpp"
+#include "base/function.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, NullCheck, &NullCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void NullCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ String output = "Hello from ";
+ output += IcingaApplication::GetInstance()->GetNodeName();
+ ServiceState state = ServiceOK;
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler("", pr);
+ } else {
+ cr->SetOutput(output);
+ cr->SetPerformanceData(new Array({
+ new PerfdataValue("time", Convert::ToDouble(Utility::GetTime()))
+ }));
+ cr->SetState(state);
+
+ checkable->ProcessCheckResult(cr);
+ }
+}
diff --git a/lib/methods/nullchecktask.hpp b/lib/methods/nullchecktask.hpp
new file mode 100644
index 0000000..954cf8d
--- /dev/null
+++ b/lib/methods/nullchecktask.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NULLCHECKTASK_H
+#define NULLCHECKTASK_H
+
+#include "methods/i2-methods.hpp"
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+
+namespace icinga
+{
+
+/**
+ * Test class for additional check types. Implements the "null" check type.
+ *
+ * @ingroup methods
+ */
+class NullCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ NullCheckTask();
+};
+
+}
+
+#endif /* NULLCHECKTASK_H */
diff --git a/lib/methods/nulleventtask.cpp b/lib/methods/nulleventtask.cpp
new file mode 100644
index 0000000..3c02f23
--- /dev/null
+++ b/lib/methods/nulleventtask.cpp
@@ -0,0 +1,26 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/nulleventtask.hpp"
+#include "base/function.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, NullEvent, &NullEventTask::ScriptFunc, "checkable:resolvedMacros:useResolvedMacros");
+
+void NullEventTask::ScriptFunc(const Checkable::Ptr& checkable, const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = "";
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = 0;
+
+ Checkable::ExecuteCommandProcessFinishedHandler("", pr);
+ }
+}
diff --git a/lib/methods/nulleventtask.hpp b/lib/methods/nulleventtask.hpp
new file mode 100644
index 0000000..153470f
--- /dev/null
+++ b/lib/methods/nulleventtask.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NULLEVENTTASK_H
+#define NULLEVENTTASK_H
+
+#include "methods/i2-methods.hpp"
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+
+namespace icinga
+{
+
+/**
+ * Test class for additional event handler types. Implements the "null" event handler type.
+ *
+ * @ingroup methods
+ */
+class NullEventTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ NullEventTask();
+};
+
+}
+
+#endif /* NULLEVENTTASK_H */
diff --git a/lib/methods/pluginchecktask.cpp b/lib/methods/pluginchecktask.cpp
new file mode 100644
index 0000000..b4749fb
--- /dev/null
+++ b/lib/methods/pluginchecktask.cpp
@@ -0,0 +1,89 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/pluginchecktask.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/function.hpp"
+#include "base/utility.hpp"
+#include "base/process.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, PluginCheck, &PluginCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void PluginCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ CheckCommand::Ptr commandObj = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", commandObj);
+
+ int timeout = commandObj->GetTimeout();
+
+ if (!checkable->GetCheckTimeout().IsEmpty())
+ timeout = checkable->GetCheckTimeout();
+
+ std::function<void(const Value& commandLine, const ProcessResult&)> callback;
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ callback = Checkable::ExecuteCommandProcessFinishedHandler;
+ } else {
+ callback = [checkable, cr](const Value& commandLine, const ProcessResult& pr) {
+ ProcessFinishedHandler(checkable, cr, commandLine, pr);
+ };
+ }
+
+ PluginUtility::ExecuteCommand(commandObj, checkable, checkable->GetLastCheckResult(),
+ resolvers, resolvedMacros, useResolvedMacros, timeout, callback);
+
+ if (!resolvedMacros || useResolvedMacros) {
+ Checkable::CurrentConcurrentChecks.fetch_add(1);
+ Checkable::IncreasePendingChecks();
+ }
+}
+
+void PluginCheckTask::ProcessFinishedHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const Value& commandLine, const ProcessResult& pr)
+{
+ Checkable::CurrentConcurrentChecks.fetch_sub(1);
+ Checkable::DecreasePendingChecks();
+
+ if (pr.ExitStatus > 3) {
+ Process::Arguments parguments = Process::PrepareCommand(commandLine);
+ Log(LogWarning, "PluginCheckTask")
+ << "Check command for object '" << checkable->GetName() << "' (PID: " << pr.PID
+ << ", arguments: " << Process::PrettyPrintArguments(parguments) << ") terminated with exit code "
+ << pr.ExitStatus << ", output: " << pr.Output;
+ }
+
+ String output = pr.Output.Trim();
+
+ std::pair<String, String> co = PluginUtility::ParseCheckOutput(output);
+ cr->SetCommand(commandLine);
+ cr->SetOutput(co.first);
+ cr->SetPerformanceData(PluginUtility::SplitPerfdata(co.second));
+ cr->SetState(PluginUtility::ExitStatusToState(pr.ExitStatus));
+ cr->SetExitStatus(pr.ExitStatus);
+ cr->SetExecutionStart(pr.ExecutionStart);
+ cr->SetExecutionEnd(pr.ExecutionEnd);
+
+ checkable->ProcessCheckResult(cr);
+}
diff --git a/lib/methods/pluginchecktask.hpp b/lib/methods/pluginchecktask.hpp
new file mode 100644
index 0000000..a4fc3a3
--- /dev/null
+++ b/lib/methods/pluginchecktask.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PLUGINCHECKTASK_H
+#define PLUGINCHECKTASK_H
+
+#include "methods/i2-methods.hpp"
+#include "base/process.hpp"
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+/**
+ * Implements service checks based on external plugins.
+ *
+ * @ingroup methods
+ */
+class PluginCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ PluginCheckTask();
+
+ static void ProcessFinishedHandler(const Checkable::Ptr& service,
+ const CheckResult::Ptr& cr, const Value& commandLine, const ProcessResult& pr);
+};
+
+}
+
+#endif /* PLUGINCHECKTASK_H */
diff --git a/lib/methods/plugineventtask.cpp b/lib/methods/plugineventtask.cpp
new file mode 100644
index 0000000..00efb6c
--- /dev/null
+++ b/lib/methods/plugineventtask.cpp
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/plugineventtask.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/function.hpp"
+#include "base/utility.hpp"
+#include "base/process.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, PluginEvent, &PluginEventTask::ScriptFunc, "checkable:resolvedMacros:useResolvedMacros");
+
+void PluginEventTask::ScriptFunc(const Checkable::Ptr& checkable,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+
+ EventCommand::Ptr commandObj = EventCommand::ExecuteOverride ? EventCommand::ExecuteOverride : checkable->GetEventCommand();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", commandObj);
+
+ int timeout = commandObj->GetTimeout();
+ std::function<void(const Value& commandLine, const ProcessResult&)> callback;
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ callback = Checkable::ExecuteCommandProcessFinishedHandler;
+ } else {
+ callback = [checkable](const Value& commandLine, const ProcessResult& pr) { ProcessFinishedHandler(checkable, commandLine, pr); };
+ }
+
+ PluginUtility::ExecuteCommand(commandObj, checkable, checkable->GetLastCheckResult(),
+ resolvers, resolvedMacros, useResolvedMacros, timeout, callback);
+}
+
+void PluginEventTask::ProcessFinishedHandler(const Checkable::Ptr& checkable, const Value& commandLine, const ProcessResult& pr)
+{
+ if (pr.ExitStatus != 0) {
+ Process::Arguments parguments = Process::PrepareCommand(commandLine);
+ Log(LogWarning, "PluginEventTask")
+ << "Event command for object '" << checkable->GetName() << "' (PID: " << pr.PID
+ << ", arguments: " << Process::PrettyPrintArguments(parguments) << ") terminated with exit code "
+ << pr.ExitStatus << ", output: " << pr.Output;
+ }
+}
diff --git a/lib/methods/plugineventtask.hpp b/lib/methods/plugineventtask.hpp
new file mode 100644
index 0000000..8908a82
--- /dev/null
+++ b/lib/methods/plugineventtask.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PLUGINEVENTTASK_H
+#define PLUGINEVENTTASK_H
+
+#include "methods/i2-methods.hpp"
+#include "icinga/service.hpp"
+#include "base/process.hpp"
+
+namespace icinga
+{
+
+/**
+ * Implements event handlers based on external plugins.
+ *
+ * @ingroup methods
+ */
+class PluginEventTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ PluginEventTask();
+
+ static void ProcessFinishedHandler(const Checkable::Ptr& checkable,
+ const Value& commandLine, const ProcessResult& pr);
+};
+
+}
+
+#endif /* PLUGINEVENTTASK_H */
diff --git a/lib/methods/pluginnotificationtask.cpp b/lib/methods/pluginnotificationtask.cpp
new file mode 100644
index 0000000..95911fa
--- /dev/null
+++ b/lib/methods/pluginnotificationtask.cpp
@@ -0,0 +1,123 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/pluginnotificationtask.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/service.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "base/function.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/process.hpp"
+#include "base/convert.hpp"
+
+#ifdef __linux__
+# include <linux/binfmts.h>
+# include <unistd.h>
+
+# ifndef PAGE_SIZE
+// MAX_ARG_STRLEN is a multiple of PAGE_SIZE which is missing
+# define PAGE_SIZE getpagesize()
+# endif /* PAGE_SIZE */
+
+// Make e.g. the $host.output$ itself even 10% shorter to leave enough room
+// for e.g. --host-output= as in --host-output=$host.output$, but without int overflow
+const static auto l_MaxOutLen = MAX_ARG_STRLEN - MAX_ARG_STRLEN / 10u;
+#endif /* __linux__ */
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, PluginNotification, &PluginNotificationTask::ScriptFunc, "notification:user:cr:itype:author:comment:resolvedMacros:useResolvedMacros");
+
+void PluginNotificationTask::ScriptFunc(const Notification::Ptr& notification,
+ const User::Ptr& user, const CheckResult::Ptr& cr, int itype,
+ const String& author, const String& comment, const Dictionary::Ptr& resolvedMacros,
+ bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(notification);
+ REQUIRE_NOT_NULL(user);
+
+ NotificationCommand::Ptr commandObj = NotificationCommand::ExecuteOverride ? NotificationCommand::ExecuteOverride : notification->GetCommand();
+
+ auto type = static_cast<NotificationType>(itype);
+
+ Checkable::Ptr checkable = notification->GetCheckable();
+
+ Dictionary::Ptr notificationExtra = new Dictionary({
+ { "type", Notification::NotificationTypeToStringCompat(type) }, //TODO: Change that to our types.
+ { "author", author },
+#ifdef __linux__
+ { "comment", comment.SubStr(0, l_MaxOutLen) }
+#else /* __linux__ */
+ { "comment", comment }
+#endif /* __linux__ */
+ });
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ resolvers.emplace_back("user", user);
+ resolvers.emplace_back("notification", notificationExtra);
+ resolvers.emplace_back("notification", notification);
+
+ if (service) {
+#ifdef __linux__
+ auto cr (service->GetLastCheckResult());
+
+ if (cr) {
+ auto output (cr->GetOutput());
+
+ if (output.GetLength() > l_MaxOutLen) {
+ resolvers.emplace_back("service", new Dictionary({{"output", output.SubStr(0, l_MaxOutLen)}}));
+ }
+ }
+#endif /* __linux__ */
+
+ resolvers.emplace_back("service", service);
+ }
+
+#ifdef __linux__
+ auto hcr (host->GetLastCheckResult());
+
+ if (hcr) {
+ auto output (hcr->GetOutput());
+
+ if (output.GetLength() > l_MaxOutLen) {
+ resolvers.emplace_back("host", new Dictionary({{"output", output.SubStr(0, l_MaxOutLen)}}));
+ }
+ }
+#endif /* __linux__ */
+
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", commandObj);
+
+ int timeout = commandObj->GetTimeout();
+ std::function<void(const Value& commandLine, const ProcessResult&)> callback;
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ callback = Checkable::ExecuteCommandProcessFinishedHandler;
+ } else {
+ callback = [checkable](const Value& commandline, const ProcessResult& pr) { ProcessFinishedHandler(checkable, commandline, pr); };
+ }
+
+ PluginUtility::ExecuteCommand(commandObj, checkable, cr, resolvers,
+ resolvedMacros, useResolvedMacros, timeout, callback);
+}
+
+void PluginNotificationTask::ProcessFinishedHandler(const Checkable::Ptr& checkable, const Value& commandLine, const ProcessResult& pr)
+{
+ if (pr.ExitStatus != 0) {
+ Process::Arguments parguments = Process::PrepareCommand(commandLine);
+ Log(LogWarning, "PluginNotificationTask")
+ << "Notification command for object '" << checkable->GetName() << "' (PID: " << pr.PID
+ << ", arguments: " << Process::PrettyPrintArguments(parguments) << ") terminated with exit code "
+ << pr.ExitStatus << ", output: " << pr.Output;
+ }
+}
diff --git a/lib/methods/pluginnotificationtask.hpp b/lib/methods/pluginnotificationtask.hpp
new file mode 100644
index 0000000..66d6539
--- /dev/null
+++ b/lib/methods/pluginnotificationtask.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PLUGINNOTIFICATIONTASK_H
+#define PLUGINNOTIFICATIONTASK_H
+
+#include "methods/i2-methods.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/service.hpp"
+#include "base/process.hpp"
+
+namespace icinga
+{
+
+/**
+ * Implements sending notifications based on external plugins.
+ *
+ * @ingroup methods
+ */
+class PluginNotificationTask
+{
+public:
+ static void ScriptFunc(const Notification::Ptr& notification,
+ const User::Ptr& user, const CheckResult::Ptr& cr, int itype,
+ const String& author, const String& comment,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ PluginNotificationTask();
+
+ static void ProcessFinishedHandler(const Checkable::Ptr& checkable,
+ const Value& commandLine, const ProcessResult& pr);
+};
+
+}
+
+#endif /* PLUGINNOTIFICATIONTASK_H */
diff --git a/lib/methods/randomchecktask.cpp b/lib/methods/randomchecktask.cpp
new file mode 100644
index 0000000..9b133ef
--- /dev/null
+++ b/lib/methods/randomchecktask.cpp
@@ -0,0 +1,65 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+# include <stdlib.h>
+#endif /* _WIN32 */
+#include "methods/randomchecktask.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/checkcommand.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/function.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, RandomCheck, &RandomCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void RandomCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ double now = Utility::GetTime();
+ double uptime = Application::GetUptime();
+
+ String output = "Hello from " + IcingaApplication::GetInstance()->GetNodeName()
+ + ". Icinga 2 has been running for " + Utility::FormatDuration(uptime)
+ + ". Version: " + Application::GetAppVersion();
+
+ CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+ String commandName = command->GetName();
+ ServiceState state = static_cast<ServiceState>(Utility::Random() % 4);
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr);
+ } else {
+ cr->SetOutput(output);
+
+ double random = Utility::Random() % 1000;
+ cr->SetPerformanceData(new Array({
+ new PerfdataValue("time", now),
+ new PerfdataValue("value", random),
+ new PerfdataValue("value_1m", random * 0.9),
+ new PerfdataValue("value_5m", random * 0.8),
+ new PerfdataValue("uptime", uptime),
+ }));
+
+ cr->SetState(state);
+ cr->SetCommand(commandName);
+
+ checkable->ProcessCheckResult(cr);
+ }
+}
diff --git a/lib/methods/randomchecktask.hpp b/lib/methods/randomchecktask.hpp
new file mode 100644
index 0000000..00ce663
--- /dev/null
+++ b/lib/methods/randomchecktask.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef RANDOMCHECKTASK_H
+#define RANDOMCHECKTASK_H
+
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+
+namespace icinga
+{
+
+/**
+ * Test class for additional check types. Implements the "null" check type.
+ *
+ * @ingroup methods
+ */
+class RandomCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& service, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ RandomCheckTask();
+};
+
+}
+
+#endif /* RANDOMCHECKTASK_H */
diff --git a/lib/methods/sleepchecktask.cpp b/lib/methods/sleepchecktask.cpp
new file mode 100644
index 0000000..af6b063
--- /dev/null
+++ b/lib/methods/sleepchecktask.cpp
@@ -0,0 +1,67 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/sleepchecktask.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/function.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, SleepCheck, &SleepCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+void SleepCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ CheckCommand::Ptr commandObj = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", commandObj);
+
+ double sleepTime = MacroProcessor::ResolveMacros("$sleep_time$", resolvers, checkable->GetLastCheckResult(),
+ nullptr, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ Utility::Sleep(sleepTime);
+
+ String output = "Slept for " + Convert::ToString(sleepTime) + " seconds.";
+
+ double now = Utility::GetTime();
+ CheckCommand::Ptr command = checkable->GetCheckCommand();
+ String commandName = command->GetName();
+
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = output;
+ pr.ExecutionStart = now - sleepTime;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = 0;
+
+ Checkable::ExecuteCommandProcessFinishedHandler("", pr);
+ } else {
+ cr->SetOutput(output);
+ cr->SetExecutionStart(now);
+ cr->SetExecutionEnd(now);
+ cr->SetCommand(commandName);
+
+ checkable->ProcessCheckResult(cr);
+ }
+} \ No newline at end of file
diff --git a/lib/methods/sleepchecktask.hpp b/lib/methods/sleepchecktask.hpp
new file mode 100644
index 0000000..b104f60
--- /dev/null
+++ b/lib/methods/sleepchecktask.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SLEEPCHECKTASK_H
+#define SLEEPCHECKTASK_H
+
+#include "methods/i2-methods.hpp"
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+
+namespace icinga
+{
+
+/**
+ * Test class for additional check types. Implements the "sleep" check type.
+ *
+ * @ingroup methods
+ */
+class SleepCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ SleepCheckTask();
+};
+
+}
+
+#endif /* SLEEPCHECKTASK_H */ \ No newline at end of file
diff --git a/lib/methods/timeperiodtask.cpp b/lib/methods/timeperiodtask.cpp
new file mode 100644
index 0000000..bb3f1bb
--- /dev/null
+++ b/lib/methods/timeperiodtask.cpp
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "methods/timeperiodtask.hpp"
+#include "base/function.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, EmptyTimePeriod, &TimePeriodTask::EmptyTimePeriodUpdate, "tp:begin:end");
+REGISTER_FUNCTION_NONCONST(Internal, EvenMinutesTimePeriod, &TimePeriodTask::EvenMinutesTimePeriodUpdate, "tp:begin:end");
+
+Array::Ptr TimePeriodTask::EmptyTimePeriodUpdate(const TimePeriod::Ptr& tp, double, double)
+{
+ REQUIRE_NOT_NULL(tp);
+
+ Array::Ptr segments = new Array();
+ return segments;
+}
+
+Array::Ptr TimePeriodTask::EvenMinutesTimePeriodUpdate(const TimePeriod::Ptr& tp, double begin, double end)
+{
+ REQUIRE_NOT_NULL(tp);
+
+ ArrayData segments;
+
+ for (long t = begin / 60 - 1; t * 60 < end; t++) {
+ if ((t % 2) == 0) {
+ segments.push_back(new Dictionary({
+ { "begin", t * 60 },
+ { "end", (t + 1) * 60 }
+ }));
+ }
+ }
+
+ return new Array(std::move(segments));
+}
diff --git a/lib/methods/timeperiodtask.hpp b/lib/methods/timeperiodtask.hpp
new file mode 100644
index 0000000..0dff1c6
--- /dev/null
+++ b/lib/methods/timeperiodtask.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TIMEPERIODTASK_H
+#define TIMEPERIODTASK_H
+
+#include "icinga/timeperiod.hpp"
+
+namespace icinga
+{
+
+/**
+* Test timeperiod functions.
+*
+* @ingroup methods
+*/
+class TimePeriodTask
+{
+public:
+ static Array::Ptr EmptyTimePeriodUpdate(const TimePeriod::Ptr& tp, double begin, double end);
+ static Array::Ptr EvenMinutesTimePeriodUpdate(const TimePeriod::Ptr& tp, double begin, double end);
+
+private:
+ TimePeriodTask();
+};
+
+}
+
+#endif /* TIMEPERIODTASK_H */
diff --git a/lib/mysql_shim/CMakeLists.txt b/lib/mysql_shim/CMakeLists.txt
new file mode 100644
index 0000000..fc7dbee
--- /dev/null
+++ b/lib/mysql_shim/CMakeLists.txt
@@ -0,0 +1,31 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+include_directories(${MYSQL_INCLUDE_DIR})
+
+set(mysql_shim_SOURCES
+ mysql_shim.def
+ mysqlinterface.cpp mysqlinterface.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(mysql_shim mysql_shim mysql_shim_SOURCES)
+endif()
+
+add_library(mysql_shim SHARED ${mysql_shim_SOURCES})
+
+include(GenerateExportHeader)
+generate_export_header(mysql_shim)
+
+target_link_libraries(mysql_shim ${MYSQL_LIB})
+
+set_target_properties (
+ mysql_shim PROPERTIES
+ FOLDER Lib
+ VERSION ${SPEC_VERSION}
+)
+
+install(
+ TARGETS mysql_shim
+ RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}
+ LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2
+)
diff --git a/lib/mysql_shim/mysql_shim.def b/lib/mysql_shim/mysql_shim.def
new file mode 100644
index 0000000..ae36765
--- /dev/null
+++ b/lib/mysql_shim/mysql_shim.def
@@ -0,0 +1,3 @@
+LIBRARY mysql_shim
+EXPORTS
+ create_mysql_shim
diff --git a/lib/mysql_shim/mysqlinterface.cpp b/lib/mysql_shim/mysqlinterface.cpp
new file mode 100644
index 0000000..43e50e8
--- /dev/null
+++ b/lib/mysql_shim/mysqlinterface.cpp
@@ -0,0 +1,119 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "mysql_shim/mysqlinterface.hpp"
+
+using namespace icinga;
+
+struct MysqlInterfaceImpl final : public MysqlInterface
+{
+ void Destroy() override
+ {
+ delete this;
+ }
+
+ my_ulonglong affected_rows(MYSQL *mysql) const override
+ {
+ return mysql_affected_rows(mysql);
+ }
+
+ void close(MYSQL *sock) const override
+ {
+ return mysql_close(sock);
+ }
+
+ const char *error(MYSQL *mysql) const override
+ {
+ return mysql_error(mysql);
+ }
+
+ MYSQL_FIELD *fetch_field(MYSQL_RES *result) const override
+ {
+ return mysql_fetch_field(result);
+ }
+
+ unsigned long *fetch_lengths(MYSQL_RES *result) const override
+ {
+ return mysql_fetch_lengths(result);
+ }
+
+ MYSQL_ROW fetch_row(MYSQL_RES *result) const override
+ {
+ return mysql_fetch_row(result);
+ }
+
+ unsigned int field_count(MYSQL *mysql) const override
+ {
+ return mysql_field_count(mysql);
+ }
+
+ MYSQL_FIELD_OFFSET field_seek(MYSQL_RES *result, MYSQL_FIELD_OFFSET offset) const override
+ {
+ return mysql_field_seek(result, offset);
+ }
+
+ void free_result(MYSQL_RES *result) const override
+ {
+ mysql_free_result(result);
+ }
+
+ MYSQL *init(MYSQL *mysql) const override
+ {
+ return mysql_init(mysql);
+ }
+
+ my_ulonglong insert_id(MYSQL *mysql) const override
+ {
+ return mysql_insert_id(mysql);
+ }
+
+ int next_result(MYSQL *mysql) const override
+ {
+ return mysql_next_result(mysql);
+ }
+
+ int ping(MYSQL *mysql) const override
+ {
+ return mysql_ping(mysql);
+ }
+
+ int query(MYSQL *mysql, const char *q) const override
+ {
+ return mysql_query(mysql, q);
+ }
+
+ MYSQL *real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd,
+ const char *db, unsigned int port, const char *unix_socket, unsigned long clientflag) const override
+ {
+ return mysql_real_connect(mysql, host, user, passwd, db, port, unix_socket, clientflag);
+ }
+
+ unsigned long real_escape_string(MYSQL *mysql, char *to, const char *from, unsigned long length) const override
+ {
+ return mysql_real_escape_string(mysql, to, from, length);
+ }
+
+ int options(MYSQL *mysql, mysql_option option, const void *arg) const override
+ {
+ return mysql_options(mysql, option, arg);
+ }
+
+ bool ssl_set(MYSQL *mysql, const char *key, const char *cert, const char *ca, const char *capath, const char *cipher) const override
+ {
+ return mysql_ssl_set(mysql, key, cert, ca, capath, cipher);
+ }
+
+ MYSQL_RES *store_result(MYSQL *mysql) const override
+ {
+ return mysql_store_result(mysql);
+ }
+
+ unsigned int thread_safe() const override
+ {
+ return mysql_thread_safe();
+ }
+};
+
+MysqlInterface *create_mysql_shim()
+{
+ return new MysqlInterfaceImpl();
+}
diff --git a/lib/mysql_shim/mysqlinterface.hpp b/lib/mysql_shim/mysqlinterface.hpp
new file mode 100644
index 0000000..04dfc30
--- /dev/null
+++ b/lib/mysql_shim/mysqlinterface.hpp
@@ -0,0 +1,65 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MYSQLINTERFACE_H
+#define MYSQLINTERFACE_H
+
+#include "mysql_shim/mysql_shim_export.h"
+#include <memory>
+#include <mysql.h>
+
+namespace icinga
+{
+
+struct MysqlInterface
+{
+ MysqlInterface(const MysqlInterface&) = delete;
+ MysqlInterface& operator=(MysqlInterface&) = delete;
+
+ virtual void Destroy() = 0;
+
+ virtual my_ulonglong affected_rows(MYSQL *mysql) const = 0;
+ virtual void close(MYSQL *sock) const = 0;
+ virtual const char *error(MYSQL *mysql) const = 0;
+ virtual MYSQL_FIELD *fetch_field(MYSQL_RES *result) const = 0;
+ virtual unsigned long *fetch_lengths(MYSQL_RES *result) const = 0;
+ virtual MYSQL_ROW fetch_row(MYSQL_RES *result) const = 0;
+ virtual unsigned int field_count(MYSQL *mysql) const = 0;
+ virtual MYSQL_FIELD_OFFSET field_seek(MYSQL_RES *result,
+ MYSQL_FIELD_OFFSET offset) const = 0;
+ virtual void free_result(MYSQL_RES *result) const = 0;
+ virtual MYSQL *init(MYSQL *mysql) const = 0;
+ virtual my_ulonglong insert_id(MYSQL *mysql) const = 0;
+ virtual int next_result(MYSQL *mysql) const = 0;
+ virtual int ping(MYSQL *mysql) const = 0;
+ virtual int query(MYSQL *mysql, const char *q) const = 0;
+ virtual MYSQL *real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd,
+ const char *db, unsigned int port, const char *unix_socket, unsigned long clientflag) const = 0;
+ virtual unsigned long real_escape_string(MYSQL *mysql, char *to, const char *from, unsigned long length) const = 0;
+ virtual int options(MYSQL *mysql, mysql_option option, const void *arg) const = 0;
+ virtual bool ssl_set(MYSQL *mysql, const char *key, const char *cert, const char *ca, const char *capath, const char *cipher) const = 0;
+ virtual MYSQL_RES *store_result(MYSQL *mysql) const = 0;
+ virtual unsigned int thread_safe() const = 0;
+
+protected:
+ MysqlInterface() = default;
+ ~MysqlInterface() = default;
+};
+
+struct MysqlInterfaceDeleter
+{
+ void operator()(MysqlInterface *ifc) const
+ {
+ ifc->Destroy();
+ }
+};
+
+}
+
+extern "C"
+{
+ MYSQL_SHIM_EXPORT icinga::MysqlInterface *create_mysql_shim();
+}
+
+typedef icinga::MysqlInterface *(*create_mysql_shim_ptr)();
+
+#endif /* MYSQLINTERFACE_H */
diff --git a/lib/notification/CMakeLists.txt b/lib/notification/CMakeLists.txt
new file mode 100644
index 0000000..783b4fa
--- /dev/null
+++ b/lib/notification/CMakeLists.txt
@@ -0,0 +1,34 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(notificationcomponent.ti notificationcomponent-ti.cpp notificationcomponent-ti.hpp)
+
+set(notification_SOURCES
+ notificationcomponent.cpp notificationcomponent.hpp notificationcomponent-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(notification notification notification_SOURCES)
+endif()
+
+add_library(notification OBJECT ${notification_SOURCES})
+
+add_dependencies(notification base config icinga)
+
+set_target_properties (
+ notification PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/notification.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+if(NOT WIN32)
+ install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_CONFIGDIR}/features-enabled\")")
+ install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E create_symlink ../features-available/notification.conf \"\$ENV{DESTDIR}${ICINGA2_FULL_CONFIGDIR}/features-enabled/notification.conf\")")
+else()
+ install_if_not_exists(${PROJECT_SOURCE_DIR}/etc/icinga2/features-enabled/notification.conf ${ICINGA2_CONFIGDIR}/features-enabled)
+endif()
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/notification/notificationcomponent.cpp b/lib/notification/notificationcomponent.cpp
new file mode 100644
index 0000000..982a838
--- /dev/null
+++ b/lib/notification/notificationcomponent.cpp
@@ -0,0 +1,271 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "notification/notificationcomponent.hpp"
+#include "notification/notificationcomponent-ti.cpp"
+#include "icinga/service.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/statsfunction.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(NotificationComponent);
+
+REGISTER_STATSFUNCTION(NotificationComponent, &NotificationComponent::StatsFunc);
+
+void NotificationComponent::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const NotificationComponent::Ptr& notification_component : ConfigType::GetObjectsByType<NotificationComponent>()) {
+ nodes.emplace_back(notification_component->GetName(), 1); //add more stats
+ }
+
+ status->Set("notificationcomponent", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * Starts the component.
+ */
+void NotificationComponent::Start(bool runtimeCreated)
+{
+ ObjectImpl<NotificationComponent>::Start(runtimeCreated);
+
+ Log(LogInformation, "NotificationComponent")
+ << "'" << GetName() << "' started.";
+
+ Checkable::OnNotificationsRequested.connect([this](const Checkable::Ptr& checkable, NotificationType type, const CheckResult::Ptr& cr,
+ const String& author, const String& text, const MessageOrigin::Ptr&) {
+ SendNotificationsHandler(checkable, type, cr, author, text);
+ });
+
+ m_NotificationTimer = Timer::Create();
+ m_NotificationTimer->SetInterval(5);
+ m_NotificationTimer->OnTimerExpired.connect([this](const Timer * const&) { NotificationTimerHandler(); });
+ m_NotificationTimer->Start();
+}
+
+void NotificationComponent::Stop(bool runtimeRemoved)
+{
+ m_NotificationTimer->Stop(true);
+
+ Log(LogInformation, "NotificationComponent")
+ << "'" << GetName() << "' stopped.";
+
+ ObjectImpl<NotificationComponent>::Stop(runtimeRemoved);
+}
+
+static inline
+void SubtractSuppressedNotificationTypes(const Notification::Ptr& notification, int types)
+{
+ ObjectLock olock (notification);
+
+ int suppressedTypesBefore (notification->GetSuppressedNotifications());
+ int suppressedTypesAfter (suppressedTypesBefore & ~types);
+
+ if (suppressedTypesAfter != suppressedTypesBefore) {
+ notification->SetSuppressedNotifications(suppressedTypesAfter);
+ }
+}
+
+static inline
+void FireSuppressedNotifications(const Notification::Ptr& notification)
+{
+ int suppressedTypes (notification->GetSuppressedNotifications());
+ if (!suppressedTypes)
+ return;
+
+ int subtract = 0;
+ auto checkable (notification->GetCheckable());
+
+ for (auto type : {NotificationProblem, NotificationRecovery, NotificationFlappingStart, NotificationFlappingEnd}) {
+ if ((suppressedTypes & type) && !checkable->NotificationReasonApplies(type)) {
+ subtract |= type;
+ suppressedTypes &= ~type;
+ }
+ }
+
+ if (suppressedTypes) {
+ auto tp (notification->GetPeriod());
+
+ if ((!tp || tp->IsInside(Utility::GetTime())) && !checkable->IsLikelyToBeCheckedSoon()) {
+ for (auto type : {NotificationProblem, NotificationRecovery, NotificationFlappingStart, NotificationFlappingEnd}) {
+ if (!(suppressedTypes & type) || checkable->NotificationReasonSuppressed(type))
+ continue;
+
+ auto notificationName (notification->GetName());
+
+ Log(LogNotice, "NotificationComponent")
+ << "Attempting to re-send previously suppressed notification '" << notificationName << "'.";
+
+ subtract |= type;
+ SubtractSuppressedNotificationTypes(notification, subtract);
+ subtract = 0;
+
+ try {
+ notification->BeginExecuteNotification(type, checkable->GetLastCheckResult(), false, false);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "NotificationComponent")
+ << "Exception occurred during notification for object '"
+ << notificationName << "': " << DiagnosticInformation(ex, false);
+ }
+ }
+ }
+ }
+
+ if (subtract) {
+ SubtractSuppressedNotificationTypes(notification, subtract);
+ }
+}
+
+/**
+ * Periodically sends notifications.
+ *
+ * @param - Event arguments for the timer.
+ */
+void NotificationComponent::NotificationTimerHandler()
+{
+ double now = Utility::GetTime();
+
+ /* Function already checks whether 'api' feature is enabled. */
+ Endpoint::Ptr myEndpoint = Endpoint::GetLocalEndpoint();
+
+ for (const Notification::Ptr& notification : ConfigType::GetObjectsByType<Notification>()) {
+ if (!notification->IsActive())
+ continue;
+
+ String notificationName = notification->GetName();
+ bool updatedObjectAuthority = ApiListener::UpdatedObjectAuthority();
+
+ /* Skip notification if paused, in a cluster setup & HA feature is enabled. */
+ if (notification->IsPaused()) {
+ if (updatedObjectAuthority) {
+ auto stashedNotifications (notification->GetStashedNotifications());
+ ObjectLock olock(stashedNotifications);
+
+ if (stashedNotifications->GetLength()) {
+ Log(LogNotice, "NotificationComponent")
+ << "Notification '" << notificationName << "': HA cluster active, this endpoint does not have the authority. Dropping all stashed notifications.";
+
+ stashedNotifications->Clear();
+ }
+ }
+
+ if (myEndpoint && GetEnableHA()) {
+ Log(LogNotice, "NotificationComponent")
+ << "Reminder notification '" << notificationName << "': HA cluster active, this endpoint does not have the authority (paused=true). Skipping.";
+ continue;
+ }
+ }
+
+ Checkable::Ptr checkable = notification->GetCheckable();
+
+ if (!IcingaApplication::GetInstance()->GetEnableNotifications() || !checkable->GetEnableNotifications())
+ continue;
+
+ bool reachable = checkable->IsReachable(DependencyNotification);
+
+ if (reachable) {
+ {
+ Array::Ptr unstashedNotifications = new Array();
+
+ {
+ auto stashedNotifications (notification->GetStashedNotifications());
+ ObjectLock olock(stashedNotifications);
+
+ stashedNotifications->CopyTo(unstashedNotifications);
+ stashedNotifications->Clear();
+ }
+
+ ObjectLock olock(unstashedNotifications);
+
+ for (Dictionary::Ptr unstashedNotification : unstashedNotifications) {
+ if (!unstashedNotification)
+ continue;
+
+ try {
+ Log(LogNotice, "NotificationComponent")
+ << "Attempting to send stashed notification '" << notificationName << "'.";
+
+ notification->BeginExecuteNotification(
+ (NotificationType)(int)unstashedNotification->Get("notification_type"),
+ (CheckResult::Ptr)unstashedNotification->Get("cr"),
+ (bool)unstashedNotification->Get("force"),
+ (bool)unstashedNotification->Get("reminder"),
+ (String)unstashedNotification->Get("author"),
+ (String)unstashedNotification->Get("text")
+ );
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "NotificationComponent")
+ << "Exception occurred during notification for object '"
+ << notificationName << "': " << DiagnosticInformation(ex, false);
+ }
+ }
+ }
+
+ FireSuppressedNotifications(notification);
+ }
+
+ if (notification->GetInterval() <= 0 && notification->GetNoMoreNotifications()) {
+ Log(LogNotice, "NotificationComponent")
+ << "Reminder notification '" << notificationName << "': Notification was sent out once and interval=0 disables reminder notifications.";
+ continue;
+ }
+
+ if (notification->GetNextNotification() > now)
+ continue;
+
+ {
+ ObjectLock olock(notification);
+ notification->SetNextNotification(Utility::GetTime() + notification->GetInterval());
+ }
+
+ {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ObjectLock olock(checkable);
+
+ if (checkable->GetStateType() == StateTypeSoft)
+ continue;
+
+ /* Don't send reminder notifications for OK/Up states. */
+ if ((service && service->GetState() == ServiceOK) || (!service && host->GetState() == HostUp))
+ continue;
+
+ /* Don't send reminder notifications before initial ones. */
+ if (checkable->GetSuppressedNotifications() & NotificationProblem || notification->GetSuppressedNotifications() & NotificationProblem)
+ continue;
+
+ /* Skip in runtime filters. */
+ if (!reachable || checkable->IsInDowntime() || checkable->IsAcknowledged() || checkable->IsFlapping())
+ continue;
+ }
+
+ try {
+ Log(LogNotice, "NotificationComponent")
+ << "Attempting to send reminder notification '" << notificationName << "'.";
+
+ notification->BeginExecuteNotification(NotificationProblem, checkable->GetLastCheckResult(), false, true);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "NotificationComponent")
+ << "Exception occurred during notification for object '"
+ << notificationName << "': " << DiagnosticInformation(ex, false);
+ }
+ }
+}
+
+/**
+ * Processes icinga::SendNotifications messages.
+ */
+void NotificationComponent::SendNotificationsHandler(const Checkable::Ptr& checkable, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text)
+{
+ checkable->SendNotifications(type, cr, author, text);
+}
diff --git a/lib/notification/notificationcomponent.hpp b/lib/notification/notificationcomponent.hpp
new file mode 100644
index 0000000..09434e2
--- /dev/null
+++ b/lib/notification/notificationcomponent.hpp
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NOTIFICATIONCOMPONENT_H
+#define NOTIFICATIONCOMPONENT_H
+
+#include "notification/notificationcomponent-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/timer.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup notification
+ */
+class NotificationComponent final : public ObjectImpl<NotificationComponent>
+{
+public:
+ DECLARE_OBJECT(NotificationComponent);
+ DECLARE_OBJECTNAME(NotificationComponent);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ Timer::Ptr m_NotificationTimer;
+
+ void NotificationTimerHandler();
+ void SendNotificationsHandler(const Checkable::Ptr& checkable, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text);
+};
+
+}
+
+#endif /* NOTIFICATIONCOMPONENT_H */
diff --git a/lib/notification/notificationcomponent.ti b/lib/notification/notificationcomponent.ti
new file mode 100644
index 0000000..13af136
--- /dev/null
+++ b/lib/notification/notificationcomponent.ti
@@ -0,0 +1,19 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library notification;
+
+namespace icinga
+{
+
+class NotificationComponent : ConfigObject
+{
+ activation_priority 200;
+
+ [config] bool enable_ha (EnableHA) {
+ default {{{ return true; }}}
+ };
+};
+
+}
diff --git a/lib/perfdata/CMakeLists.txt b/lib/perfdata/CMakeLists.txt
new file mode 100644
index 0000000..168938c
--- /dev/null
+++ b/lib/perfdata/CMakeLists.txt
@@ -0,0 +1,74 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(gelfwriter.ti gelfwriter-ti.cpp gelfwriter-ti.hpp)
+mkclass_target(graphitewriter.ti graphitewriter-ti.cpp graphitewriter-ti.hpp)
+mkclass_target(influxdbcommonwriter.ti influxdbcommonwriter-ti.cpp influxdbcommonwriter-ti.hpp)
+mkclass_target(influxdbwriter.ti influxdbwriter-ti.cpp influxdbwriter-ti.hpp)
+mkclass_target(influxdb2writer.ti influxdb2writer-ti.cpp influxdb2writer-ti.hpp)
+mkclass_target(elasticsearchwriter.ti elasticsearchwriter-ti.cpp elasticsearchwriter-ti.hpp)
+mkclass_target(opentsdbwriter.ti opentsdbwriter-ti.cpp opentsdbwriter-ti.hpp)
+mkclass_target(perfdatawriter.ti perfdatawriter-ti.cpp perfdatawriter-ti.hpp)
+
+set(perfdata_SOURCES
+ elasticsearchwriter.cpp elasticsearchwriter.hpp elasticsearchwriter-ti.hpp
+ gelfwriter.cpp gelfwriter.hpp gelfwriter-ti.hpp
+ graphitewriter.cpp graphitewriter.hpp graphitewriter-ti.hpp
+ influxdbcommonwriter.cpp influxdbcommonwriter.hpp influxdbcommonwriter-ti.hpp
+ influxdbwriter.cpp influxdbwriter.hpp influxdbwriter-ti.hpp
+ influxdb2writer.cpp influxdb2writer.hpp influxdb2writer-ti.hpp
+ opentsdbwriter.cpp opentsdbwriter.hpp opentsdbwriter-ti.hpp
+ perfdatawriter.cpp perfdatawriter.hpp perfdatawriter-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(perfdata perfdata perfdata_SOURCES)
+endif()
+
+add_library(perfdata OBJECT ${perfdata_SOURCES})
+
+add_dependencies(perfdata base config icinga)
+
+set_target_properties (
+ perfdata PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/gelf.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/graphite.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/influxdb.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/influxdb2.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/elasticsearch.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/opentsdb.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/perfdata.conf
+ ${ICINGA2_CONFIGDIR}/features-available
+)
+
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_SPOOLDIR}/perfdata\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_SPOOLDIR}/tmp\")")
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/perfdata/elasticsearchwriter.cpp b/lib/perfdata/elasticsearchwriter.cpp
new file mode 100644
index 0000000..9fb2aa9
--- /dev/null
+++ b/lib/perfdata/elasticsearchwriter.cpp
@@ -0,0 +1,685 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/elasticsearchwriter.hpp"
+#include "perfdata/elasticsearchwriter-ti.cpp"
+#include "remote/url.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/service.hpp"
+#include "icinga/checkcommand.hpp"
+#include "base/application.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/stream.hpp"
+#include "base/base64.hpp"
+#include "base/json.hpp"
+#include "base/utility.hpp"
+#include "base/networkstream.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/exception.hpp"
+#include "base/statsfunction.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/beast/core/flat_buffer.hpp>
+#include <boost/beast/http/field.hpp>
+#include <boost/beast/http/message.hpp>
+#include <boost/beast/http/parser.hpp>
+#include <boost/beast/http/read.hpp>
+#include <boost/beast/http/status.hpp>
+#include <boost/beast/http/string_body.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <boost/beast/http/write.hpp>
+#include <boost/scoped_array.hpp>
+#include <memory>
+#include <string>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(ElasticsearchWriter);
+
+REGISTER_STATSFUNCTION(ElasticsearchWriter, &ElasticsearchWriter::StatsFunc);
+
+void ElasticsearchWriter::OnConfigLoaded()
+{
+ ObjectImpl<ElasticsearchWriter>::OnConfigLoaded();
+
+ m_WorkQueue.SetName("ElasticsearchWriter, " + GetName());
+
+ if (!GetEnableHa()) {
+ Log(LogDebug, "ElasticsearchWriter")
+ << "HA functionality disabled. Won't pause connection: " << GetName();
+
+ SetHAMode(HARunEverywhere);
+ } else {
+ SetHAMode(HARunOnce);
+ }
+}
+
+void ElasticsearchWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const ElasticsearchWriter::Ptr& elasticsearchwriter : ConfigType::GetObjectsByType<ElasticsearchWriter>()) {
+ size_t workQueueItems = elasticsearchwriter->m_WorkQueue.GetLength();
+ double workQueueItemRate = elasticsearchwriter->m_WorkQueue.GetTaskCount(60) / 60.0;
+
+ nodes.emplace_back(elasticsearchwriter->GetName(), new Dictionary({
+ { "work_queue_items", workQueueItems },
+ { "work_queue_item_rate", workQueueItemRate }
+ }));
+
+ perfdata->Add(new PerfdataValue("elasticsearchwriter_" + elasticsearchwriter->GetName() + "_work_queue_items", workQueueItems));
+ perfdata->Add(new PerfdataValue("elasticsearchwriter_" + elasticsearchwriter->GetName() + "_work_queue_item_rate", workQueueItemRate));
+ }
+
+ status->Set("elasticsearchwriter", new Dictionary(std::move(nodes)));
+}
+
+void ElasticsearchWriter::Resume()
+{
+ ObjectImpl<ElasticsearchWriter>::Resume();
+
+ m_EventPrefix = "icinga2.event.";
+
+ Log(LogInformation, "ElasticsearchWriter")
+ << "'" << GetName() << "' resumed.";
+
+ m_WorkQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); });
+
+ /* Setup timer for periodically flushing m_DataBuffer */
+ m_FlushTimer = Timer::Create();
+ m_FlushTimer->SetInterval(GetFlushInterval());
+ m_FlushTimer->OnTimerExpired.connect([this](const Timer * const&) { FlushTimeout(); });
+ m_FlushTimer->Start();
+ m_FlushTimer->Reschedule(0);
+
+ /* Register for new metrics. */
+ m_HandleCheckResults = Checkable::OnNewCheckResult.connect([this](const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ CheckResultHandler(checkable, cr);
+ });
+ m_HandleStateChanges = Checkable::OnStateChange.connect([this](const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr&) {
+ StateChangeHandler(checkable, cr, type);
+ });
+ m_HandleNotifications = Checkable::OnNotificationSentToAllUsers.connect([this](const Notification::Ptr& notification,
+ const Checkable::Ptr& checkable, const std::set<User::Ptr>& users, const NotificationType& type,
+ const CheckResult::Ptr& cr, const String& author, const String& text, const MessageOrigin::Ptr&) {
+ NotificationSentToAllUsersHandler(notification, checkable, users, type, cr, author, text);
+ });
+}
+
+/* Pause is equivalent to Stop, but with HA capabilities to resume at runtime. */
+void ElasticsearchWriter::Pause()
+{
+ m_HandleCheckResults.disconnect();
+ m_HandleStateChanges.disconnect();
+ m_HandleNotifications.disconnect();
+
+ m_FlushTimer->Stop(true);
+ m_WorkQueue.Join();
+
+ {
+ std::unique_lock<std::mutex> lock (m_DataBufferMutex);
+ Flush();
+ }
+
+ Log(LogInformation, "ElasticsearchWriter")
+ << "'" << GetName() << "' paused.";
+
+ ObjectImpl<ElasticsearchWriter>::Pause();
+}
+
+void ElasticsearchWriter::AddCheckResult(const Dictionary::Ptr& fields, const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ String prefix = "check_result.";
+
+ fields->Set(prefix + "output", cr->GetOutput());
+ fields->Set(prefix + "check_source", cr->GetCheckSource());
+ fields->Set(prefix + "exit_status", cr->GetExitStatus());
+ fields->Set(prefix + "command", cr->GetCommand());
+ fields->Set(prefix + "state", cr->GetState());
+ fields->Set(prefix + "vars_before", cr->GetVarsBefore());
+ fields->Set(prefix + "vars_after", cr->GetVarsAfter());
+
+ fields->Set(prefix + "execution_start", FormatTimestamp(cr->GetExecutionStart()));
+ fields->Set(prefix + "execution_end", FormatTimestamp(cr->GetExecutionEnd()));
+ fields->Set(prefix + "schedule_start", FormatTimestamp(cr->GetScheduleStart()));
+ fields->Set(prefix + "schedule_end", FormatTimestamp(cr->GetScheduleEnd()));
+
+ /* Add extra calculated field. */
+ fields->Set(prefix + "latency", cr->CalculateLatency());
+ fields->Set(prefix + "execution_time", cr->CalculateExecutionTime());
+
+ if (!GetEnableSendPerfdata())
+ return;
+
+ Array::Ptr perfdata = cr->GetPerformanceData();
+
+ CheckCommand::Ptr checkCommand = checkable->GetCheckCommand();
+
+ if (perfdata) {
+ ObjectLock olock(perfdata);
+ for (const Value& val : perfdata) {
+ PerfdataValue::Ptr pdv;
+
+ if (val.IsObjectType<PerfdataValue>())
+ pdv = val;
+ else {
+ try {
+ pdv = PerfdataValue::Parse(val);
+ } catch (const std::exception&) {
+ Log(LogWarning, "ElasticsearchWriter")
+ << "Ignoring invalid perfdata for checkable '"
+ << checkable->GetName() << "' and command '"
+ << checkCommand->GetName() << "' with value: " << val;
+ continue;
+ }
+ }
+
+ String escapedKey = pdv->GetLabel();
+ boost::replace_all(escapedKey, " ", "_");
+ boost::replace_all(escapedKey, ".", "_");
+ boost::replace_all(escapedKey, "\\", "_");
+ boost::algorithm::replace_all(escapedKey, "::", ".");
+
+ String perfdataPrefix = prefix + "perfdata." + escapedKey;
+
+ fields->Set(perfdataPrefix + ".value", pdv->GetValue());
+
+ if (!pdv->GetMin().IsEmpty())
+ fields->Set(perfdataPrefix + ".min", pdv->GetMin());
+ if (!pdv->GetMax().IsEmpty())
+ fields->Set(perfdataPrefix + ".max", pdv->GetMax());
+ if (!pdv->GetWarn().IsEmpty())
+ fields->Set(perfdataPrefix + ".warn", pdv->GetWarn());
+ if (!pdv->GetCrit().IsEmpty())
+ fields->Set(perfdataPrefix + ".crit", pdv->GetCrit());
+
+ if (!pdv->GetUnit().IsEmpty())
+ fields->Set(perfdataPrefix + ".unit", pdv->GetUnit());
+ }
+ }
+}
+
+void ElasticsearchWriter::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this, checkable, cr]() { InternalCheckResultHandler(checkable, cr); });
+}
+
+void ElasticsearchWriter::InternalCheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("Elasticwriter processing check result for '" << checkable->GetName() << "'");
+
+ if (!IcingaApplication::GetInstance()->GetEnablePerfdata() || !checkable->GetEnablePerfdata())
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr fields = new Dictionary();
+
+ if (service) {
+ fields->Set("service", service->GetShortName());
+ fields->Set("state", service->GetState());
+ fields->Set("last_state", service->GetLastState());
+ fields->Set("last_hard_state", service->GetLastHardState());
+ } else {
+ fields->Set("state", host->GetState());
+ fields->Set("last_state", host->GetLastState());
+ fields->Set("last_hard_state", host->GetLastHardState());
+ }
+
+ fields->Set("host", host->GetName());
+ fields->Set("state_type", checkable->GetStateType());
+
+ fields->Set("current_check_attempt", checkable->GetCheckAttempt());
+ fields->Set("max_check_attempts", checkable->GetMaxCheckAttempts());
+
+ fields->Set("reachable", checkable->IsReachable());
+
+ CheckCommand::Ptr commandObj = checkable->GetCheckCommand();
+
+ if (commandObj)
+ fields->Set("check_command", commandObj->GetName());
+
+ double ts = Utility::GetTime();
+
+ if (cr) {
+ AddCheckResult(fields, checkable, cr);
+ ts = cr->GetExecutionEnd();
+ }
+
+ Enqueue(checkable, "checkresult", fields, ts);
+}
+
+void ElasticsearchWriter::StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type)
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this, checkable, cr, type]() { StateChangeHandlerInternal(checkable, cr, type); });
+}
+
+void ElasticsearchWriter::StateChangeHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type)
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("Elasticwriter processing state change '" << checkable->GetName() << "'");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr fields = new Dictionary();
+
+ fields->Set("current_check_attempt", checkable->GetCheckAttempt());
+ fields->Set("max_check_attempts", checkable->GetMaxCheckAttempts());
+ fields->Set("host", host->GetName());
+
+ if (service) {
+ fields->Set("service", service->GetShortName());
+ fields->Set("state", service->GetState());
+ fields->Set("last_state", service->GetLastState());
+ fields->Set("last_hard_state", service->GetLastHardState());
+ } else {
+ fields->Set("state", host->GetState());
+ fields->Set("last_state", host->GetLastState());
+ fields->Set("last_hard_state", host->GetLastHardState());
+ }
+
+ CheckCommand::Ptr commandObj = checkable->GetCheckCommand();
+
+ if (commandObj)
+ fields->Set("check_command", commandObj->GetName());
+
+ double ts = Utility::GetTime();
+
+ if (cr) {
+ AddCheckResult(fields, checkable, cr);
+ ts = cr->GetExecutionEnd();
+ }
+
+ Enqueue(checkable, "statechange", fields, ts);
+}
+
+void ElasticsearchWriter::NotificationSentToAllUsersHandler(const Notification::Ptr& notification,
+ const Checkable::Ptr& checkable, const std::set<User::Ptr>& users, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text)
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this, notification, checkable, users, type, cr, author, text]() {
+ NotificationSentToAllUsersHandlerInternal(notification, checkable, users, type, cr, author, text);
+ });
+}
+
+void ElasticsearchWriter::NotificationSentToAllUsersHandlerInternal(const Notification::Ptr& notification,
+ const Checkable::Ptr& checkable, const std::set<User::Ptr>& users, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text)
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("Elasticwriter processing notification to all users '" << checkable->GetName() << "'");
+
+ Log(LogDebug, "ElasticsearchWriter")
+ << "Processing notification for '" << checkable->GetName() << "'";
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ String notificationTypeString = Notification::NotificationTypeToStringCompat(type); //TODO: Change that to our own types.
+
+ Dictionary::Ptr fields = new Dictionary();
+
+ if (service) {
+ fields->Set("service", service->GetShortName());
+ fields->Set("state", service->GetState());
+ fields->Set("last_state", service->GetLastState());
+ fields->Set("last_hard_state", service->GetLastHardState());
+ } else {
+ fields->Set("state", host->GetState());
+ fields->Set("last_state", host->GetLastState());
+ fields->Set("last_hard_state", host->GetLastHardState());
+ }
+
+ fields->Set("host", host->GetName());
+
+ ArrayData userNames;
+
+ for (const User::Ptr& user : users) {
+ userNames.push_back(user->GetName());
+ }
+
+ fields->Set("users", new Array(std::move(userNames)));
+ fields->Set("notification_type", notificationTypeString);
+ fields->Set("author", author);
+ fields->Set("text", text);
+
+ CheckCommand::Ptr commandObj = checkable->GetCheckCommand();
+
+ if (commandObj)
+ fields->Set("check_command", commandObj->GetName());
+
+ double ts = Utility::GetTime();
+
+ if (cr) {
+ AddCheckResult(fields, checkable, cr);
+ ts = cr->GetExecutionEnd();
+ }
+
+ Enqueue(checkable, "notification", fields, ts);
+}
+
+void ElasticsearchWriter::Enqueue(const Checkable::Ptr& checkable, const String& type,
+ const Dictionary::Ptr& fields, double ts)
+{
+ /* Atomically buffer the data point. */
+ std::unique_lock<std::mutex> lock(m_DataBufferMutex);
+
+ /* Format the timestamps to dynamically select the date datatype inside the index. */
+ fields->Set("@timestamp", FormatTimestamp(ts));
+ fields->Set("timestamp", FormatTimestamp(ts));
+
+ String eventType = m_EventPrefix + type;
+ fields->Set("type", eventType);
+
+ /* Every payload needs a line describing the index.
+ * We do it this way to avoid problems with a near full queue.
+ */
+ String indexBody = "{\"index\": {} }\n";
+ String fieldsBody = JsonEncode(fields);
+
+ Log(LogDebug, "ElasticsearchWriter")
+ << "Checkable '" << checkable->GetName() << "' adds to metric list: '" << fieldsBody << "'.";
+
+ m_DataBuffer.emplace_back(indexBody + fieldsBody);
+
+ /* Flush if we've buffered too much to prevent excessive memory use. */
+ if (static_cast<int>(m_DataBuffer.size()) >= GetFlushThreshold()) {
+ Log(LogDebug, "ElasticsearchWriter")
+ << "Data buffer overflow writing " << m_DataBuffer.size() << " data points";
+ Flush();
+ }
+}
+
+void ElasticsearchWriter::FlushTimeout()
+{
+ /* Prevent new data points from being added to the array, there is a
+ * race condition where they could disappear.
+ */
+ std::unique_lock<std::mutex> lock(m_DataBufferMutex);
+
+ /* Flush if there are any data available. */
+ if (m_DataBuffer.size() > 0) {
+ Log(LogDebug, "ElasticsearchWriter")
+ << "Timer expired writing " << m_DataBuffer.size() << " data points";
+ Flush();
+ }
+}
+
+void ElasticsearchWriter::Flush()
+{
+ /* Flush can be called from 1) Timeout 2) Threshold 3) on shutdown/reload. */
+ if (m_DataBuffer.empty())
+ return;
+
+ /* Ensure you hold a lock against m_DataBuffer so that things
+ * don't go missing after creating the body and clearing the buffer.
+ */
+ String body = boost::algorithm::join(m_DataBuffer, "\n");
+ m_DataBuffer.clear();
+
+ /* Elasticsearch 6.x requires a new line. This is compatible to 5.x.
+ * Tested with 6.0.0 and 5.6.4.
+ */
+ body += "\n";
+
+ SendRequest(body);
+}
+
+void ElasticsearchWriter::SendRequest(const String& body)
+{
+ namespace beast = boost::beast;
+ namespace http = beast::http;
+
+ Url::Ptr url = new Url();
+
+ url->SetScheme(GetEnableTls() ? "https" : "http");
+ url->SetHost(GetHost());
+ url->SetPort(GetPort());
+
+ std::vector<String> path;
+
+ /* Specify the index path. Best practice is a daily rotation.
+ * Example: http://localhost:9200/icinga2-2017.09.11?pretty=1
+ */
+ path.emplace_back(GetIndex() + "-" + Utility::FormatDateTime("%Y.%m.%d", Utility::GetTime()));
+
+ /* Use the bulk message format. */
+ path.emplace_back("_bulk");
+
+ url->SetPath(path);
+
+ OptionalTlsStream stream;
+
+ try {
+ stream = Connect();
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ElasticsearchWriter")
+ << "Flush failed, cannot connect to Elasticsearch: " << DiagnosticInformation(ex, false);
+ return;
+ }
+
+ Defer s ([&stream]() {
+ if (stream.first) {
+ stream.first->next_layer().shutdown();
+ }
+ });
+
+ http::request<http::string_body> request (http::verb::post, std::string(url->Format(true)), 10);
+
+ request.set(http::field::user_agent, "Icinga/" + Application::GetAppVersion());
+ request.set(http::field::host, url->GetHost() + ":" + url->GetPort());
+
+ /* Specify required headers by Elasticsearch. */
+ request.set(http::field::accept, "application/json");
+
+ /* Use application/x-ndjson for bulk streams. While ES
+ * is able to handle application/json, the newline separator
+ * causes problems with Logstash (#6609).
+ */
+ request.set(http::field::content_type, "application/x-ndjson");
+
+ /* Send authentication if configured. */
+ String username = GetUsername();
+ String password = GetPassword();
+
+ if (!username.IsEmpty() && !password.IsEmpty())
+ request.set(http::field::authorization, "Basic " + Base64::Encode(username + ":" + password));
+
+ request.body() = body;
+ request.content_length(request.body().size());
+
+ /* Don't log the request body to debug log, this is already done above. */
+ Log(LogDebug, "ElasticsearchWriter")
+ << "Sending " << request.method_string() << " request" << ((!username.IsEmpty() && !password.IsEmpty()) ? " with basic auth" : "" )
+ << " to '" << url->Format() << "'.";
+
+ try {
+ if (stream.first) {
+ http::write(*stream.first, request);
+ stream.first->flush();
+ } else {
+ http::write(*stream.second, request);
+ stream.second->flush();
+ }
+ } catch (const std::exception&) {
+ Log(LogWarning, "ElasticsearchWriter")
+ << "Cannot write to HTTP API on host '" << GetHost() << "' port '" << GetPort() << "'.";
+ throw;
+ }
+
+ http::parser<false, http::string_body> parser;
+ beast::flat_buffer buf;
+
+ try {
+ if (stream.first) {
+ http::read(*stream.first, buf, parser);
+ } else {
+ http::read(*stream.second, buf, parser);
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ElasticsearchWriter")
+ << "Failed to parse HTTP response from host '" << GetHost() << "' port '" << GetPort() << "': " << DiagnosticInformation(ex, false);
+ throw;
+ }
+
+ auto& response (parser.get());
+
+ if (response.result_int() > 299) {
+ if (response.result() == http::status::unauthorized) {
+ /* More verbose error logging with Elasticsearch is hidden behind a proxy. */
+ if (!username.IsEmpty() && !password.IsEmpty()) {
+ Log(LogCritical, "ElasticsearchWriter")
+ << "401 Unauthorized. Please ensure that the user '" << username
+ << "' is able to authenticate against the HTTP API/Proxy.";
+ } else {
+ Log(LogCritical, "ElasticsearchWriter")
+ << "401 Unauthorized. The HTTP API requires authentication but no username/password has been configured.";
+ }
+
+ return;
+ }
+
+ std::ostringstream msgbuf;
+ msgbuf << "Unexpected response code " << response.result_int() << " from URL '" << url->Format() << "'";
+
+ auto& contentType (response[http::field::content_type]);
+
+ if (contentType != "application/json" && contentType != "application/json; charset=utf-8") {
+ msgbuf << "; Unexpected Content-Type: '" << contentType << "'";
+ }
+
+ auto& body (response.body());
+
+#ifdef I2_DEBUG
+ msgbuf << "; Response body: '" << body << "'";
+#endif /* I2_DEBUG */
+
+ Dictionary::Ptr jsonResponse;
+
+ try {
+ jsonResponse = JsonDecode(body);
+ } catch (...) {
+ Log(LogWarning, "ElasticsearchWriter")
+ << "Unable to parse JSON response:\n" << body;
+ return;
+ }
+
+ String error = jsonResponse->Get("error");
+
+ Log(LogCritical, "ElasticsearchWriter")
+ << "Error: '" << error << "'. " << msgbuf.str();
+ }
+}
+
+OptionalTlsStream ElasticsearchWriter::Connect()
+{
+ Log(LogNotice, "ElasticsearchWriter")
+ << "Connecting to Elasticsearch on host '" << GetHost() << "' port '" << GetPort() << "'.";
+
+ OptionalTlsStream stream;
+ bool tls = GetEnableTls();
+
+ if (tls) {
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext(GetCertPath(), GetKeyPath(), GetCaPath());
+ } catch (const std::exception&) {
+ Log(LogWarning, "ElasticsearchWriter")
+ << "Unable to create SSL context.";
+ throw;
+ }
+
+ stream.first = Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, GetHost());
+
+ } else {
+ stream.second = Shared<AsioTcpStream>::Make(IoEngine::Get().GetIoContext());
+ }
+
+ try {
+ icinga::Connect(tls ? stream.first->lowest_layer() : stream.second->lowest_layer(), GetHost(), GetPort());
+ } catch (const std::exception&) {
+ Log(LogWarning, "ElasticsearchWriter")
+ << "Can't connect to Elasticsearch on host '" << GetHost() << "' port '" << GetPort() << "'.";
+ throw;
+ }
+
+ if (tls) {
+ auto& tlsStream (stream.first->next_layer());
+
+ try {
+ tlsStream.handshake(tlsStream.client);
+ } catch (const std::exception&) {
+ Log(LogWarning, "ElasticsearchWriter")
+ << "TLS handshake with host '" << GetHost() << "' on port " << GetPort() << " failed.";
+ throw;
+ }
+
+ if (!GetInsecureNoverify()) {
+ if (!tlsStream.GetPeerCertificate()) {
+ BOOST_THROW_EXCEPTION(std::runtime_error("Elasticsearch didn't present any TLS certificate."));
+ }
+
+ if (!tlsStream.IsVerifyOK()) {
+ BOOST_THROW_EXCEPTION(std::runtime_error(
+ "TLS certificate validation failed: " + std::string(tlsStream.GetVerifyError())
+ ));
+ }
+ }
+ }
+
+ return stream;
+}
+
+void ElasticsearchWriter::AssertOnWorkQueue()
+{
+ ASSERT(m_WorkQueue.IsWorkerThread());
+}
+
+void ElasticsearchWriter::ExceptionHandler(boost::exception_ptr exp)
+{
+ Log(LogCritical, "ElasticsearchWriter", "Exception during Elastic operation: Verify that your backend is operational!");
+
+ Log(LogDebug, "ElasticsearchWriter")
+ << "Exception during Elasticsearch operation: " << DiagnosticInformation(std::move(exp));
+}
+
+String ElasticsearchWriter::FormatTimestamp(double ts)
+{
+ /* The date format must match the default dynamic date detection
+ * pattern in indexes. This enables applications like Kibana to
+ * detect a qualified timestamp index for time-series data.
+ *
+ * Example: 2017-09-11T10:56:21.463+0200
+ *
+ * References:
+ * https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html#date-detection
+ * https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html
+ * https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html
+ */
+ auto milliSeconds = static_cast<int>((ts - static_cast<int>(ts)) * 1000);
+
+ return Utility::FormatDateTime("%Y-%m-%dT%H:%M:%S", ts) + "." + Convert::ToString(milliSeconds) + Utility::FormatDateTime("%z", ts);
+}
diff --git a/lib/perfdata/elasticsearchwriter.hpp b/lib/perfdata/elasticsearchwriter.hpp
new file mode 100644
index 0000000..a988094
--- /dev/null
+++ b/lib/perfdata/elasticsearchwriter.hpp
@@ -0,0 +1,65 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ELASTICSEARCHWRITER_H
+#define ELASTICSEARCHWRITER_H
+
+#include "perfdata/elasticsearchwriter-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/workqueue.hpp"
+#include "base/timer.hpp"
+#include "base/tlsstream.hpp"
+
+namespace icinga
+{
+
+class ElasticsearchWriter final : public ObjectImpl<ElasticsearchWriter>
+{
+public:
+ DECLARE_OBJECT(ElasticsearchWriter);
+ DECLARE_OBJECTNAME(ElasticsearchWriter);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ static String FormatTimestamp(double ts);
+
+protected:
+ void OnConfigLoaded() override;
+ void Resume() override;
+ void Pause() override;
+
+private:
+ String m_EventPrefix;
+ WorkQueue m_WorkQueue{10000000, 1};
+ boost::signals2::connection m_HandleCheckResults, m_HandleStateChanges, m_HandleNotifications;
+ Timer::Ptr m_FlushTimer;
+ std::vector<String> m_DataBuffer;
+ std::mutex m_DataBufferMutex;
+
+ void AddCheckResult(const Dictionary::Ptr& fields, const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+
+ void StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type);
+ void StateChangeHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type);
+ void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void InternalCheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void NotificationSentToAllUsersHandler(const Notification::Ptr& notification,
+ const Checkable::Ptr& checkable, const std::set<User::Ptr>& users, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text);
+ void NotificationSentToAllUsersHandlerInternal(const Notification::Ptr& notification,
+ const Checkable::Ptr& checkable, const std::set<User::Ptr>& users, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text);
+
+ void Enqueue(const Checkable::Ptr& checkable, const String& type,
+ const Dictionary::Ptr& fields, double ts);
+
+ OptionalTlsStream Connect();
+ void AssertOnWorkQueue();
+ void ExceptionHandler(boost::exception_ptr exp);
+ void FlushTimeout();
+ void Flush();
+ void SendRequest(const String& body);
+};
+
+}
+
+#endif /* ELASTICSEARCHWRITER_H */
diff --git a/lib/perfdata/elasticsearchwriter.ti b/lib/perfdata/elasticsearchwriter.ti
new file mode 100644
index 0000000..e3b8e27
--- /dev/null
+++ b/lib/perfdata/elasticsearchwriter.ti
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library perfdata;
+
+namespace icinga
+{
+
+class ElasticsearchWriter : ConfigObject
+{
+ activation_priority 100;
+
+ [config, required] String host {
+ default {{{ return "127.0.0.1"; }}}
+ };
+ [config, required] String port {
+ default {{{ return "9200"; }}}
+ };
+ [config, required] String index {
+ default {{{ return "icinga2"; }}}
+ };
+ [config] bool enable_send_perfdata {
+ default {{{ return false; }}}
+ };
+ [config] String username;
+ [config, no_user_view, no_user_modify] String password;
+
+ [config] bool enable_tls {
+ default {{{ return false; }}}
+ };
+ [config] bool insecure_noverify {
+ default {{{ return false; }}}
+ };
+ [config] String ca_path;
+ [config] String cert_path;
+ [config] String key_path;
+
+ [config] int flush_interval {
+ default {{{ return 10; }}}
+ };
+ [config] int flush_threshold {
+ default {{{ return 1024; }}}
+ };
+ [config] bool enable_ha {
+ default {{{ return false; }}}
+ };
+};
+
+}
diff --git a/lib/perfdata/gelfwriter.cpp b/lib/perfdata/gelfwriter.cpp
new file mode 100644
index 0000000..c5b2bbd
--- /dev/null
+++ b/lib/perfdata/gelfwriter.cpp
@@ -0,0 +1,535 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/gelfwriter.hpp"
+#include "perfdata/gelfwriter-ti.cpp"
+#include "icinga/service.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/compatutility.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/application.hpp"
+#include "base/stream.hpp"
+#include "base/networkstream.hpp"
+#include "base/context.hpp"
+#include "base/exception.hpp"
+#include "base/json.hpp"
+#include "base/statsfunction.hpp"
+#include <boost/algorithm/string/replace.hpp>
+#include <utility>
+#include "base/io-engine.hpp"
+#include <boost/asio/write.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/asio/error.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE(GelfWriter);
+
+REGISTER_STATSFUNCTION(GelfWriter, &GelfWriter::StatsFunc);
+
+void GelfWriter::OnConfigLoaded()
+{
+ ObjectImpl<GelfWriter>::OnConfigLoaded();
+
+ m_WorkQueue.SetName("GelfWriter, " + GetName());
+
+ if (!GetEnableHa()) {
+ Log(LogDebug, "GelfWriter")
+ << "HA functionality disabled. Won't pause connection: " << GetName();
+
+ SetHAMode(HARunEverywhere);
+ } else {
+ SetHAMode(HARunOnce);
+ }
+}
+
+void GelfWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const GelfWriter::Ptr& gelfwriter : ConfigType::GetObjectsByType<GelfWriter>()) {
+ size_t workQueueItems = gelfwriter->m_WorkQueue.GetLength();
+ double workQueueItemRate = gelfwriter->m_WorkQueue.GetTaskCount(60) / 60.0;
+
+ nodes.emplace_back(gelfwriter->GetName(), new Dictionary({
+ { "work_queue_items", workQueueItems },
+ { "work_queue_item_rate", workQueueItemRate },
+ { "connected", gelfwriter->GetConnected() },
+ { "source", gelfwriter->GetSource() }
+ }));
+
+ perfdata->Add(new PerfdataValue("gelfwriter_" + gelfwriter->GetName() + "_work_queue_items", workQueueItems));
+ perfdata->Add(new PerfdataValue("gelfwriter_" + gelfwriter->GetName() + "_work_queue_item_rate", workQueueItemRate));
+ }
+
+ status->Set("gelfwriter", new Dictionary(std::move(nodes)));
+}
+
+void GelfWriter::Resume()
+{
+ ObjectImpl<GelfWriter>::Resume();
+
+ Log(LogInformation, "GelfWriter")
+ << "'" << GetName() << "' resumed.";
+
+ /* Register exception handler for WQ tasks. */
+ m_WorkQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); });
+
+ /* Timer for reconnecting */
+ m_ReconnectTimer = Timer::Create();
+ m_ReconnectTimer->SetInterval(10);
+ m_ReconnectTimer->OnTimerExpired.connect([this](const Timer * const&) { ReconnectTimerHandler(); });
+ m_ReconnectTimer->Start();
+ m_ReconnectTimer->Reschedule(0);
+
+ /* Register event handlers. */
+ m_HandleCheckResults = Checkable::OnNewCheckResult.connect([this](const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ CheckResultHandler(checkable, cr);
+ });
+ m_HandleNotifications = Checkable::OnNotificationSentToUser.connect([this](const Notification::Ptr& notification,
+ const Checkable::Ptr& checkable, const User::Ptr& user, const NotificationType& type, const CheckResult::Ptr& cr,
+ const String& author, const String& commentText, const String& commandName, const MessageOrigin::Ptr&) {
+ NotificationToUserHandler(notification, checkable, user, type, cr, author, commentText, commandName);
+ });
+ m_HandleStateChanges = Checkable::OnStateChange.connect([this](const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr&) {
+ StateChangeHandler(checkable, cr, type);
+ });
+}
+
+/* Pause is equivalent to Stop, but with HA capabilities to resume at runtime. */
+void GelfWriter::Pause()
+{
+ m_HandleCheckResults.disconnect();
+ m_HandleNotifications.disconnect();
+ m_HandleStateChanges.disconnect();
+
+ m_ReconnectTimer->Stop(true);
+
+ m_WorkQueue.Enqueue([this]() {
+ try {
+ ReconnectInternal();
+ } catch (const std::exception&) {
+ Log(LogInformation, "GelfWriter")
+ << "Unable to connect, not flushing buffers. Data may be lost.";
+ }
+ }, PriorityImmediate);
+
+ m_WorkQueue.Enqueue([this]() { DisconnectInternal(); }, PriorityLow);
+ m_WorkQueue.Join();
+
+ Log(LogInformation, "GelfWriter")
+ << "'" << GetName() << "' paused.";
+
+ ObjectImpl<GelfWriter>::Pause();
+}
+
+void GelfWriter::AssertOnWorkQueue()
+{
+ ASSERT(m_WorkQueue.IsWorkerThread());
+}
+
+void GelfWriter::ExceptionHandler(boost::exception_ptr exp)
+{
+ Log(LogCritical, "GelfWriter") << "Exception during Graylog Gelf operation: " << DiagnosticInformation(exp, false);
+ Log(LogDebug, "GelfWriter") << "Exception during Graylog Gelf operation: " << DiagnosticInformation(exp, true);
+
+ DisconnectInternal();
+}
+
+void GelfWriter::Reconnect()
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused()) {
+ SetConnected(false);
+ return;
+ }
+
+ ReconnectInternal();
+}
+
+void GelfWriter::ReconnectInternal()
+{
+ double startTime = Utility::GetTime();
+
+ CONTEXT("Reconnecting to Graylog Gelf '" << GetName() << "'");
+
+ SetShouldConnect(true);
+
+ if (GetConnected())
+ return;
+
+ Log(LogNotice, "GelfWriter")
+ << "Reconnecting to Graylog Gelf on host '" << GetHost() << "' port '" << GetPort() << "'.";
+
+ bool ssl = GetEnableTls();
+
+ if (ssl) {
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext(GetCertPath(), GetKeyPath(), GetCaPath());
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "GelfWriter")
+ << "Unable to create SSL context.";
+ throw;
+ }
+
+ m_Stream.first = Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, GetHost());
+
+ } else {
+ m_Stream.second = Shared<AsioTcpStream>::Make(IoEngine::Get().GetIoContext());
+ }
+
+ try {
+ icinga::Connect(ssl ? m_Stream.first->lowest_layer() : m_Stream.second->lowest_layer(), GetHost(), GetPort());
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "GelfWriter")
+ << "Can't connect to Graylog Gelf on host '" << GetHost() << "' port '" << GetPort() << ".'";
+ throw;
+ }
+
+ if (ssl) {
+ auto& tlsStream (m_Stream.first->next_layer());
+
+ try {
+ tlsStream.handshake(tlsStream.client);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "GelfWriter")
+ << "TLS handshake with host '" << GetHost() << " failed.'";
+ throw;
+ }
+
+ if (!GetInsecureNoverify()) {
+ if (!tlsStream.GetPeerCertificate()) {
+ BOOST_THROW_EXCEPTION(std::runtime_error("Graylog Gelf didn't present any TLS certificate."));
+ }
+
+ if (!tlsStream.IsVerifyOK()) {
+ BOOST_THROW_EXCEPTION(std::runtime_error(
+ "TLS certificate validation failed: " + std::string(tlsStream.GetVerifyError())
+ ));
+ }
+ }
+ }
+
+ SetConnected(true);
+
+ Log(LogInformation, "GelfWriter")
+ << "Finished reconnecting to Graylog Gelf in " << std::setw(2) << Utility::GetTime() - startTime << " second(s).";
+}
+
+void GelfWriter::ReconnectTimerHandler()
+{
+ m_WorkQueue.Enqueue([this]() { Reconnect(); }, PriorityNormal);
+}
+
+void GelfWriter::Disconnect()
+{
+ AssertOnWorkQueue();
+
+ DisconnectInternal();
+}
+
+void GelfWriter::DisconnectInternal()
+{
+ if (!GetConnected())
+ return;
+
+ if (m_Stream.first) {
+ boost::system::error_code ec;
+ m_Stream.first->next_layer().shutdown(ec);
+
+ // https://stackoverflow.com/a/25703699
+ // As long as the error code's category is not an SSL category, then the protocol was securely shutdown
+ if (ec.category() == boost::asio::error::get_ssl_category()) {
+ Log(LogCritical, "GelfWriter")
+ << "TLS shutdown with host '" << GetHost() << "' could not be done securely.";
+ }
+ } else if (m_Stream.second) {
+ m_Stream.second->close();
+ }
+
+ SetConnected(false);
+
+}
+
+void GelfWriter::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this, checkable, cr]() { CheckResultHandlerInternal(checkable, cr); });
+}
+
+void GelfWriter::CheckResultHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("GELF Processing check result for '" << checkable->GetName() << "'");
+
+ Log(LogDebug, "GelfWriter")
+ << "Processing check result for '" << checkable->GetName() << "'";
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr fields = new Dictionary();
+
+ if (service) {
+ fields->Set("_service_name", service->GetShortName());
+ fields->Set("_service_state", Service::StateToString(service->GetState()));
+ fields->Set("_last_state", service->GetLastState());
+ fields->Set("_last_hard_state", service->GetLastHardState());
+ } else {
+ fields->Set("_last_state", host->GetLastState());
+ fields->Set("_last_hard_state", host->GetLastHardState());
+ }
+
+ fields->Set("_hostname", host->GetName());
+ fields->Set("_type", "CHECK RESULT");
+ fields->Set("_state", service ? Service::StateToString(service->GetState()) : Host::StateToString(host->GetState()));
+
+ fields->Set("_current_check_attempt", checkable->GetCheckAttempt());
+ fields->Set("_max_check_attempts", checkable->GetMaxCheckAttempts());
+
+ fields->Set("_reachable", checkable->IsReachable());
+
+ CheckCommand::Ptr checkCommand = checkable->GetCheckCommand();
+
+ if (checkCommand)
+ fields->Set("_check_command", checkCommand->GetName());
+
+ double ts = Utility::GetTime();
+
+ if (cr) {
+ fields->Set("_latency", cr->CalculateLatency());
+ fields->Set("_execution_time", cr->CalculateExecutionTime());
+ fields->Set("short_message", CompatUtility::GetCheckResultOutput(cr));
+ fields->Set("full_message", cr->GetOutput());
+ fields->Set("_check_source", cr->GetCheckSource());
+ ts = cr->GetExecutionEnd();
+ }
+
+ if (cr && GetEnableSendPerfdata()) {
+ Array::Ptr perfdata = cr->GetPerformanceData();
+
+ if (perfdata) {
+ ObjectLock olock(perfdata);
+ for (const Value& val : perfdata) {
+ PerfdataValue::Ptr pdv;
+
+ if (val.IsObjectType<PerfdataValue>())
+ pdv = val;
+ else {
+ try {
+ pdv = PerfdataValue::Parse(val);
+ } catch (const std::exception&) {
+ Log(LogWarning, "GelfWriter")
+ << "Ignoring invalid perfdata for checkable '"
+ << checkable->GetName() << "' and command '"
+ << checkCommand->GetName() << "' with value: " << val;
+ continue;
+ }
+ }
+
+ String escaped_key = pdv->GetLabel();
+ boost::replace_all(escaped_key, " ", "_");
+ boost::replace_all(escaped_key, ".", "_");
+ boost::replace_all(escaped_key, "\\", "_");
+ boost::algorithm::replace_all(escaped_key, "::", ".");
+
+ fields->Set("_" + escaped_key, pdv->GetValue());
+
+ if (!pdv->GetMin().IsEmpty())
+ fields->Set("_" + escaped_key + "_min", pdv->GetMin());
+ if (!pdv->GetMax().IsEmpty())
+ fields->Set("_" + escaped_key + "_max", pdv->GetMax());
+ if (!pdv->GetWarn().IsEmpty())
+ fields->Set("_" + escaped_key + "_warn", pdv->GetWarn());
+ if (!pdv->GetCrit().IsEmpty())
+ fields->Set("_" + escaped_key + "_crit", pdv->GetCrit());
+
+ if (!pdv->GetUnit().IsEmpty())
+ fields->Set("_" + escaped_key + "_unit", pdv->GetUnit());
+ }
+ }
+ }
+
+ SendLogMessage(checkable, ComposeGelfMessage(fields, GetSource(), ts));
+}
+
+void GelfWriter::NotificationToUserHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const User::Ptr& user, NotificationType notificationType, CheckResult::Ptr const& cr,
+ const String& author, const String& commentText, const String& commandName)
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this, notification, checkable, user, notificationType, cr, author, commentText, commandName]() {
+ NotificationToUserHandlerInternal(notification, checkable, user, notificationType, cr, author, commentText, commandName);
+ });
+}
+
+void GelfWriter::NotificationToUserHandlerInternal(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const User::Ptr& user, NotificationType notificationType, CheckResult::Ptr const& cr,
+ const String& author, const String& commentText, const String& commandName)
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("GELF Processing notification to all users '" << checkable->GetName() << "'");
+
+ Log(LogDebug, "GelfWriter")
+ << "Processing notification for '" << checkable->GetName() << "'";
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ String notificationTypeString = Notification::NotificationTypeToStringCompat(notificationType); //TODO: Change that to our own types.
+
+ String authorComment = "";
+
+ if (notificationType == NotificationCustom || notificationType == NotificationAcknowledgement) {
+ authorComment = author + ";" + commentText;
+ }
+
+ String output;
+ double ts = Utility::GetTime();
+
+ if (cr) {
+ output = CompatUtility::GetCheckResultOutput(cr);
+ ts = cr->GetExecutionEnd();
+ }
+
+ Dictionary::Ptr fields = new Dictionary();
+
+ if (service) {
+ fields->Set("_type", "SERVICE NOTIFICATION");
+ //TODO: fix this to _service_name
+ fields->Set("_service", service->GetShortName());
+ fields->Set("short_message", output);
+ } else {
+ fields->Set("_type", "HOST NOTIFICATION");
+ fields->Set("short_message", output);
+ }
+
+ fields->Set("_state", service ? Service::StateToString(service->GetState()) : Host::StateToString(host->GetState()));
+
+ fields->Set("_hostname", host->GetName());
+ fields->Set("_command", commandName);
+ fields->Set("_notification_type", notificationTypeString);
+ fields->Set("_comment", authorComment);
+
+ CheckCommand::Ptr commandObj = checkable->GetCheckCommand();
+
+ if (commandObj)
+ fields->Set("_check_command", commandObj->GetName());
+
+ SendLogMessage(checkable, ComposeGelfMessage(fields, GetSource(), ts));
+}
+
+void GelfWriter::StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type)
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this, checkable, cr, type]() { StateChangeHandlerInternal(checkable, cr, type); });
+}
+
+void GelfWriter::StateChangeHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type)
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("GELF Processing state change '" << checkable->GetName() << "'");
+
+ Log(LogDebug, "GelfWriter")
+ << "Processing state change for '" << checkable->GetName() << "'";
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr fields = new Dictionary();
+
+ fields->Set("_state", service ? Service::StateToString(service->GetState()) : Host::StateToString(host->GetState()));
+ fields->Set("_type", "STATE CHANGE");
+ fields->Set("_current_check_attempt", checkable->GetCheckAttempt());
+ fields->Set("_max_check_attempts", checkable->GetMaxCheckAttempts());
+ fields->Set("_hostname", host->GetName());
+
+ if (service) {
+ fields->Set("_service_name", service->GetShortName());
+ fields->Set("_service_state", Service::StateToString(service->GetState()));
+ fields->Set("_last_state", service->GetLastState());
+ fields->Set("_last_hard_state", service->GetLastHardState());
+ } else {
+ fields->Set("_last_state", host->GetLastState());
+ fields->Set("_last_hard_state", host->GetLastHardState());
+ }
+
+ CheckCommand::Ptr commandObj = checkable->GetCheckCommand();
+
+ if (commandObj)
+ fields->Set("_check_command", commandObj->GetName());
+
+ double ts = Utility::GetTime();
+
+ if (cr) {
+ fields->Set("short_message", CompatUtility::GetCheckResultOutput(cr));
+ fields->Set("full_message", cr->GetOutput());
+ fields->Set("_check_source", cr->GetCheckSource());
+ ts = cr->GetExecutionEnd();
+ }
+
+ SendLogMessage(checkable, ComposeGelfMessage(fields, GetSource(), ts));
+}
+
+String GelfWriter::ComposeGelfMessage(const Dictionary::Ptr& fields, const String& source, double ts)
+{
+ fields->Set("version", "1.1");
+ fields->Set("host", source);
+ fields->Set("timestamp", ts);
+
+ return JsonEncode(fields);
+}
+
+void GelfWriter::SendLogMessage(const Checkable::Ptr& checkable, const String& gelfMessage)
+{
+ std::ostringstream msgbuf;
+ msgbuf << gelfMessage;
+ msgbuf << '\0';
+
+ String log = msgbuf.str();
+
+ if (!GetConnected())
+ return;
+
+ try {
+ Log(LogDebug, "GelfWriter")
+ << "Checkable '" << checkable->GetName() << "' sending message '" << log << "'.";
+
+ if (m_Stream.first) {
+ boost::asio::write(*m_Stream.first, boost::asio::buffer(msgbuf.str()));
+ m_Stream.first->flush();
+ } else {
+ boost::asio::write(*m_Stream.second, boost::asio::buffer(msgbuf.str()));
+ m_Stream.second->flush();
+ }
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "GelfWriter")
+ << "Cannot write to TCP socket on host '" << GetHost() << "' port '" << GetPort() << "'.";
+
+ throw ex;
+ }
+}
diff --git a/lib/perfdata/gelfwriter.hpp b/lib/perfdata/gelfwriter.hpp
new file mode 100644
index 0000000..ce9ee35
--- /dev/null
+++ b/lib/perfdata/gelfwriter.hpp
@@ -0,0 +1,70 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef GELFWRITER_H
+#define GELFWRITER_H
+
+#include "perfdata/gelfwriter-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include <fstream>
+
+namespace icinga
+{
+
+/**
+ * An Icinga Gelf writer for Graylog.
+ *
+ * @ingroup perfdata
+ */
+class GelfWriter final : public ObjectImpl<GelfWriter>
+{
+public:
+ DECLARE_OBJECT(GelfWriter);
+ DECLARE_OBJECTNAME(GelfWriter);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+protected:
+ void OnConfigLoaded() override;
+ void Resume() override;
+ void Pause() override;
+
+private:
+ OptionalTlsStream m_Stream;
+ WorkQueue m_WorkQueue{10000000, 1};
+
+ boost::signals2::connection m_HandleCheckResults, m_HandleNotifications, m_HandleStateChanges;
+ Timer::Ptr m_ReconnectTimer;
+
+ void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void CheckResultHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void NotificationToUserHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const User::Ptr& user, NotificationType notificationType, const CheckResult::Ptr& cr,
+ const String& author, const String& commentText, const String& commandName);
+ void NotificationToUserHandlerInternal(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const User::Ptr& user, NotificationType notification_type, const CheckResult::Ptr& cr,
+ const String& author, const String& comment_text, const String& command_name);
+ void StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type);
+ void StateChangeHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type);
+
+ String ComposeGelfMessage(const Dictionary::Ptr& fields, const String& source, double ts);
+ void SendLogMessage(const Checkable::Ptr& checkable, const String& gelfMessage);
+
+ void ReconnectTimerHandler();
+
+ void Disconnect();
+ void DisconnectInternal();
+ void Reconnect();
+ void ReconnectInternal();
+
+ void AssertOnWorkQueue();
+
+ void ExceptionHandler(boost::exception_ptr exp);
+};
+
+}
+
+#endif /* GELFWRITER_H */
diff --git a/lib/perfdata/gelfwriter.ti b/lib/perfdata/gelfwriter.ti
new file mode 100644
index 0000000..387ee14
--- /dev/null
+++ b/lib/perfdata/gelfwriter.ti
@@ -0,0 +1,45 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library perfdata;
+
+namespace icinga
+{
+
+class GelfWriter : ConfigObject
+{
+ activation_priority 100;
+
+ [config] String host {
+ default {{{ return "127.0.0.1"; }}}
+ };
+ [config] String port {
+ default {{{ return "12201"; }}}
+ };
+ [config] String source {
+ default {{{ return "icinga2"; }}}
+ };
+ [config] bool enable_send_perfdata {
+ default {{{ return false; }}}
+ };
+
+ [no_user_modify] bool connected;
+ [no_user_modify] bool should_connect {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_ha {
+ default {{{ return false; }}}
+ };
+ [config] bool enable_tls {
+ default {{{ return false; }}}
+ };
+ [config] bool insecure_noverify {
+ default {{{ return false; }}}
+ };
+ [config] String ca_path;
+ [config] String cert_path;
+ [config] String key_path;
+};
+
+}
diff --git a/lib/perfdata/graphitewriter.cpp b/lib/perfdata/graphitewriter.cpp
new file mode 100644
index 0000000..6adae02
--- /dev/null
+++ b/lib/perfdata/graphitewriter.cpp
@@ -0,0 +1,514 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/graphitewriter.hpp"
+#include "perfdata/graphitewriter-ti.cpp"
+#include "icinga/service.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/application.hpp"
+#include "base/stream.hpp"
+#include "base/networkstream.hpp"
+#include "base/exception.hpp"
+#include "base/statsfunction.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(GraphiteWriter);
+
+REGISTER_STATSFUNCTION(GraphiteWriter, &GraphiteWriter::StatsFunc);
+
+/*
+ * Enable HA capabilities once the config object is loaded.
+ */
+void GraphiteWriter::OnConfigLoaded()
+{
+ ObjectImpl<GraphiteWriter>::OnConfigLoaded();
+
+ m_WorkQueue.SetName("GraphiteWriter, " + GetName());
+
+ if (!GetEnableHa()) {
+ Log(LogDebug, "GraphiteWriter")
+ << "HA functionality disabled. Won't pause connection: " << GetName();
+
+ SetHAMode(HARunEverywhere);
+ } else {
+ SetHAMode(HARunOnce);
+ }
+}
+
+/**
+ * Feature stats interface
+ *
+ * @param status Key value pairs for feature stats
+ * @param perfdata Array of PerfdataValue objects
+ */
+void GraphiteWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const GraphiteWriter::Ptr& graphitewriter : ConfigType::GetObjectsByType<GraphiteWriter>()) {
+ size_t workQueueItems = graphitewriter->m_WorkQueue.GetLength();
+ double workQueueItemRate = graphitewriter->m_WorkQueue.GetTaskCount(60) / 60.0;
+
+ nodes.emplace_back(graphitewriter->GetName(), new Dictionary({
+ { "work_queue_items", workQueueItems },
+ { "work_queue_item_rate", workQueueItemRate },
+ { "connected", graphitewriter->GetConnected() }
+ }));
+
+ perfdata->Add(new PerfdataValue("graphitewriter_" + graphitewriter->GetName() + "_work_queue_items", workQueueItems));
+ perfdata->Add(new PerfdataValue("graphitewriter_" + graphitewriter->GetName() + "_work_queue_item_rate", workQueueItemRate));
+ }
+
+ status->Set("graphitewriter", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * Resume is equivalent to Start, but with HA capabilities to resume at runtime.
+ */
+void GraphiteWriter::Resume()
+{
+ ObjectImpl<GraphiteWriter>::Resume();
+
+ Log(LogInformation, "GraphiteWriter")
+ << "'" << GetName() << "' resumed.";
+
+ /* Register exception handler for WQ tasks. */
+ m_WorkQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); });
+
+ /* Timer for reconnecting */
+ m_ReconnectTimer = Timer::Create();
+ m_ReconnectTimer->SetInterval(10);
+ m_ReconnectTimer->OnTimerExpired.connect([this](const Timer * const&) { ReconnectTimerHandler(); });
+ m_ReconnectTimer->Start();
+ m_ReconnectTimer->Reschedule(0);
+
+ /* Register event handlers. */
+ m_HandleCheckResults = Checkable::OnNewCheckResult.connect([this](const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ CheckResultHandler(checkable, cr);
+ });
+}
+
+/**
+ * Pause is equivalent to Stop, but with HA capabilities to resume at runtime.
+ */
+void GraphiteWriter::Pause()
+{
+ m_HandleCheckResults.disconnect();
+ m_ReconnectTimer->Stop(true);
+
+ try {
+ ReconnectInternal();
+ } catch (const std::exception&) {
+ Log(LogInformation, "GraphiteWriter")
+ << "'" << GetName() << "' paused. Unable to connect, not flushing buffers. Data may be lost on reload.";
+
+ ObjectImpl<GraphiteWriter>::Pause();
+ return;
+ }
+
+ m_WorkQueue.Join();
+ DisconnectInternal();
+
+ Log(LogInformation, "GraphiteWriter")
+ << "'" << GetName() << "' paused.";
+
+ ObjectImpl<GraphiteWriter>::Pause();
+}
+
+/**
+ * Check if method is called inside the WQ thread.
+ */
+void GraphiteWriter::AssertOnWorkQueue()
+{
+ ASSERT(m_WorkQueue.IsWorkerThread());
+}
+
+/**
+ * Exception handler for the WQ.
+ *
+ * Closes the connection if connected.
+ *
+ * @param exp Exception pointer
+ */
+void GraphiteWriter::ExceptionHandler(boost::exception_ptr exp)
+{
+ Log(LogCritical, "GraphiteWriter", "Exception during Graphite operation: Verify that your backend is operational!");
+
+ Log(LogDebug, "GraphiteWriter")
+ << "Exception during Graphite operation: " << DiagnosticInformation(std::move(exp));
+
+ if (GetConnected()) {
+ m_Stream->close();
+
+ SetConnected(false);
+ }
+}
+
+/**
+ * Reconnect method, stops when the feature is paused in HA zones.
+ *
+ * Called inside the WQ.
+ */
+void GraphiteWriter::Reconnect()
+{
+ AssertOnWorkQueue();
+
+ if (IsPaused()) {
+ SetConnected(false);
+ return;
+ }
+
+ ReconnectInternal();
+}
+
+/**
+ * Reconnect method, connects to a TCP Stream
+ */
+void GraphiteWriter::ReconnectInternal()
+{
+ double startTime = Utility::GetTime();
+
+ CONTEXT("Reconnecting to Graphite '" << GetName() << "'");
+
+ SetShouldConnect(true);
+
+ if (GetConnected())
+ return;
+
+ Log(LogNotice, "GraphiteWriter")
+ << "Reconnecting to Graphite on host '" << GetHost() << "' port '" << GetPort() << "'.";
+
+ m_Stream = Shared<AsioTcpStream>::Make(IoEngine::Get().GetIoContext());
+
+ try {
+ icinga::Connect(m_Stream->lowest_layer(), GetHost(), GetPort());
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "GraphiteWriter")
+ << "Can't connect to Graphite on host '" << GetHost() << "' port '" << GetPort() << ".'";
+
+ SetConnected(false);
+
+ throw;
+ }
+
+ SetConnected(true);
+
+ Log(LogInformation, "GraphiteWriter")
+ << "Finished reconnecting to Graphite in " << std::setw(2) << Utility::GetTime() - startTime << " second(s).";
+}
+
+/**
+ * Reconnect handler called by the timer.
+ *
+ * Enqueues a reconnect task into the WQ.
+ */
+void GraphiteWriter::ReconnectTimerHandler()
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this]() { Reconnect(); }, PriorityHigh);
+}
+
+/**
+ * Disconnect the stream.
+ *
+ * Called inside the WQ.
+ */
+void GraphiteWriter::Disconnect()
+{
+ AssertOnWorkQueue();
+
+ DisconnectInternal();
+}
+
+/**
+ * Disconnect the stream.
+ *
+ * Called outside the WQ.
+ */
+void GraphiteWriter::DisconnectInternal()
+{
+ if (!GetConnected())
+ return;
+
+ m_Stream->close();
+
+ SetConnected(false);
+}
+
+/**
+ * Check result event handler, checks whether feature is not paused in HA setups.
+ *
+ * @param checkable Host/Service object
+ * @param cr Check result including performance data
+ */
+void GraphiteWriter::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this, checkable, cr]() { CheckResultHandlerInternal(checkable, cr); });
+}
+
+/**
+ * Check result event handler, prepares metadata and perfdata values and calls Send*()
+ *
+ * Called inside the WQ.
+ *
+ * @param checkable Host/Service object
+ * @param cr Check result including performance data
+ */
+void GraphiteWriter::CheckResultHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("Processing check result for '" << checkable->GetName() << "'");
+
+ /* TODO: Deal with missing connection here. Needs refactoring
+ * into parsing the actual performance data and then putting it
+ * into a queue for re-inserting. */
+
+ if (!IcingaApplication::GetInstance()->GetEnablePerfdata() || !checkable->GetEnablePerfdata())
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+
+ String prefix;
+
+ if (service) {
+ prefix = MacroProcessor::ResolveMacros(GetServiceNameTemplate(), resolvers, cr, nullptr, [](const Value& value) -> Value {
+ return EscapeMacroMetric(value);
+ });
+ } else {
+ prefix = MacroProcessor::ResolveMacros(GetHostNameTemplate(), resolvers, cr, nullptr, [](const Value& value) -> Value {
+ return EscapeMacroMetric(value);
+ });
+ }
+
+ String prefixPerfdata = prefix + ".perfdata";
+ String prefixMetadata = prefix + ".metadata";
+
+ double ts = cr->GetExecutionEnd();
+
+ if (GetEnableSendMetadata()) {
+ if (service) {
+ SendMetric(checkable, prefixMetadata, "state", service->GetState(), ts);
+ } else {
+ SendMetric(checkable, prefixMetadata, "state", host->GetState(), ts);
+ }
+
+ SendMetric(checkable, prefixMetadata, "current_attempt", checkable->GetCheckAttempt(), ts);
+ SendMetric(checkable, prefixMetadata, "max_check_attempts", checkable->GetMaxCheckAttempts(), ts);
+ SendMetric(checkable, prefixMetadata, "state_type", checkable->GetStateType(), ts);
+ SendMetric(checkable, prefixMetadata, "reachable", checkable->IsReachable(), ts);
+ SendMetric(checkable, prefixMetadata, "downtime_depth", checkable->GetDowntimeDepth(), ts);
+ SendMetric(checkable, prefixMetadata, "acknowledgement", checkable->GetAcknowledgement(), ts);
+ SendMetric(checkable, prefixMetadata, "latency", cr->CalculateLatency(), ts);
+ SendMetric(checkable, prefixMetadata, "execution_time", cr->CalculateExecutionTime(), ts);
+ }
+
+ SendPerfdata(checkable, prefixPerfdata, cr, ts);
+}
+
+/**
+ * Parse performance data from check result and call SendMetric()
+ *
+ * @param checkable Host/service object
+ * @param prefix Metric prefix string
+ * @param cr Check result including performance data
+ * @param ts Timestamp when the check result was created
+ */
+void GraphiteWriter::SendPerfdata(const Checkable::Ptr& checkable, const String& prefix, const CheckResult::Ptr& cr, double ts)
+{
+ Array::Ptr perfdata = cr->GetPerformanceData();
+
+ if (!perfdata)
+ return;
+
+ CheckCommand::Ptr checkCommand = checkable->GetCheckCommand();
+
+ ObjectLock olock(perfdata);
+ for (const Value& val : perfdata) {
+ PerfdataValue::Ptr pdv;
+
+ if (val.IsObjectType<PerfdataValue>())
+ pdv = val;
+ else {
+ try {
+ pdv = PerfdataValue::Parse(val);
+ } catch (const std::exception&) {
+ Log(LogWarning, "GraphiteWriter")
+ << "Ignoring invalid perfdata for checkable '"
+ << checkable->GetName() << "' and command '"
+ << checkCommand->GetName() << "' with value: " << val;
+ continue;
+ }
+ }
+
+ String escapedKey = EscapeMetricLabel(pdv->GetLabel());
+
+ SendMetric(checkable, prefix, escapedKey + ".value", pdv->GetValue(), ts);
+
+ if (GetEnableSendThresholds()) {
+ if (!pdv->GetCrit().IsEmpty())
+ SendMetric(checkable, prefix, escapedKey + ".crit", pdv->GetCrit(), ts);
+ if (!pdv->GetWarn().IsEmpty())
+ SendMetric(checkable, prefix, escapedKey + ".warn", pdv->GetWarn(), ts);
+ if (!pdv->GetMin().IsEmpty())
+ SendMetric(checkable, prefix, escapedKey + ".min", pdv->GetMin(), ts);
+ if (!pdv->GetMax().IsEmpty())
+ SendMetric(checkable, prefix, escapedKey + ".max", pdv->GetMax(), ts);
+ }
+ }
+}
+
+/**
+ * Computes metric data and sends to Graphite
+ *
+ * @param checkable Host/service object
+ * @param prefix Computed metric prefix string
+ * @param name Metric name
+ * @param value Metric value
+ * @param ts Timestamp when the check result was created
+ */
+void GraphiteWriter::SendMetric(const Checkable::Ptr& checkable, const String& prefix, const String& name, double value, double ts)
+{
+ namespace asio = boost::asio;
+
+ std::ostringstream msgbuf;
+ msgbuf << prefix << "." << name << " " << Convert::ToString(value) << " " << static_cast<long>(ts);
+
+ Log(LogDebug, "GraphiteWriter")
+ << "Checkable '" << checkable->GetName() << "' adds to metric list: '" << msgbuf.str() << "'.";
+
+ // do not send \n to debug log
+ msgbuf << "\n";
+
+ std::unique_lock<std::mutex> lock(m_StreamMutex);
+
+ if (!GetConnected())
+ return;
+
+ try {
+ asio::write(*m_Stream, asio::buffer(msgbuf.str()));
+ m_Stream->flush();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "GraphiteWriter")
+ << "Cannot write to TCP socket on host '" << GetHost() << "' port '" << GetPort() << "'.";
+
+ throw ex;
+ }
+}
+
+/**
+ * Escape metric tree elements
+ *
+ * Dots are not allowed, e.g. in host names
+ *
+ * @param str Metric part name
+ * @return Escape string
+ */
+String GraphiteWriter::EscapeMetric(const String& str)
+{
+ String result = str;
+
+ //don't allow '.' in metric prefixes
+ boost::replace_all(result, " ", "_");
+ boost::replace_all(result, ".", "_");
+ boost::replace_all(result, "\\", "_");
+ boost::replace_all(result, "/", "_");
+
+ return result;
+}
+
+/**
+ * Escape metric label
+ *
+ * Dots are allowed - users can create trees from perfdata labels
+ *
+ * @param str Metric label name
+ * @return Escaped string
+ */
+String GraphiteWriter::EscapeMetricLabel(const String& str)
+{
+ String result = str;
+
+ //allow to pass '.' in perfdata labels
+ boost::replace_all(result, " ", "_");
+ boost::replace_all(result, "\\", "_");
+ boost::replace_all(result, "/", "_");
+ boost::replace_all(result, "::", ".");
+
+ return result;
+}
+
+/**
+ * Escape macro metrics found via host/service name templates
+ *
+ * @param value Array or string with macro metric names
+ * @return Escaped string. Arrays are joined with dots.
+ */
+Value GraphiteWriter::EscapeMacroMetric(const Value& value)
+{
+ if (value.IsObjectType<Array>()) {
+ Array::Ptr arr = value;
+ ArrayData result;
+
+ ObjectLock olock(arr);
+ for (const Value& arg : arr) {
+ result.push_back(EscapeMetric(arg));
+ }
+
+ return Utility::Join(new Array(std::move(result)), '.');
+ } else
+ return EscapeMetric(value);
+}
+
+/**
+ * Validate the configuration setting 'host_name_template'
+ *
+ * @param lvalue String containing runtime macros.
+ * @param utils Helper, unused
+ */
+void GraphiteWriter::ValidateHostNameTemplate(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<GraphiteWriter>::ValidateHostNameTemplate(lvalue, utils);
+
+ if (!MacroProcessor::ValidateMacroString(lvalue()))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "host_name_template" }, "Closing $ not found in macro format string '" + lvalue() + "'."));
+}
+
+/**
+ * Validate the configuration setting 'service_name_template'
+ *
+ * @param lvalue String containing runtime macros.
+ * @param utils Helper, unused
+ */
+void GraphiteWriter::ValidateServiceNameTemplate(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<GraphiteWriter>::ValidateServiceNameTemplate(lvalue, utils);
+
+ if (!MacroProcessor::ValidateMacroString(lvalue()))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "service_name_template" }, "Closing $ not found in macro format string '" + lvalue() + "'."));
+}
diff --git a/lib/perfdata/graphitewriter.hpp b/lib/perfdata/graphitewriter.hpp
new file mode 100644
index 0000000..e0c8b78
--- /dev/null
+++ b/lib/perfdata/graphitewriter.hpp
@@ -0,0 +1,69 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef GRAPHITEWRITER_H
+#define GRAPHITEWRITER_H
+
+#include "perfdata/graphitewriter-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include <fstream>
+#include <mutex>
+
+namespace icinga
+{
+
+/**
+ * An Icinga graphite writer.
+ *
+ * @ingroup perfdata
+ */
+class GraphiteWriter final : public ObjectImpl<GraphiteWriter>
+{
+public:
+ DECLARE_OBJECT(GraphiteWriter);
+ DECLARE_OBJECTNAME(GraphiteWriter);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void ValidateHostNameTemplate(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+ void ValidateServiceNameTemplate(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void OnConfigLoaded() override;
+ void Resume() override;
+ void Pause() override;
+
+private:
+ Shared<AsioTcpStream>::Ptr m_Stream;
+ std::mutex m_StreamMutex;
+ WorkQueue m_WorkQueue{10000000, 1};
+
+ boost::signals2::connection m_HandleCheckResults;
+ Timer::Ptr m_ReconnectTimer;
+
+ void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void CheckResultHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void SendMetric(const Checkable::Ptr& checkable, const String& prefix, const String& name, double value, double ts);
+ void SendPerfdata(const Checkable::Ptr& checkable, const String& prefix, const CheckResult::Ptr& cr, double ts);
+ static String EscapeMetric(const String& str);
+ static String EscapeMetricLabel(const String& str);
+ static Value EscapeMacroMetric(const Value& value);
+
+ void ReconnectTimerHandler();
+
+ void Disconnect();
+ void DisconnectInternal();
+ void Reconnect();
+ void ReconnectInternal();
+
+ void AssertOnWorkQueue();
+
+ void ExceptionHandler(boost::exception_ptr exp);
+};
+
+}
+
+#endif /* GRAPHITEWRITER_H */
diff --git a/lib/perfdata/graphitewriter.ti b/lib/perfdata/graphitewriter.ti
new file mode 100644
index 0000000..c8db067
--- /dev/null
+++ b/lib/perfdata/graphitewriter.ti
@@ -0,0 +1,38 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library perfdata;
+
+namespace icinga
+{
+
+class GraphiteWriter : ConfigObject
+{
+ activation_priority 100;
+
+ [config] String host {
+ default {{{ return "127.0.0.1"; }}}
+ };
+ [config] String port {
+ default {{{ return "2003"; }}}
+ };
+ [config] String host_name_template {
+ default {{{ return "icinga2.$host.name$.host.$host.check_command$"; }}}
+ };
+ [config] String service_name_template {
+ default {{{ return "icinga2.$host.name$.services.$service.name$.$service.check_command$"; }}}
+ };
+ [config] bool enable_send_thresholds;
+ [config] bool enable_send_metadata;
+
+ [no_user_modify] bool connected;
+ [no_user_modify] bool should_connect {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_ha {
+ default {{{ return false; }}}
+ };
+};
+
+}
diff --git a/lib/perfdata/influxdb2writer.cpp b/lib/perfdata/influxdb2writer.cpp
new file mode 100644
index 0000000..c92d7d4
--- /dev/null
+++ b/lib/perfdata/influxdb2writer.cpp
@@ -0,0 +1,44 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/influxdb2writer.hpp"
+#include "perfdata/influxdb2writer-ti.cpp"
+#include "remote/url.hpp"
+#include "base/configtype.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/statsfunction.hpp"
+#include <utility>
+#include <boost/beast/http/message.hpp>
+#include <boost/beast/http/string_body.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE(Influxdb2Writer);
+
+REGISTER_STATSFUNCTION(Influxdb2Writer, &Influxdb2Writer::StatsFunc);
+
+void Influxdb2Writer::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ InfluxdbCommonWriter::StatsFunc<Influxdb2Writer>(status, perfdata);
+}
+
+boost::beast::http::request<boost::beast::http::string_body> Influxdb2Writer::AssembleRequest(String body)
+{
+ auto request (AssembleBaseRequest(std::move(body)));
+
+ request.set(boost::beast::http::field::authorization, "Token " + GetAuthToken());
+
+ return request;
+}
+
+Url::Ptr Influxdb2Writer::AssembleUrl()
+{
+ auto url (AssembleBaseUrl());
+
+ std::vector<String> path ({"api", "v2", "write"});
+ url->SetPath(path);
+
+ url->AddQueryElement("org", GetOrganization());
+ url->AddQueryElement("bucket", GetBucket());
+
+ return url;
+}
diff --git a/lib/perfdata/influxdb2writer.hpp b/lib/perfdata/influxdb2writer.hpp
new file mode 100644
index 0000000..3b20f8b
--- /dev/null
+++ b/lib/perfdata/influxdb2writer.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#ifndef INFLUXDB2WRITER_H
+#define INFLUXDB2WRITER_H
+
+#include "perfdata/influxdb2writer-ti.hpp"
+#include <boost/beast/http/message.hpp>
+#include <boost/beast/http/string_body.hpp>
+
+namespace icinga
+{
+
+/**
+ * An Icinga InfluxDB v2 writer.
+ *
+ * @ingroup perfdata
+ */
+class Influxdb2Writer final : public ObjectImpl<Influxdb2Writer>
+{
+public:
+ DECLARE_OBJECT(Influxdb2Writer);
+ DECLARE_OBJECTNAME(Influxdb2Writer);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+protected:
+ boost::beast::http::request<boost::beast::http::string_body> AssembleRequest(String body) override;
+ Url::Ptr AssembleUrl() override;
+};
+
+}
+
+#endif /* INFLUXDB2WRITER_H */
diff --git a/lib/perfdata/influxdb2writer.ti b/lib/perfdata/influxdb2writer.ti
new file mode 100644
index 0000000..f806187
--- /dev/null
+++ b/lib/perfdata/influxdb2writer.ti
@@ -0,0 +1,19 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/influxdbcommonwriter.hpp"
+
+library perfdata;
+
+namespace icinga
+{
+
+class Influxdb2Writer : InfluxdbCommonWriter
+{
+ activation_priority 100;
+
+ [config, required] String organization;
+ [config, required] String bucket;
+ [config, required, no_user_view] String auth_token;
+};
+
+}
diff --git a/lib/perfdata/influxdbcommonwriter.cpp b/lib/perfdata/influxdbcommonwriter.cpp
new file mode 100644
index 0000000..fb0bcc9
--- /dev/null
+++ b/lib/perfdata/influxdbcommonwriter.cpp
@@ -0,0 +1,596 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/influxdbcommonwriter.hpp"
+#include "perfdata/influxdbcommonwriter-ti.cpp"
+#include "remote/url.hpp"
+#include "icinga/service.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/checkcommand.hpp"
+#include "base/application.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/stream.hpp"
+#include "base/json.hpp"
+#include "base/networkstream.hpp"
+#include "base/exception.hpp"
+#include "base/statsfunction.hpp"
+#include "base/tlsutility.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/beast/core/flat_buffer.hpp>
+#include <boost/beast/http/field.hpp>
+#include <boost/beast/http/message.hpp>
+#include <boost/beast/http/parser.hpp>
+#include <boost/beast/http/read.hpp>
+#include <boost/beast/http/status.hpp>
+#include <boost/beast/http/string_body.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <boost/beast/http/write.hpp>
+#include <boost/math/special_functions/fpclassify.hpp>
+#include <boost/regex.hpp>
+#include <boost/scoped_array.hpp>
+#include <memory>
+#include <string>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(InfluxdbCommonWriter);
+
+class InfluxdbInteger final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(InfluxdbInteger);
+
+ InfluxdbInteger(int value)
+ : m_Value(value)
+ { }
+
+ int GetValue() const
+ {
+ return m_Value;
+ }
+
+private:
+ int m_Value;
+};
+
+void InfluxdbCommonWriter::OnConfigLoaded()
+{
+ ObjectImpl<InfluxdbCommonWriter>::OnConfigLoaded();
+
+ m_WorkQueue.SetName(GetReflectionType()->GetName() + ", " + GetName());
+
+ if (!GetEnableHa()) {
+ Log(LogDebug, GetReflectionType()->GetName())
+ << "HA functionality disabled. Won't pause connection: " << GetName();
+
+ SetHAMode(HARunEverywhere);
+ } else {
+ SetHAMode(HARunOnce);
+ }
+}
+
+void InfluxdbCommonWriter::Resume()
+{
+ ObjectImpl<InfluxdbCommonWriter>::Resume();
+
+ Log(LogInformation, GetReflectionType()->GetName())
+ << "'" << GetName() << "' resumed.";
+
+ /* Register exception handler for WQ tasks. */
+ m_WorkQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); });
+
+ /* Setup timer for periodically flushing m_DataBuffer */
+ m_FlushTimer = Timer::Create();
+ m_FlushTimer->SetInterval(GetFlushInterval());
+ m_FlushTimer->OnTimerExpired.connect([this](const Timer * const&) { FlushTimeout(); });
+ m_FlushTimer->Start();
+ m_FlushTimer->Reschedule(0);
+
+ /* Register for new metrics. */
+ m_HandleCheckResults = Checkable::OnNewCheckResult.connect([this](const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ CheckResultHandler(checkable, cr);
+ });
+}
+
+/* Pause is equivalent to Stop, but with HA capabilities to resume at runtime. */
+void InfluxdbCommonWriter::Pause()
+{
+ m_HandleCheckResults.disconnect();
+
+ /* Force a flush. */
+ Log(LogDebug, GetReflectionType()->GetName())
+ << "Processing pending tasks and flushing data buffers.";
+
+ m_FlushTimer->Stop(true);
+ m_WorkQueue.Enqueue([this]() { FlushWQ(); }, PriorityLow);
+
+ /* Wait for the flush to complete, implicitly waits for all WQ tasks enqueued prior to pausing. */
+ m_WorkQueue.Join();
+
+ Log(LogInformation, GetReflectionType()->GetName())
+ << "'" << GetName() << "' paused.";
+
+ ObjectImpl<InfluxdbCommonWriter>::Pause();
+}
+
+void InfluxdbCommonWriter::AssertOnWorkQueue()
+{
+ ASSERT(m_WorkQueue.IsWorkerThread());
+}
+
+void InfluxdbCommonWriter::ExceptionHandler(boost::exception_ptr exp)
+{
+ Log(LogCritical, GetReflectionType()->GetName(), "Exception during InfluxDB operation: Verify that your backend is operational!");
+
+ Log(LogDebug, GetReflectionType()->GetName())
+ << "Exception during InfluxDB operation: " << DiagnosticInformation(std::move(exp));
+
+ //TODO: Close the connection, if we keep it open.
+}
+
+OptionalTlsStream InfluxdbCommonWriter::Connect()
+{
+ Log(LogNotice, GetReflectionType()->GetName())
+ << "Reconnecting to InfluxDB on host '" << GetHost() << "' port '" << GetPort() << "'.";
+
+ OptionalTlsStream stream;
+ bool ssl = GetSslEnable();
+
+ if (ssl) {
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext(GetSslCert(), GetSslKey(), GetSslCaCert());
+ } catch (const std::exception& ex) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Unable to create SSL context.";
+ throw;
+ }
+
+ stream.first = Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, GetHost());
+
+ } else {
+ stream.second = Shared<AsioTcpStream>::Make(IoEngine::Get().GetIoContext());
+ }
+
+ try {
+ icinga::Connect(ssl ? stream.first->lowest_layer() : stream.second->lowest_layer(), GetHost(), GetPort());
+ } catch (const std::exception& ex) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Can't connect to InfluxDB on host '" << GetHost() << "' port '" << GetPort() << "'.";
+ throw;
+ }
+
+ if (ssl) {
+ auto& tlsStream (stream.first->next_layer());
+
+ try {
+ tlsStream.handshake(tlsStream.client);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "TLS handshake with host '" << GetHost() << "' failed.";
+ throw;
+ }
+
+ if (!GetSslInsecureNoverify()) {
+ if (!tlsStream.GetPeerCertificate()) {
+ BOOST_THROW_EXCEPTION(std::runtime_error("InfluxDB didn't present any TLS certificate."));
+ }
+
+ if (!tlsStream.IsVerifyOK()) {
+ BOOST_THROW_EXCEPTION(std::runtime_error(
+ "TLS certificate validation failed: " + std::string(tlsStream.GetVerifyError())
+ ));
+ }
+ }
+ }
+
+ return stream;
+}
+
+void InfluxdbCommonWriter::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ if (IsPaused())
+ return;
+
+ m_WorkQueue.Enqueue([this, checkable, cr]() { CheckResultHandlerWQ(checkable, cr); }, PriorityLow);
+}
+
+void InfluxdbCommonWriter::CheckResultHandlerWQ(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ AssertOnWorkQueue();
+
+ CONTEXT("Processing check result for '" << checkable->GetName() << "'");
+
+ if (!IcingaApplication::GetInstance()->GetEnablePerfdata() || !checkable->GetEnablePerfdata())
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+
+ String prefix;
+
+ double ts = cr->GetExecutionEnd();
+
+ // Clone the template and perform an in-place macro expansion of measurement and tag values
+ Dictionary::Ptr tmpl_clean = service ? GetServiceTemplate() : GetHostTemplate();
+ Dictionary::Ptr tmpl = static_pointer_cast<Dictionary>(tmpl_clean->ShallowClone());
+ tmpl->Set("measurement", MacroProcessor::ResolveMacros(tmpl->Get("measurement"), resolvers, cr));
+
+ Dictionary::Ptr tagsClean = tmpl->Get("tags");
+ if (tagsClean) {
+ Dictionary::Ptr tags = new Dictionary();
+
+ {
+ ObjectLock olock(tagsClean);
+ for (const Dictionary::Pair& pair : tagsClean) {
+ String missing_macro;
+ Value value = MacroProcessor::ResolveMacros(pair.second, resolvers, cr, &missing_macro);
+
+ if (missing_macro.IsEmpty()) {
+ tags->Set(pair.first, value);
+ }
+ }
+ }
+
+ tmpl->Set("tags", tags);
+ }
+
+ CheckCommand::Ptr checkCommand = checkable->GetCheckCommand();
+
+ Array::Ptr perfdata = cr->GetPerformanceData();
+
+ if (perfdata) {
+ ObjectLock olock(perfdata);
+ for (const Value& val : perfdata) {
+ PerfdataValue::Ptr pdv;
+
+ if (val.IsObjectType<PerfdataValue>())
+ pdv = val;
+ else {
+ try {
+ pdv = PerfdataValue::Parse(val);
+ } catch (const std::exception&) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Ignoring invalid perfdata for checkable '"
+ << checkable->GetName() << "' and command '"
+ << checkCommand->GetName() << "' with value: " << val;
+ continue;
+ }
+ }
+
+ Dictionary::Ptr fields = new Dictionary();
+ fields->Set("value", pdv->GetValue());
+
+ if (GetEnableSendThresholds()) {
+ if (!pdv->GetCrit().IsEmpty())
+ fields->Set("crit", pdv->GetCrit());
+ if (!pdv->GetWarn().IsEmpty())
+ fields->Set("warn", pdv->GetWarn());
+ if (!pdv->GetMin().IsEmpty())
+ fields->Set("min", pdv->GetMin());
+ if (!pdv->GetMax().IsEmpty())
+ fields->Set("max", pdv->GetMax());
+ }
+ if (!pdv->GetUnit().IsEmpty()) {
+ fields->Set("unit", pdv->GetUnit());
+ }
+
+ SendMetric(checkable, tmpl, pdv->GetLabel(), fields, ts);
+ }
+ }
+
+ if (GetEnableSendMetadata()) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr fields = new Dictionary();
+
+ if (service)
+ fields->Set("state", new InfluxdbInteger(service->GetState()));
+ else
+ fields->Set("state", new InfluxdbInteger(host->GetState()));
+
+ fields->Set("current_attempt", new InfluxdbInteger(checkable->GetCheckAttempt()));
+ fields->Set("max_check_attempts", new InfluxdbInteger(checkable->GetMaxCheckAttempts()));
+ fields->Set("state_type", new InfluxdbInteger(checkable->GetStateType()));
+ fields->Set("reachable", checkable->IsReachable());
+ fields->Set("downtime_depth", new InfluxdbInteger(checkable->GetDowntimeDepth()));
+ fields->Set("acknowledgement", new InfluxdbInteger(checkable->GetAcknowledgement()));
+ fields->Set("latency", cr->CalculateLatency());
+ fields->Set("execution_time", cr->CalculateExecutionTime());
+
+ SendMetric(checkable, tmpl, Empty, fields, ts);
+ }
+}
+
+String InfluxdbCommonWriter::EscapeKeyOrTagValue(const String& str)
+{
+ // Iterate over the key name and escape commas and spaces with a backslash
+ String result = str;
+ boost::algorithm::replace_all(result, "\"", "\\\"");
+ boost::algorithm::replace_all(result, "=", "\\=");
+ boost::algorithm::replace_all(result, ",", "\\,");
+ boost::algorithm::replace_all(result, " ", "\\ ");
+
+ // InfluxDB 'feature': although backslashes are allowed in keys they also act
+ // as escape sequences when followed by ',' or ' '. When your tag is like
+ // 'metric=C:\' bad things happen. Backslashes themselves cannot be escaped
+ // and through experimentation they also escape '='. To be safe we replace
+ // trailing backslashes with and underscore.
+ // See https://github.com/influxdata/influxdb/issues/8587 for more info
+ size_t length = result.GetLength();
+ if (result[length - 1] == '\\')
+ result[length - 1] = '_';
+
+ return result;
+}
+
+String InfluxdbCommonWriter::EscapeValue(const Value& value)
+{
+ if (value.IsObjectType<InfluxdbInteger>()) {
+ std::ostringstream os;
+ os << static_cast<InfluxdbInteger::Ptr>(value)->GetValue() << "i";
+ return os.str();
+ }
+
+ if (value.IsBoolean())
+ return value ? "true" : "false";
+
+ if (value.IsString())
+ return "\"" + EscapeKeyOrTagValue(value) + "\"";
+
+ return value;
+}
+
+void InfluxdbCommonWriter::SendMetric(const Checkable::Ptr& checkable, const Dictionary::Ptr& tmpl,
+ const String& label, const Dictionary::Ptr& fields, double ts)
+{
+ std::ostringstream msgbuf;
+ msgbuf << EscapeKeyOrTagValue(tmpl->Get("measurement"));
+
+ Dictionary::Ptr tags = tmpl->Get("tags");
+ if (tags) {
+ ObjectLock olock(tags);
+ for (const Dictionary::Pair& pair : tags) {
+ // Empty macro expansion, no tag
+ if (!pair.second.IsEmpty()) {
+ msgbuf << "," << EscapeKeyOrTagValue(pair.first) << "=" << EscapeKeyOrTagValue(pair.second);
+ }
+ }
+ }
+
+ // Label may be empty in the case of metadata
+ if (!label.IsEmpty())
+ msgbuf << ",metric=" << EscapeKeyOrTagValue(label);
+
+ msgbuf << " ";
+
+ {
+ bool first = true;
+
+ ObjectLock fieldLock(fields);
+ for (const Dictionary::Pair& pair : fields) {
+ if (first)
+ first = false;
+ else
+ msgbuf << ",";
+
+ msgbuf << EscapeKeyOrTagValue(pair.first) << "=" << EscapeValue(pair.second);
+ }
+ }
+
+ msgbuf << " " << static_cast<unsigned long>(ts);
+
+ Log(LogDebug, GetReflectionType()->GetName())
+ << "Checkable '" << checkable->GetName() << "' adds to metric list:'" << msgbuf.str() << "'.";
+
+ // Buffer the data point
+ m_DataBuffer.emplace_back(msgbuf.str());
+ m_DataBufferSize = m_DataBuffer.size();
+
+ // Flush if we've buffered too much to prevent excessive memory use
+ if (static_cast<int>(m_DataBuffer.size()) >= GetFlushThreshold()) {
+ Log(LogDebug, GetReflectionType()->GetName())
+ << "Data buffer overflow writing " << m_DataBuffer.size() << " data points";
+
+ try {
+ FlushWQ();
+ } catch (...) {
+ /* Do nothing. */
+ }
+ }
+}
+
+void InfluxdbCommonWriter::FlushTimeout()
+{
+ m_WorkQueue.Enqueue([this]() { FlushTimeoutWQ(); }, PriorityHigh);
+}
+
+void InfluxdbCommonWriter::FlushTimeoutWQ()
+{
+ AssertOnWorkQueue();
+
+ Log(LogDebug, GetReflectionType()->GetName())
+ << "Timer expired writing " << m_DataBuffer.size() << " data points";
+
+ FlushWQ();
+}
+
+void InfluxdbCommonWriter::FlushWQ()
+{
+ AssertOnWorkQueue();
+
+ namespace beast = boost::beast;
+ namespace http = beast::http;
+
+ /* Flush can be called from 1) Timeout 2) Threshold 3) on shutdown/reload. */
+ if (m_DataBuffer.empty())
+ return;
+
+ Log(LogDebug, GetReflectionType()->GetName())
+ << "Flushing data buffer to InfluxDB.";
+
+ String body = boost::algorithm::join(m_DataBuffer, "\n");
+ m_DataBuffer.clear();
+ m_DataBufferSize = 0;
+
+ OptionalTlsStream stream;
+
+ try {
+ stream = Connect();
+ } catch (const std::exception& ex) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Flush failed, cannot connect to InfluxDB: " << DiagnosticInformation(ex, false);
+ return;
+ }
+
+ Defer s ([&stream]() {
+ if (stream.first) {
+ stream.first->next_layer().shutdown();
+ }
+ });
+
+ auto request (AssembleRequest(std::move(body)));
+
+ try {
+ if (stream.first) {
+ http::write(*stream.first, request);
+ stream.first->flush();
+ } else {
+ http::write(*stream.second, request);
+ stream.second->flush();
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Cannot write to TCP socket on host '" << GetHost() << "' port '" << GetPort() << "'.";
+ throw;
+ }
+
+ http::parser<false, http::string_body> parser;
+ beast::flat_buffer buf;
+
+ try {
+ if (stream.first) {
+ http::read(*stream.first, buf, parser);
+ } else {
+ http::read(*stream.second, buf, parser);
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Failed to parse HTTP response from host '" << GetHost() << "' port '" << GetPort() << "': " << DiagnosticInformation(ex);
+ throw;
+ }
+
+ auto& response (parser.get());
+
+ if (response.result() != http::status::no_content) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Unexpected response code: " << response.result();
+
+ auto& contentType (response[http::field::content_type]);
+ if (contentType != "application/json") {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Unexpected Content-Type: " << contentType;
+ return;
+ }
+
+ Dictionary::Ptr jsonResponse;
+ auto& body (response.body());
+
+ try {
+ jsonResponse = JsonDecode(body);
+ } catch (...) {
+ Log(LogWarning, GetReflectionType()->GetName())
+ << "Unable to parse JSON response:\n" << body;
+ return;
+ }
+
+ String error = jsonResponse->Get("error");
+
+ Log(LogCritical, GetReflectionType()->GetName())
+ << "InfluxDB error message:\n" << error;
+ }
+}
+
+boost::beast::http::request<boost::beast::http::string_body> InfluxdbCommonWriter::AssembleBaseRequest(String body)
+{
+ namespace http = boost::beast::http;
+
+ auto url (AssembleUrl());
+ http::request<http::string_body> request (http::verb::post, std::string(url->Format(true)), 10);
+
+ request.set(http::field::user_agent, "Icinga/" + Application::GetAppVersion());
+ request.set(http::field::host, url->GetHost() + ":" + url->GetPort());
+ request.body() = std::move(body);
+ request.content_length(request.body().size());
+
+ return request;
+}
+
+Url::Ptr InfluxdbCommonWriter::AssembleBaseUrl()
+{
+ Url::Ptr url = new Url();
+
+ url->SetScheme(GetSslEnable() ? "https" : "http");
+ url->SetHost(GetHost());
+ url->SetPort(GetPort());
+ url->AddQueryElement("precision", "s");
+
+ return url;
+}
+
+void InfluxdbCommonWriter::ValidateHostTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<InfluxdbCommonWriter>::ValidateHostTemplate(lvalue, utils);
+
+ String measurement = lvalue()->Get("measurement");
+ if (!MacroProcessor::ValidateMacroString(measurement))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "host_template", "measurement" }, "Closing $ not found in macro format string '" + measurement + "'."));
+
+ Dictionary::Ptr tags = lvalue()->Get("tags");
+ if (tags) {
+ ObjectLock olock(tags);
+ for (const Dictionary::Pair& pair : tags) {
+ if (!MacroProcessor::ValidateMacroString(pair.second))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "host_template", "tags", pair.first }, "Closing $ not found in macro format string '" + pair.second));
+ }
+ }
+}
+
+void InfluxdbCommonWriter::ValidateServiceTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<InfluxdbCommonWriter>::ValidateServiceTemplate(lvalue, utils);
+
+ String measurement = lvalue()->Get("measurement");
+ if (!MacroProcessor::ValidateMacroString(measurement))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "service_template", "measurement" }, "Closing $ not found in macro format string '" + measurement + "'."));
+
+ Dictionary::Ptr tags = lvalue()->Get("tags");
+ if (tags) {
+ ObjectLock olock(tags);
+ for (const Dictionary::Pair& pair : tags) {
+ if (!MacroProcessor::ValidateMacroString(pair.second))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "service_template", "tags", pair.first }, "Closing $ not found in macro format string '" + pair.second));
+ }
+ }
+}
+
diff --git a/lib/perfdata/influxdbcommonwriter.hpp b/lib/perfdata/influxdbcommonwriter.hpp
new file mode 100644
index 0000000..380b20c
--- /dev/null
+++ b/lib/perfdata/influxdbcommonwriter.hpp
@@ -0,0 +1,101 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#ifndef INFLUXDBCOMMONWRITER_H
+#define INFLUXDBCOMMONWRITER_H
+
+#include "perfdata/influxdbcommonwriter-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/timer.hpp"
+#include "base/tlsstream.hpp"
+#include "base/workqueue.hpp"
+#include "remote/url.hpp"
+#include <boost/beast/http/message.hpp>
+#include <boost/beast/http/string_body.hpp>
+#include <atomic>
+#include <fstream>
+
+namespace icinga
+{
+
+/**
+ * Common base class for InfluxDB v1/v2 writers.
+ *
+ * @ingroup perfdata
+ */
+class InfluxdbCommonWriter : public ObjectImpl<InfluxdbCommonWriter>
+{
+public:
+ DECLARE_OBJECT(InfluxdbCommonWriter);
+
+ template<class InfluxWriter>
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void ValidateHostTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateServiceTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void OnConfigLoaded() override;
+ void Resume() override;
+ void Pause() override;
+
+ boost::beast::http::request<boost::beast::http::string_body> AssembleBaseRequest(String body);
+ Url::Ptr AssembleBaseUrl();
+ virtual boost::beast::http::request<boost::beast::http::string_body> AssembleRequest(String body) = 0;
+ virtual Url::Ptr AssembleUrl() = 0;
+
+private:
+ boost::signals2::connection m_HandleCheckResults;
+ Timer::Ptr m_FlushTimer;
+ WorkQueue m_WorkQueue{10000000, 1};
+ std::vector<String> m_DataBuffer;
+ std::atomic_size_t m_DataBufferSize{0};
+
+ void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void CheckResultHandlerWQ(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void SendMetric(const Checkable::Ptr& checkable, const Dictionary::Ptr& tmpl,
+ const String& label, const Dictionary::Ptr& fields, double ts);
+ void FlushTimeout();
+ void FlushTimeoutWQ();
+ void FlushWQ();
+
+ static String EscapeKeyOrTagValue(const String& str);
+ static String EscapeValue(const Value& value);
+
+ OptionalTlsStream Connect();
+
+ void AssertOnWorkQueue();
+
+ void ExceptionHandler(boost::exception_ptr exp);
+};
+
+template<class InfluxWriter>
+void InfluxdbCommonWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+ auto typeName (InfluxWriter::TypeInstance->GetName().ToLower());
+
+ for (const typename InfluxWriter::Ptr& influxwriter : ConfigType::GetObjectsByType<InfluxWriter>()) {
+ size_t workQueueItems = influxwriter->m_WorkQueue.GetLength();
+ double workQueueItemRate = influxwriter->m_WorkQueue.GetTaskCount(60) / 60.0;
+ size_t dataBufferItems = influxwriter->m_DataBufferSize;
+
+ nodes.emplace_back(influxwriter->GetName(), new Dictionary({
+ { "work_queue_items", workQueueItems },
+ { "work_queue_item_rate", workQueueItemRate },
+ { "data_buffer_items", dataBufferItems }
+ }));
+
+ perfdata->Add(new PerfdataValue(typeName + "_" + influxwriter->GetName() + "_work_queue_items", workQueueItems));
+ perfdata->Add(new PerfdataValue(typeName + "_" + influxwriter->GetName() + "_work_queue_item_rate", workQueueItemRate));
+ perfdata->Add(new PerfdataValue(typeName + "_" + influxwriter->GetName() + "_data_queue_items", dataBufferItems));
+ }
+
+ status->Set(typeName, new Dictionary(std::move(nodes)));
+}
+
+}
+
+#endif /* INFLUXDBCOMMONWRITER_H */
diff --git a/lib/perfdata/influxdbcommonwriter.ti b/lib/perfdata/influxdbcommonwriter.ti
new file mode 100644
index 0000000..5cfe83f
--- /dev/null
+++ b/lib/perfdata/influxdbcommonwriter.ti
@@ -0,0 +1,88 @@
+/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library perfdata;
+
+namespace icinga
+{
+
+abstract class InfluxdbCommonWriter : ConfigObject
+{
+ [config, required] String host {
+ default {{{ return "127.0.0.1"; }}}
+ };
+ [config, required] String port {
+ default {{{ return "8086"; }}}
+ };
+ [config] bool ssl_enable {
+ default {{{ return false; }}}
+ };
+ [config] bool ssl_insecure_noverify {
+ default {{{ return false; }}}
+ };
+ [config] String ssl_ca_cert {
+ default {{{ return ""; }}}
+ };
+ [config] String ssl_cert {
+ default {{{ return ""; }}}
+ };
+ [config] String ssl_key{
+ default {{{ return ""; }}}
+ };
+ [config, required] Dictionary::Ptr host_template {
+ default {{{
+ return new Dictionary({
+ { "measurement", "$host.check_command$" },
+ { "tags", new Dictionary({
+ { "hostname", "$host.name$" }
+ }) }
+ });
+ }}}
+ };
+ [config, required] Dictionary::Ptr service_template {
+ default {{{
+ return new Dictionary({
+ { "measurement", "$service.check_command$" },
+ { "tags", new Dictionary({
+ { "hostname", "$host.name$" },
+ { "service", "$service.name$" }
+ }) }
+ });
+ }}}
+ };
+ [config] bool enable_send_thresholds {
+ default {{{ return false; }}}
+ };
+ [config] bool enable_send_metadata {
+ default {{{ return false; }}}
+ };
+ [config] int flush_interval {
+ default {{{ return 10; }}}
+ };
+ [config] int flush_threshold {
+ default {{{ return 1024; }}}
+ };
+ [config] bool enable_ha {
+ default {{{ return false; }}}
+ };
+};
+
+validator InfluxdbCommonWriter {
+ Dictionary host_template {
+ required measurement;
+ String measurement;
+ Dictionary "tags" {
+ String "*";
+ };
+ };
+ Dictionary service_template {
+ required measurement;
+ String measurement;
+ Dictionary "tags" {
+ String "*";
+ };
+ };
+};
+
+}
diff --git a/lib/perfdata/influxdbwriter.cpp b/lib/perfdata/influxdbwriter.cpp
new file mode 100644
index 0000000..4bc992d
--- /dev/null
+++ b/lib/perfdata/influxdbwriter.cpp
@@ -0,0 +1,56 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/influxdbwriter.hpp"
+#include "perfdata/influxdbwriter-ti.cpp"
+#include "base/base64.hpp"
+#include "remote/url.hpp"
+#include "base/configtype.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/statsfunction.hpp"
+#include <boost/beast/http/message.hpp>
+#include <boost/beast/http/string_body.hpp>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(InfluxdbWriter);
+
+REGISTER_STATSFUNCTION(InfluxdbWriter, &InfluxdbWriter::StatsFunc);
+
+void InfluxdbWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ InfluxdbCommonWriter::StatsFunc<InfluxdbWriter>(status, perfdata);
+}
+
+boost::beast::http::request<boost::beast::http::string_body> InfluxdbWriter::AssembleRequest(String body)
+{
+ auto request (AssembleBaseRequest(std::move(body)));
+ Dictionary::Ptr basicAuth = GetBasicAuth();
+
+ if (basicAuth) {
+ request.set(
+ boost::beast::http::field::authorization,
+ "Basic " + Base64::Encode(basicAuth->Get("username") + ":" + basicAuth->Get("password"))
+ );
+ }
+
+ return request;
+}
+
+Url::Ptr InfluxdbWriter::AssembleUrl()
+{
+ auto url (AssembleBaseUrl());
+
+ std::vector<String> path;
+ path.emplace_back("write");
+ url->SetPath(path);
+
+ url->AddQueryElement("db", GetDatabase());
+
+ if (!GetUsername().IsEmpty())
+ url->AddQueryElement("u", GetUsername());
+ if (!GetPassword().IsEmpty())
+ url->AddQueryElement("p", GetPassword());
+
+ return url;
+}
diff --git a/lib/perfdata/influxdbwriter.hpp b/lib/perfdata/influxdbwriter.hpp
new file mode 100644
index 0000000..48676cc
--- /dev/null
+++ b/lib/perfdata/influxdbwriter.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef INFLUXDBWRITER_H
+#define INFLUXDBWRITER_H
+
+#include "perfdata/influxdbwriter-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * An Icinga InfluxDB v1 writer.
+ *
+ * @ingroup perfdata
+ */
+class InfluxdbWriter final : public ObjectImpl<InfluxdbWriter>
+{
+public:
+ DECLARE_OBJECT(InfluxdbWriter);
+ DECLARE_OBJECTNAME(InfluxdbWriter);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+protected:
+ boost::beast::http::request<boost::beast::http::string_body> AssembleRequest(String body) override;
+ Url::Ptr AssembleUrl() override;
+};
+
+}
+
+#endif /* INFLUXDBWRITER_H */
diff --git a/lib/perfdata/influxdbwriter.ti b/lib/perfdata/influxdbwriter.ti
new file mode 100644
index 0000000..e6fc84e
--- /dev/null
+++ b/lib/perfdata/influxdbwriter.ti
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/influxdbcommonwriter.hpp"
+
+library perfdata;
+
+namespace icinga
+{
+
+class InfluxdbWriter : InfluxdbCommonWriter
+{
+ activation_priority 100;
+
+ [config, required] String database {
+ default {{{ return "icinga2"; }}}
+ };
+ [config] String username {
+ default {{{ return ""; }}}
+ };
+ [config, no_user_view] String password {
+ default {{{ return ""; }}}
+ };
+ [config, no_user_view] Dictionary::Ptr basic_auth;
+};
+
+validator InfluxdbWriter {
+ Dictionary basic_auth {
+ required username;
+ String username;
+ required password;
+ String password;
+ };
+};
+
+}
diff --git a/lib/perfdata/opentsdbwriter.cpp b/lib/perfdata/opentsdbwriter.cpp
new file mode 100644
index 0000000..2a9cfc0
--- /dev/null
+++ b/lib/perfdata/opentsdbwriter.cpp
@@ -0,0 +1,525 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/opentsdbwriter.hpp"
+#include "perfdata/opentsdbwriter-ti.cpp"
+#include "icinga/service.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/compatutility.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/application.hpp"
+#include "base/stream.hpp"
+#include "base/networkstream.hpp"
+#include "base/exception.hpp"
+#include "base/statsfunction.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/replace.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE(OpenTsdbWriter);
+
+REGISTER_STATSFUNCTION(OpenTsdbWriter, &OpenTsdbWriter::StatsFunc);
+
+/*
+ * Enable HA capabilities once the config object is loaded.
+ */
+void OpenTsdbWriter::OnConfigLoaded()
+{
+ ObjectImpl<OpenTsdbWriter>::OnConfigLoaded();
+
+ if (!GetEnableHa()) {
+ Log(LogDebug, "OpenTsdbWriter")
+ << "HA functionality disabled. Won't pause connection: " << GetName();
+
+ SetHAMode(HARunEverywhere);
+ } else {
+ SetHAMode(HARunOnce);
+ }
+}
+
+/**
+ * Feature stats interface
+ *
+ * @param status Key value pairs for feature stats
+ */
+void OpenTsdbWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const OpenTsdbWriter::Ptr& opentsdbwriter : ConfigType::GetObjectsByType<OpenTsdbWriter>()) {
+ nodes.emplace_back(opentsdbwriter->GetName(), new Dictionary({
+ { "connected", opentsdbwriter->GetConnected() }
+ }));
+ }
+
+ status->Set("opentsdbwriter", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * Resume is equivalent to Start, but with HA capabilities to resume at runtime.
+ */
+void OpenTsdbWriter::Resume()
+{
+ ObjectImpl<OpenTsdbWriter>::Resume();
+
+ Log(LogInformation, "OpentsdbWriter")
+ << "'" << GetName() << "' resumed.";
+
+ ReadConfigTemplate(m_ServiceConfigTemplate, m_HostConfigTemplate);
+
+ m_ReconnectTimer = Timer::Create();
+ m_ReconnectTimer->SetInterval(10);
+ m_ReconnectTimer->OnTimerExpired.connect([this](const Timer * const&) { ReconnectTimerHandler(); });
+ m_ReconnectTimer->Start();
+ m_ReconnectTimer->Reschedule(0);
+
+ m_HandleCheckResults = Service::OnNewCheckResult.connect([this](const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ CheckResultHandler(checkable, cr);
+ });
+}
+
+/**
+ * Pause is equivalent to Stop, but with HA capabilities to resume at runtime.
+ */
+void OpenTsdbWriter::Pause()
+{
+ m_HandleCheckResults.disconnect();
+ m_ReconnectTimer->Stop(true);
+
+ Log(LogInformation, "OpentsdbWriter")
+ << "'" << GetName() << "' paused.";
+
+ m_Stream->close();
+
+ SetConnected(false);
+
+ ObjectImpl<OpenTsdbWriter>::Pause();
+}
+
+/**
+ * Reconnect handler called by the timer.
+ * Handles TLS
+ */
+void OpenTsdbWriter::ReconnectTimerHandler()
+{
+ if (IsPaused())
+ return;
+
+ SetShouldConnect(true);
+
+ if (GetConnected())
+ return;
+
+ double startTime = Utility::GetTime();
+
+ Log(LogNotice, "OpenTsdbWriter")
+ << "Reconnecting to OpenTSDB TSD on host '" << GetHost() << "' port '" << GetPort() << "'.";
+
+ /*
+ * We're using telnet as input method. Future PRs may change this into using the HTTP API.
+ * http://opentsdb.net/docs/build/html/user_guide/writing/index.html#telnet
+ */
+ m_Stream = Shared<AsioTcpStream>::Make(IoEngine::Get().GetIoContext());
+
+ try {
+ icinga::Connect(m_Stream->lowest_layer(), GetHost(), GetPort());
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "OpenTsdbWriter")
+ << "Can't connect to OpenTSDB on host '" << GetHost() << "' port '" << GetPort() << "'.";
+
+ SetConnected(false);
+
+ return;
+ }
+
+ SetConnected(true);
+
+ Log(LogInformation, "OpenTsdbWriter")
+ << "Finished reconnecting to OpenTSDB in " << std::setw(2) << Utility::GetTime() - startTime << " second(s).";
+}
+
+/**
+ * Registered check result handler processing data.
+ * Calculates tags from the config.
+ *
+ * @param checkable Host/service object
+ * @param cr Check result
+ */
+void OpenTsdbWriter::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ if (IsPaused())
+ return;
+
+ CONTEXT("Processing check result for '" << checkable->GetName() << "'");
+
+ if (!IcingaApplication::GetInstance()->GetEnablePerfdata() || !checkable->GetEnablePerfdata())
+ return;
+
+ Service::Ptr service = dynamic_pointer_cast<Service>(checkable);
+ Host::Ptr host;
+ Dictionary::Ptr config_tmpl;
+ Dictionary::Ptr config_tmpl_tags;
+ String config_tmpl_metric;
+
+ if (service) {
+ host = service->GetHost();
+ config_tmpl = m_ServiceConfigTemplate;
+ }
+ else {
+ host = static_pointer_cast<Host>(checkable);
+ config_tmpl = m_HostConfigTemplate;
+ }
+
+ // Get the tags nested dictionary in the service/host template in the config
+ if (config_tmpl) {
+ config_tmpl_tags = config_tmpl->Get("tags");
+ config_tmpl_metric = config_tmpl->Get("metric");
+ }
+
+ String metric;
+ std::map<String, String> tags;
+
+ // Resolve macros in configuration template and build custom tag list
+ if (config_tmpl_tags || !config_tmpl_metric.IsEmpty()) {
+
+ // Configure config template macro resolver
+ MacroProcessor::ResolverList resolvers;
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+
+ // Resolve macros for the service and host template config line
+ if (config_tmpl_tags) {
+ ObjectLock olock(config_tmpl_tags);
+
+ for (const Dictionary::Pair& pair : config_tmpl_tags) {
+
+ String missing_macro;
+ Value value = MacroProcessor::ResolveMacros(pair.second, resolvers, cr, &missing_macro);
+
+ if (!missing_macro.IsEmpty()) {
+ Log(LogDebug, "OpenTsdbWriter")
+ << "Unable to resolve macro:'" << missing_macro
+ << "' for this host or service.";
+
+ continue;
+ }
+
+ String tagname = Convert::ToString(pair.first);
+ tags[tagname] = EscapeTag(value);
+
+ }
+ }
+
+ // Resolve macros for the metric config line
+ if (!config_tmpl_metric.IsEmpty()) {
+
+ String missing_macro;
+ Value value = MacroProcessor::ResolveMacros(config_tmpl_metric, resolvers, cr, &missing_macro);
+
+ if (!missing_macro.IsEmpty()) {
+ Log(LogDebug, "OpenTsdbWriter")
+ << "Unable to resolve macro:'" << missing_macro
+ << "' for this host or service.";
+
+ }
+ else {
+
+ config_tmpl_metric = Convert::ToString(value);
+
+ }
+ }
+ }
+
+ String escaped_hostName = EscapeTag(host->GetName());
+ tags["host"] = escaped_hostName;
+
+ double ts = cr->GetExecutionEnd();
+
+ if (service) {
+
+ if (!config_tmpl_metric.IsEmpty()) {
+ metric = config_tmpl_metric;
+ } else {
+ String serviceName = service->GetShortName();
+ String escaped_serviceName = EscapeMetric(serviceName);
+ metric = "icinga.service." + escaped_serviceName;
+ }
+
+ SendMetric(checkable, metric + ".state", tags, service->GetState(), ts);
+
+ } else {
+ if (!config_tmpl_metric.IsEmpty()) {
+ metric = config_tmpl_metric;
+ } else {
+ metric = "icinga.host";
+ }
+ SendMetric(checkable, metric + ".state", tags, host->GetState(), ts);
+ }
+
+ SendMetric(checkable, metric + ".state_type", tags, checkable->GetStateType(), ts);
+ SendMetric(checkable, metric + ".reachable", tags, checkable->IsReachable(), ts);
+ SendMetric(checkable, metric + ".downtime_depth", tags, checkable->GetDowntimeDepth(), ts);
+ SendMetric(checkable, metric + ".acknowledgement", tags, checkable->GetAcknowledgement(), ts);
+
+ SendPerfdata(checkable, metric, tags, cr, ts);
+
+ metric = "icinga.check";
+
+ if (service) {
+ tags["type"] = "service";
+ String serviceName = service->GetShortName();
+ String escaped_serviceName = EscapeTag(serviceName);
+ tags["service"] = escaped_serviceName;
+ } else {
+ tags["type"] = "host";
+ }
+
+ SendMetric(checkable, metric + ".current_attempt", tags, checkable->GetCheckAttempt(), ts);
+ SendMetric(checkable, metric + ".max_check_attempts", tags, checkable->GetMaxCheckAttempts(), ts);
+ SendMetric(checkable, metric + ".latency", tags, cr->CalculateLatency(), ts);
+ SendMetric(checkable, metric + ".execution_time", tags, cr->CalculateExecutionTime(), ts);
+}
+
+/**
+ * Parse and send performance data metrics to OpenTSDB
+ *
+ * @param checkable Host/service object
+ * @param metric Full metric name
+ * @param tags Tag key pairs
+ * @param cr Check result containing performance data
+ * @param ts Timestamp when the check result was received
+ */
+void OpenTsdbWriter::SendPerfdata(const Checkable::Ptr& checkable, const String& metric,
+ const std::map<String, String>& tags, const CheckResult::Ptr& cr, double ts)
+{
+ Array::Ptr perfdata = cr->GetPerformanceData();
+
+ if (!perfdata)
+ return;
+
+ CheckCommand::Ptr checkCommand = checkable->GetCheckCommand();
+
+ ObjectLock olock(perfdata);
+ for (const Value& val : perfdata) {
+ PerfdataValue::Ptr pdv;
+
+ if (val.IsObjectType<PerfdataValue>())
+ pdv = val;
+ else {
+ try {
+ pdv = PerfdataValue::Parse(val);
+ } catch (const std::exception&) {
+ Log(LogWarning, "OpenTsdbWriter")
+ << "Ignoring invalid perfdata for checkable '"
+ << checkable->GetName() << "' and command '"
+ << checkCommand->GetName() << "' with value: " << val;
+ continue;
+ }
+ }
+
+ String metric_name;
+ std::map<String, String> tags_new = tags;
+
+ // Do not break original functionality where perfdata labels form
+ // part of the metric name
+ if (!GetEnableGenericMetrics()) {
+ String escaped_key = EscapeMetric(pdv->GetLabel());
+ boost::algorithm::replace_all(escaped_key, "::", ".");
+ metric_name = metric + "." + escaped_key;
+ } else {
+ String escaped_key = EscapeTag(pdv->GetLabel());
+ metric_name = metric;
+ tags_new["label"] = escaped_key;
+ }
+
+ SendMetric(checkable, metric_name, tags_new, pdv->GetValue(), ts);
+
+ if (!pdv->GetCrit().IsEmpty())
+ SendMetric(checkable, metric_name + "_crit", tags_new, pdv->GetCrit(), ts);
+ if (!pdv->GetWarn().IsEmpty())
+ SendMetric(checkable, metric_name + "_warn", tags_new, pdv->GetWarn(), ts);
+ if (!pdv->GetMin().IsEmpty())
+ SendMetric(checkable, metric_name + "_min", tags_new, pdv->GetMin(), ts);
+ if (!pdv->GetMax().IsEmpty())
+ SendMetric(checkable, metric_name + "_max", tags_new, pdv->GetMax(), ts);
+ }
+}
+
+/**
+ * Send given metric to OpenTSDB
+ *
+ * @param checkable Host/service object
+ * @param metric Full metric name
+ * @param tags Tag key pairs
+ * @param value Floating point metric value
+ * @param ts Timestamp where the metric was received from the check result
+ */
+void OpenTsdbWriter::SendMetric(const Checkable::Ptr& checkable, const String& metric,
+ const std::map<String, String>& tags, double value, double ts)
+{
+ String tags_string = "";
+
+ for (const Dictionary::Pair& tag : tags) {
+ tags_string += " " + tag.first + "=" + Convert::ToString(tag.second);
+ }
+
+ std::ostringstream msgbuf;
+ /*
+ * must be (http://opentsdb.net/docs/build/html/user_guide/query/timeseries.html)
+ * put <metric> <timestamp> <value> <tagk1=tagv1[ tagk2=tagv2 ...tagkN=tagvN]>
+ * "tags" must include at least one tag, we use "host=HOSTNAME"
+ */
+ msgbuf << "put " << metric << " " << static_cast<long>(ts) << " " << Convert::ToString(value) << tags_string;
+
+ Log(LogDebug, "OpenTsdbWriter")
+ << "Checkable '" << checkable->GetName() << "' adds to metric list: '" << msgbuf.str() << "'.";
+
+ /* do not send \n to debug log */
+ msgbuf << "\n";
+ String put = msgbuf.str();
+
+ ObjectLock olock(this);
+
+ if (!GetConnected())
+ return;
+
+ try {
+ Log(LogDebug, "OpenTsdbWriter")
+ << "Checkable '" << checkable->GetName() << "' sending message '" << put << "'.";
+
+ boost::asio::write(*m_Stream, boost::asio::buffer(msgbuf.str()));
+ m_Stream->flush();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "OpenTsdbWriter")
+ << "Cannot write to TCP socket on host '" << GetHost() << "' port '" << GetPort() << "'.";
+ }
+}
+
+/**
+ * Escape tags for OpenTSDB
+ * http://opentsdb.net/docs/build/html/user_guide/query/timeseries.html#precisions-on-metrics-and-tags
+ *
+ * @param str Tag name
+ * @return Escaped tag
+ */
+String OpenTsdbWriter::EscapeTag(const String& str)
+{
+ String result = str;
+
+ boost::replace_all(result, " ", "_");
+ boost::replace_all(result, "\\", "_");
+ boost::replace_all(result, ":", "_");
+
+ return result;
+}
+
+/**
+ * Escape metric name for OpenTSDB
+ * http://opentsdb.net/docs/build/html/user_guide/query/timeseries.html#precisions-on-metrics-and-tags
+ *
+ * @param str Metric name
+ * @return Escaped metric
+ */
+String OpenTsdbWriter::EscapeMetric(const String& str)
+{
+ String result = str;
+
+ boost::replace_all(result, " ", "_");
+ boost::replace_all(result, ".", "_");
+ boost::replace_all(result, "\\", "_");
+ boost::replace_all(result, ":", "_");
+
+ return result;
+}
+
+/**
+* Saves the template dictionaries defined in the config file into running memory
+*
+* @param stemplate The dictionary to save the service configuration to
+* @param htemplate The dictionary to save the host configuration to
+*/
+void OpenTsdbWriter::ReadConfigTemplate(const Dictionary::Ptr& stemplate,
+ const Dictionary::Ptr& htemplate)
+{
+
+ m_ServiceConfigTemplate = GetServiceTemplate();
+
+ if (!m_ServiceConfigTemplate) {
+ Log(LogDebug, "OpenTsdbWriter")
+ << "Unable to locate service template configuration.";
+ } else if (m_ServiceConfigTemplate->GetLength() == 0) {
+ Log(LogDebug, "OpenTsdbWriter")
+ << "The service template configuration is empty.";
+ }
+
+ m_HostConfigTemplate = GetHostTemplate();
+
+ if (!m_HostConfigTemplate) {
+ Log(LogDebug, "OpenTsdbWriter")
+ << "Unable to locate host template configuration.";
+ } else if (m_HostConfigTemplate->GetLength() == 0) {
+ Log(LogDebug, "OpenTsdbWriter")
+ << "The host template configuration is empty.";
+ }
+
+}
+
+
+/**
+* Validates the host_template configuration block in the configuration
+* file and checks for syntax errors.
+*
+* @param lvalue The host_template dictionary
+* @param utils Validation helper utilities
+*/
+void OpenTsdbWriter::ValidateHostTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<OpenTsdbWriter>::ValidateHostTemplate(lvalue, utils);
+
+ String metric = lvalue()->Get("metric");
+ if (!MacroProcessor::ValidateMacroString(metric))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "host_template", "metric" }, "Closing $ not found in macro format string '" + metric + "'."));
+
+ Dictionary::Ptr tags = lvalue()->Get("tags");
+ if (tags) {
+ ObjectLock olock(tags);
+ for (const Dictionary::Pair& pair : tags) {
+ if (!MacroProcessor::ValidateMacroString(pair.second))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "host_template", "tags", pair.first }, "Closing $ not found in macro format string '" + pair.second));
+ }
+ }
+}
+
+/**
+* Validates the service_template configuration block in the
+* configuration file and checks for syntax errors.
+*
+* @param lvalue The service_template dictionary
+* @param utils Validation helper utilities
+*/
+void OpenTsdbWriter::ValidateServiceTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<OpenTsdbWriter>::ValidateServiceTemplate(lvalue, utils);
+
+ String metric = lvalue()->Get("metric");
+ if (!MacroProcessor::ValidateMacroString(metric))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "service_template", "metric" }, "Closing $ not found in macro format string '" + metric + "'."));
+
+ Dictionary::Ptr tags = lvalue()->Get("tags");
+ if (tags) {
+ ObjectLock olock(tags);
+ for (const Dictionary::Pair& pair : tags) {
+ if (!MacroProcessor::ValidateMacroString(pair.second))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "service_template", "tags", pair.first }, "Closing $ not found in macro format string '" + pair.second));
+ }
+ }
+}
diff --git a/lib/perfdata/opentsdbwriter.hpp b/lib/perfdata/opentsdbwriter.hpp
new file mode 100644
index 0000000..e37ef42
--- /dev/null
+++ b/lib/perfdata/opentsdbwriter.hpp
@@ -0,0 +1,62 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OPENTSDBWRITER_H
+#define OPENTSDBWRITER_H
+
+#include "perfdata/opentsdbwriter-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/timer.hpp"
+#include <fstream>
+
+namespace icinga
+{
+
+/**
+ * An Icinga opentsdb writer.
+ *
+ * @ingroup perfdata
+ */
+class OpenTsdbWriter final : public ObjectImpl<OpenTsdbWriter>
+{
+public:
+ DECLARE_OBJECT(OpenTsdbWriter);
+ DECLARE_OBJECTNAME(OpenTsdbWriter);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void ValidateHostTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateServiceTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void OnConfigLoaded() override;
+ void Resume() override;
+ void Pause() override;
+
+private:
+ Shared<AsioTcpStream>::Ptr m_Stream;
+
+ boost::signals2::connection m_HandleCheckResults;
+ Timer::Ptr m_ReconnectTimer;
+
+ Dictionary::Ptr m_ServiceConfigTemplate;
+ Dictionary::Ptr m_HostConfigTemplate;
+
+ void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ void SendMetric(const Checkable::Ptr& checkable, const String& metric,
+ const std::map<String, String>& tags, double value, double ts);
+ void SendPerfdata(const Checkable::Ptr& checkable, const String& metric,
+ const std::map<String, String>& tags, const CheckResult::Ptr& cr, double ts);
+ static String EscapeTag(const String& str);
+ static String EscapeMetric(const String& str);
+
+ void ReconnectTimerHandler();
+
+ void ReadConfigTemplate(const Dictionary::Ptr& stemplate,
+ const Dictionary::Ptr& htemplate);
+};
+
+}
+
+#endif /* OPENTSDBWRITER_H */
diff --git a/lib/perfdata/opentsdbwriter.ti b/lib/perfdata/opentsdbwriter.ti
new file mode 100644
index 0000000..626350a
--- /dev/null
+++ b/lib/perfdata/opentsdbwriter.ti
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library perfdata;
+
+namespace icinga
+{
+
+class OpenTsdbWriter : ConfigObject
+{
+ activation_priority 100;
+
+ [config] String host {
+ default {{{ return "127.0.0.1"; }}}
+ };
+ [config] String port {
+ default {{{ return "4242"; }}}
+ };
+ [config] bool enable_ha {
+ default {{{ return false; }}}
+ };
+ [config] Dictionary::Ptr host_template {
+ default {{{ return new Dictionary(); }}}
+
+ };
+ [config] Dictionary::Ptr service_template {
+ default {{{ return new Dictionary(); }}}
+ };
+ [config] bool enable_generic_metrics {
+ default {{{ return false; }}}
+ };
+
+ [no_user_modify] bool connected;
+ [no_user_modify] bool should_connect {
+ default {{{ return true; }}}
+ };
+};
+
+validator OpenTsdbWriter {
+ Dictionary host_template {
+ String metric;
+ Dictionary "tags" {
+ String "*";
+ };
+ };
+ Dictionary service_template {
+ String metric;
+ Dictionary "tags" {
+ String "*";
+ };
+ };
+};
+
+}
diff --git a/lib/perfdata/perfdatawriter.cpp b/lib/perfdata/perfdatawriter.cpp
new file mode 100644
index 0000000..849f19e
--- /dev/null
+++ b/lib/perfdata/perfdatawriter.cpp
@@ -0,0 +1,201 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "perfdata/perfdatawriter.hpp"
+#include "perfdata/perfdatawriter-ti.cpp"
+#include "icinga/service.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/context.hpp"
+#include "base/exception.hpp"
+#include "base/application.hpp"
+#include "base/statsfunction.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(PerfdataWriter);
+
+REGISTER_STATSFUNCTION(PerfdataWriter, &PerfdataWriter::StatsFunc);
+
+void PerfdataWriter::OnConfigLoaded()
+{
+ ObjectImpl<PerfdataWriter>::OnConfigLoaded();
+
+ if (!GetEnableHa()) {
+ Log(LogDebug, "PerfdataWriter")
+ << "HA functionality disabled. Won't pause connection: " << GetName();
+
+ SetHAMode(HARunEverywhere);
+ } else {
+ SetHAMode(HARunOnce);
+ }
+}
+
+void PerfdataWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
+{
+ DictionaryData nodes;
+
+ for (const PerfdataWriter::Ptr& perfdatawriter : ConfigType::GetObjectsByType<PerfdataWriter>()) {
+ nodes.emplace_back(perfdatawriter->GetName(), 1); //add more stats
+ }
+
+ status->Set("perfdatawriter", new Dictionary(std::move(nodes)));
+}
+
+void PerfdataWriter::Resume()
+{
+ ObjectImpl<PerfdataWriter>::Resume();
+
+ Log(LogInformation, "PerfdataWriter")
+ << "'" << GetName() << "' resumed.";
+
+ m_HandleCheckResults = Checkable::OnNewCheckResult.connect([this](const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MessageOrigin::Ptr&) {
+ CheckResultHandler(checkable, cr);
+ });
+
+ m_RotationTimer = Timer::Create();
+ m_RotationTimer->OnTimerExpired.connect([this](const Timer * const&) { RotationTimerHandler(); });
+ m_RotationTimer->SetInterval(GetRotationInterval());
+ m_RotationTimer->Start();
+
+ RotateFile(m_ServiceOutputFile, GetServiceTempPath(), GetServicePerfdataPath());
+ RotateFile(m_HostOutputFile, GetHostTempPath(), GetHostPerfdataPath());
+}
+
+void PerfdataWriter::Pause()
+{
+ m_HandleCheckResults.disconnect();
+ m_RotationTimer->Stop(true);
+
+#ifdef I2_DEBUG
+ //m_HostOutputFile << "\n# Pause the feature" << "\n\n";
+ //m_ServiceOutputFile << "\n# Pause the feature" << "\n\n";
+#endif /* I2_DEBUG */
+
+ /* Force a rotation closing the file stream. */
+ RotateAllFiles();
+
+ Log(LogInformation, "PerfdataWriter")
+ << "'" << GetName() << "' paused.";
+
+ ObjectImpl<PerfdataWriter>::Pause();
+}
+
+Value PerfdataWriter::EscapeMacroMetric(const Value& value)
+{
+ if (value.IsObjectType<Array>())
+ return Utility::Join(value, ';');
+ else
+ return value;
+}
+
+void PerfdataWriter::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ if (IsPaused())
+ return;
+
+ CONTEXT("Writing performance data for object '" << checkable->GetName() << "'");
+
+ if (!IcingaApplication::GetInstance()->GetEnablePerfdata() || !checkable->GetEnablePerfdata())
+ return;
+
+ Service::Ptr service = dynamic_pointer_cast<Service>(checkable);
+ Host::Ptr host;
+
+ if (service)
+ host = service->GetHost();
+ else
+ host = static_pointer_cast<Host>(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+
+ if (service) {
+ String line = MacroProcessor::ResolveMacros(GetServiceFormatTemplate(), resolvers, cr, nullptr, &PerfdataWriter::EscapeMacroMetric);
+
+ {
+ std::unique_lock<std::mutex> lock(m_StreamMutex);
+
+ if (!m_ServiceOutputFile.good())
+ return;
+
+ m_ServiceOutputFile << line << "\n";
+ }
+ } else {
+ String line = MacroProcessor::ResolveMacros(GetHostFormatTemplate(), resolvers, cr, nullptr, &PerfdataWriter::EscapeMacroMetric);
+
+ {
+ std::unique_lock<std::mutex> lock(m_StreamMutex);
+
+ if (!m_HostOutputFile.good())
+ return;
+
+ m_HostOutputFile << line << "\n";
+ }
+ }
+}
+
+void PerfdataWriter::RotateFile(std::ofstream& output, const String& temp_path, const String& perfdata_path)
+{
+ Log(LogDebug, "PerfdataWriter")
+ << "Rotating perfdata files.";
+
+ std::unique_lock<std::mutex> lock(m_StreamMutex);
+
+ if (output.good()) {
+ output.close();
+
+ if (Utility::PathExists(temp_path)) {
+ String finalFile = perfdata_path + "." + Convert::ToString((long)Utility::GetTime());
+
+ Log(LogDebug, "PerfdataWriter")
+ << "Closed output file and renaming into '" << finalFile << "'.";
+
+ Utility::RenameFile(temp_path, finalFile);
+ }
+ }
+
+ output.open(temp_path.CStr());
+
+ if (!output.good()) {
+ Log(LogWarning, "PerfdataWriter")
+ << "Could not open perfdata file '" << temp_path << "' for writing. Perfdata will be lost.";
+ }
+}
+
+void PerfdataWriter::RotationTimerHandler()
+{
+ if (IsPaused())
+ return;
+
+ RotateAllFiles();
+}
+
+void PerfdataWriter::RotateAllFiles()
+{
+ RotateFile(m_ServiceOutputFile, GetServiceTempPath(), GetServicePerfdataPath());
+ RotateFile(m_HostOutputFile, GetHostTempPath(), GetHostPerfdataPath());
+}
+
+void PerfdataWriter::ValidateHostFormatTemplate(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<PerfdataWriter>::ValidateHostFormatTemplate(lvalue, utils);
+
+ if (!MacroProcessor::ValidateMacroString(lvalue()))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "host_format_template" }, "Closing $ not found in macro format string '" + lvalue() + "'."));
+}
+
+void PerfdataWriter::ValidateServiceFormatTemplate(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<PerfdataWriter>::ValidateServiceFormatTemplate(lvalue, utils);
+
+ if (!MacroProcessor::ValidateMacroString(lvalue()))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "service_format_template" }, "Closing $ not found in macro format string '" + lvalue() + "'."));
+}
diff --git a/lib/perfdata/perfdatawriter.hpp b/lib/perfdata/perfdatawriter.hpp
new file mode 100644
index 0000000..961d4e9
--- /dev/null
+++ b/lib/perfdata/perfdatawriter.hpp
@@ -0,0 +1,53 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PERFDATAWRITER_H
+#define PERFDATAWRITER_H
+
+#include "perfdata/perfdatawriter-ti.hpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/timer.hpp"
+#include <fstream>
+
+namespace icinga
+{
+
+/**
+ * An Icinga perfdata writer.
+ *
+ * @ingroup icinga
+ */
+class PerfdataWriter final : public ObjectImpl<PerfdataWriter>
+{
+public:
+ DECLARE_OBJECT(PerfdataWriter);
+ DECLARE_OBJECTNAME(PerfdataWriter);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ void ValidateHostFormatTemplate(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+ void ValidateServiceFormatTemplate(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void OnConfigLoaded() override;
+ void Resume() override;
+ void Pause() override;
+
+private:
+ boost::signals2::connection m_HandleCheckResults;
+ Timer::Ptr m_RotationTimer;
+ std::ofstream m_ServiceOutputFile;
+ std::ofstream m_HostOutputFile;
+ std::mutex m_StreamMutex;
+
+ void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+ static Value EscapeMacroMetric(const Value& value);
+
+ void RotationTimerHandler();
+ void RotateAllFiles();
+ void RotateFile(std::ofstream& output, const String& temp_path, const String& perfdata_path);
+};
+
+}
+
+#endif /* PERFDATAWRITER_H */
diff --git a/lib/perfdata/perfdatawriter.ti b/lib/perfdata/perfdatawriter.ti
new file mode 100644
index 0000000..d6d99e8
--- /dev/null
+++ b/lib/perfdata/perfdatawriter.ti
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/application.hpp"
+
+library perfdata;
+
+namespace icinga
+{
+
+class PerfdataWriter : ConfigObject
+{
+ activation_priority 100;
+
+ [config] String host_perfdata_path {
+ default {{{ return Configuration::SpoolDir + "/perfdata/host-perfdata"; }}}
+ };
+ [config] String service_perfdata_path {
+ default {{{ return Configuration::SpoolDir + "/perfdata/service-perfdata"; }}}
+ };
+ [config] String host_temp_path {
+ default {{{ return Configuration::SpoolDir + "/tmp/host-perfdata"; }}}
+ };
+ [config] String service_temp_path {
+ default {{{ return Configuration::SpoolDir + "/tmp/service-perfdata"; }}}
+ };
+ [config] String host_format_template {
+ default {{{
+ return "DATATYPE::HOSTPERFDATA\t"
+ "TIMET::$host.last_check$\t"
+ "HOSTNAME::$host.name$\t"
+ "HOSTPERFDATA::$host.perfdata$\t"
+ "HOSTCHECKCOMMAND::$host.check_command$\t"
+ "HOSTSTATE::$host.state$\t"
+ "HOSTSTATETYPE::$host.state_type$";
+ }}}
+ };
+ [config] String service_format_template {
+ default {{{
+ return "DATATYPE::SERVICEPERFDATA\t"
+ "TIMET::$service.last_check$\t"
+ "HOSTNAME::$host.name$\t"
+ "SERVICEDESC::$service.name$\t"
+ "SERVICEPERFDATA::$service.perfdata$\t"
+ "SERVICECHECKCOMMAND::$service.check_command$\t"
+ "HOSTSTATE::$host.state$\t"
+ "HOSTSTATETYPE::$host.state_type$\t"
+ "SERVICESTATE::$service.state$\t"
+ "SERVICESTATETYPE::$service.state_type$";
+ }}}
+ };
+
+ [config] double rotation_interval {
+ default {{{ return 30; }}}
+ };
+ [config] bool enable_ha {
+ default {{{ return false; }}}
+ };
+};
+
+}
diff --git a/lib/pgsql_shim/CMakeLists.txt b/lib/pgsql_shim/CMakeLists.txt
new file mode 100644
index 0000000..327b64a
--- /dev/null
+++ b/lib/pgsql_shim/CMakeLists.txt
@@ -0,0 +1,32 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+link_directories(${PostgreSQL_LIBRARY_DIRS})
+include_directories(${PostgreSQL_INCLUDE_DIRS})
+
+set(pgsql_shim_SOURCES
+ pgsql_shim.def
+ pgsqlinterface.cpp pgsqlinterface.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(pgsql_shim pgsql_shim pgsql_shim_SOURCES)
+endif()
+
+add_library(pgsql_shim SHARED ${pgsql_shim_SOURCES})
+
+include(GenerateExportHeader)
+generate_export_header(pgsql_shim)
+
+target_link_libraries(pgsql_shim ${PostgreSQL_LIBRARIES})
+
+set_target_properties (
+ pgsql_shim PROPERTIES
+ FOLDER Lib
+ VERSION ${SPEC_VERSION}
+)
+
+install(
+ TARGETS pgsql_shim
+ RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}
+ LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2
+)
diff --git a/lib/pgsql_shim/pgsql_shim.def b/lib/pgsql_shim/pgsql_shim.def
new file mode 100644
index 0000000..7580d67
--- /dev/null
+++ b/lib/pgsql_shim/pgsql_shim.def
@@ -0,0 +1,3 @@
+LIBRARY pgsql_shim
+EXPORTS
+ create_pgsql_shim
diff --git a/lib/pgsql_shim/pgsqlinterface.cpp b/lib/pgsql_shim/pgsqlinterface.cpp
new file mode 100644
index 0000000..95b6e7d
--- /dev/null
+++ b/lib/pgsql_shim/pgsqlinterface.cpp
@@ -0,0 +1,108 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "pgsql_shim/pgsqlinterface.hpp"
+
+using namespace icinga;
+
+struct PgsqlInterfaceImpl final : public PgsqlInterface
+{
+ void Destroy() override
+ {
+ delete this;
+ }
+
+ void clear(PGresult *res) const override
+ {
+ PQclear(res);
+ }
+
+ char *cmdTuples(PGresult *res) const override
+ {
+ return PQcmdTuples(res);
+ }
+
+ char *errorMessage(const PGconn *conn) const override
+ {
+ return PQerrorMessage(conn);
+ }
+
+ size_t escapeStringConn(PGconn *conn, char *to, const char *from, size_t length, int *error) const override
+ {
+ return PQescapeStringConn(conn, to, from, length, error);
+ }
+
+ PGresult *exec(PGconn *conn, const char *query) const override
+ {
+ return PQexec(conn, query);
+ }
+
+ void finish(PGconn *conn) const override
+ {
+ PQfinish(conn);
+ }
+
+ char *fname(const PGresult *res, int field_num) const override
+ {
+ return PQfname(res, field_num);
+ }
+
+ int getisnull(const PGresult *res, int tup_num, int field_num) const override
+ {
+ return PQgetisnull(res, tup_num, field_num);
+ }
+
+ char *getvalue(const PGresult *res, int tup_num, int field_num) const override
+ {
+ return PQgetvalue(res, tup_num, field_num);
+ }
+
+ int isthreadsafe() const override
+ {
+ return PQisthreadsafe();
+ }
+
+ int nfields(const PGresult *res) const override
+ {
+ return PQnfields(res);
+ }
+
+ int ntuples(const PGresult *res) const override
+ {
+ return PQntuples(res);
+ }
+
+ char *resultErrorMessage(const PGresult *res) const override
+ {
+ return PQresultErrorMessage(res);
+ }
+
+ ExecStatusType resultStatus(const PGresult *res) const override
+ {
+ return PQresultStatus(res);
+ }
+
+ int serverVersion(const PGconn *conn) const override
+ {
+ return PQserverVersion(conn);
+ }
+
+ PGconn *setdbLogin(const char *pghost, const char *pgport, const char *pgoptions, const char *pgtty, const char *dbName, const char *login, const char *pwd) const override
+ {
+ return PQsetdbLogin(pghost, pgport, pgoptions, pgtty, dbName, login, pwd);
+ }
+
+ PGconn *connectdb(const char *conninfo) const override
+ {
+ return PQconnectdb(conninfo);
+ }
+
+ ConnStatusType status(const PGconn *conn) const override
+ {
+ return PQstatus(conn);
+ }
+};
+
+PgsqlInterface *create_pgsql_shim()
+{
+ return new PgsqlInterfaceImpl();
+}
diff --git a/lib/pgsql_shim/pgsqlinterface.hpp b/lib/pgsql_shim/pgsqlinterface.hpp
new file mode 100644
index 0000000..2fe3303
--- /dev/null
+++ b/lib/pgsql_shim/pgsqlinterface.hpp
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PGSQLINTERFACE_H
+#define PGSQLINTERFACE_H
+
+#include "pgsql_shim/pgsql_shim_export.h"
+#include <memory>
+#include <libpq-fe.h>
+
+namespace icinga
+{
+
+struct PgsqlInterface
+{
+ PgsqlInterface(const PgsqlInterface&) = delete;
+ PgsqlInterface& operator=(PgsqlInterface&) = delete;
+
+ virtual void Destroy() = 0;
+
+ virtual void clear(PGresult *res) const = 0;
+ virtual char *cmdTuples(PGresult *res) const = 0;
+ virtual char *errorMessage(const PGconn *conn) const = 0;
+ virtual size_t escapeStringConn(PGconn *conn, char *to, const char *from, size_t length, int *error) const = 0;
+ virtual PGresult *exec(PGconn *conn, const char *query) const = 0;
+ virtual void finish(PGconn *conn) const = 0;
+ virtual char *fname(const PGresult *res, int field_num) const = 0;
+ virtual int getisnull(const PGresult *res, int tup_num, int field_num) const = 0;
+ virtual char *getvalue(const PGresult *res, int tup_num, int field_num) const = 0;
+ virtual int isthreadsafe() const = 0;
+ virtual int nfields(const PGresult *res) const = 0;
+ virtual int ntuples(const PGresult *res) const = 0;
+ virtual char *resultErrorMessage(const PGresult *res) const = 0;
+ virtual ExecStatusType resultStatus(const PGresult *res) const = 0;
+ virtual int serverVersion(const PGconn *conn) const = 0;
+ virtual PGconn *setdbLogin(const char *pghost, const char *pgport, const char *pgoptions, const char *pgtty, const char *dbName, const char *login, const char *pwd) const = 0;
+ virtual PGconn *connectdb(const char *conninfo) const = 0;
+ virtual ConnStatusType status(const PGconn *conn) const = 0;
+
+protected:
+ PgsqlInterface() = default;
+ ~PgsqlInterface() = default;
+};
+
+struct PgsqlInterfaceDeleter
+{
+ void operator()(PgsqlInterface *ifc) const
+ {
+ ifc->Destroy();
+ }
+};
+
+}
+
+extern "C"
+{
+ PGSQL_SHIM_EXPORT icinga::PgsqlInterface *create_pgsql_shim();
+}
+
+typedef icinga::PgsqlInterface *(*create_pgsql_shim_ptr)();
+
+#endif /* PGSQLINTERFACE_H */
diff --git a/lib/remote/CMakeLists.txt b/lib/remote/CMakeLists.txt
new file mode 100644
index 0000000..740b112
--- /dev/null
+++ b/lib/remote/CMakeLists.txt
@@ -0,0 +1,67 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(apilistener.ti apilistener-ti.cpp apilistener-ti.hpp)
+mkclass_target(apiuser.ti apiuser-ti.cpp apiuser-ti.hpp)
+mkclass_target(endpoint.ti endpoint-ti.cpp endpoint-ti.hpp)
+mkclass_target(zone.ti zone-ti.cpp zone-ti.hpp)
+
+set(remote_SOURCES
+ i2-remote.hpp
+ actionshandler.cpp actionshandler.hpp
+ apiaction.cpp apiaction.hpp
+ apifunction.cpp apifunction.hpp
+ apilistener.cpp apilistener.hpp apilistener-ti.hpp apilistener-configsync.cpp apilistener-filesync.cpp
+ apilistener-authority.cpp
+ apiuser.cpp apiuser.hpp apiuser-ti.hpp
+ configfileshandler.cpp configfileshandler.hpp
+ configobjectslock.cpp configobjectslock.hpp
+ configobjectutility.cpp configobjectutility.hpp
+ configpackageshandler.cpp configpackageshandler.hpp
+ configpackageutility.cpp configpackageutility.hpp
+ configstageshandler.cpp configstageshandler.hpp
+ consolehandler.cpp consolehandler.hpp
+ createobjecthandler.cpp createobjecthandler.hpp
+ deleteobjecthandler.cpp deleteobjecthandler.hpp
+ endpoint.cpp endpoint.hpp endpoint-ti.hpp
+ eventqueue.cpp eventqueue.hpp
+ eventshandler.cpp eventshandler.hpp
+ filterutility.cpp filterutility.hpp
+ httphandler.cpp httphandler.hpp
+ httpserverconnection.cpp httpserverconnection.hpp
+ httputility.cpp httputility.hpp
+ infohandler.cpp infohandler.hpp
+ jsonrpc.cpp jsonrpc.hpp
+ jsonrpcconnection.cpp jsonrpcconnection.hpp jsonrpcconnection-heartbeat.cpp jsonrpcconnection-pki.cpp
+ messageorigin.cpp messageorigin.hpp
+ modifyobjecthandler.cpp modifyobjecthandler.hpp
+ objectqueryhandler.cpp objectqueryhandler.hpp
+ pkiutility.cpp pkiutility.hpp
+ statushandler.cpp statushandler.hpp
+ templatequeryhandler.cpp templatequeryhandler.hpp
+ typequeryhandler.cpp typequeryhandler.hpp
+ url.cpp url.hpp url-characters.hpp
+ variablequeryhandler.cpp variablequeryhandler.hpp
+ zone.cpp zone.hpp zone-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(remote remote remote_SOURCES)
+endif()
+
+add_library(remote OBJECT ${remote_SOURCES})
+
+add_dependencies(remote base config)
+
+set_target_properties (
+ remote PROPERTIES
+ FOLDER Lib
+)
+
+#install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/log\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/zones\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/zones-stage\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/certs\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/certificate-requests\")")
+
+
diff --git a/lib/remote/actionshandler.cpp b/lib/remote/actionshandler.cpp
new file mode 100644
index 0000000..016c76d
--- /dev/null
+++ b/lib/remote/actionshandler.cpp
@@ -0,0 +1,145 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/actionshandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/apiaction.hpp"
+#include "base/defer.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include <set>
+
+using namespace icinga;
+
+thread_local ApiUser::Ptr ActionsHandler::AuthenticatedApiUser;
+
+REGISTER_URLHANDLER("/v1/actions", ActionsHandler);
+
+bool ActionsHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 3)
+ return false;
+
+ if (request.method() != http::verb::post)
+ return false;
+
+ String actionName = url->GetPath()[2];
+
+ ApiAction::Ptr action = ApiAction::GetByName(actionName);
+
+ if (!action) {
+ HttpUtility::SendJsonError(response, params, 404, "Action '" + actionName + "' does not exist.");
+ return true;
+ }
+
+ QueryDescription qd;
+
+ const std::vector<String>& types = action->GetTypes();
+ std::vector<Value> objs;
+
+ String permission = "actions/" + actionName;
+
+ if (!types.empty()) {
+ qd.Types = std::set<String>(types.begin(), types.end());
+ qd.Permission = permission;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ if (objs.empty()) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.");
+ return true;
+ }
+ } else {
+ FilterUtility::CheckPermission(user, permission);
+ objs.emplace_back(nullptr);
+ }
+
+ ArrayData results;
+
+ Log(LogNotice, "ApiActionHandler")
+ << "Running action " << actionName;
+
+ bool verbose = false;
+
+ ActionsHandler::AuthenticatedApiUser = user;
+ Defer a ([]() {
+ ActionsHandler::AuthenticatedApiUser = nullptr;
+ });
+
+ if (params)
+ verbose = HttpUtility::GetLastParameter(params, "verbose");
+
+ for (const ConfigObject::Ptr& obj : objs) {
+ try {
+ results.emplace_back(action->Invoke(obj, params));
+ } catch (const std::exception& ex) {
+ Dictionary::Ptr fail = new Dictionary({
+ { "code", 500 },
+ { "status", "Action execution failed: '" + DiagnosticInformation(ex, false) + "'." }
+ });
+
+ /* Exception for actions. Normally we would handle this inside SendJsonError(). */
+ if (verbose)
+ fail->Set("diagnostic_information", DiagnosticInformation(ex));
+
+ results.emplace_back(std::move(fail));
+ }
+ }
+
+ int statusCode = 500;
+ std::set<int> okStatusCodes, nonOkStatusCodes;
+
+ for (const Dictionary::Ptr& res : results) {
+ if (!res->Contains("code")) {
+ continue;
+ }
+
+ auto code = res->Get("code");
+
+ if (code >= 200 && code <= 299) {
+ okStatusCodes.insert(code);
+ } else {
+ nonOkStatusCodes.insert(code);
+ }
+ }
+
+ size_t okSize = okStatusCodes.size();
+ size_t nonOkSize = nonOkStatusCodes.size();
+
+ if (okSize == 1u && nonOkSize == 0u) {
+ statusCode = *okStatusCodes.begin();
+ } else if (nonOkSize == 1u) {
+ statusCode = *nonOkStatusCodes.begin();
+ } else if (okSize >= 2u && nonOkSize == 0u) {
+ statusCode = 200;
+ }
+
+ response.result(statusCode);
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/actionshandler.hpp b/lib/remote/actionshandler.hpp
new file mode 100644
index 0000000..ca662ca
--- /dev/null
+++ b/lib/remote/actionshandler.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ACTIONSHANDLER_H
+#define ACTIONSHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ActionsHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ActionsHandler);
+
+ static thread_local ApiUser::Ptr AuthenticatedApiUser;
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* ACTIONSHANDLER_H */
diff --git a/lib/remote/apiaction.cpp b/lib/remote/apiaction.cpp
new file mode 100644
index 0000000..4da91f0
--- /dev/null
+++ b/lib/remote/apiaction.cpp
@@ -0,0 +1,40 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apiaction.hpp"
+#include "base/singleton.hpp"
+
+using namespace icinga;
+
+ApiAction::ApiAction(std::vector<String> types, Callback action)
+ : m_Types(std::move(types)), m_Callback(std::move(action))
+{ }
+
+Value ApiAction::Invoke(const ConfigObject::Ptr& target, const Dictionary::Ptr& params)
+{
+ return m_Callback(target, params);
+}
+
+const std::vector<String>& ApiAction::GetTypes() const
+{
+ return m_Types;
+}
+
+ApiAction::Ptr ApiAction::GetByName(const String& name)
+{
+ return ApiActionRegistry::GetInstance()->GetItem(name);
+}
+
+void ApiAction::Register(const String& name, const ApiAction::Ptr& action)
+{
+ ApiActionRegistry::GetInstance()->Register(name, action);
+}
+
+void ApiAction::Unregister(const String& name)
+{
+ ApiActionRegistry::GetInstance()->Unregister(name);
+}
+
+ApiActionRegistry *ApiActionRegistry::GetInstance()
+{
+ return Singleton<ApiActionRegistry>::GetInstance();
+}
diff --git a/lib/remote/apiaction.hpp b/lib/remote/apiaction.hpp
new file mode 100644
index 0000000..f2719c1
--- /dev/null
+++ b/lib/remote/apiaction.hpp
@@ -0,0 +1,69 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIACTION_H
+#define APIACTION_H
+
+#include "remote/i2-remote.hpp"
+#include "base/registry.hpp"
+#include "base/value.hpp"
+#include "base/dictionary.hpp"
+#include "base/configobject.hpp"
+#include <vector>
+#include <boost/algorithm/string/replace.hpp>
+
+namespace icinga
+{
+
+/**
+ * An action available over the external HTTP API.
+ *
+ * @ingroup remote
+ */
+class ApiAction final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ApiAction);
+
+ typedef std::function<Value(const ConfigObject::Ptr& target, const Dictionary::Ptr& params)> Callback;
+
+ ApiAction(std::vector<String> registerTypes, Callback function);
+
+ Value Invoke(const ConfigObject::Ptr& target, const Dictionary::Ptr& params);
+
+ const std::vector<String>& GetTypes() const;
+
+ static ApiAction::Ptr GetByName(const String& name);
+ static void Register(const String& name, const ApiAction::Ptr& action);
+ static void Unregister(const String& name);
+
+private:
+ std::vector<String> m_Types;
+ Callback m_Callback;
+};
+
+/**
+ * A registry for API actions.
+ *
+ * @ingroup remote
+ */
+class ApiActionRegistry : public Registry<ApiActionRegistry, ApiAction::Ptr>
+{
+public:
+ static ApiActionRegistry *GetInstance();
+};
+
+#define REGISTER_APIACTION(name, types, callback) \
+ INITIALIZE_ONCE([]() { \
+ String registerName = #name; \
+ boost::algorithm::replace_all(registerName, "_", "-"); \
+ std::vector<String> registerTypes; \
+ String typeNames = types; \
+ if (!typeNames.IsEmpty()) \
+ registerTypes = typeNames.Split(";"); \
+ ApiAction::Ptr action = new ApiAction(registerTypes, callback); \
+ ApiActionRegistry::GetInstance()->Register(registerName, action); \
+ })
+
+}
+
+#endif /* APIACTION_H */
diff --git a/lib/remote/apifunction.cpp b/lib/remote/apifunction.cpp
new file mode 100644
index 0000000..5b855cc
--- /dev/null
+++ b/lib/remote/apifunction.cpp
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apifunction.hpp"
+#include "base/singleton.hpp"
+
+using namespace icinga;
+
+ApiFunction::ApiFunction(Callback function)
+ : m_Callback(std::move(function))
+{ }
+
+Value ApiFunction::Invoke(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& arguments)
+{
+ return m_Callback(origin, arguments);
+}
+
+ApiFunction::Ptr ApiFunction::GetByName(const String& name)
+{
+ return ApiFunctionRegistry::GetInstance()->GetItem(name);
+}
+
+void ApiFunction::Register(const String& name, const ApiFunction::Ptr& function)
+{
+ ApiFunctionRegistry::GetInstance()->Register(name, function);
+}
+
+void ApiFunction::Unregister(const String& name)
+{
+ ApiFunctionRegistry::GetInstance()->Unregister(name);
+}
+
+ApiFunctionRegistry *ApiFunctionRegistry::GetInstance()
+{
+ return Singleton<ApiFunctionRegistry>::GetInstance();
+}
diff --git a/lib/remote/apifunction.hpp b/lib/remote/apifunction.hpp
new file mode 100644
index 0000000..e611320
--- /dev/null
+++ b/lib/remote/apifunction.hpp
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIFUNCTION_H
+#define APIFUNCTION_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/messageorigin.hpp"
+#include "base/registry.hpp"
+#include "base/value.hpp"
+#include "base/dictionary.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * A function available over the internal cluster API.
+ *
+ * @ingroup base
+ */
+class ApiFunction final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ApiFunction);
+
+ typedef std::function<Value(const MessageOrigin::Ptr& origin, const Dictionary::Ptr&)> Callback;
+
+ ApiFunction(Callback function);
+
+ Value Invoke(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& arguments);
+
+ static ApiFunction::Ptr GetByName(const String& name);
+ static void Register(const String& name, const ApiFunction::Ptr& function);
+ static void Unregister(const String& name);
+
+private:
+ Callback m_Callback;
+};
+
+/**
+ * A registry for API functions.
+ *
+ * @ingroup base
+ */
+class ApiFunctionRegistry : public Registry<ApiFunctionRegistry, ApiFunction::Ptr>
+{
+public:
+ static ApiFunctionRegistry *GetInstance();
+};
+
+#define REGISTER_APIFUNCTION(name, ns, callback) \
+ INITIALIZE_ONCE([]() { \
+ ApiFunction::Ptr func = new ApiFunction(callback); \
+ ApiFunctionRegistry::GetInstance()->Register(#ns "::" #name, func); \
+ })
+
+}
+
+#endif /* APIFUNCTION_H */
diff --git a/lib/remote/apilistener-authority.cpp b/lib/remote/apilistener-authority.cpp
new file mode 100644
index 0000000..f33a190
--- /dev/null
+++ b/lib/remote/apilistener-authority.cpp
@@ -0,0 +1,84 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/zone.hpp"
+#include "remote/apilistener.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+std::atomic<bool> ApiListener::m_UpdatedObjectAuthority (false);
+
+void ApiListener::UpdateObjectAuthority()
+{
+ /* Always run this, even if there is no 'api' feature enabled. */
+ if (auto listener = ApiListener::GetInstance()) {
+ Log(LogNotice, "ApiListener")
+ << "Updating object authority for objects at endpoint '" << listener->GetIdentity() << "'.";
+ } else {
+ Log(LogNotice, "ApiListener")
+ << "Updating object authority for local objects.";
+ }
+
+ Zone::Ptr my_zone = Zone::GetLocalZone();
+
+ std::vector<Endpoint::Ptr> endpoints;
+ Endpoint::Ptr my_endpoint;
+
+ if (my_zone) {
+ my_endpoint = Endpoint::GetLocalEndpoint();
+
+ int num_total = 0;
+
+ for (const Endpoint::Ptr& endpoint : my_zone->GetEndpoints()) {
+ num_total++;
+
+ if (endpoint != my_endpoint && !endpoint->GetConnected())
+ continue;
+
+ endpoints.push_back(endpoint);
+ }
+
+ double startTime = Application::GetStartTime();
+
+ /* 30 seconds cold startup, don't update any authority to give the secondary endpoint time to reconnect. */
+ if (num_total > 1 && endpoints.size() <= 1 && (startTime == 0 || Utility::GetTime() - startTime < 30))
+ return;
+
+ std::sort(endpoints.begin(), endpoints.end(),
+ [](const ConfigObject::Ptr& a, const ConfigObject::Ptr& b) {
+ return a->GetName() < b->GetName();
+ }
+ );
+ }
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+ if (!object->IsActive() || object->GetHAMode() != HARunOnce)
+ continue;
+
+ bool authority;
+
+ if (!my_zone)
+ authority = true;
+ else
+ authority = endpoints[Utility::SDBM(object->GetName()) % endpoints.size()] == my_endpoint;
+
+#ifdef I2_DEBUG
+// //Enable on demand, causes heavy logging on each run.
+// Log(LogDebug, "ApiListener")
+// << "Setting authority '" << Convert::ToString(authority) << "' for object '" << object->GetName() << "' of type '" << object->GetReflectionType()->GetName() << "'.";
+#endif /* I2_DEBUG */
+
+ object->SetAuthority(authority);
+ }
+ }
+
+ m_UpdatedObjectAuthority.store(true);
+}
diff --git a/lib/remote/apilistener-configsync.cpp b/lib/remote/apilistener-configsync.cpp
new file mode 100644
index 0000000..a12db0b
--- /dev/null
+++ b/lib/remote/apilistener-configsync.cpp
@@ -0,0 +1,464 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/configobjectutility.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/configtype.hpp"
+#include "base/json.hpp"
+#include "base/convert.hpp"
+#include "config/vmops.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+REGISTER_APIFUNCTION(UpdateObject, config, &ApiListener::ConfigUpdateObjectAPIHandler);
+REGISTER_APIFUNCTION(DeleteObject, config, &ApiListener::ConfigDeleteObjectAPIHandler);
+
+INITIALIZE_ONCE([]() {
+ ConfigObject::OnActiveChanged.connect(&ApiListener::ConfigUpdateObjectHandler);
+ ConfigObject::OnVersionChanged.connect(&ApiListener::ConfigUpdateObjectHandler);
+});
+
+void ApiListener::ConfigUpdateObjectHandler(const ConfigObject::Ptr& object, const Value& cookie)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ if (object->IsActive()) {
+ /* Sync object config */
+ listener->UpdateConfigObject(object, cookie);
+ } else if (!object->IsActive() && object->GetExtension("ConfigObjectDeleted")) {
+ /* Delete object */
+ listener->DeleteConfigObject(object, cookie);
+ }
+}
+
+Value ApiListener::ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Log(LogNotice, "ApiListener")
+ << "Received config update for object: " << JsonEncode(params);
+
+ /* check permissions */
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ String objType = params->Get("type");
+ String objName = params->Get("name");
+
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ String identity = origin->FromClient->GetIdentity();
+
+ /* discard messages if the client is not configured on this node */
+ if (!endpoint) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config update object' message from '" << identity << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Zone::Ptr endpointZone = endpoint->GetZone();
+
+ /* discard messages if the sender is in a child zone */
+ if (!Zone::GetLocalZone()->IsChildOf(endpointZone)) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config update object' message"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. Sender is in a child zone.";
+ return Empty;
+ }
+
+ String objZone = params->Get("zone");
+
+ if (!objZone.IsEmpty() && !Zone::GetByName(objZone)) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config update object' message"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. Objects zone '" << objZone << "' isn't known locally.";
+ return Empty;
+ }
+
+ /* ignore messages if the endpoint does not accept config */
+ if (!listener->GetAcceptConfig()) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring config update"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. '" << listener->GetName() << "' does not accept config.";
+ return Empty;
+ }
+
+ /* update the object */
+ double objVersion = params->Get("version");
+
+ Type::Ptr ptype = Type::GetByName(objType);
+ auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (!ctype) {
+ // This never happens with icinga cluster endpoints, only with development errors.
+ Log(LogCritical, "ApiListener")
+ << "Config type '" << objType << "' does not exist.";
+ return Empty;
+ }
+
+ ConfigObject::Ptr object = ctype->GetObject(objName);
+
+ String config = params->Get("config");
+
+ bool newObject = false;
+
+ if (!object && !config.IsEmpty()) {
+ newObject = true;
+
+ /* object does not exist, create it through the API */
+ Array::Ptr errors = new Array();
+
+ /*
+ * Create the config object through our internal API.
+ * IMPORTANT: Pass the origin to prevent cluster sync loops.
+ */
+ if (!ConfigObjectUtility::CreateObject(ptype, objName, config, errors, nullptr, origin)) {
+ Log(LogCritical, "ApiListener")
+ << "Could not create object '" << objName << "':";
+
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "ApiListener", error);
+ }
+
+ return Empty;
+ }
+
+ object = ctype->GetObject(objName);
+
+ if (!object)
+ return Empty;
+
+ /* object was created, update its version */
+ object->SetVersion(objVersion, false, origin);
+ }
+
+ if (!object)
+ return Empty;
+
+ /* update object attributes if version was changed or if this is a new object */
+ if (newObject || objVersion <= object->GetVersion()) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding config update"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << object->GetName()
+ << "': Object version " << std::fixed << object->GetVersion()
+ << " is more recent than the received version " << std::fixed << objVersion << ".";
+
+ return Empty;
+ }
+
+ Log(LogNotice, "ApiListener")
+ << "Processing config update"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << object->GetName()
+ << "': Object version " << object->GetVersion()
+ << " is older than the received version " << objVersion << ".";
+
+ Dictionary::Ptr modified_attributes = params->Get("modified_attributes");
+
+ if (modified_attributes) {
+ ObjectLock olock(modified_attributes);
+ for (const Dictionary::Pair& kv : modified_attributes) {
+ /* update all modified attributes
+ * but do not update the object version yet.
+ * This triggers cluster events otherwise.
+ */
+ object->ModifyAttribute(kv.first, kv.second, false);
+ }
+ }
+
+ /* 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);
+ for (const Dictionary::Pair& kv : objOriginalAttributes) {
+ /* original attribute was removed, restore it */
+ if (!newOriginalAttributes->Contains(kv.first))
+ restoreAttrs.push_back(kv.first);
+ }
+ }
+
+ for (const String& key : restoreAttrs) {
+ /* do not update the object version yet. */
+ object->RestoreAttribute(key, false);
+ }
+ }
+
+ /* keep the object version in sync with the sender */
+ object->SetVersion(objVersion, false, origin);
+
+ return Empty;
+}
+
+Value ApiListener::ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Log(LogNotice, "ApiListener")
+ << "Received config delete for object: " << JsonEncode(params);
+
+ /* check permissions */
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ String objType = params->Get("type");
+ String objName = params->Get("name");
+
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ String identity = origin->FromClient->GetIdentity();
+
+ if (!endpoint) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config delete object' message from '" << identity << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Zone::Ptr endpointZone = endpoint->GetZone();
+
+ /* discard messages if the sender is in a child zone */
+ if (!Zone::GetLocalZone()->IsChildOf(endpointZone)) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config delete object' message"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. Sender is in a child zone.";
+ return Empty;
+ }
+
+ if (!listener->GetAcceptConfig()) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring config delete"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. '" << listener->GetName() << "' does not accept config.";
+ return Empty;
+ }
+
+ /* delete the object */
+ Type::Ptr ptype = Type::GetByName(objType);
+ auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (!ctype) {
+ // This never happens with icinga cluster endpoints, only with development errors.
+ Log(LogCritical, "ApiListener")
+ << "Config type '" << objType << "' does not exist.";
+ return Empty;
+ }
+
+ ConfigObject::Ptr object = ctype->GetObject(objName);
+
+ if (!object) {
+ Log(LogNotice, "ApiListener")
+ << "Could not delete non-existent object '" << objName << "' with type '" << params->Get("type") << "'.";
+ return Empty;
+ }
+
+ if (object->GetPackage() != "_api") {
+ Log(LogCritical, "ApiListener")
+ << "Could not delete object '" << objName << "': Not created by the API.";
+ return Empty;
+ }
+
+ Log(LogNotice, "ApiListener")
+ << "Processing config delete"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << object->GetName() << "'.";
+
+ Array::Ptr errors = new Array();
+
+ /*
+ * Delete the config object through our internal API.
+ * IMPORTANT: Pass the origin to prevent cluster sync loops.
+ */
+ if (!ConfigObjectUtility::DeleteObject(object, true, errors, nullptr, origin)) {
+ Log(LogCritical, "ApiListener", "Could not delete object:");
+
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "ApiListener", error);
+ }
+ }
+
+ return Empty;
+}
+
+void ApiListener::UpdateConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
+ const JsonRpcConnection::Ptr& client)
+{
+ /* 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")
+ << "Not sending 'update config' message to unauthorized zone '" << target_zone->GetName() << "'"
+ << " for object: '" << object->GetName() << "'.";
+
+ return;
+ }
+ }
+
+ if (object->GetPackage() != "_api" && object->GetVersion() == 0)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "config::UpdateObject" },
+ { "params", params }
+ });
+
+ params->Set("name", object->GetName());
+ params->Set("type", object->GetReflectionType()->GetName());
+ params->Set("version", object->GetVersion());
+
+ String zoneName = object->GetZoneName();
+
+ if (!zoneName.IsEmpty())
+ params->Set("zone", zoneName);
+
+ if (object->GetPackage() == "_api") {
+ std::ifstream fp(ConfigObjectUtility::GetExistingObjectConfigPath(object).CStr(), std::ifstream::binary);
+ if (!fp)
+ return;
+
+ String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+ params->Set("config", content);
+ }
+
+ Dictionary::Ptr original_attributes = object->GetOriginalAttributes();
+ Dictionary::Ptr modified_attributes = new Dictionary();
+ ArrayData newOriginalAttributes;
+
+ if (original_attributes) {
+ ObjectLock olock(original_attributes);
+ for (const Dictionary::Pair& kv : original_attributes) {
+ std::vector<String> tokens = kv.first.Split(".");
+
+ Value value = object;
+ for (const String& token : tokens) {
+ value = VMOps::GetField(value, token);
+ }
+
+ modified_attributes->Set(kv.first, value);
+
+ newOriginalAttributes.push_back(kv.first);
+ }
+ }
+
+ params->Set("modified_attributes", modified_attributes);
+
+ /* only send the original attribute keys */
+ params->Set("original_attributes", new Array(std::move(newOriginalAttributes)));
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "ApiListener")
+ << "Sent update for object '" << object->GetName() << "': " << JsonEncode(params);
+#endif /* I2_DEBUG */
+
+ if (client)
+ client->SendMessage(message);
+ else {
+ Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone());
+
+ if (!target)
+ target = Zone::GetLocalZone();
+
+ RelayMessage(origin, target, message, false);
+ }
+}
+
+
+void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
+ const JsonRpcConnection::Ptr& client)
+{
+ if (object->GetPackage() != "_api")
+ return;
+
+ /* 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")
+ << "Not sending 'delete config' message to unauthorized zone '" << target_zone->GetName() << "'"
+ << " for object: '" << object->GetName() << "'.";
+
+ return;
+ }
+ }
+
+ Dictionary::Ptr params = new Dictionary();
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "config::DeleteObject" },
+ { "params", params }
+ });
+
+ params->Set("name", object->GetName());
+ params->Set("type", object->GetReflectionType()->GetName());
+ params->Set("version", object->GetVersion());
+
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "ApiListener")
+ << "Sent delete for object '" << object->GetName() << "': " << JsonEncode(params);
+#endif /* I2_DEBUG */
+
+ if (client)
+ client->SendMessage(message);
+ else {
+ Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone());
+
+ if (!target)
+ target = Zone::GetLocalZone();
+
+ RelayMessage(origin, target, message, true);
+ }
+}
+
+/* Initial sync on connect for new endpoints */
+void ApiListener::SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient)
+{
+ Endpoint::Ptr endpoint = aclient->GetEndpoint();
+ ASSERT(endpoint);
+
+ Zone::Ptr azone = endpoint->GetZone();
+
+ Log(LogInformation, "ApiListener")
+ << "Syncing runtime objects to endpoint '" << endpoint->GetName() << "'.";
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+ /* don't sync objects for non-matching parent-child zones */
+ if (!azone->CanAccessObject(object))
+ continue;
+
+ /* send the config object to the connected client */
+ UpdateConfigObject(object, nullptr, aclient);
+ }
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Finished syncing runtime objects to endpoint '" << endpoint->GetName() << "'.";
+}
diff --git a/lib/remote/apilistener-filesync.cpp b/lib/remote/apilistener-filesync.cpp
new file mode 100644
index 0000000..acf8deb
--- /dev/null
+++ b/lib/remote/apilistener-filesync.cpp
@@ -0,0 +1,887 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "config/configcompiler.hpp"
+#include "base/tlsutility.hpp"
+#include "base/json.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/application.hpp"
+#include "base/exception.hpp"
+#include "base/shared.hpp"
+#include "base/utility.hpp"
+#include <fstream>
+#include <iomanip>
+#include <thread>
+
+using namespace icinga;
+
+REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
+
+std::mutex ApiListener::m_ConfigSyncStageLock;
+
+/**
+ * Entrypoint for updating all authoritative configs from /etc/zones.d, packages, etc.
+ * into var/lib/icinga2/api/zones
+ */
+void ApiListener::SyncLocalZoneDirs() const
+{
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ try {
+ SyncLocalZoneDir(zone);
+ } catch (const std::exception&) {
+ continue;
+ }
+ }
+}
+
+/**
+ * Sync a zone directory where we have an authoritative copy (zones.d, packages, etc.)
+ *
+ * This function collects the registered zone config dirs from
+ * the config compiler and reads the file content into the config
+ * information structure.
+ *
+ * Returns early when there are no updates.
+ *
+ * @param zone Pointer to the zone object being synced.
+ */
+void ApiListener::SyncLocalZoneDir(const Zone::Ptr& zone) const
+{
+ if (!zone)
+ return;
+
+ ConfigDirInformation newConfigInfo;
+ newConfigInfo.UpdateV1 = new Dictionary();
+ newConfigInfo.UpdateV2 = new Dictionary();
+ newConfigInfo.Checksums = new Dictionary();
+
+ String zoneName = zone->GetName();
+
+ // Load registered zone paths, e.g. '_etc', '_api' and user packages.
+ for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
+ ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
+
+ // Config files '*.conf'.
+ {
+ ObjectLock olock(newConfigPart.UpdateV1);
+ for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
+ String path = "/" + zf.Tag + kv.first;
+
+ newConfigInfo.UpdateV1->Set(path, kv.second);
+ newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
+ }
+ }
+
+ // Meta files.
+ {
+ ObjectLock olock(newConfigPart.UpdateV2);
+ for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
+ String path = "/" + zf.Tag + kv.first;
+
+ newConfigInfo.UpdateV2->Set(path, kv.second);
+ newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
+ }
+ }
+ }
+
+ size_t sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
+
+ // Return early if there are no updates.
+ if (sumUpdates == 0)
+ return;
+
+ String productionZonesDir = GetApiZonesDir() + zoneName;
+
+ Log(LogInformation, "ApiListener")
+ << "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << productionZonesDir << "'.";
+
+ // Purge files to allow deletion via zones.d.
+ if (Utility::PathExists(productionZonesDir))
+ Utility::RemoveDirRecursive(productionZonesDir);
+
+ Utility::MkDirP(productionZonesDir, 0700);
+
+ // Copy content and add additional meta data.
+ size_t numBytes = 0;
+
+ /* Note: We cannot simply copy directories here.
+ *
+ * Zone directories are registered from everywhere and we already
+ * have read their content into memory with LoadConfigDir().
+ */
+ Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
+
+ {
+ ObjectLock olock(newConfig);
+
+ for (const Dictionary::Pair& kv : newConfig) {
+ String dst = productionZonesDir + "/" + kv.first;
+
+ Utility::MkDirP(Utility::DirName(dst), 0755);
+
+ Log(LogInformation, "ApiListener")
+ << "Updating configuration file: " << dst;
+
+ String content = kv.second;
+
+ std::ofstream fp(dst.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+
+ fp << content;
+ fp.close();
+
+ numBytes += content.GetLength();
+ }
+ }
+
+ // Additional metadata.
+ String tsPath = productionZonesDir + "/.timestamp";
+
+ if (!Utility::PathExists(tsPath)) {
+ std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
+
+ fp << std::fixed << Utility::GetTime();
+ fp.close();
+ }
+
+ String authPath = productionZonesDir + "/.authoritative";
+
+ if (!Utility::PathExists(authPath)) {
+ std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
+ fp.close();
+ }
+
+ // Checksums.
+ String checksumsPath = productionZonesDir + "/.checksums";
+
+ if (Utility::PathExists(checksumsPath))
+ Utility::Remove(checksumsPath);
+
+ std::ofstream fp(checksumsPath.CStr(), std::ofstream::out | std::ostream::trunc);
+
+ fp << std::fixed << JsonEncode(newConfigInfo.Checksums);
+ fp.close();
+
+ Log(LogNotice, "ApiListener")
+ << "Updated meta data for cluster config sync. Checksum: '" << checksumsPath
+ << "', timestamp: '" << tsPath << "', auth: '" << authPath << "'.";
+}
+
+/**
+ * Entrypoint for sending a file based config update to a cluster client.
+ * This includes security checks for zone relations.
+ * Loads the zone config files where this client belongs to
+ * and sends the 'config::Update' JSON-RPC message.
+ *
+ * @param aclient Connected JSON-RPC client.
+ */
+void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
+{
+ Endpoint::Ptr endpoint = aclient->GetEndpoint();
+ ASSERT(endpoint);
+
+ Zone::Ptr clientZone = endpoint->GetZone();
+ Zone::Ptr localZone = Zone::GetLocalZone();
+
+ // Don't send config updates to parent zones
+ if (!clientZone->IsChildOf(localZone))
+ return;
+
+ Dictionary::Ptr configUpdateV1 = new Dictionary();
+ Dictionary::Ptr configUpdateV2 = new Dictionary();
+ Dictionary::Ptr configUpdateChecksums = new Dictionary(); // new since 2.11
+
+ String zonesDir = GetApiZonesDir();
+
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ String zoneName = zone->GetName();
+ String zoneDir = zonesDir + zoneName;
+
+ // Only sync child and global zones.
+ if (!zone->IsChildOf(clientZone) && !zone->IsGlobal())
+ continue;
+
+ // Zone was configured, but there's no configuration directory.
+ if (!Utility::PathExists(zoneDir))
+ continue;
+
+ Log(LogInformation, "ApiListener")
+ << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
+ << "zone '" << zoneName << "' to endpoint '" << endpoint->GetName() << "'.";
+
+ ConfigDirInformation config = LoadConfigDir(zoneDir);
+
+ configUpdateV1->Set(zoneName, config.UpdateV1);
+ configUpdateV2->Set(zoneName, config.UpdateV2);
+ configUpdateChecksums->Set(zoneName, config.Checksums); // new since 2.11
+ }
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "config::Update" },
+ { "params", new Dictionary({
+ { "update", configUpdateV1 },
+ { "update_v2", configUpdateV2 }, // Since 2.4.2.
+ { "checksums", configUpdateChecksums } // Since 2.11.0.
+ }) }
+ });
+
+ aclient->SendMessage(message);
+}
+
+static bool CompareTimestampsConfigChange(const Dictionary::Ptr& productionConfig, const Dictionary::Ptr& receivedConfig,
+ const String& stageConfigZoneDir)
+{
+ double productionTimestamp;
+ double receivedTimestamp;
+
+ // Missing production timestamp means that something really broke. Always trigger a config change then.
+ if (!productionConfig->Contains("/.timestamp"))
+ productionTimestamp = 0;
+ else
+ productionTimestamp = productionConfig->Get("/.timestamp");
+
+ // Missing received config timestamp means that something really broke. Always trigger a config change then.
+ if (!receivedConfig->Contains("/.timestamp"))
+ receivedTimestamp = Utility::GetTime() + 10;
+ else
+ receivedTimestamp = receivedConfig->Get("/.timestamp");
+
+ bool configChange;
+
+ // Skip update if our configuration files are more recent.
+ if (productionTimestamp >= receivedTimestamp) {
+
+ Log(LogInformation, "ApiListener")
+ << "Our production configuration is more recent than the received configuration update."
+ << " Ignoring configuration file update for path '" << stageConfigZoneDir << "'. Current timestamp '"
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", productionTimestamp) << "' ("
+ << std::fixed << std::setprecision(6) << productionTimestamp
+ << ") >= received timestamp '"
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", receivedTimestamp) << "' ("
+ << receivedTimestamp << ").";
+
+ configChange = false;
+
+ } else {
+ configChange = true;
+ }
+
+ // Update the .timestamp file inside the staging directory.
+ String tsPath = stageConfigZoneDir + "/.timestamp";
+
+ if (!Utility::PathExists(tsPath)) {
+ std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
+ fp << std::fixed << receivedTimestamp;
+ fp.close();
+ }
+
+ return configChange;
+}
+
+/**
+ * Registered handler when a new config::Update message is received.
+ *
+ * Checks destination and permissions first, locks the transaction and analyses the update.
+ * The newly received configuration is not copied to production immediately,
+ * but into the staging directory first.
+ * Last, the async validation and restart is triggered.
+ *
+ * @param origin Where this message came from.
+ * @param params Message parameters including the config updates.
+ * @returns Empty, required by the interface.
+ */
+Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ // Verify permissions and trust relationship.
+ if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
+ return Empty;
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener) {
+ Log(LogCritical, "ApiListener", "No instance available.");
+ return Empty;
+ }
+
+ if (!listener->GetAcceptConfig()) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
+ return Empty;
+ }
+
+ std::thread([origin, params, listener]() {
+ try {
+ listener->HandleConfigUpdate(origin, params);
+ } catch (const std::exception& ex) {
+ auto msg ("Exception during config sync: " + DiagnosticInformation(ex));
+
+ Log(LogCritical, "ApiListener") << msg;
+ listener->UpdateLastFailedZonesStageValidation(msg);
+ }
+ }).detach();
+ return Empty;
+}
+
+void ApiListener::HandleConfigUpdate(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ /* Only one transaction is allowed, concurrent message handlers need to wait.
+ * This affects two parent endpoints sending the config in the same moment.
+ */
+ std::lock_guard<std::mutex> lock(m_ConfigSyncStageLock);
+
+ String apiZonesStageDir = GetApiZonesStageDir();
+ String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
+ String fromZoneName = GetFromZoneName(origin->FromZone);
+
+ Log(LogInformation, "ApiListener")
+ << "Applying config update from endpoint '" << fromEndpointName
+ << "' of zone '" << fromZoneName << "'.";
+
+ // Config files.
+ Dictionary::Ptr updateV1 = params->Get("update");
+ // Meta data files: .timestamp, etc.
+ Dictionary::Ptr updateV2 = params->Get("update_v2");
+
+ // New since 2.11.0.
+ Dictionary::Ptr checksums;
+
+ if (params->Contains("checksums"))
+ checksums = params->Get("checksums");
+
+ bool configChange = false;
+
+ // Keep track of the relative config paths for later validation and copying. TODO: Find a better algorithm.
+ std::vector<String> relativePaths;
+
+ /*
+ * We can and must safely purge the staging directory, as the difference is taken between
+ * runtime production config and newly received configuration.
+ * This is needed to not mix deleted/changed content between received and stage
+ * config.
+ */
+ if (Utility::PathExists(apiZonesStageDir))
+ Utility::RemoveDirRecursive(apiZonesStageDir);
+
+ Utility::MkDirP(apiZonesStageDir, 0700);
+
+ // Analyse and process the update.
+ size_t count = 0;
+
+ ObjectLock olock(updateV1);
+
+ for (const Dictionary::Pair& kv : updateV1) {
+
+ // Check for the configured zones.
+ String zoneName = kv.first;
+ Zone::Ptr zone = Zone::GetByName(zoneName);
+
+ if (!zone) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring config update from endpoint '" << fromEndpointName
+ << "' for unknown zone '" << zoneName << "'.";
+
+ continue;
+ }
+
+ // Ignore updates where we have an authoritive copy in etc/zones.d, packages, etc.
+ if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
+ Log(LogInformation, "ApiListener")
+ << "Ignoring config update from endpoint '" << fromEndpointName
+ << "' for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";
+
+ continue;
+ }
+
+ // Put the received configuration into our stage directory.
+ String productionConfigZoneDir = GetApiZonesDir() + zoneName;
+ String stageConfigZoneDir = GetApiZonesStageDir() + zoneName;
+
+ Utility::MkDirP(productionConfigZoneDir, 0700);
+ Utility::MkDirP(stageConfigZoneDir, 0700);
+
+ // Merge the config information.
+ ConfigDirInformation newConfigInfo;
+ newConfigInfo.UpdateV1 = kv.second;
+
+ // Load metadata.
+ if (updateV2)
+ newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
+
+ // Load checksums. New since 2.11.
+ if (checksums)
+ newConfigInfo.Checksums = checksums->Get(kv.first);
+
+ // Load the current production config details.
+ ConfigDirInformation productionConfigInfo = LoadConfigDir(productionConfigZoneDir);
+
+ // Merge updateV1 and updateV2
+ Dictionary::Ptr productionConfig = MergeConfigUpdate(productionConfigInfo);
+ Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
+
+ bool timestampChanged = false;
+
+ if (CompareTimestampsConfigChange(productionConfig, newConfig, stageConfigZoneDir)) {
+ timestampChanged = true;
+ }
+
+ /* If we have received 'checksums' via cluster message, go for it.
+ * Otherwise do the old timestamp dance for versions < 2.11.
+ */
+ if (checksums) {
+ Log(LogInformation, "ApiListener")
+ << "Received configuration for zone '" << zoneName << "' from endpoint '"
+ << fromEndpointName << "'. Comparing the timestamp and checksums.";
+
+ if (timestampChanged) {
+
+ if (CheckConfigChange(productionConfigInfo, newConfigInfo))
+ configChange = true;
+ }
+
+ } else {
+ /* Fallback to timestamp handling when the parent endpoint didn't send checks.
+ * This can happen when the satellite is 2.11 and the master is 2.10.
+ *
+ * TODO: Deprecate and remove this behaviour in 2.13+.
+ */
+
+ Log(LogWarning, "ApiListener")
+ << "Received configuration update without checksums from parent endpoint "
+ << fromEndpointName << ". This behaviour is deprecated. Please upgrade the parent endpoint to 2.11+";
+
+ if (timestampChanged) {
+ configChange = true;
+ }
+
+ // Keep another hack when there's a timestamp file missing.
+ {
+ ObjectLock olock(newConfig);
+
+ for (const Dictionary::Pair &kv : newConfig) {
+
+ // This is super expensive with a string content comparison.
+ if (productionConfig->Get(kv.first) != kv.second) {
+ if (!Utility::Match("*/.timestamp", kv.first))
+ configChange = true;
+ }
+ }
+ }
+ }
+
+ // Dump the received configuration for this zone into the stage directory.
+ size_t numBytes = 0;
+
+ {
+ ObjectLock olock(newConfig);
+
+ for (const Dictionary::Pair& kv : newConfig) {
+
+ /* Store the relative config file path for later validation and activation.
+ * IMPORTANT: Store this prior to any filters.
+ * */
+ relativePaths.push_back(zoneName + "/" + kv.first);
+
+ String path = stageConfigZoneDir + "/" + kv.first;
+
+ if (Utility::Match("*.conf", path)) {
+ Log(LogInformation, "ApiListener")
+ << "Stage: Updating received configuration file '" << path << "' for zone '" << zoneName << "'.";
+ }
+
+ // Parent nodes < 2.11 always send this, avoid this bug and deny its receival prior to writing it on disk.
+ if (Utility::BaseName(path) == ".authoritative")
+ continue;
+
+ // Sync string content only.
+ String content = kv.second;
+
+ // Generate a directory tree (zones/1/2/3 might not exist yet).
+ Utility::MkDirP(Utility::DirName(path), 0755);
+
+ // Write the content to file.
+ std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fp << content;
+ fp.close();
+
+ numBytes += content.GetLength();
+ }
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Applying configuration file update for path '" << stageConfigZoneDir << "' ("
+ << numBytes << " Bytes).";
+
+ if (timestampChanged) {
+ // If the update removes a path, delete it on disk and signal a config change.
+ ObjectLock xlock(productionConfig);
+
+ for (const Dictionary::Pair& kv : productionConfig) {
+ if (!newConfig->Contains(kv.first)) {
+ configChange = true;
+
+ String path = stageConfigZoneDir + "/" + kv.first;
+ Utility::Remove(path);
+ }
+ }
+ }
+
+ count++;
+ }
+
+ /*
+ * We have processed all configuration files and stored them in the staging directory.
+ *
+ * We need to store them locally for later analysis. A config change means
+ * that we will validate the configuration in a separate process sandbox,
+ * and only copy the configuration to production when everything is ok.
+ *
+ * A successful validation also triggers the final restart.
+ */
+ if (configChange) {
+ Log(LogInformation, "ApiListener")
+ << "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
+ << "' are different to production, triggering validation and reload.";
+ TryActivateZonesStage(relativePaths);
+ } else {
+ Log(LogInformation, "ApiListener")
+ << "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
+ << "' are equal to production, skipping validation and reload.";
+ ClearLastFailedZonesStageValidation();
+ }
+}
+
+/**
+ * Spawns a new validation process with 'Internal.ZonesStageVarDir' set to override the config validation zone dirs with
+ * our current stage. Then waits for the validation result and if it was successful, the configuration is copied from
+ * stage to production and a restart is triggered. On validation failure, there is no restart and this is logged.
+ *
+ * The caller of this function must hold m_ConfigSyncStageLock.
+ *
+ * @param relativePaths Collected paths including the zone name, which are copied from stage to current directories.
+ */
+void ApiListener::TryActivateZonesStage(const std::vector<String>& relativePaths)
+{
+ VERIFY(Application::GetArgC() >= 1);
+
+ /* Inherit parent process args. */
+ Array::Ptr args = new Array({
+ Application::GetExePath(Application::GetArgV()[0]),
+ });
+
+ for (int i = 1; i < Application::GetArgC(); i++) {
+ String argV = Application::GetArgV()[i];
+
+ if (argV == "-d" || argV == "--daemonize")
+ continue;
+
+ args->Add(argV);
+ }
+
+ args->Add("--validate");
+
+ // Set the ZonesStageDir. This creates our own local chroot without any additional automated zone includes.
+ args->Add("--define");
+ args->Add("Internal.ZonesStageVarDir=" + GetApiZonesStageDir());
+
+ Process::Ptr process = new Process(Process::PrepareCommand(args));
+ process->SetTimeout(Application::GetReloadTimeout());
+
+ process->Run();
+ const ProcessResult& pr = process->WaitForResult();
+
+ String apiDir = GetApiDir();
+ String apiZonesDir = GetApiZonesDir();
+ String apiZonesStageDir = GetApiZonesStageDir();
+
+ String logFile = apiDir + "/zones-stage-startup.log";
+ std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpLog << pr.Output;
+ fpLog.close();
+
+ String statusFile = apiDir + "/zones-stage-status";
+ std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpStatus << pr.ExitStatus;
+ fpStatus.close();
+
+ // Validation went fine, copy stage and reload.
+ if (pr.ExitStatus == 0) {
+ Log(LogInformation, "ApiListener")
+ << "Config validation for stage '" << apiZonesStageDir << "' was OK, replacing into '" << apiZonesDir << "' and triggering reload.";
+
+ // Purge production before copying stage.
+ if (Utility::PathExists(apiZonesDir))
+ Utility::RemoveDirRecursive(apiZonesDir);
+
+ Utility::MkDirP(apiZonesDir, 0700);
+
+ // Copy all synced configuration files from stage to production.
+ for (const String& path : relativePaths) {
+ if (!Utility::PathExists(apiZonesStageDir + path))
+ continue;
+
+ Log(LogInformation, "ApiListener")
+ << "Copying file '" << path << "' from config sync staging to production zones directory.";
+
+ String stagePath = apiZonesStageDir + path;
+ String currentPath = apiZonesDir + path;
+
+ Utility::MkDirP(Utility::DirName(currentPath), 0700);
+
+ Utility::CopyFile(stagePath, currentPath);
+ }
+
+ // Clear any failed deployment before
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener)
+ listener->ClearLastFailedZonesStageValidation();
+
+ Application::RequestRestart();
+
+ // All good, return early.
+ return;
+ }
+
+ String failedLogFile = apiDir + "/zones-stage-startup-last-failed.log";
+ std::ofstream fpFailedLog(failedLogFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpFailedLog << pr.Output;
+ fpFailedLog.close();
+
+ // Error case.
+ Log(LogCritical, "ApiListener")
+ << "Config validation failed for staged cluster config sync in '" << apiZonesStageDir
+ << "'. Aborting. Logs: '" << failedLogFile << "'";
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener)
+ listener->UpdateLastFailedZonesStageValidation(pr.Output);
+}
+
+/**
+ * Update the structure from the last failed validation output.
+ * Uses the current timestamp.
+ *
+ * @param log The process output from the config validation.
+ */
+void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
+{
+ Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
+ { "log", log },
+ { "ts", Utility::GetTime() }
+ });
+
+ SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
+}
+
+/**
+ * Clear the structure for the last failed reload.
+ *
+ */
+void ApiListener::ClearLastFailedZonesStageValidation()
+{
+ SetLastFailedZonesStageValidation(Dictionary::Ptr());
+}
+
+/**
+ * Generate a config checksum.
+ *
+ * @param content String content used for generating the checksum.
+ * @returns The checksum as string.
+ */
+String ApiListener::GetChecksum(const String& content)
+{
+ return SHA256(content);
+}
+
+bool ApiListener::CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig)
+{
+ Dictionary::Ptr oldChecksums = oldConfig.Checksums;
+ Dictionary::Ptr newChecksums = newConfig.Checksums;
+
+ // TODO: Figure out whether normal users need this for debugging.
+ Log(LogDebug, "ApiListener")
+ << "Checking for config change between stage and production. Old (" << oldChecksums->GetLength() << "): '"
+ << JsonEncode(oldChecksums)
+ << "' vs. new (" << newChecksums->GetLength() << "): '"
+ << JsonEncode(newChecksums) << "'.";
+
+ /* Since internal files are synced here too, we can not depend on length.
+ * So we need to go through both checksum sets to cover the cases"everything is new" and "everything was deleted".
+ */
+ {
+ ObjectLock olock(oldChecksums);
+ for (const Dictionary::Pair& kv : oldChecksums) {
+ String path = kv.first;
+ String oldChecksum = kv.second;
+
+ /* Ignore internal files, especially .timestamp and .checksums.
+ *
+ * If we don't, this results in "always change" restart loops.
+ */
+ if (Utility::Match("/.*", path)) {
+ Log(LogDebug, "ApiListener")
+ << "Ignoring old internal file '" << path << "'.";
+
+ continue;
+ }
+
+ Log(LogDebug, "ApiListener")
+ << "Checking " << path << " for old checksum: " << oldChecksum << ".";
+
+ // Check if key exists first for more verbose logging.
+ // Note: Don't do this later on.
+ if (!newChecksums->Contains(path)) {
+ Log(LogDebug, "ApiListener")
+ << "File '" << path << "' was deleted by remote.";
+
+ return true;
+ }
+
+ String newChecksum = newChecksums->Get(path);
+
+ if (newChecksum != kv.second) {
+ Log(LogDebug, "ApiListener")
+ << "Path '" << path << "' doesn't match old checksum '"
+ << oldChecksum << "' with new checksum '" << newChecksum << "'.";
+
+ return true;
+ }
+ }
+ }
+
+ {
+ ObjectLock olock(newChecksums);
+ for (const Dictionary::Pair& kv : newChecksums) {
+ String path = kv.first;
+ String newChecksum = kv.second;
+
+ /* Ignore internal files, especially .timestamp and .checksums.
+ *
+ * If we don't, this results in "always change" restart loops.
+ */
+ if (Utility::Match("/.*", path)) {
+ Log(LogDebug, "ApiListener")
+ << "Ignoring new internal file '" << path << "'.";
+
+ continue;
+ }
+
+ Log(LogDebug, "ApiListener")
+ << "Checking " << path << " for new checksum: " << newChecksum << ".";
+
+ // Check if the checksum exists, checksums in both sets have already been compared
+ if (!oldChecksums->Contains(path)) {
+ Log(LogDebug, "ApiListener")
+ << "File '" << path << "' was added by remote.";
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Load the given config dir and read their file content into the config structure.
+ *
+ * @param dir Path to the config directory.
+ * @returns ConfigDirInformation structure.
+ */
+ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
+{
+ ConfigDirInformation config;
+ config.UpdateV1 = new Dictionary();
+ config.UpdateV2 = new Dictionary();
+ config.Checksums = new Dictionary();
+
+ Utility::GlobRecursive(dir, "*", [&config, dir](const String& file) { ConfigGlobHandler(config, dir, file); }, GlobFile);
+ return config;
+}
+
+/**
+ * Read the given file and store it in the config information structure.
+ * Callback function for Glob().
+ *
+ * @param config Reference to the config information object.
+ * @param path File path.
+ * @param file Full file name.
+ */
+void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
+{
+ // Avoid loading the authoritative marker for syncs at all cost.
+ if (Utility::BaseName(file) == ".authoritative")
+ return;
+
+ CONTEXT("Creating config update for file '" << file << "'");
+
+ Log(LogNotice, "ApiListener")
+ << "Creating config update for file '" << file << "'.";
+
+ std::ifstream fp(file.CStr(), std::ifstream::binary);
+ if (!fp)
+ return;
+
+ String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+
+ Dictionary::Ptr update;
+ String relativePath = file.SubStr(path.GetLength());
+
+ /*
+ * 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp).
+ *
+ * **Keep this intact to stay compatible with older clients.**
+ */
+ String sanitizedContent = Utility::ValidateUTF8(content);
+
+ if (Utility::Match("*.conf", file)) {
+ update = config.UpdateV1;
+
+ // Configuration files should be automatically sanitized with UTF8.
+ update->Set(relativePath, sanitizedContent);
+ } else {
+ update = config.UpdateV2;
+
+ /*
+ * Ensure that only valid UTF8 content is being read for the cluster config sync.
+ * Binary files are not supported when wrapped into JSON encoded messages.
+ * Rationale: https://github.com/Icinga/icinga2/issues/7382
+ */
+ if (content != sanitizedContent) {
+ Log(LogCritical, "ApiListener")
+ << "Ignoring file '" << file << "' for cluster config sync: Does not contain valid UTF8. Binary files are not supported.";
+ return;
+ }
+
+ update->Set(relativePath, content);
+ }
+
+ /* Calculate a checksum for each file (and a global one later).
+ *
+ * IMPORTANT: Ignore the .authoritative file above, this must not be synced.
+ * */
+ config.Checksums->Set(relativePath, GetChecksum(content));
+}
+
+/**
+ * Compatibility helper for merging config update v1 and v2 into a global result.
+ *
+ * @param config Config information structure.
+ * @returns Dictionary which holds the merged information.
+ */
+Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
+{
+ Dictionary::Ptr result = new Dictionary();
+
+ if (config.UpdateV1)
+ config.UpdateV1->CopyTo(result);
+
+ if (config.UpdateV2)
+ config.UpdateV2->CopyTo(result);
+
+ return result;
+}
diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp
new file mode 100644
index 0000000..85443e2
--- /dev/null
+++ b/lib/remote/apilistener.cpp
@@ -0,0 +1,1970 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apilistener.hpp"
+#include "remote/apilistener-ti.cpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/jsonrpc.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/configobjectutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/convert.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/netstring.hpp"
+#include "base/json.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/stdiostream.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/application.hpp"
+#include "base/context.hpp"
+#include "base/statsfunction.hpp"
+#include "base/exception.hpp"
+#include "base/tcpsocket.hpp"
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/io_context_strand.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/regex.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/thread/locks.hpp>
+#include <climits>
+#include <cstdint>
+#include <fstream>
+#include <memory>
+#include <openssl/ssl.h>
+#include <openssl/tls1.h>
+#include <openssl/x509.h>
+#include <sstream>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(ApiListener);
+
+boost::signals2::signal<void(bool)> ApiListener::OnMasterChanged;
+ApiListener::Ptr ApiListener::m_Instance;
+
+REGISTER_STATSFUNCTION(ApiListener, &ApiListener::StatsFunc);
+
+REGISTER_APIFUNCTION(Hello, icinga, &ApiListener::HelloAPIHandler);
+
+ApiListener::ApiListener()
+{
+ m_RelayQueue.SetName("ApiListener, RelayQueue");
+ m_SyncQueue.SetName("ApiListener, SyncQueue");
+}
+
+String ApiListener::GetApiDir()
+{
+ return Configuration::DataDir + "/api/";
+}
+
+String ApiListener::GetApiZonesDir()
+{
+ return GetApiDir() + "zones/";
+}
+
+String ApiListener::GetApiZonesStageDir()
+{
+ return GetApiDir() + "zones-stage/";
+}
+
+String ApiListener::GetCertsDir()
+{
+ return Configuration::DataDir + "/certs/";
+}
+
+String ApiListener::GetCaDir()
+{
+ return Configuration::DataDir + "/ca/";
+}
+
+String ApiListener::GetCertificateRequestsDir()
+{
+ return Configuration::DataDir + "/certificate-requests/";
+}
+
+String ApiListener::GetDefaultCertPath()
+{
+ return GetCertsDir() + "/" + ScriptGlobal::Get("NodeName") + ".crt";
+}
+
+String ApiListener::GetDefaultKeyPath()
+{
+ return GetCertsDir() + "/" + ScriptGlobal::Get("NodeName") + ".key";
+}
+
+String ApiListener::GetDefaultCaPath()
+{
+ return GetCertsDir() + "/ca.crt";
+}
+
+double ApiListener::GetTlsHandshakeTimeout() const
+{
+ return Configuration::TlsHandshakeTimeout;
+}
+
+void ApiListener::SetTlsHandshakeTimeout(double value, bool suppress_events, const Value& cookie)
+{
+ Configuration::TlsHandshakeTimeout = value;
+}
+
+void ApiListener::CopyCertificateFile(const String& oldCertPath, const String& newCertPath)
+{
+ struct stat st1, st2;
+
+ if (!oldCertPath.IsEmpty() && stat(oldCertPath.CStr(), &st1) >= 0 && (stat(newCertPath.CStr(), &st2) < 0 || st1.st_mtime > st2.st_mtime)) {
+ Log(LogWarning, "ApiListener")
+ << "Copying '" << oldCertPath << "' certificate file to '" << newCertPath << "'";
+
+ Utility::MkDirP(Utility::DirName(newCertPath), 0700);
+ Utility::CopyFile(oldCertPath, newCertPath);
+ }
+}
+
+void ApiListener::OnConfigLoaded()
+{
+ if (m_Instance)
+ BOOST_THROW_EXCEPTION(ScriptError("Only one ApiListener object is allowed.", GetDebugInfo()));
+
+ m_Instance = this;
+
+ String defaultCertPath = GetDefaultCertPath();
+ String defaultKeyPath = GetDefaultKeyPath();
+ String defaultCaPath = GetDefaultCaPath();
+
+ /* Migrate certificate location < 2.8 to the new default path. */
+ String oldCertPath = GetCertPath();
+ String oldKeyPath = GetKeyPath();
+ String oldCaPath = GetCaPath();
+
+ CopyCertificateFile(oldCertPath, defaultCertPath);
+ CopyCertificateFile(oldKeyPath, defaultKeyPath);
+ CopyCertificateFile(oldCaPath, defaultCaPath);
+
+ if (!oldCertPath.IsEmpty() && !oldKeyPath.IsEmpty() && !oldCaPath.IsEmpty()) {
+ Log(LogWarning, "ApiListener", "Please read the upgrading documentation for v2.8: https://icinga.com/docs/icinga2/latest/doc/16-upgrading-icinga-2/");
+ }
+
+ /* Create the internal API object storage. */
+ ConfigObjectUtility::CreateStorage();
+
+ /* Cache API packages and their active stage name. */
+ UpdateActivePackageStagesCache();
+
+ /* set up SSL context */
+ std::shared_ptr<X509> cert;
+ try {
+ cert = GetX509Certificate(defaultCertPath);
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot get certificate from cert path: '"
+ + defaultCertPath + "'.", GetDebugInfo()));
+ }
+
+ try {
+ SetIdentity(GetCertificateCN(cert));
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot get certificate common name from cert path: '"
+ + defaultCertPath + "'.", GetDebugInfo()));
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "My API identity: " << GetIdentity();
+
+ UpdateSSLContext();
+}
+
+std::shared_ptr<X509> ApiListener::RenewCert(const std::shared_ptr<X509>& cert, bool ca)
+{
+ std::shared_ptr<EVP_PKEY> pubkey (X509_get_pubkey(cert.get()), EVP_PKEY_free);
+ auto subject (X509_get_subject_name(cert.get()));
+ auto cacert (GetX509Certificate(GetDefaultCaPath()));
+ auto newcert (CreateCertIcingaCA(pubkey.get(), subject, ca));
+
+ /* verify that the new cert matches the CA we're using for the ApiListener;
+ * this ensures that the CA we have in /var/lib/icinga2/ca matches the one
+ * we're using for cluster connections (there's no point in sending a client
+ * a certificate it wouldn't be able to use to connect to us anyway) */
+ try {
+ if (!VerifyCertificate(cacert, newcert, GetCrlPath())) {
+ Log(LogWarning, "ApiListener")
+ << "The CA in '" << GetDefaultCaPath() << "' does not match the CA which Icinga uses "
+ << "for its own cluster connections. This is most likely a configuration problem.";
+
+ return nullptr;
+ }
+ } catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */
+
+ return newcert;
+}
+
+void ApiListener::UpdateSSLContext()
+{
+ auto ctx (SetupSslContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath(), GetCrlPath(), GetCipherList(), GetTlsProtocolmin(), GetDebugInfo()));
+
+ {
+ boost::unique_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
+
+ m_SSLContext = std::move(ctx);
+ }
+
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ client->Disconnect();
+ }
+ }
+
+ for (const JsonRpcConnection::Ptr& client : m_AnonymousClients) {
+ client->Disconnect();
+ }
+}
+
+void ApiListener::OnAllConfigLoaded()
+{
+ m_LocalEndpoint = Endpoint::GetByName(GetIdentity());
+
+ if (!m_LocalEndpoint)
+ BOOST_THROW_EXCEPTION(ScriptError("Endpoint object for '" + GetIdentity() + "' is missing.", GetDebugInfo()));
+}
+
+/**
+ * Starts the component.
+ */
+void ApiListener::Start(bool runtimeCreated)
+{
+ Log(LogInformation, "ApiListener")
+ << "'" << GetName() << "' started.";
+
+ SyncLocalZoneDirs();
+
+ m_RenewOwnCertTimer = Timer::Create();
+
+ if (Utility::PathExists(GetIcingaCADir() + "/ca.key")) {
+ RenewOwnCert();
+ RenewCA();
+
+ m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) {
+ RenewOwnCert();
+ RenewCA();
+ });
+ } else {
+ m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) {
+ JsonRpcConnection::SendCertificateRequest(nullptr, nullptr, String());
+ });
+ }
+
+ m_RenewOwnCertTimer->SetInterval(RENEW_INTERVAL);
+ m_RenewOwnCertTimer->Start();
+
+ ObjectImpl<ApiListener>::Start(runtimeCreated);
+
+ {
+ std::unique_lock<std::mutex> lock(m_LogLock);
+ OpenLogFile();
+ }
+
+ /* create the primary JSON-RPC listener */
+ if (!AddListener(GetBindHost(), GetBindPort())) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot add listener on host '" << GetBindHost() << "' for port '" << GetBindPort() << "'.";
+ Application::Exit(EXIT_FAILURE);
+ }
+
+ m_Timer = Timer::Create();
+ m_Timer->OnTimerExpired.connect([this](const Timer * const&) { ApiTimerHandler(); });
+ m_Timer->SetInterval(5);
+ m_Timer->Start();
+ m_Timer->Reschedule(0);
+
+ m_ReconnectTimer = Timer::Create();
+ m_ReconnectTimer->OnTimerExpired.connect([this](const Timer * const&) { ApiReconnectTimerHandler(); });
+ m_ReconnectTimer->SetInterval(10);
+ m_ReconnectTimer->Start();
+ m_ReconnectTimer->Reschedule(0);
+
+ /* Keep this in relative sync with the cold startup in UpdateObjectAuthority() and the reconnect interval above.
+ * Previous: 60s reconnect, 30s OA, 60s cold startup.
+ * Now: 10s reconnect, 10s OA, 30s cold startup.
+ */
+ m_AuthorityTimer = Timer::Create();
+ m_AuthorityTimer->OnTimerExpired.connect([](const Timer * const&) { UpdateObjectAuthority(); });
+ m_AuthorityTimer->SetInterval(10);
+ m_AuthorityTimer->Start();
+
+ m_CleanupCertificateRequestsTimer = Timer::Create();
+ m_CleanupCertificateRequestsTimer->OnTimerExpired.connect([this](const Timer * const&) { CleanupCertificateRequestsTimerHandler(); });
+ m_CleanupCertificateRequestsTimer->SetInterval(3600);
+ m_CleanupCertificateRequestsTimer->Start();
+ m_CleanupCertificateRequestsTimer->Reschedule(0);
+
+ m_ApiPackageIntegrityTimer = Timer::Create();
+ m_ApiPackageIntegrityTimer->OnTimerExpired.connect([this](const Timer * const&) { CheckApiPackageIntegrity(); });
+ m_ApiPackageIntegrityTimer->SetInterval(300);
+ m_ApiPackageIntegrityTimer->Start();
+
+ OnMasterChanged(true);
+}
+
+void ApiListener::RenewOwnCert()
+{
+ auto certPath (GetDefaultCertPath());
+ auto cert (GetX509Certificate(certPath));
+
+ if (IsCertUptodate(cert)) {
+ return;
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Our certificate will expire soon, but we own the CA. Renewing.";
+
+ cert = RenewCert(cert);
+
+ if (!cert) {
+ return;
+ }
+
+ AtomicFile::Write(certPath, 0644, CertificateToString(cert));
+ UpdateSSLContext();
+}
+
+void ApiListener::RenewCA()
+{
+ auto certPath (GetCaDir() + "/ca.crt");
+ auto cert (GetX509Certificate(certPath));
+
+ if (IsCaUptodate(cert.get())) {
+ return;
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Our CA will expire soon, but we own it. Renewing.";
+
+ cert = RenewCert(cert, true);
+
+ if (!cert) {
+ return;
+ }
+
+ auto certStr (CertificateToString(cert));
+
+ AtomicFile::Write(GetDefaultCaPath(), 0644, certStr);
+ AtomicFile::Write(certPath, 0644, certStr);
+ UpdateSSLContext();
+}
+
+void ApiListener::Stop(bool runtimeDeleted)
+{
+ m_ApiPackageIntegrityTimer->Stop(true);
+ m_CleanupCertificateRequestsTimer->Stop(true);
+ m_AuthorityTimer->Stop(true);
+ m_ReconnectTimer->Stop(true);
+ m_Timer->Stop(true);
+ m_RenewOwnCertTimer->Stop(true);
+
+ ObjectImpl<ApiListener>::Stop(runtimeDeleted);
+
+ Log(LogInformation, "ApiListener")
+ << "'" << GetName() << "' stopped.";
+
+ {
+ std::unique_lock<std::mutex> lock(m_LogLock);
+ CloseLogFile();
+ RotateLogFile();
+ }
+
+ RemoveStatusFile();
+}
+
+ApiListener::Ptr ApiListener::GetInstance()
+{
+ return m_Instance;
+}
+
+Endpoint::Ptr ApiListener::GetMaster() const
+{
+ Zone::Ptr zone = Zone::GetLocalZone();
+
+ if (!zone)
+ return nullptr;
+
+ std::vector<String> names;
+
+ for (const Endpoint::Ptr& endpoint : zone->GetEndpoints())
+ if (endpoint->GetConnected() || endpoint->GetName() == GetIdentity())
+ names.push_back(endpoint->GetName());
+
+ std::sort(names.begin(), names.end());
+
+ return Endpoint::GetByName(*names.begin());
+}
+
+bool ApiListener::IsMaster() const
+{
+ Endpoint::Ptr master = GetMaster();
+
+ if (!master)
+ return false;
+
+ return master == GetLocalEndpoint();
+}
+
+/**
+ * Creates a new JSON-RPC listener on the specified port.
+ *
+ * @param node The host the listener should be bound to.
+ * @param service The port to listen on.
+ */
+bool ApiListener::AddListener(const String& node, const String& service)
+{
+ namespace asio = boost::asio;
+ namespace ip = asio::ip;
+ using ip::tcp;
+
+ ObjectLock olock(this);
+
+ if (!m_SSLContext) {
+ Log(LogCritical, "ApiListener", "SSL context is required for AddListener()");
+ return false;
+ }
+
+ auto& io (IoEngine::Get().GetIoContext());
+ auto acceptor (Shared<tcp::acceptor>::Make(io));
+
+ try {
+ tcp::resolver resolver (io);
+ tcp::resolver::query query (node, service, tcp::resolver::query::passive);
+
+ auto result (resolver.resolve(query));
+ auto current (result.begin());
+
+ for (;;) {
+ try {
+ acceptor->open(current->endpoint().protocol());
+
+ {
+ auto fd (acceptor->native_handle());
+
+ const int optFalse = 0;
+ setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char *>(&optFalse), sizeof(optFalse));
+
+ const int optTrue = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char *>(&optTrue), sizeof(optTrue));
+#ifdef SO_REUSEPORT
+ setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<const char *>(&optTrue), sizeof(optTrue));
+#endif /* SO_REUSEPORT */
+ }
+
+ acceptor->bind(current->endpoint());
+
+ break;
+ } catch (const std::exception&) {
+ if (++current == result.end()) {
+ throw;
+ }
+
+ if (acceptor->is_open()) {
+ acceptor->close();
+ }
+ }
+ }
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot bind TCP socket for host '" << node << "' on port '" << service << "': " << ex.what();
+ return false;
+ }
+
+ acceptor->listen(INT_MAX);
+
+ auto localEndpoint (acceptor->local_endpoint());
+
+ Log(LogInformation, "ApiListener")
+ << "Started new listener on '[" << localEndpoint.address() << "]:" << localEndpoint.port() << "'";
+
+ IoEngine::SpawnCoroutine(io, [this, acceptor](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor); });
+
+ UpdateStatusFile(localEndpoint);
+
+ return true;
+}
+
+void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server)
+{
+ namespace asio = boost::asio;
+
+ auto& io (IoEngine::Get().GetIoContext());
+
+ time_t lastModified = -1;
+ const String crlPath = GetCrlPath();
+
+ if (!crlPath.IsEmpty()) {
+ lastModified = Utility::GetFileCreationTime(crlPath);
+ }
+
+ for (;;) {
+ try {
+ asio::ip::tcp::socket socket (io);
+
+ server->async_accept(socket.lowest_layer(), yc);
+
+ if (!crlPath.IsEmpty()) {
+ time_t currentCreationTime = Utility::GetFileCreationTime(crlPath);
+
+ if (lastModified != currentCreationTime) {
+ UpdateSSLContext();
+
+ lastModified = currentCreationTime;
+ }
+ }
+
+ boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
+ auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext));
+
+ lock.unlock();
+ sslConn->lowest_layer() = std::move(socket);
+
+ auto strand (Shared<asio::io_context::strand>::Make(io));
+
+ IoEngine::SpawnCoroutine(*strand, [this, strand, sslConn](asio::yield_context yc) {
+ Timeout::Ptr timeout(new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(GetConnectTimeout() * 1e6)),
+ [sslConn](asio::yield_context yc) {
+ Log(LogWarning, "ApiListener")
+ << "Timeout while processing incoming connection from "
+ << sslConn->lowest_layer().remote_endpoint();
+
+ boost::system::error_code ec;
+ sslConn->lowest_layer().cancel(ec);
+ }
+ ));
+ Defer cancelTimeout([timeout]() { timeout->Cancel(); });
+
+ NewClientHandler(yc, strand, sslConn, String(), RoleServer);
+ });
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot accept new connection: " << ex.what();
+ }
+ }
+}
+
+/**
+ * Creates a new JSON-RPC client and connects to the specified endpoint.
+ *
+ * @param endpoint The endpoint.
+ */
+void ApiListener::AddConnection(const Endpoint::Ptr& endpoint)
+{
+ namespace asio = boost::asio;
+ using asio::ip::tcp;
+
+ if (!m_SSLContext) {
+ Log(LogCritical, "ApiListener", "SSL context is required for AddConnection()");
+ return;
+ }
+
+ auto& io (IoEngine::Get().GetIoContext());
+ auto strand (Shared<asio::io_context::strand>::Make(io));
+
+ IoEngine::SpawnCoroutine(*strand, [this, strand, endpoint, &io](asio::yield_context yc) {
+ String host = endpoint->GetHost();
+ String port = endpoint->GetPort();
+
+ Log(LogInformation, "ApiListener")
+ << "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'";
+
+ try {
+ boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
+ auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext, endpoint->GetName()));
+
+ lock.unlock();
+
+ Timeout::Ptr timeout(new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(GetConnectTimeout() * 1e6)),
+ [sslConn, endpoint, host, port](asio::yield_context yc) {
+ Log(LogCritical, "ApiListener")
+ << "Timeout while reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host
+ << "' and port '" << port << "', cancelling attempt";
+
+ boost::system::error_code ec;
+ sslConn->lowest_layer().cancel(ec);
+ }
+ ));
+ Defer cancelTimeout([&timeout]() { timeout->Cancel(); });
+
+ Connect(sslConn->lowest_layer(), host, port, yc);
+
+ NewClientHandler(yc, strand, sslConn, endpoint->GetName(), RoleClient);
+
+ endpoint->SetConnecting(false);
+ Log(LogInformation, "ApiListener")
+ << "Finished reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'";
+ } catch (const std::exception& ex) {
+ endpoint->SetConnecting(false);
+
+ Log(LogCritical, "ApiListener")
+ << "Cannot connect to host '" << host << "' on port '" << port << "': " << ex.what();
+ }
+ });
+}
+
+void ApiListener::NewClientHandler(
+ boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
+ const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
+)
+{
+ try {
+ NewClientHandlerInternal(yc, strand, client, hostname, role);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Exception while handling new API client connection: " << DiagnosticInformation(ex, false);
+
+ Log(LogDebug, "ApiListener")
+ << "Exception while handling new API client connection: " << DiagnosticInformation(ex);
+ }
+}
+
+static const auto l_AppVersionInt (([]() -> unsigned long {
+ auto appVersion (Application::GetAppVersion());
+ boost::regex rgx (R"EOF(^[rv]?(\d+)\.(\d+)\.(\d+))EOF");
+ boost::smatch match;
+
+ if (!boost::regex_search(appVersion.GetData(), match, rgx)) {
+ return 0;
+ }
+
+ return 100u * 100u * boost::lexical_cast<unsigned long>(match[1].str())
+ + 100u * boost::lexical_cast<unsigned long>(match[2].str())
+ + boost::lexical_cast<unsigned long>(match[3].str());
+})());
+
+static const auto l_MyCapabilities (
+ (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand | (uint_fast64_t)ApiCapabilities::IfwApiCheckCommand
+);
+
+/**
+ * Processes a new client connection.
+ *
+ * @param client The new client.
+ */
+void ApiListener::NewClientHandlerInternal(
+ boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
+ const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
+)
+{
+ namespace asio = boost::asio;
+ namespace ssl = asio::ssl;
+
+ String conninfo;
+
+ {
+ std::ostringstream conninfo_;
+
+ if (role == RoleClient) {
+ conninfo_ << "to";
+ } else {
+ conninfo_ << "from";
+ }
+
+ auto endpoint (client->lowest_layer().remote_endpoint());
+
+ conninfo_ << " [" << endpoint.address() << "]:" << endpoint.port();
+
+ conninfo = conninfo_.str();
+ }
+
+ auto& sslConn (client->next_layer());
+
+ boost::system::error_code ec;
+
+ {
+ Timeout::Ptr handshakeTimeout (new Timeout(
+ strand->context(),
+ *strand,
+ boost::posix_time::microseconds(intmax_t(Configuration::TlsHandshakeTimeout * 1000000)),
+ [strand, client](asio::yield_context yc) {
+ boost::system::error_code ec;
+ client->lowest_layer().cancel(ec);
+ }
+ ));
+
+ sslConn.async_handshake(role == RoleClient ? sslConn.client : sslConn.server, yc[ec]);
+
+ handshakeTimeout->Cancel();
+ }
+
+ if (ec) {
+ // https://github.com/boostorg/beast/issues/915
+ // Google Chrome 73+ seems not close the connection properly, https://stackoverflow.com/questions/56272906/how-to-fix-certificate-unknown-error-from-chrome-v73
+ if (ec == asio::ssl::error::stream_truncated) {
+ Log(LogNotice, "ApiListener")
+ << "TLS stream was truncated, ignoring connection from " << conninfo;
+ return;
+ }
+
+ Log(LogCritical, "ApiListener")
+ << "Client TLS handshake failed (" << conninfo << "): " << ec.message();
+ return;
+ }
+
+ bool willBeShutDown = false;
+
+ Defer shutDownIfNeeded ([&sslConn, &willBeShutDown, &yc]() {
+ if (!willBeShutDown) {
+ // Ignore the error, but do not throw an exception being swallowed at all cost.
+ // https://github.com/Icinga/icinga2/issues/7351
+ boost::system::error_code ec;
+ sslConn.async_shutdown(yc[ec]);
+ }
+ });
+
+ std::shared_ptr<X509> cert (sslConn.GetPeerCertificate());
+ bool verify_ok = false;
+ String identity;
+ Endpoint::Ptr endpoint;
+
+ if (cert) {
+ verify_ok = sslConn.IsVerifyOK();
+
+ String verifyError = sslConn.GetVerifyError();
+
+ try {
+ identity = GetCertificateCN(cert);
+ } catch (const std::exception&) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot get certificate common name from peer (" << conninfo << ") cert.";
+ return;
+ }
+
+ if (!hostname.IsEmpty()) {
+ if (identity != hostname) {
+ Log(LogWarning, "ApiListener")
+ << "Unexpected certificate common name while connecting to endpoint '"
+ << hostname << "': got '" << identity << "'";
+ return;
+ } else if (!verify_ok) {
+ Log(LogWarning, "ApiListener")
+ << "Certificate validation failed for endpoint '" << hostname
+ << "': " << verifyError;
+ }
+ }
+
+ if (verify_ok) {
+ endpoint = Endpoint::GetByName(identity);
+ }
+
+ Log log(LogInformation, "ApiListener");
+
+ log << "New client connection for identity '" << identity << "' " << conninfo;
+
+ if (!verify_ok) {
+ log << " (certificate validation failed: " << verifyError << ")";
+ } else if (!endpoint) {
+ log << " (no Endpoint object found for identity)";
+ }
+ } else {
+ Log(LogInformation, "ApiListener")
+ << "New client connection " << conninfo << " (no client certificate)";
+ }
+
+ ClientType ctype;
+
+ try {
+ if (role == RoleClient) {
+ JsonRpc::SendMessage(client, new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "icinga::Hello" },
+ { "params", new Dictionary({
+ { "version", (double)l_AppVersionInt },
+ { "capabilities", (double)l_MyCapabilities }
+ }) }
+ }), yc);
+
+ client->async_flush(yc);
+
+ ctype = ClientJsonRpc;
+ } else {
+ {
+ boost::system::error_code ec;
+
+ if (client->async_fill(yc[ec]) == 0u) {
+ if (identity.IsEmpty()) {
+ Log(LogInformation, "ApiListener")
+ << "No data received on new API connection " << conninfo << ". "
+ << "Ensure that the remote endpoints are properly configured in a cluster setup.";
+ } else {
+ Log(LogWarning, "ApiListener")
+ << "No data received on new API connection " << conninfo << " for identity '" << identity << "'. "
+ << "Ensure that the remote endpoints are properly configured in a cluster setup.";
+ }
+
+ return;
+ }
+ }
+
+ char firstByte = 0;
+
+ {
+ asio::mutable_buffer firstByteBuf (&firstByte, 1);
+ client->peek(firstByteBuf);
+ }
+
+ if (firstByte >= '0' && firstByte <= '9') {
+ JsonRpc::SendMessage(client, new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "icinga::Hello" },
+ { "params", new Dictionary({
+ { "version", (double)l_AppVersionInt },
+ { "capabilities", (double)l_MyCapabilities }
+ }) }
+ }), yc);
+
+ client->async_flush(yc);
+
+ ctype = ClientJsonRpc;
+ } else {
+ ctype = ClientHttp;
+ }
+ }
+ } catch (const boost::system::system_error& systemError) {
+ if (systemError.code() == boost::asio::error::operation_aborted) {
+ shutDownIfNeeded.Cancel();
+ }
+
+ throw;
+ }
+
+ if (ctype == ClientJsonRpc) {
+ Log(LogNotice, "ApiListener", "New JSON-RPC client");
+
+ if (endpoint && endpoint->GetConnected()) {
+ Log(LogNotice, "ApiListener")
+ << "Ignoring JSON-RPC connection " << conninfo
+ << ". We're already connected to Endpoint '" << endpoint->GetName() << "'.";
+ return;
+ }
+
+ JsonRpcConnection::Ptr aclient = new JsonRpcConnection(identity, verify_ok, client, role);
+
+ if (endpoint) {
+ endpoint->AddClient(aclient);
+
+ Utility::QueueAsyncCallback([this, aclient, endpoint]() {
+ SyncClient(aclient, endpoint, true);
+ });
+ } else if (!AddAnonymousClient(aclient)) {
+ Log(LogNotice, "ApiListener")
+ << "Ignoring anonymous JSON-RPC connection " << conninfo
+ << ". Max connections (" << GetMaxAnonymousClients() << ") exceeded.";
+
+ aclient = nullptr;
+ }
+
+ if (aclient) {
+ aclient->Start();
+
+ willBeShutDown = true;
+ }
+ } else {
+ Log(LogNotice, "ApiListener", "New HTTP client");
+
+ HttpServerConnection::Ptr aclient = new HttpServerConnection(identity, verify_ok, client);
+ AddHttpClient(aclient);
+ aclient->Start();
+
+ willBeShutDown = true;
+ }
+}
+
+void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoint::Ptr& endpoint, bool needSync)
+{
+ Zone::Ptr eZone = endpoint->GetZone();
+
+ try {
+ {
+ ObjectLock olock(endpoint);
+
+ endpoint->SetSyncing(true);
+ }
+
+ Zone::Ptr myZone = Zone::GetLocalZone();
+ auto parent (myZone->GetParent());
+
+ if (parent == eZone || (!parent && eZone == myZone)) {
+ JsonRpcConnection::SendCertificateRequest(aclient, nullptr, String());
+
+ if (Utility::PathExists(ApiListener::GetCertificateRequestsDir())) {
+ Utility::Glob(ApiListener::GetCertificateRequestsDir() + "/*.json", [aclient](const String& newPath) {
+ JsonRpcConnection::SendCertificateRequest(aclient, nullptr, newPath);
+ }, GlobFile);
+ }
+ }
+
+ /* Make sure that the config updates are synced
+ * before the logs are replayed.
+ */
+
+ Log(LogInformation, "ApiListener")
+ << "Sending config updates for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+
+ /* sync zone file config */
+ SendConfigUpdate(aclient);
+
+ Log(LogInformation, "ApiListener")
+ << "Finished sending config file updates for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+
+ /* sync runtime config */
+ SendRuntimeConfigObjects(aclient);
+
+ Log(LogInformation, "ApiListener")
+ << "Finished sending runtime config updates for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+
+ if (!needSync) {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ return;
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Sending replay log for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+
+ ReplayLog(aclient);
+
+ if (eZone == Zone::GetLocalZone())
+ UpdateObjectAuthority();
+
+ Log(LogInformation, "ApiListener")
+ << "Finished sending replay log for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+ } catch (const std::exception& ex) {
+ {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ }
+
+ Log(LogCritical, "ApiListener")
+ << "Error while syncing endpoint '" << endpoint->GetName() << "': " << DiagnosticInformation(ex, false);
+
+ Log(LogDebug, "ApiListener")
+ << "Error while syncing endpoint '" << endpoint->GetName() << "': " << DiagnosticInformation(ex);
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Finished syncing endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+}
+
+void ApiListener::ApiTimerHandler()
+{
+ double now = Utility::GetTime();
+
+ std::vector<int> files;
+ Utility::Glob(GetApiDir() + "log/*", [&files](const String& file) { LogGlobHandler(files, file); }, GlobFile);
+ std::sort(files.begin(), files.end());
+
+ for (int ts : files) {
+ bool need = false;
+ auto localZone (GetLocalEndpoint()->GetZone());
+
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
+ if (endpoint == GetLocalEndpoint())
+ continue;
+
+ auto zone (endpoint->GetZone());
+
+ /* only care for endpoints in a) the same zone b) our parent zone c) immediate child zones */
+ if (!(zone == localZone || zone == localZone->GetParent() || zone->GetParent() == localZone)) {
+ continue;
+ }
+
+ if (endpoint->GetLogDuration() >= 0 && ts < now - endpoint->GetLogDuration())
+ continue;
+
+ if (ts > endpoint->GetLocalLogPosition()) {
+ need = true;
+ break;
+ }
+ }
+
+ if (!need) {
+ String path = GetApiDir() + "log/" + Convert::ToString(ts);
+ Log(LogNotice, "ApiListener")
+ << "Removing old log file: " << path;
+ (void)unlink(path.CStr());
+ }
+ }
+
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
+ if (!endpoint->GetConnected())
+ continue;
+
+ double ts = endpoint->GetRemoteLogPosition();
+
+ if (ts == 0)
+ continue;
+
+ Dictionary::Ptr lmessage = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "log::SetLogPosition" },
+ { "params", new Dictionary({
+ { "log_position", ts }
+ }) }
+ });
+
+ double maxTs = 0;
+
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ if (client->GetTimestamp() > maxTs)
+ maxTs = client->GetTimestamp();
+ }
+
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ if (client->GetTimestamp() == maxTs) {
+ client->SendMessage(lmessage);
+ } else {
+ client->Disconnect();
+ }
+ }
+
+ Log(LogNotice, "ApiListener")
+ << "Setting log position for identity '" << endpoint->GetName() << "': "
+ << Utility::FormatDateTime("%Y/%m/%d %H:%M:%S", ts);
+ }
+}
+
+void ApiListener::ApiReconnectTimerHandler()
+{
+ Zone::Ptr my_zone = Zone::GetLocalZone();
+
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ /* don't connect to global zones */
+ if (zone->GetGlobal())
+ continue;
+
+ /* only connect to endpoints in a) the same zone b) our parent zone c) immediate child zones */
+ if (my_zone != zone && my_zone != zone->GetParent() && zone != my_zone->GetParent()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Zone '" << zone->GetName()
+ << "' because it's not in the same zone, a parent or a child zone.";
+ continue;
+ }
+
+ for (const Endpoint::Ptr& endpoint : zone->GetEndpoints()) {
+ /* don't connect to ourselves */
+ if (endpoint == GetLocalEndpoint()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Endpoint '" << endpoint->GetName() << "' because that's us.";
+ continue;
+ }
+
+ /* don't try to connect to endpoints which don't have a host and port */
+ if (endpoint->GetHost().IsEmpty() || endpoint->GetPort().IsEmpty()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Endpoint '" << endpoint->GetName()
+ << "' because the host/port attributes are missing.";
+ continue;
+ }
+
+ /* don't try to connect if there's already a connection attempt */
+ if (endpoint->GetConnecting()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Endpoint '" << endpoint->GetName()
+ << "' because we're already trying to connect to it.";
+ continue;
+ }
+
+ /* don't try to connect if we're already connected */
+ if (endpoint->GetConnected()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Endpoint '" << endpoint->GetName()
+ << "' because we're already connected to it.";
+ continue;
+ }
+
+ /* Set connecting state to prevent duplicated queue inserts later. */
+ endpoint->SetConnecting(true);
+
+ AddConnection(endpoint);
+ }
+ }
+
+ Endpoint::Ptr master = GetMaster();
+
+ if (master)
+ Log(LogNotice, "ApiListener")
+ << "Current zone master: " << master->GetName();
+
+ std::vector<String> names;
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>())
+ if (endpoint->GetConnected())
+ names.emplace_back(endpoint->GetName() + " (" + Convert::ToString(endpoint->GetClients().size()) + ")");
+
+ Log(LogNotice, "ApiListener")
+ << "Connected endpoints: " << Utility::NaturalJoin(names);
+}
+
+static void CleanupCertificateRequest(const String& path, double expiryTime)
+{
+#ifndef _WIN32
+ struct stat statbuf;
+ if (lstat(path.CStr(), &statbuf) < 0)
+ return;
+#else /* _WIN32 */
+ struct _stat statbuf;
+ if (_stat(path.CStr(), &statbuf) < 0)
+ return;
+#endif /* _WIN32 */
+
+ if (statbuf.st_mtime < expiryTime)
+ (void) unlink(path.CStr());
+}
+
+void ApiListener::CleanupCertificateRequestsTimerHandler()
+{
+ String requestsDir = GetCertificateRequestsDir();
+
+ if (Utility::PathExists(requestsDir)) {
+ /* remove certificate requests that are older than a week */
+ double expiryTime = Utility::GetTime() - 7 * 24 * 60 * 60;
+ Utility::Glob(requestsDir + "/*.json", [expiryTime](const String& path) {
+ CleanupCertificateRequest(path, expiryTime);
+ }, GlobFile);
+ }
+}
+
+void ApiListener::RelayMessage(const MessageOrigin::Ptr& origin,
+ const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log)
+{
+ if (!IsActive())
+ return;
+
+ m_RelayQueue.Enqueue([this, origin, secobj, message, log]() { SyncRelayMessage(origin, secobj, message, log); }, PriorityNormal, true);
+}
+
+void ApiListener::PersistMessage(const Dictionary::Ptr& message, const ConfigObject::Ptr& secobj)
+{
+ double ts = message->Get("ts");
+
+ ASSERT(ts != 0);
+
+ Dictionary::Ptr pmessage = new Dictionary();
+ pmessage->Set("timestamp", ts);
+
+ pmessage->Set("message", JsonEncode(message));
+
+ if (secobj) {
+ Dictionary::Ptr secname = new Dictionary();
+ secname->Set("type", secobj->GetReflectionType()->GetName());
+ secname->Set("name", secobj->GetName());
+ pmessage->Set("secobj", secname);
+ }
+
+ std::unique_lock<std::mutex> lock(m_LogLock);
+ if (m_LogFile) {
+ NetString::WriteStringToStream(m_LogFile, JsonEncode(pmessage));
+ m_LogMessageCount++;
+ SetLogMessageTimestamp(ts);
+
+ if (m_LogMessageCount > 50000) {
+ CloseLogFile();
+ RotateLogFile();
+ OpenLogFile();
+ }
+ }
+}
+
+void ApiListener::SyncSendMessage(const Endpoint::Ptr& endpoint, const Dictionary::Ptr& message)
+{
+ ObjectLock olock(endpoint);
+
+ if (!endpoint->GetSyncing()) {
+ Log(LogNotice, "ApiListener")
+ << "Sending message '" << message->Get("method") << "' to '" << endpoint->GetName() << "'";
+
+ double maxTs = 0;
+
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ if (client->GetTimestamp() > maxTs)
+ maxTs = client->GetTimestamp();
+ }
+
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ if (client->GetTimestamp() != maxTs)
+ continue;
+
+ client->SendMessage(message);
+ }
+ }
+}
+
+/**
+ * Relay a message to a directly connected zone or to a global zone.
+ * If some other zone is passed as the target zone, it is not relayed.
+ *
+ * @param targetZone The zone to relay to
+ * @param origin Information about where this message is relayed from (if it was not generated locally)
+ * @param message The message to relay
+ * @param currentZoneMaster The current master node of the local zone
+ * @return true if the message has been relayed to all relevant endpoints,
+ * false if it hasn't and must be persisted in the replay log
+ */
+bool ApiListener::RelayMessageOne(const Zone::Ptr& targetZone, const MessageOrigin::Ptr& origin, const Dictionary::Ptr& message, const Endpoint::Ptr& currentZoneMaster)
+{
+ ASSERT(targetZone);
+
+ Zone::Ptr localZone = Zone::GetLocalZone();
+
+ /* only relay the message to a) the same local zone, b) the parent zone and c) direct child zones. Exception is a global zone. */
+ if (!targetZone->GetGlobal() &&
+ targetZone != localZone &&
+ targetZone != localZone->GetParent() &&
+ targetZone->GetParent() != localZone) {
+ return true;
+ }
+
+ Endpoint::Ptr localEndpoint = GetLocalEndpoint();
+
+ std::vector<Endpoint::Ptr> skippedEndpoints;
+
+ std::set<Zone::Ptr> allTargetZones;
+ if (targetZone->GetGlobal()) {
+ /* if the zone is global, the message has to be relayed to our local zone and direct children */
+ allTargetZones.insert(localZone);
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ if (zone->GetParent() == localZone) {
+ allTargetZones.insert(zone);
+ }
+ }
+ } else {
+ /* whereas if it's not global, the message is just relayed to the zone itself */
+ allTargetZones.insert(targetZone);
+ }
+
+ bool needsReplay = false;
+
+ for (const Zone::Ptr& currentTargetZone : allTargetZones) {
+ bool relayed = false, log_needed = false, log_done = false;
+
+ for (const Endpoint::Ptr& targetEndpoint : currentTargetZone->GetEndpoints()) {
+ /* Don't relay messages to ourselves. */
+ if (targetEndpoint == localEndpoint)
+ continue;
+
+ log_needed = true;
+
+ /* Don't relay messages to disconnected endpoints. */
+ if (!targetEndpoint->GetConnected()) {
+ if (currentTargetZone == localZone)
+ log_done = false;
+
+ continue;
+ }
+
+ log_done = true;
+
+ /* Don't relay the message to the zone through more than one endpoint unless this is our own zone.
+ * 'relayed' is set to true on success below, enabling the checks in the second iteration.
+ */
+ if (relayed && currentTargetZone != localZone) {
+ skippedEndpoints.push_back(targetEndpoint);
+ continue;
+ }
+
+ /* Don't relay messages back to the endpoint which we got the message from. */
+ if (origin && origin->FromClient && targetEndpoint == origin->FromClient->GetEndpoint()) {
+ skippedEndpoints.push_back(targetEndpoint);
+ continue;
+ }
+
+ /* Don't relay messages back to the zone which we got the message from. */
+ if (origin && origin->FromZone && currentTargetZone == origin->FromZone) {
+ skippedEndpoints.push_back(targetEndpoint);
+ continue;
+ }
+
+ /* Only relay message to the zone master if we're not currently the zone master.
+ * e1 is zone master, e2 and e3 are zone members.
+ *
+ * Message is sent from e2 or e3:
+ * !isMaster == true
+ * targetEndpoint e1 is zone master -> send the message
+ * targetEndpoint e3 is not zone master -> skip it, avoid routing loops
+ *
+ * Message is sent from e1:
+ * !isMaster == false -> send the messages to e2 and e3 being the zone routing master.
+ */
+ bool isMaster = (currentZoneMaster == localEndpoint);
+
+ if (!isMaster && targetEndpoint != currentZoneMaster) {
+ skippedEndpoints.push_back(targetEndpoint);
+ continue;
+ }
+
+ relayed = true;
+
+ SyncSendMessage(targetEndpoint, message);
+ }
+
+ if (log_needed && !log_done) {
+ needsReplay = true;
+ }
+ }
+
+ if (!skippedEndpoints.empty()) {
+ double ts = message->Get("ts");
+
+ for (const Endpoint::Ptr& skippedEndpoint : skippedEndpoints)
+ skippedEndpoint->SetLocalLogPosition(ts);
+ }
+
+ return !needsReplay;
+}
+
+void ApiListener::SyncRelayMessage(const MessageOrigin::Ptr& origin,
+ const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log)
+{
+ double ts = Utility::GetTime();
+ message->Set("ts", ts);
+
+ Log(LogNotice, "ApiListener")
+ << "Relaying '" << message->Get("method") << "' message";
+
+ if (origin && origin->FromZone)
+ message->Set("originZone", origin->FromZone->GetName());
+
+ Zone::Ptr target_zone;
+
+ if (secobj) {
+ if (secobj->GetReflectionType() == Zone::TypeInstance)
+ target_zone = static_pointer_cast<Zone>(secobj);
+ else
+ target_zone = static_pointer_cast<Zone>(secobj->GetZone());
+ }
+
+ if (!target_zone)
+ target_zone = Zone::GetLocalZone();
+
+ Endpoint::Ptr master = GetMaster();
+
+ bool need_log = !RelayMessageOne(target_zone, origin, message, master);
+
+ for (const Zone::Ptr& zone : target_zone->GetAllParentsRaw()) {
+ if (!RelayMessageOne(zone, origin, message, master))
+ need_log = true;
+ }
+
+ if (log && need_log)
+ PersistMessage(message, secobj);
+}
+
+/* must hold m_LogLock */
+void ApiListener::OpenLogFile()
+{
+ String path = GetApiDir() + "log/current";
+
+ Utility::MkDirP(Utility::DirName(path), 0750);
+
+ std::unique_ptr<std::fstream> fp = std::make_unique<std::fstream>(path.CStr(), std::fstream::out | std::ofstream::app);
+
+ if (!fp->good()) {
+ Log(LogWarning, "ApiListener")
+ << "Could not open spool file: " << path;
+ return;
+ }
+
+ m_LogFile = new StdioStream(fp.release(), true);
+ m_LogMessageCount = 0;
+ SetLogMessageTimestamp(Utility::GetTime());
+}
+
+/* must hold m_LogLock */
+void ApiListener::CloseLogFile()
+{
+ if (!m_LogFile)
+ return;
+
+ m_LogFile->Close();
+ m_LogFile.reset();
+}
+
+/* must hold m_LogLock */
+void ApiListener::RotateLogFile()
+{
+ double ts = GetLogMessageTimestamp();
+
+ if (ts == 0)
+ ts = Utility::GetTime();
+
+ String oldpath = GetApiDir() + "log/current";
+ String newpath = GetApiDir() + "log/" + Convert::ToString(static_cast<int>(ts)+1);
+
+ // If the log is being rotated more than once per second,
+ // don't overwrite the previous one, but silently deny rotation.
+ if (!Utility::PathExists(newpath)) {
+ try {
+ Utility::RenameFile(oldpath, newpath);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot rotate replay log file from '" << oldpath << "' to '"
+ << newpath << "': " << ex.what();
+ }
+ }
+}
+
+void ApiListener::LogGlobHandler(std::vector<int>& files, const String& file)
+{
+ String name = Utility::BaseName(file);
+
+ if (name == "current")
+ return;
+
+ int ts;
+
+ try {
+ ts = Convert::ToLong(name);
+ } catch (const std::exception&) {
+ return;
+ }
+
+ files.push_back(ts);
+}
+
+void ApiListener::ReplayLog(const JsonRpcConnection::Ptr& client)
+{
+ Endpoint::Ptr endpoint = client->GetEndpoint();
+
+ if (endpoint->GetLogDuration() == 0) {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ return;
+ }
+
+ CONTEXT("Replaying log for Endpoint '" << endpoint->GetName() << "'");
+
+ int count = -1;
+ double peer_ts = endpoint->GetLocalLogPosition();
+ double logpos_ts = peer_ts;
+ bool last_sync = false;
+
+ Endpoint::Ptr target_endpoint = client->GetEndpoint();
+ ASSERT(target_endpoint);
+
+ Zone::Ptr target_zone = target_endpoint->GetZone();
+
+ if (!target_zone) {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ return;
+ }
+
+ for (;;) {
+ std::unique_lock<std::mutex> lock(m_LogLock);
+
+ CloseLogFile();
+
+ if (count == -1 || count > 50000) {
+ OpenLogFile();
+ lock.unlock();
+ } else {
+ last_sync = true;
+ }
+
+ count = 0;
+
+ std::vector<int> files;
+ Utility::Glob(GetApiDir() + "log/*", [&files](const String& file) { LogGlobHandler(files, file); }, GlobFile);
+ std::sort(files.begin(), files.end());
+
+ std::vector<std::pair<int, String>> allFiles;
+
+ for (int ts : files) {
+ if (ts >= peer_ts) {
+ allFiles.emplace_back(ts, GetApiDir() + "log/" + Convert::ToString(ts));
+ }
+ }
+
+ allFiles.emplace_back(Utility::GetTime() + 1, GetApiDir() + "log/current");
+
+ for (auto& file : allFiles) {
+ Log(LogNotice, "ApiListener")
+ << "Replaying log: " << file.second;
+
+ auto *fp = new std::fstream(file.second.CStr(), std::fstream::in | std::fstream::binary);
+ StdioStream::Ptr logStream = new StdioStream(fp, true);
+
+ String message;
+ StreamReadContext src;
+ while (true) {
+ Dictionary::Ptr pmessage;
+
+ try {
+ StreamReadStatus srs = NetString::ReadStringFromStream(logStream, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ pmessage = JsonDecode(message);
+ } catch (const std::exception&) {
+ Log(LogWarning, "ApiListener")
+ << "Unexpected end-of-file for cluster log: " << file.second;
+
+ /* Log files may be incomplete or corrupted. This is perfectly OK. */
+ break;
+ }
+
+ if (pmessage->Get("timestamp") <= peer_ts)
+ continue;
+
+ Dictionary::Ptr secname = pmessage->Get("secobj");
+
+ if (secname) {
+ ConfigObject::Ptr secobj = ConfigObject::GetObject(secname->Get("type"), secname->Get("name"));
+
+ if (!secobj)
+ continue;
+
+ if (!target_zone->CanAccessObject(secobj))
+ continue;
+ }
+
+ try {
+ client->SendRawMessage(pmessage->Get("message"));
+ count++;
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ApiListener")
+ << "Error while replaying log for endpoint '" << endpoint->GetName() << "': " << DiagnosticInformation(ex, false);
+
+ Log(LogDebug, "ApiListener")
+ << "Error while replaying log for endpoint '" << endpoint->GetName() << "': " << DiagnosticInformation(ex);
+
+ break;
+ }
+
+ peer_ts = pmessage->Get("timestamp");
+
+ if (file.first > logpos_ts + 10) {
+ logpos_ts = file.first;
+
+ Dictionary::Ptr lmessage = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "log::SetLogPosition" },
+ { "params", new Dictionary({
+ { "log_position", logpos_ts }
+ }) }
+ });
+
+ client->SendMessage(lmessage);
+ }
+ }
+
+ logStream->Close();
+ }
+
+ if (count > 0) {
+ Log(LogInformation, "ApiListener")
+ << "Replayed " << count << " messages.";
+ }
+ else {
+ Log(LogNotice, "ApiListener")
+ << "Replayed " << count << " messages.";
+ }
+
+ if (last_sync) {
+ {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ }
+
+ OpenLogFile();
+
+ break;
+ }
+ }
+}
+
+void ApiListener::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ std::pair<Dictionary::Ptr, Dictionary::Ptr> stats;
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ stats = listener->GetStatus();
+
+ ObjectLock olock(stats.second);
+ for (const Dictionary::Pair& kv : stats.second)
+ perfdata->Add(new PerfdataValue("api_" + kv.first, kv.second));
+
+ status->Set("api", stats.first);
+}
+
+std::pair<Dictionary::Ptr, Dictionary::Ptr> ApiListener::GetStatus()
+{
+ Dictionary::Ptr perfdata = new Dictionary();
+
+ /* cluster stats */
+
+ double allEndpoints = 0;
+ Array::Ptr allNotConnectedEndpoints = new Array();
+ Array::Ptr allConnectedEndpoints = new Array();
+
+ Zone::Ptr my_zone = Zone::GetLocalZone();
+
+ Dictionary::Ptr connectedZones = new Dictionary();
+
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ /* only check endpoints in a) the same zone b) our parent zone c) immediate child zones */
+ if (my_zone != zone && my_zone != zone->GetParent() && zone != my_zone->GetParent()) {
+ Log(LogDebug, "ApiListener")
+ << "Not checking connection to Zone '" << zone->GetName() << "' because it's not in the same zone, a parent or a child zone.";
+ continue;
+ }
+
+ bool zoneConnected = false;
+ int countZoneEndpoints = 0;
+ double zoneLag = 0;
+
+ ArrayData zoneEndpoints;
+
+ for (const Endpoint::Ptr& endpoint : zone->GetEndpoints()) {
+ zoneEndpoints.emplace_back(endpoint->GetName());
+
+ if (endpoint->GetName() == GetIdentity())
+ continue;
+
+ double eplag = CalculateZoneLag(endpoint);
+
+ if (eplag > 0 && eplag > zoneLag)
+ zoneLag = eplag;
+
+ allEndpoints++;
+ countZoneEndpoints++;
+
+ if (!endpoint->GetConnected()) {
+ allNotConnectedEndpoints->Add(endpoint->GetName());
+ } else {
+ allConnectedEndpoints->Add(endpoint->GetName());
+ zoneConnected = true;
+ }
+ }
+
+ /* if there's only one endpoint inside the zone, we're not connected - that's us, fake it */
+ if (zone->GetEndpoints().size() == 1 && countZoneEndpoints == 0)
+ zoneConnected = true;
+
+ String parentZoneName;
+ Zone::Ptr parentZone = zone->GetParent();
+ if (parentZone)
+ parentZoneName = parentZone->GetName();
+
+ Dictionary::Ptr zoneStats = new Dictionary({
+ { "connected", zoneConnected },
+ { "client_log_lag", zoneLag },
+ { "endpoints", new Array(std::move(zoneEndpoints)) },
+ { "parent_zone", parentZoneName }
+ });
+
+ connectedZones->Set(zone->GetName(), zoneStats);
+ }
+
+ /* connection stats */
+ size_t jsonRpcAnonymousClients = GetAnonymousClients().size();
+ size_t httpClients = GetHttpClients().size();
+ size_t syncQueueItems = m_SyncQueue.GetLength();
+ size_t relayQueueItems = m_RelayQueue.GetLength();
+ double workQueueItemRate = JsonRpcConnection::GetWorkQueueRate();
+ double syncQueueItemRate = m_SyncQueue.GetTaskCount(60) / 60.0;
+ double relayQueueItemRate = m_RelayQueue.GetTaskCount(60) / 60.0;
+
+ Dictionary::Ptr status = new Dictionary({
+ { "identity", GetIdentity() },
+ { "num_endpoints", allEndpoints },
+ { "num_conn_endpoints", allConnectedEndpoints->GetLength() },
+ { "num_not_conn_endpoints", allNotConnectedEndpoints->GetLength() },
+ { "conn_endpoints", allConnectedEndpoints },
+ { "not_conn_endpoints", allNotConnectedEndpoints },
+
+ { "zones", connectedZones },
+
+ { "json_rpc", new Dictionary({
+ { "anonymous_clients", jsonRpcAnonymousClients },
+ { "sync_queue_items", syncQueueItems },
+ { "relay_queue_items", relayQueueItems },
+ { "work_queue_item_rate", workQueueItemRate },
+ { "sync_queue_item_rate", syncQueueItemRate },
+ { "relay_queue_item_rate", relayQueueItemRate }
+ }) },
+
+ { "http", new Dictionary({
+ { "clients", httpClients }
+ }) }
+ });
+
+ /* performance data */
+ perfdata->Set("num_endpoints", allEndpoints);
+ perfdata->Set("num_conn_endpoints", Convert::ToDouble(allConnectedEndpoints->GetLength()));
+ perfdata->Set("num_not_conn_endpoints", Convert::ToDouble(allNotConnectedEndpoints->GetLength()));
+
+ perfdata->Set("num_json_rpc_anonymous_clients", jsonRpcAnonymousClients);
+ perfdata->Set("num_http_clients", httpClients);
+ perfdata->Set("num_json_rpc_sync_queue_items", syncQueueItems);
+ perfdata->Set("num_json_rpc_relay_queue_items", relayQueueItems);
+
+ perfdata->Set("num_json_rpc_work_queue_item_rate", workQueueItemRate);
+ perfdata->Set("num_json_rpc_sync_queue_item_rate", syncQueueItemRate);
+ perfdata->Set("num_json_rpc_relay_queue_item_rate", relayQueueItemRate);
+
+ return std::make_pair(status, perfdata);
+}
+
+double ApiListener::CalculateZoneLag(const Endpoint::Ptr& endpoint)
+{
+ double remoteLogPosition = endpoint->GetRemoteLogPosition();
+ double eplag = Utility::GetTime() - remoteLogPosition;
+
+ if ((endpoint->GetSyncing() || !endpoint->GetConnected()) && remoteLogPosition != 0)
+ return eplag;
+
+ return 0;
+}
+
+bool ApiListener::AddAnonymousClient(const JsonRpcConnection::Ptr& aclient)
+{
+ std::unique_lock<std::mutex> lock(m_AnonymousClientsLock);
+
+ if (GetMaxAnonymousClients() >= 0 && (long)m_AnonymousClients.size() + 1 > (long)GetMaxAnonymousClients())
+ return false;
+
+ m_AnonymousClients.insert(aclient);
+ return true;
+}
+
+void ApiListener::RemoveAnonymousClient(const JsonRpcConnection::Ptr& aclient)
+{
+ std::unique_lock<std::mutex> lock(m_AnonymousClientsLock);
+ m_AnonymousClients.erase(aclient);
+}
+
+std::set<JsonRpcConnection::Ptr> ApiListener::GetAnonymousClients() const
+{
+ std::unique_lock<std::mutex> lock(m_AnonymousClientsLock);
+ return m_AnonymousClients;
+}
+
+void ApiListener::AddHttpClient(const HttpServerConnection::Ptr& aclient)
+{
+ std::unique_lock<std::mutex> lock(m_HttpClientsLock);
+ m_HttpClients.insert(aclient);
+}
+
+void ApiListener::RemoveHttpClient(const HttpServerConnection::Ptr& aclient)
+{
+ std::unique_lock<std::mutex> lock(m_HttpClientsLock);
+ m_HttpClients.erase(aclient);
+}
+
+std::set<HttpServerConnection::Ptr> ApiListener::GetHttpClients() const
+{
+ std::unique_lock<std::mutex> lock(m_HttpClientsLock);
+ return m_HttpClients;
+}
+
+static void LogAppVersion(unsigned long version, Log& log)
+{
+ log << version / 100u << "." << version % 100u << ".x";
+}
+
+Value ApiListener::HelloAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ if (origin) {
+ auto client (origin->FromClient);
+
+ if (client) {
+ auto endpoint (client->GetEndpoint());
+
+ if (endpoint) {
+ unsigned long nodeVersion = params->Get("version");
+
+ endpoint->SetIcingaVersion(nodeVersion);
+ endpoint->SetCapabilities((double)params->Get("capabilities"));
+
+ if (nodeVersion == 0u) {
+ nodeVersion = 21200;
+ }
+
+ if (endpoint->GetZone()->GetParent() == Zone::GetLocalZone()) {
+ switch (l_AppVersionInt / 100 - nodeVersion / 100) {
+ case 0:
+ case 1:
+ break;
+ default:
+ Log log (LogWarning, "ApiListener");
+ log << "Unexpected Icinga version of endpoint '" << endpoint->GetName() << "': ";
+
+ LogAppVersion(nodeVersion / 100u, log);
+ log << " Expected one of: ";
+
+ LogAppVersion(l_AppVersionInt / 100u, log);
+ log << ", ";
+
+ LogAppVersion((l_AppVersionInt / 100u - 1u), log);
+ }
+ }
+ }
+ }
+ }
+
+ return Empty;
+}
+
+Endpoint::Ptr ApiListener::GetLocalEndpoint() const
+{
+ return m_LocalEndpoint;
+}
+
+void ApiListener::UpdateActivePackageStagesCache()
+{
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+
+ for (auto package : ConfigPackageUtility::GetPackages()) {
+ String activeStage;
+
+ try {
+ activeStage = ConfigPackageUtility::GetActiveStageFromFile(package);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << ex.what();
+ continue;
+ }
+
+ Log(LogNotice, "ApiListener")
+ << "Updating cache: Config package '" << package << "' has active stage '" << activeStage << "'.";
+
+ m_ActivePackageStages[package] = activeStage;
+ }
+}
+
+void ApiListener::CheckApiPackageIntegrity()
+{
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+
+ for (auto package : ConfigPackageUtility::GetPackages()) {
+ String activeStage;
+ try {
+ activeStage = ConfigPackageUtility::GetActiveStageFromFile(package);
+ } catch (const std::exception& ex) {
+ /* An error means that the stage is broken, try to repair it. */
+ auto it = m_ActivePackageStages.find(package);
+
+ if (it == m_ActivePackageStages.end())
+ continue;
+
+ String activeStageCached = it->second;
+
+ Log(LogInformation, "ApiListener")
+ << "Repairing broken API config package '" << package
+ << "', setting active stage '" << activeStageCached << "'.";
+
+ ConfigPackageUtility::SetActiveStageToFile(package, activeStageCached);
+ }
+ }
+}
+
+void ApiListener::SetActivePackageStage(const String& package, const String& stage)
+{
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+ m_ActivePackageStages[package] = stage;
+}
+
+String ApiListener::GetActivePackageStage(const String& package)
+{
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+
+ if (m_ActivePackageStages.find(package) == m_ActivePackageStages.end())
+ BOOST_THROW_EXCEPTION(ScriptError("Package " + package + " has no active stage."));
+
+ return m_ActivePackageStages[package];
+}
+
+void ApiListener::RemoveActivePackageStage(const String& package)
+{
+ /* This is the rare occassion when a package has been deleted. */
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+
+ auto it = m_ActivePackageStages.find(package);
+
+ if (it == m_ActivePackageStages.end())
+ return;
+
+ m_ActivePackageStages.erase(it);
+}
+
+void ApiListener::ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<ApiListener>::ValidateTlsProtocolmin(lvalue, utils);
+
+ try {
+ ResolveTlsProtocolVersion(lvalue());
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "tls_protocolmin" }, ex.what()));
+ }
+}
+
+void ApiListener::ValidateTlsHandshakeTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<ApiListener>::ValidateTlsHandshakeTimeout(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "tls_handshake_timeout" }, "Value must be greater than 0."));
+}
+
+bool ApiListener::IsHACluster()
+{
+ Zone::Ptr zone = Zone::GetLocalZone();
+
+ if (!zone)
+ return false;
+
+ return zone->IsSingleInstance();
+}
+
+/* Provide a helper function for zone origin name. */
+String ApiListener::GetFromZoneName(const Zone::Ptr& fromZone)
+{
+ String fromZoneName;
+
+ if (fromZone) {
+ fromZoneName = fromZone->GetName();
+ } else {
+ Zone::Ptr lzone = Zone::GetLocalZone();
+
+ if (lzone)
+ fromZoneName = lzone->GetName();
+ }
+
+ return fromZoneName;
+}
+
+void ApiListener::UpdateStatusFile(boost::asio::ip::tcp::endpoint localEndpoint)
+{
+ String path = Configuration::CacheDir + "/api-state.json";
+
+ Utility::SaveJsonFile(path, 0644, new Dictionary({
+ {"host", String(localEndpoint.address().to_string())},
+ {"port", localEndpoint.port()}
+ }));
+}
+
+void ApiListener::RemoveStatusFile()
+{
+ String path = Configuration::CacheDir + "/api-state.json";
+
+ Utility::Remove(path);
+}
diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp
new file mode 100644
index 0000000..fced0a8
--- /dev/null
+++ b/lib/remote/apilistener.hpp
@@ -0,0 +1,265 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APILISTENER_H
+#define APILISTENER_H
+
+#include "remote/apilistener-ti.hpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/httpserverconnection.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/messageorigin.hpp"
+#include "base/configobject.hpp"
+#include "base/process.hpp"
+#include "base/shared.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/tlsstream.hpp"
+#include "base/threadpool.hpp"
+#include <atomic>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/thread/shared_mutex.hpp>
+#include <cstdint>
+#include <mutex>
+#include <set>
+
+namespace icinga
+{
+
+class JsonRpcConnection;
+
+/**
+ * @ingroup remote
+ */
+struct ConfigDirInformation
+{
+ Dictionary::Ptr UpdateV1;
+ Dictionary::Ptr UpdateV2;
+ Dictionary::Ptr Checksums;
+};
+
+/**
+ * If the version reported by icinga::Hello is not enough to tell whether
+ * the peer has a specific capability, add the latter to this bitmask.
+ *
+ * Note that due to the capability exchange via JSON-RPC and the state storage via JSON
+ * the bitmask numbers are stored in IEEE 754 64-bit floats.
+ * The latter have 53 digit bits which limit the bitmask.
+ * Not to run out of bits:
+ *
+ * Once all Icinga versions which don't have a specific capability are completely EOL,
+ * remove the respective capability checks and assume the peer has the capability.
+ * Once all Icinga versions which still check for the capability are completely EOL,
+ * remove the respective bit from icinga::Hello.
+ * Once all Icinga versions which still have the respective bit in icinga::Hello
+ * are completely EOL, remove the bit here.
+ * Once all Icinga versions which still have the respective bit here
+ * are completely EOL, feel free to re-use the bit.
+ *
+ * completely EOL = not supported, even if an important customer of us used it and
+ * not expected to appear in a multi-level cluster, e.g. a 4 level cluster with
+ * v2.11 -> v2.10 -> v2.9 -> v2.8 - v2.7 isn't here
+ *
+ * @ingroup remote
+ */
+enum class ApiCapabilities : uint_fast64_t
+{
+ ExecuteArbitraryCommand = 1u << 0u,
+ IfwApiCheckCommand = 1u << 1u,
+};
+
+/**
+* @ingroup remote
+*/
+class ApiListener final : public ObjectImpl<ApiListener>
+{
+public:
+ DECLARE_OBJECT(ApiListener);
+ DECLARE_OBJECTNAME(ApiListener);
+
+ static boost::signals2::signal<void(bool)> OnMasterChanged;
+
+ ApiListener();
+
+ static String GetApiDir();
+ static String GetApiZonesDir();
+ static String GetApiZonesStageDir();
+ static String GetCertsDir();
+ static String GetCaDir();
+ static String GetCertificateRequestsDir();
+
+ std::shared_ptr<X509> RenewCert(const std::shared_ptr<X509>& cert, bool ca = false);
+ void UpdateSSLContext();
+
+ static ApiListener::Ptr GetInstance();
+
+ Endpoint::Ptr GetMaster() const;
+ bool IsMaster() const;
+
+ Endpoint::Ptr GetLocalEndpoint() const;
+
+ void SyncSendMessage(const Endpoint::Ptr& endpoint, const Dictionary::Ptr& message);
+ void RelayMessage(const MessageOrigin::Ptr& origin, const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+ std::pair<Dictionary::Ptr, Dictionary::Ptr> GetStatus();
+
+ bool AddAnonymousClient(const JsonRpcConnection::Ptr& aclient);
+ void RemoveAnonymousClient(const JsonRpcConnection::Ptr& aclient);
+ std::set<JsonRpcConnection::Ptr> GetAnonymousClients() const;
+
+ void AddHttpClient(const HttpServerConnection::Ptr& aclient);
+ void RemoveHttpClient(const HttpServerConnection::Ptr& aclient);
+ std::set<HttpServerConnection::Ptr> GetHttpClients() const;
+
+ static double CalculateZoneLag(const Endpoint::Ptr& endpoint);
+
+ /* filesync */
+ static Value ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ void HandleConfigUpdate(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ /* configsync */
+ static void ConfigUpdateObjectHandler(const ConfigObject::Ptr& object, const Value& cookie);
+ static Value ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static Value ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ /* API config packages */
+ void SetActivePackageStage(const String& package, const String& stage);
+ String GetActivePackageStage(const String& package);
+ void RemoveActivePackageStage(const String& package);
+
+ static Value HelloAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void UpdateObjectAuthority();
+
+ static bool IsHACluster();
+ static String GetFromZoneName(const Zone::Ptr& fromZone);
+
+ static String GetDefaultCertPath();
+ static String GetDefaultKeyPath();
+ static String GetDefaultCaPath();
+
+ static inline
+ bool UpdatedObjectAuthority()
+ {
+ return m_UpdatedObjectAuthority.load();
+ }
+
+ double GetTlsHandshakeTimeout() const override;
+ void SetTlsHandshakeTimeout(double value, bool suppress_events, const Value& cookie) override;
+
+protected:
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeDeleted) override;
+
+ void ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+ void ValidateTlsHandshakeTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ Shared<boost::asio::ssl::context>::Ptr m_SSLContext;
+ boost::shared_mutex m_SSLContextMutex;
+
+ mutable std::mutex m_AnonymousClientsLock;
+ mutable std::mutex m_HttpClientsLock;
+ std::set<JsonRpcConnection::Ptr> m_AnonymousClients;
+ std::set<HttpServerConnection::Ptr> m_HttpClients;
+
+ Timer::Ptr m_Timer;
+ Timer::Ptr m_ReconnectTimer;
+ Timer::Ptr m_AuthorityTimer;
+ Timer::Ptr m_CleanupCertificateRequestsTimer;
+ Timer::Ptr m_ApiPackageIntegrityTimer;
+ Timer::Ptr m_RenewOwnCertTimer;
+
+ Endpoint::Ptr m_LocalEndpoint;
+
+ static ApiListener::Ptr m_Instance;
+ static std::atomic<bool> m_UpdatedObjectAuthority;
+
+ void ApiTimerHandler();
+ void ApiReconnectTimerHandler();
+ void CleanupCertificateRequestsTimerHandler();
+ void CheckApiPackageIntegrity();
+
+ bool AddListener(const String& node, const String& service);
+ void AddConnection(const Endpoint::Ptr& endpoint);
+
+ void NewClientHandler(
+ boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
+ const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
+ );
+ void NewClientHandlerInternal(
+ boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
+ const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
+ );
+ void ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server);
+
+ WorkQueue m_RelayQueue;
+ WorkQueue m_SyncQueue{0, 4};
+
+ std::mutex m_LogLock;
+ Stream::Ptr m_LogFile;
+ size_t m_LogMessageCount{0};
+
+ bool RelayMessageOne(const Zone::Ptr& zone, const MessageOrigin::Ptr& origin, const Dictionary::Ptr& message, const Endpoint::Ptr& currentZoneMaster);
+ void SyncRelayMessage(const MessageOrigin::Ptr& origin, const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log);
+ void PersistMessage(const Dictionary::Ptr& message, const ConfigObject::Ptr& secobj);
+
+ void OpenLogFile();
+ void RotateLogFile();
+ void CloseLogFile();
+ static void LogGlobHandler(std::vector<int>& files, const String& file);
+ void ReplayLog(const JsonRpcConnection::Ptr& client);
+
+ static void CopyCertificateFile(const String& oldCertPath, const String& newCertPath);
+
+ void UpdateStatusFile(boost::asio::ip::tcp::endpoint localEndpoint);
+ void RemoveStatusFile();
+
+ /* filesync */
+ static std::mutex m_ConfigSyncStageLock;
+
+ void SyncLocalZoneDirs() const;
+ void SyncLocalZoneDir(const Zone::Ptr& zone) const;
+ void RenewOwnCert();
+ void RenewCA();
+
+ void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient);
+
+ static Dictionary::Ptr MergeConfigUpdate(const ConfigDirInformation& config);
+
+ static ConfigDirInformation LoadConfigDir(const String& dir);
+ static void ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file);
+
+ static void TryActivateZonesStage(const std::vector<String>& relativePaths);
+
+ static String GetChecksum(const String& content);
+ static bool CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig);
+
+ void UpdateLastFailedZonesStageValidation(const String& log);
+ void ClearLastFailedZonesStageValidation();
+
+ /* configsync */
+ void UpdateConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
+ const JsonRpcConnection::Ptr& client = nullptr);
+ void DeleteConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
+ const JsonRpcConnection::Ptr& client = nullptr);
+ void SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient);
+
+ void SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoint::Ptr& endpoint, bool needSync);
+
+ /* API Config Packages */
+ mutable std::mutex m_ActivePackageStagesLock;
+ std::map<String, String> m_ActivePackageStages;
+
+ void UpdateActivePackageStagesCache();
+};
+
+}
+
+#endif /* APILISTENER_H */
diff --git a/lib/remote/apilistener.ti b/lib/remote/apilistener.ti
new file mode 100644
index 0000000..8317abc
--- /dev/null
+++ b/lib/remote/apilistener.ti
@@ -0,0 +1,66 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/i2-remote.hpp"
+#include "base/configobject.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+
+library remote;
+
+namespace icinga
+{
+
+class ApiListener : ConfigObject
+{
+ activation_priority 50;
+
+ [config, deprecated] String cert_path;
+ [config, deprecated] String key_path;
+ [config, deprecated] String ca_path;
+ [config] String crl_path;
+ [config] String cipher_list {
+ default {{{ return DEFAULT_TLS_CIPHERS; }}}
+ };
+ [config] String tls_protocolmin {
+ default {{{ return DEFAULT_TLS_PROTOCOLMIN; }}}
+ };
+
+ [config] String bind_host {
+ default {{{ return Configuration::ApiBindHost; }}}
+ };
+ [config] String bind_port {
+ default {{{ return Configuration::ApiBindPort; }}}
+ };
+
+ [config] bool accept_config;
+ [config] bool accept_commands;
+ [config] int max_anonymous_clients {
+ default {{{ return -1; }}}
+ };
+
+ [config, deprecated] double tls_handshake_timeout {
+ get;
+ set;
+ default {{{ return Configuration::TlsHandshakeTimeout; }}}
+ };
+
+ [config] double connect_timeout {
+ default {{{ return DEFAULT_CONNECT_TIMEOUT; }}}
+ };
+
+ [config, no_user_view, no_user_modify] String ticket_salt;
+
+ [config] Array::Ptr access_control_allow_origin;
+ [config, deprecated] bool access_control_allow_credentials;
+ [config, deprecated] String access_control_allow_headers;
+ [config, deprecated] String access_control_allow_methods;
+
+
+ [state, no_user_modify] Timestamp log_message_timestamp;
+
+ [no_user_modify] String identity;
+
+ [state, no_user_modify] Dictionary::Ptr last_failed_zones_stage_validation;
+};
+
+}
diff --git a/lib/remote/apiuser.cpp b/lib/remote/apiuser.cpp
new file mode 100644
index 0000000..2959d89
--- /dev/null
+++ b/lib/remote/apiuser.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apiuser.hpp"
+#include "remote/apiuser-ti.cpp"
+#include "base/configtype.hpp"
+#include "base/base64.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(ApiUser);
+
+ApiUser::Ptr ApiUser::GetByClientCN(const String& cn)
+{
+ for (const ApiUser::Ptr& user : ConfigType::GetObjectsByType<ApiUser>()) {
+ if (user->GetClientCN() == cn)
+ return user;
+ }
+
+ return nullptr;
+}
+
+ApiUser::Ptr ApiUser::GetByAuthHeader(const String& auth_header)
+{
+ String::SizeType pos = auth_header.FindFirstOf(" ");
+ String username, password;
+
+ if (pos != String::NPos && auth_header.SubStr(0, pos) == "Basic") {
+ String credentials_base64 = auth_header.SubStr(pos + 1);
+ String credentials = Base64::Decode(credentials_base64);
+
+ String::SizeType cpos = credentials.FindFirstOf(":");
+
+ if (cpos != String::NPos) {
+ username = credentials.SubStr(0, cpos);
+ password = credentials.SubStr(cpos + 1);
+ }
+ }
+
+ const ApiUser::Ptr& user = ApiUser::GetByName(username);
+
+ /* Deny authentication if:
+ * 1) user does not exist
+ * 2) given password is empty
+ * 2) configured password does not match.
+ */
+ if (!user || password.IsEmpty())
+ return nullptr;
+ else if (user && !Utility::ComparePasswords(password, user->GetPassword()))
+ return nullptr;
+
+ return user;
+}
+
diff --git a/lib/remote/apiuser.hpp b/lib/remote/apiuser.hpp
new file mode 100644
index 0000000..fc132ee
--- /dev/null
+++ b/lib/remote/apiuser.hpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIUSER_H
+#define APIUSER_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/apiuser-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup remote
+ */
+class ApiUser final : public ObjectImpl<ApiUser>
+{
+public:
+ DECLARE_OBJECT(ApiUser);
+ DECLARE_OBJECTNAME(ApiUser);
+
+ static ApiUser::Ptr GetByClientCN(const String& cn);
+ static ApiUser::Ptr GetByAuthHeader(const String& auth_header);
+};
+
+}
+
+#endif /* APIUSER_H */
diff --git a/lib/remote/apiuser.ti b/lib/remote/apiuser.ti
new file mode 100644
index 0000000..0b49a1d
--- /dev/null
+++ b/lib/remote/apiuser.ti
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/function.hpp"
+
+library remote;
+
+namespace icinga
+{
+
+class ApiUser : ConfigObject
+{
+ /* No show config */
+ [config, no_user_view] String password;
+ [deprecated, config, no_user_view] String password_hash;
+ [config] String client_cn (ClientCN);
+ [config] array(Value) permissions;
+};
+
+validator ApiUser {
+ Array permissions {
+ String "*";
+ Dictionary "*" {
+ required permission;
+ String permission;
+ Function filter;
+ };
+ };
+};
+
+}
diff --git a/lib/remote/configfileshandler.cpp b/lib/remote/configfileshandler.cpp
new file mode 100644
index 0000000..779ecd1
--- /dev/null
+++ b/lib/remote/configfileshandler.cpp
@@ -0,0 +1,94 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configfileshandler.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <fstream>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/config/files", ConfigFilesHandler);
+
+bool ConfigFilesHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ const std::vector<String>& urlPath = url->GetPath();
+
+ if (urlPath.size() >= 4)
+ params->Set("package", urlPath[3]);
+
+ if (urlPath.size() >= 5)
+ params->Set("stage", urlPath[4]);
+
+ if (urlPath.size() >= 6) {
+ std::vector<String> tmpPath(urlPath.begin() + 5, urlPath.end());
+ params->Set("path", boost::algorithm::join(tmpPath, "/"));
+ }
+
+ if (request[http::field::accept] == "application/json") {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid Accept header. Either remove the Accept header or set it to 'application/octet-stream'.");
+ return true;
+ }
+
+ FilterUtility::CheckPermission(user, "config/query");
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+ String stageName = HttpUtility::GetLastParameter(params, "stage");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName)) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid package name.");
+ return true;
+ }
+
+ if (!ConfigPackageUtility::ValidateStageName(stageName)) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid stage name.");
+ return true;
+ }
+
+ String relativePath = HttpUtility::GetLastParameter(params, "path");
+
+ if (ConfigPackageUtility::ContainsDotDot(relativePath)) {
+ HttpUtility::SendJsonError(response, params, 400, "Path contains '..' (not allowed).");
+ return true;
+ }
+
+ String path = ConfigPackageUtility::GetPackageDir() + "/" + packageName + "/" + stageName + "/" + relativePath;
+
+ if (!Utility::PathExists(path)) {
+ HttpUtility::SendJsonError(response, params, 404, "Path not found.");
+ return true;
+ }
+
+ try {
+ std::ifstream fp(path.CStr(), std::ifstream::in | std::ifstream::binary);
+ fp.exceptions(std::ifstream::badbit);
+
+ String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+ response.result(http::status::ok);
+ response.set(http::field::content_type, "application/octet-stream");
+ response.body() = content;
+ response.content_length(response.body().size());
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 500, "Could not read file.",
+ DiagnosticInformation(ex));
+ }
+
+ return true;
+}
diff --git a/lib/remote/configfileshandler.hpp b/lib/remote/configfileshandler.hpp
new file mode 100644
index 0000000..ea48b1e
--- /dev/null
+++ b/lib/remote/configfileshandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGFILESHANDLER_H
+#define CONFIGFILESHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ConfigFilesHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigFilesHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* CONFIGFILESHANDLER_H */
diff --git a/lib/remote/configobjectslock.cpp b/lib/remote/configobjectslock.cpp
new file mode 100644
index 0000000..e529c83
--- /dev/null
+++ b/lib/remote/configobjectslock.cpp
@@ -0,0 +1,24 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+
+#include "base/shared-memory.hpp"
+#include "remote/configobjectslock.hpp"
+#include <boost/interprocess/sync/lock_options.hpp>
+
+using namespace icinga;
+
+// On *nix one process may write config objects while another is loading the config, so this uses IPC.
+static SharedMemory<boost::interprocess::interprocess_sharable_mutex> l_ConfigObjectsMutex;
+
+ConfigObjectsExclusiveLock::ConfigObjectsExclusiveLock()
+ : m_Lock(l_ConfigObjectsMutex.Get())
+{
+}
+
+ConfigObjectsSharedLock::ConfigObjectsSharedLock(std::try_to_lock_t)
+ : m_Lock(l_ConfigObjectsMutex.Get(), boost::interprocess::try_to_lock)
+{
+}
+
+#endif /* _WIN32 */
diff --git a/lib/remote/configobjectslock.hpp b/lib/remote/configobjectslock.hpp
new file mode 100644
index 0000000..ee90981
--- /dev/null
+++ b/lib/remote/configobjectslock.hpp
@@ -0,0 +1,72 @@
+/* Icinga 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+#pragma once
+
+#include <mutex>
+
+#ifndef _WIN32
+#include <boost/interprocess/sync/interprocess_sharable_mutex.hpp>
+#include <boost/interprocess/sync/scoped_lock.hpp>
+#include <boost/interprocess/sync/sharable_lock.hpp>
+#endif /* _WIN32 */
+
+namespace icinga
+{
+
+#ifdef _WIN32
+
+class ConfigObjectsSharedLock
+{
+public:
+ inline ConfigObjectsSharedLock(std::try_to_lock_t)
+ {
+ }
+
+ constexpr explicit operator bool() const
+ {
+ return true;
+ }
+};
+
+#else /* _WIN32 */
+
+/**
+ * Waits until all ConfigObjects*Lock-s have vanished. For its lifetime disallows such.
+ * Keep an instance alive during reload to forbid runtime config changes!
+ * This way Icinga reads a consistent config which doesn't suddenly get runtime-changed.
+ *
+ * @ingroup remote
+ */
+class ConfigObjectsExclusiveLock
+{
+public:
+ ConfigObjectsExclusiveLock();
+
+private:
+ boost::interprocess::scoped_lock<boost::interprocess::interprocess_sharable_mutex> m_Lock;
+};
+
+/**
+ * Waits until the only ConfigObjectsExclusiveLock has vanished (if any). For its lifetime disallows such.
+ * Keep an instance alive during runtime config changes to delay a reload (if any)!
+ * This way Icinga reads a consistent config which doesn't suddenly get runtime-changed.
+ *
+ * @ingroup remote
+ */
+class ConfigObjectsSharedLock
+{
+public:
+ ConfigObjectsSharedLock(std::try_to_lock_t);
+
+ inline explicit operator bool() const
+ {
+ return m_Lock.owns();
+ }
+
+private:
+ boost::interprocess::sharable_lock<boost::interprocess::interprocess_sharable_mutex> m_Lock;
+};
+
+#endif /* _WIN32 */
+
+}
diff --git a/lib/remote/configobjectutility.cpp b/lib/remote/configobjectutility.cpp
new file mode 100644
index 0000000..62c910b
--- /dev/null
+++ b/lib/remote/configobjectutility.cpp
@@ -0,0 +1,377 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configobjectutility.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/apilistener.hpp"
+#include "config/configcompiler.hpp"
+#include "config/configitem.hpp"
+#include "base/configwriter.hpp"
+#include "base/exception.hpp"
+#include "base/dependencygraph.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/system/error_code.hpp>
+#include <fstream>
+#include <utility>
+
+using namespace icinga;
+
+String ConfigObjectUtility::GetConfigDir()
+{
+ String prefix = ConfigPackageUtility::GetPackageDir() + "/_api/";
+ String activeStage = ConfigPackageUtility::GetActiveStage("_api");
+
+ if (activeStage.IsEmpty())
+ RepairPackage("_api");
+
+ return prefix + activeStage;
+}
+
+String ConfigObjectUtility::ComputeNewObjectConfigPath(const Type::Ptr& type, const String& fullName)
+{
+ String typeDir = type->GetPluralName();
+ boost::algorithm::to_lower(typeDir);
+
+ /* This may throw an exception the caller above must handle. */
+ String prefix = GetConfigDir() + "/conf.d/" + type->GetPluralName().ToLower() + "/";
+
+ String escapedName = EscapeName(fullName);
+
+ String longPath = prefix + escapedName + ".conf";
+
+ /*
+ * The long path may cause trouble due to exceeding the allowed filename length of the filesystem. Therefore, the
+ * preferred solution would be to use the truncated and hashed version as returned at the end of this function.
+ * However, for compatibility reasons, we have to keep the old long version in some cases. Notably, this could lead
+ * to the creation of objects that can't be synced to child nodes if they are running an older version. Thus, for
+ * now, the fix is only enabled for comments and downtimes, as these are the object types for which the issue is
+ * most likely triggered but can't be worked around easily (you'd have to rename the host and/or service in order to
+ * be able to schedule a downtime or add an acknowledgement, which is not feasible) and the impact of not syncing
+ * these objects through the whole cluster is limited. For other object types, we currently prefer to fail the
+ * creation early so that configuration inconsistencies throughout the cluster are avoided.
+ *
+ * TODO: Remove this in v2.16 and truncate all.
+ */
+ if (type->GetName() != "Comment" && type->GetName() != "Downtime") {
+ return longPath;
+ }
+
+ /* Maximum length 80 bytes object name + 3 bytes "..." + 40 bytes SHA1 (hex-encoded) */
+ return prefix + Utility::TruncateUsingHash<80+3+40>(escapedName) + ".conf";
+}
+
+String ConfigObjectUtility::GetExistingObjectConfigPath(const ConfigObject::Ptr& object)
+{
+ return object->GetDebugInfo().Path;
+}
+
+void ConfigObjectUtility::RepairPackage(const String& package)
+{
+ /* Try to fix the active stage, whenever we find a directory in there.
+ * This automatically heals packages < 2.11 which remained broken.
+ */
+ String dir = ConfigPackageUtility::GetPackageDir() + "/" + package + "/";
+
+ namespace fs = boost::filesystem;
+
+ /* Use iterators to workaround VS builds on Windows. */
+ fs::path path(dir.Begin(), dir.End());
+
+ fs::recursive_directory_iterator end;
+
+ String foundActiveStage;
+
+ for (fs::recursive_directory_iterator it(path); it != end; ++it) {
+ boost::system::error_code ec;
+
+ const fs::path d = *it;
+ if (fs::is_directory(d, ec)) {
+ /* Extract the relative directory name. */
+ foundActiveStage = d.stem().string();
+
+ break; // Use the first found directory.
+ }
+ }
+
+ if (!foundActiveStage.IsEmpty()) {
+ Log(LogInformation, "ConfigObjectUtility")
+ << "Repairing config package '" << package << "' with stage '" << foundActiveStage << "'.";
+
+ ConfigPackageUtility::ActivateStage(package, foundActiveStage);
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot repair package '" + package + "', please check the troubleshooting docs."));
+ }
+}
+
+void ConfigObjectUtility::CreateStorage()
+{
+ std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
+
+ /* For now, we only use _api as our creation target. */
+ String package = "_api";
+
+ if (!ConfigPackageUtility::PackageExists(package)) {
+ Log(LogNotice, "ConfigObjectUtility")
+ << "Package " << package << " doesn't exist yet, creating it.";
+
+ ConfigPackageUtility::CreatePackage(package);
+
+ String stage = ConfigPackageUtility::CreateStage(package);
+ ConfigPackageUtility::ActivateStage(package, stage);
+ }
+}
+
+String ConfigObjectUtility::EscapeName(const String& name)
+{
+ return Utility::EscapeString(name, "<>:\"/\\|?*", true);
+}
+
+String ConfigObjectUtility::CreateObjectConfig(const Type::Ptr& type, const String& fullName,
+ bool ignoreOnError, const Array::Ptr& templates, const Dictionary::Ptr& attrs)
+{
+ auto *nc = dynamic_cast<NameComposer *>(type.get());
+ Dictionary::Ptr nameParts;
+ String name;
+
+ if (nc) {
+ nameParts = nc->ParseName(fullName);
+ name = nameParts->Get("name");
+ } else
+ name = fullName;
+
+ Dictionary::Ptr allAttrs = new Dictionary();
+
+ if (attrs) {
+ attrs->CopyTo(allAttrs);
+
+ ObjectLock olock(attrs);
+ for (const Dictionary::Pair& kv : attrs) {
+ int fid = type->GetFieldId(kv.first.SubStr(0, kv.first.FindFirstOf(".")));
+
+ if (fid < 0)
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid attribute specified: " + kv.first));
+
+ Field field = type->GetFieldInfo(fid);
+
+ if (!(field.Attributes & FAConfig) || kv.first == "name")
+ BOOST_THROW_EXCEPTION(ScriptError("Attribute is marked for internal use only and may not be set: " + kv.first));
+ }
+ }
+
+ if (nameParts)
+ nameParts->CopyTo(allAttrs);
+
+ allAttrs->Remove("name");
+
+ /* update the version for config sync */
+ allAttrs->Set("version", Utility::GetTime());
+
+ std::ostringstream config;
+ ConfigWriter::EmitConfigItem(config, type->GetName(), name, false, ignoreOnError, templates, allAttrs);
+ ConfigWriter::EmitRaw(config, "\n");
+
+ return config.str();
+}
+
+bool ConfigObjectUtility::CreateObject(const Type::Ptr& type, const String& fullName,
+ const String& config, const Array::Ptr& errors, const Array::Ptr& diagnosticInformation, const Value& cookie)
+{
+ CreateStorage();
+
+ {
+ auto configType (dynamic_cast<ConfigType*>(type.get()));
+
+ if (configType && configType->GetObject(fullName)) {
+ errors->Add("Object '" + fullName + "' already exists.");
+ return false;
+ }
+ }
+
+ String path;
+
+ try {
+ path = ComputeNewObjectConfigPath(type, fullName);
+ } catch (const std::exception& ex) {
+ errors->Add("Config package broken: " + DiagnosticInformation(ex, false));
+ return false;
+ }
+
+ Utility::MkDirP(Utility::DirName(path), 0700);
+
+ std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::trunc);
+ fp << config;
+ fp.close();
+
+ std::unique_ptr<Expression> expr = ConfigCompiler::CompileFile(path, String(), "_api");
+
+ try {
+ ActivationScope ascope;
+
+ ScriptFrame frame(true);
+ expr->Evaluate(frame);
+ expr.reset();
+
+ WorkQueue upq;
+ upq.SetName("ConfigObjectUtility::CreateObject");
+
+ std::vector<ConfigItem::Ptr> newItems;
+
+ /*
+ * Disable logging for object creation, but do so ourselves later on.
+ * Duplicate the error handling for better logging and debugging here.
+ */
+ if (!ConfigItem::CommitItems(ascope.GetContext(), upq, newItems, true)) {
+ if (errors) {
+ Log(LogNotice, "ConfigObjectUtility")
+ << "Failed to commit config item '" << fullName << "'. Aborting and removing config path '" << path << "'.";
+
+ Utility::Remove(path);
+
+ for (const boost::exception_ptr& ex : upq.GetExceptions()) {
+ errors->Add(DiagnosticInformation(ex, false));
+
+ if (diagnosticInformation)
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * Activate the config object.
+ * uq, items, runtimeCreated, silent, withModAttrs, cookie
+ * IMPORTANT: Forward the cookie aka origin in order to prevent sync loops in the same zone!
+ */
+ if (!ConfigItem::ActivateItems(newItems, true, false, false, cookie)) {
+ if (errors) {
+ Log(LogNotice, "ConfigObjectUtility")
+ << "Failed to activate config object '" << fullName << "'. Aborting and removing config path '" << path << "'.";
+
+ Utility::Remove(path);
+
+ for (const boost::exception_ptr& ex : upq.GetExceptions()) {
+ errors->Add(DiagnosticInformation(ex, false));
+
+ if (diagnosticInformation)
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+ }
+ }
+
+ return false;
+ }
+
+ /* if (type != Comment::TypeInstance && type != Downtime::TypeInstance)
+ * Does not work since this would require libicinga, which has a dependency on libremote
+ * Would work if these libs were static.
+ */
+ if (type->GetName() != "Comment" && type->GetName() != "Downtime")
+ ApiListener::UpdateObjectAuthority();
+
+ // At this stage we should have a config object already. If not, it was ignored before.
+ auto *ctype = dynamic_cast<ConfigType *>(type.get());
+ ConfigObject::Ptr obj = ctype->GetObject(fullName);
+
+ if (obj) {
+ Log(LogInformation, "ConfigObjectUtility")
+ << "Created and activated object '" << fullName << "' of type '" << type->GetName() << "'.";
+ } else {
+ Log(LogNotice, "ConfigObjectUtility")
+ << "Object '" << fullName << "' was not created but ignored due to errors.";
+ }
+
+ } catch (const std::exception& ex) {
+ Utility::Remove(path);
+
+ if (errors)
+ errors->Add(DiagnosticInformation(ex, false));
+
+ if (diagnosticInformation)
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+
+ return false;
+ }
+
+ return true;
+}
+
+bool ConfigObjectUtility::DeleteObjectHelper(const ConfigObject::Ptr& object, bool cascade,
+ const Array::Ptr& errors, const Array::Ptr& diagnosticInformation, const Value& cookie)
+{
+ std::vector<Object::Ptr> parents = DependencyGraph::GetParents(object);
+
+ Type::Ptr type = object->GetReflectionType();
+
+ String name = object->GetName();
+
+ if (!parents.empty() && !cascade) {
+ if (errors) {
+ errors->Add("Object '" + name + "' of type '" + type->GetName() +
+ "' cannot be deleted because other objects depend on it. "
+ "Use cascading delete to delete it anyway.");
+ }
+
+ return false;
+ }
+
+ for (const Object::Ptr& pobj : parents) {
+ ConfigObject::Ptr parentObj = dynamic_pointer_cast<ConfigObject>(pobj);
+
+ if (!parentObj)
+ continue;
+
+ DeleteObjectHelper(parentObj, cascade, errors, diagnosticInformation, cookie);
+ }
+
+ ConfigItem::Ptr item = ConfigItem::GetByTypeAndName(type, name);
+
+ try {
+ /* mark this object for cluster delete event */
+ object->SetExtension("ConfigObjectDeleted", true);
+
+ /*
+ * Trigger deactivation signal for DB IDO and runtime object delections.
+ * IMPORTANT: Specify the cookie aka origin in order to prevent sync loops
+ * in the same zone!
+ */
+ object->Deactivate(true, cookie);
+
+ if (item)
+ item->Unregister();
+ else
+ object->Unregister();
+
+ } catch (const std::exception& ex) {
+ if (errors)
+ errors->Add(DiagnosticInformation(ex, false));
+
+ if (diagnosticInformation)
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+
+ return false;
+ }
+
+ if (object->GetPackage() == "_api") {
+ Utility::Remove(GetExistingObjectConfigPath(object));
+ }
+
+ Log(LogInformation, "ConfigObjectUtility")
+ << "Deleted object '" << name << "' of type '" << type->GetName() << "'.";
+
+ return true;
+}
+
+bool ConfigObjectUtility::DeleteObject(const ConfigObject::Ptr& object, bool cascade, const Array::Ptr& errors,
+ const Array::Ptr& diagnosticInformation, const Value& cookie)
+{
+ if (object->GetPackage() != "_api") {
+ if (errors)
+ errors->Add("Object cannot be deleted because it was not created using the API.");
+
+ return false;
+ }
+
+ return DeleteObjectHelper(object, cascade, errors, diagnosticInformation, cookie);
+}
diff --git a/lib/remote/configobjectutility.hpp b/lib/remote/configobjectutility.hpp
new file mode 100644
index 0000000..5a113c8
--- /dev/null
+++ b/lib/remote/configobjectutility.hpp
@@ -0,0 +1,47 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGOBJECTUTILITY_H
+#define CONFIGOBJECTUTILITY_H
+
+#include "remote/i2-remote.hpp"
+#include "base/array.hpp"
+#include "base/configobject.hpp"
+#include "base/dictionary.hpp"
+#include "base/type.hpp"
+
+namespace icinga
+{
+
+/**
+ * Helper functions.
+ *
+ * @ingroup remote
+ */
+class ConfigObjectUtility
+{
+
+public:
+ static String GetConfigDir();
+ static String ComputeNewObjectConfigPath(const Type::Ptr& type, const String& fullName);
+ static String GetExistingObjectConfigPath(const ConfigObject::Ptr& object);
+ static void RepairPackage(const String& package);
+ static void CreateStorage();
+
+ static String CreateObjectConfig(const Type::Ptr& type, const String& fullName,
+ bool ignoreOnError, const Array::Ptr& templates, const Dictionary::Ptr& attrs);
+
+ static bool CreateObject(const Type::Ptr& type, const String& fullName,
+ const String& config, const Array::Ptr& errors, const Array::Ptr& diagnosticInformation, const Value& cookie = Empty);
+
+ static bool DeleteObject(const ConfigObject::Ptr& object, bool cascade, const Array::Ptr& errors,
+ const Array::Ptr& diagnosticInformation, const Value& cookie = Empty);
+
+private:
+ static String EscapeName(const String& name);
+ static bool DeleteObjectHelper(const ConfigObject::Ptr& object, bool cascade, const Array::Ptr& errors,
+ const Array::Ptr& diagnosticInformation, const Value& cookie = Empty);
+};
+
+}
+
+#endif /* CONFIGOBJECTUTILITY_H */
diff --git a/lib/remote/configpackageshandler.cpp b/lib/remote/configpackageshandler.cpp
new file mode 100644
index 0000000..98b3268
--- /dev/null
+++ b/lib/remote/configpackageshandler.cpp
@@ -0,0 +1,179 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configpackageshandler.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/config/packages", ConfigPackagesHandler);
+
+bool ConfigPackagesHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() == http::verb::get)
+ HandleGet(user, request, url, response, params);
+ else if (request.method() == http::verb::post)
+ HandlePost(user, request, url, response, params);
+ else if (request.method() == http::verb::delete_)
+ HandleDelete(user, request, url, response, params);
+ else
+ return false;
+
+ return true;
+}
+
+void ConfigPackagesHandler::HandleGet(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/query");
+
+ std::vector<String> packages;
+
+ try {
+ packages = ConfigPackageUtility::GetPackages();
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 500, "Could not retrieve packages.",
+ DiagnosticInformation(ex));
+ return;
+ }
+
+ ArrayData results;
+
+ {
+ std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
+
+ for (const String& package : packages) {
+ String activeStage;
+
+ try {
+ activeStage = ConfigPackageUtility::GetActiveStage(package);
+ } catch (const std::exception&) { } /* Should never happen. */
+
+ results.emplace_back(new Dictionary({
+ { "name", package },
+ { "stages", Array::FromVector(ConfigPackageUtility::GetStages(package)) },
+ { "active-stage", activeStage }
+ }));
+ }
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
+void ConfigPackagesHandler::HandlePost(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/modify");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName)) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+ return;
+ }
+
+ try {
+ std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
+
+ ConfigPackageUtility::CreatePackage(packageName);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 500, "Could not create package '" + packageName + "'.",
+ DiagnosticInformation(ex));
+ return;
+ }
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "code", 200 },
+ { "package", packageName },
+ { "status", "Created package." }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
+void ConfigPackagesHandler::HandleDelete(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/modify");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName)) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+ return;
+ }
+
+ try {
+ ConfigPackageUtility::DeletePackage(packageName);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 500, "Failed to delete package '" + packageName + "'.",
+ DiagnosticInformation(ex));
+ return;
+ }
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "code", 200 },
+ { "package", packageName },
+ { "status", "Deleted package." }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
diff --git a/lib/remote/configpackageshandler.hpp b/lib/remote/configpackageshandler.hpp
new file mode 100644
index 0000000..0a05ea1
--- /dev/null
+++ b/lib/remote/configpackageshandler.hpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGMODULESHANDLER_H
+#define CONFIGMODULESHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ConfigPackagesHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigPackagesHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+
+private:
+ void HandleGet(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+ void HandlePost(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+ void HandleDelete(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+
+};
+
+}
+
+#endif /* CONFIGMODULESHANDLER_H */
diff --git a/lib/remote/configpackageutility.cpp b/lib/remote/configpackageutility.cpp
new file mode 100644
index 0000000..e795401
--- /dev/null
+++ b/lib/remote/configpackageutility.cpp
@@ -0,0 +1,413 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configpackageutility.hpp"
+#include "remote/apilistener.hpp"
+#include "base/application.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/regex.hpp>
+#include <algorithm>
+#include <cctype>
+#include <fstream>
+
+using namespace icinga;
+
+String ConfigPackageUtility::GetPackageDir()
+{
+ return Configuration::DataDir + "/api/packages";
+}
+
+void ConfigPackageUtility::CreatePackage(const String& name)
+{
+ String path = GetPackageDir() + "/" + name;
+
+ if (Utility::PathExists(path))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Package already exists."));
+
+ Utility::MkDirP(path, 0700);
+ WritePackageConfig(name);
+}
+
+void ConfigPackageUtility::DeletePackage(const String& name)
+{
+ String path = GetPackageDir() + "/" + name;
+
+ if (!Utility::PathExists(path))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Package does not exist."));
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ /* config packages without API make no sense. */
+ if (!listener)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("No ApiListener instance configured."));
+
+ listener->RemoveActivePackageStage(name);
+
+ Utility::RemoveDirRecursive(path);
+ Application::RequestRestart();
+}
+
+std::vector<String> ConfigPackageUtility::GetPackages()
+{
+ String packageDir = GetPackageDir();
+
+ std::vector<String> packages;
+
+ /* Package directory does not exist, no packages have been created thus far. */
+ if (!Utility::PathExists(packageDir))
+ return packages;
+
+ Utility::Glob(packageDir + "/*", [&packages](const String& path) { packages.emplace_back(Utility::BaseName(path)); }, GlobDirectory);
+
+ return packages;
+}
+
+bool ConfigPackageUtility::PackageExists(const String& name)
+{
+ auto packages (GetPackages());
+ return std::find(packages.begin(), packages.end(), name) != packages.end();
+}
+
+String ConfigPackageUtility::CreateStage(const String& packageName, const Dictionary::Ptr& files)
+{
+ String stageName = Utility::NewUniqueID();
+
+ String path = GetPackageDir() + "/" + packageName;
+
+ if (!Utility::PathExists(path))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Package does not exist."));
+
+ path += "/" + stageName;
+
+ Utility::MkDirP(path, 0700);
+ Utility::MkDirP(path + "/conf.d", 0700);
+ Utility::MkDirP(path + "/zones.d", 0700);
+ WriteStageConfig(packageName, stageName);
+
+ bool foundDotDot = false;
+
+ if (files) {
+ ObjectLock olock(files);
+ for (const Dictionary::Pair& kv : files) {
+ if (ContainsDotDot(kv.first)) {
+ foundDotDot = true;
+ break;
+ }
+
+ String filePath = path + "/" + kv.first;
+
+ Log(LogInformation, "ConfigPackageUtility")
+ << "Updating configuration file: " << filePath;
+
+ // Pass the directory and generate a dir tree, if it does not already exist
+ Utility::MkDirP(Utility::DirName(filePath), 0750);
+ std::ofstream fp(filePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fp << kv.second;
+ fp.close();
+ }
+ }
+
+ if (foundDotDot) {
+ Utility::RemoveDirRecursive(path);
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Path must not contain '..'."));
+ }
+
+ return stageName;
+}
+
+void ConfigPackageUtility::WritePackageConfig(const String& packageName)
+{
+ String stageName = GetActiveStage(packageName);
+
+ String includePath = GetPackageDir() + "/" + packageName + "/include.conf";
+ std::ofstream fpInclude(includePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpInclude << "include \"*/include.conf\"\n";
+ fpInclude.close();
+
+ String activePath = GetPackageDir() + "/" + packageName + "/active.conf";
+ std::ofstream fpActive(activePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpActive << "if (!globals.contains(\"ActiveStages\")) {\n"
+ << " globals.ActiveStages = {}\n"
+ << "}\n"
+ << "\n"
+ << "if (globals.contains(\"ActiveStageOverride\")) {\n"
+ << " var arr = ActiveStageOverride.split(\":\")\n"
+ << " if (arr[0] == \"" << packageName << "\") {\n"
+ << " if (arr.len() < 2) {\n"
+ << " log(LogCritical, \"Config\", \"Invalid value for ActiveStageOverride\")\n"
+ << " } else {\n"
+ << " ActiveStages[\"" << packageName << "\"] = arr[1]\n"
+ << " }\n"
+ << " }\n"
+ << "}\n"
+ << "\n"
+ << "if (!ActiveStages.contains(\"" << packageName << "\")) {\n"
+ << " ActiveStages[\"" << packageName << "\"] = \"" << stageName << "\"\n"
+ << "}\n";
+ fpActive.close();
+}
+
+void ConfigPackageUtility::WriteStageConfig(const String& packageName, const String& stageName)
+{
+ String path = GetPackageDir() + "/" + packageName + "/" + stageName + "/include.conf";
+ std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fp << "include \"../active.conf\"\n"
+ << "if (ActiveStages[\"" << packageName << "\"] == \"" << stageName << "\") {\n"
+ << " include_recursive \"conf.d\"\n"
+ << " include_zones \"" << packageName << "\", \"zones.d\"\n"
+ << "}\n";
+ fp.close();
+}
+
+void ConfigPackageUtility::ActivateStage(const String& packageName, const String& stageName)
+{
+ SetActiveStage(packageName, stageName);
+
+ WritePackageConfig(packageName);
+}
+
+void ConfigPackageUtility::TryActivateStageCallback(const ProcessResult& pr, const String& packageName, const String& stageName,
+ bool activate, bool reload, const Shared<Defer>::Ptr& resetPackageUpdates)
+{
+ String logFile = GetPackageDir() + "/" + packageName + "/" + stageName + "/startup.log";
+ std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpLog << pr.Output;
+ fpLog.close();
+
+ String statusFile = GetPackageDir() + "/" + packageName + "/" + stageName + "/status";
+ std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpStatus << pr.ExitStatus;
+ fpStatus.close();
+
+ /* validation went fine, activate stage and reload */
+ if (pr.ExitStatus == 0) {
+ if (activate) {
+ {
+ std::unique_lock<std::mutex> lock(GetStaticPackageMutex());
+
+ ActivateStage(packageName, stageName);
+ }
+
+ if (reload) {
+ /*
+ * Cancel the deferred callback before going out of scope so that the config stages handler
+ * flag isn't resetting earlier and allowing other clients to submit further requests while
+ * Icinga2 is reloading. Otherwise, the ongoing request will be cancelled halfway before the
+ * operation is completed once the new worker becomes ready.
+ */
+ resetPackageUpdates->Cancel();
+
+ Application::RequestRestart();
+ }
+ }
+ } else {
+ Log(LogCritical, "ConfigPackageUtility")
+ << "Config validation failed for package '"
+ << packageName << "' and stage '" << stageName << "'.";
+ }
+}
+
+void ConfigPackageUtility::AsyncTryActivateStage(const String& packageName, const String& stageName, bool activate, bool reload,
+ const Shared<Defer>::Ptr& resetPackageUpdates)
+{
+ VERIFY(Application::GetArgC() >= 1);
+
+ // prepare arguments
+ Array::Ptr args = new Array({
+ Application::GetExePath(Application::GetArgV()[0]),
+ });
+
+ // copy all arguments of parent process
+ for (int i = 1; i < Application::GetArgC(); i++) {
+ String argV = Application::GetArgV()[i];
+
+ if (argV == "-d" || argV == "--daemonize")
+ continue;
+
+ args->Add(argV);
+ }
+
+ // add arguments for validation
+ args->Add("--validate");
+ args->Add("--define");
+ args->Add("ActiveStageOverride=" + packageName + ":" + stageName);
+
+ Process::Ptr process = new Process(Process::PrepareCommand(args));
+ process->SetTimeout(Application::GetReloadTimeout());
+ process->Run([packageName, stageName, activate, reload, resetPackageUpdates](const ProcessResult& pr) {
+ TryActivateStageCallback(pr, packageName, stageName, activate, reload, resetPackageUpdates);
+ });
+}
+
+void ConfigPackageUtility::DeleteStage(const String& packageName, const String& stageName)
+{
+ String path = GetPackageDir() + "/" + packageName + "/" + stageName;
+
+ if (!Utility::PathExists(path))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Stage does not exist."));
+
+ if (GetActiveStage(packageName) == stageName)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Active stage cannot be deleted."));
+
+ Utility::RemoveDirRecursive(path);
+}
+
+std::vector<String> ConfigPackageUtility::GetStages(const String& packageName)
+{
+ std::vector<String> stages;
+ Utility::Glob(GetPackageDir() + "/" + packageName + "/*", [&stages](const String& path) { stages.emplace_back(Utility::BaseName(path)); }, GlobDirectory);
+ return stages;
+}
+
+String ConfigPackageUtility::GetActiveStageFromFile(const String& packageName)
+{
+ /* Lock the transaction, reading this only happens on startup or when something really is broken. */
+ std::unique_lock<std::mutex> lock(GetStaticActiveStageMutex());
+
+ String path = GetPackageDir() + "/" + packageName + "/active-stage";
+
+ std::ifstream fp;
+ fp.open(path.CStr());
+
+ String stage;
+ std::getline(fp, stage.GetData());
+
+ fp.close();
+
+ if (fp.fail())
+ return ""; /* Don't use exceptions here. The caller must deal with empty stages at this point. Happens on initial package creation for example. */
+
+ return stage.Trim();
+}
+
+void ConfigPackageUtility::SetActiveStageToFile(const String& packageName, const String& stageName)
+{
+ std::unique_lock<std::mutex> lock(GetStaticActiveStageMutex());
+
+ String activeStagePath = GetPackageDir() + "/" + packageName + "/active-stage";
+
+ std::ofstream fpActiveStage(activeStagePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); //TODO: fstream exceptions
+ fpActiveStage << stageName;
+ fpActiveStage.close();
+}
+
+String ConfigPackageUtility::GetActiveStage(const String& packageName)
+{
+ String activeStage;
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ /* If we don't have an API feature, just use the file storage without caching this.
+ * This happens when ScheduledDowntime objects generate Downtime objects.
+ * TODO: Make the API a first class citizen.
+ */
+ if (!listener)
+ return GetActiveStageFromFile(packageName);
+
+ /* First use runtime state. */
+ try {
+ activeStage = listener->GetActivePackageStage(packageName);
+ } catch (const std::exception& ex) {
+ /* Fallback to reading the file, happens on restarts. */
+ activeStage = GetActiveStageFromFile(packageName);
+
+ /* When we've read something, correct memory. */
+ if (!activeStage.IsEmpty())
+ listener->SetActivePackageStage(packageName, activeStage);
+ }
+
+ return activeStage;
+}
+
+void ConfigPackageUtility::SetActiveStage(const String& packageName, const String& stageName)
+{
+ /* Update the marker on disk for restarts. */
+ SetActiveStageToFile(packageName, stageName);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ /* No API, no caching. */
+ if (!listener)
+ return;
+
+ listener->SetActivePackageStage(packageName, stageName);
+}
+
+std::vector<std::pair<String, bool> > ConfigPackageUtility::GetFiles(const String& packageName, const String& stageName)
+{
+ std::vector<std::pair<String, bool> > paths;
+ Utility::GlobRecursive(GetPackageDir() + "/" + packageName + "/" + stageName, "*", [&paths](const String& path) {
+ CollectPaths(path, paths);
+ }, GlobDirectory | GlobFile);
+
+ return paths;
+}
+
+void ConfigPackageUtility::CollectPaths(const String& path, std::vector<std::pair<String, bool> >& paths)
+{
+#ifndef _WIN32
+ struct stat statbuf;
+ int rc = lstat(path.CStr(), &statbuf);
+ if (rc < 0)
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("lstat")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(path));
+
+ paths.emplace_back(path, S_ISDIR(statbuf.st_mode));
+#else /* _WIN32 */
+ struct _stat statbuf;
+ int rc = _stat(path.CStr(), &statbuf);
+ if (rc < 0)
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("_stat")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(path));
+
+ paths.emplace_back(path, ((statbuf.st_mode & S_IFMT) == S_IFDIR));
+#endif /* _WIN32 */
+}
+
+bool ConfigPackageUtility::ContainsDotDot(const String& path)
+{
+ std::vector<String> tokens = path.Split("/\\");
+
+ for (const String& part : tokens) {
+ if (part == "..")
+ return true;
+ }
+
+ return false;
+}
+
+bool ConfigPackageUtility::ValidatePackageName(const String& packageName)
+{
+ return ValidateFreshName(packageName) || PackageExists(packageName);
+}
+
+bool ConfigPackageUtility::ValidateFreshName(const String& name)
+{
+ if (name.IsEmpty())
+ return false;
+
+ /* check for path injection */
+ if (ContainsDotDot(name))
+ return false;
+
+ return std::all_of(name.Begin(), name.End(), [](char c) {
+ return std::isalnum(c, std::locale::classic()) || c == '_' || c == '-';
+ });
+}
+
+std::mutex& ConfigPackageUtility::GetStaticPackageMutex()
+{
+ static std::mutex mutex;
+ return mutex;
+}
+
+std::mutex& ConfigPackageUtility::GetStaticActiveStageMutex()
+{
+ static std::mutex mutex;
+ return mutex;
+}
diff --git a/lib/remote/configpackageutility.hpp b/lib/remote/configpackageutility.hpp
new file mode 100644
index 0000000..240f591
--- /dev/null
+++ b/lib/remote/configpackageutility.hpp
@@ -0,0 +1,73 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGMODULEUTILITY_H
+#define CONFIGMODULEUTILITY_H
+
+#include "remote/i2-remote.hpp"
+#include "base/application.hpp"
+#include "base/dictionary.hpp"
+#include "base/process.hpp"
+#include "base/string.hpp"
+#include "base/defer.hpp"
+#include "base/shared.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * Helper functions.
+ *
+ * @ingroup remote
+ */
+class ConfigPackageUtility
+{
+
+public:
+ static String GetPackageDir();
+
+ static void CreatePackage(const String& name);
+ static void DeletePackage(const String& name);
+ static std::vector<String> GetPackages();
+ static bool PackageExists(const String& name);
+
+ static String CreateStage(const String& packageName, const Dictionary::Ptr& files = nullptr);
+ static void DeleteStage(const String& packageName, const String& stageName);
+ static std::vector<String> GetStages(const String& packageName);
+ static String GetActiveStageFromFile(const String& packageName);
+ static String GetActiveStage(const String& packageName);
+ static void SetActiveStage(const String& packageName, const String& stageName);
+ static void SetActiveStageToFile(const String& packageName, const String& stageName);
+ static void ActivateStage(const String& packageName, const String& stageName);
+ static void AsyncTryActivateStage(const String& packageName, const String& stageName, bool activate, bool reload,
+ const Shared<Defer>::Ptr& resetPackageUpdates);
+
+ static std::vector<std::pair<String, bool> > GetFiles(const String& packageName, const String& stageName);
+
+ static bool ContainsDotDot(const String& path);
+ static bool ValidatePackageName(const String& packageName);
+
+ static inline
+ bool ValidateStageName(const String& stageName)
+ {
+ return ValidateFreshName(stageName);
+ }
+
+ static std::mutex& GetStaticPackageMutex();
+ static std::mutex& GetStaticActiveStageMutex();
+
+private:
+ static void CollectPaths(const String& path, std::vector<std::pair<String, bool> >& paths);
+
+ static void WritePackageConfig(const String& packageName);
+ static void WriteStageConfig(const String& packageName, const String& stageName);
+
+ static void TryActivateStageCallback(const ProcessResult& pr, const String& packageName, const String& stageName, bool activate,
+ bool reload, const Shared<Defer>::Ptr& resetPackageUpdates);
+
+ static bool ValidateFreshName(const String& name);
+};
+
+}
+
+#endif /* CONFIGMODULEUTILITY_H */
diff --git a/lib/remote/configstageshandler.cpp b/lib/remote/configstageshandler.cpp
new file mode 100644
index 0000000..b5aaadd
--- /dev/null
+++ b/lib/remote/configstageshandler.cpp
@@ -0,0 +1,225 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configstageshandler.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/application.hpp"
+#include "base/defer.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/config/stages", ConfigStagesHandler);
+
+std::atomic<bool> ConfigStagesHandler::m_RunningPackageUpdates (false);
+
+bool ConfigStagesHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 5)
+ return false;
+
+ if (request.method() == http::verb::get)
+ HandleGet(user, request, url, response, params);
+ else if (request.method() == http::verb::post)
+ HandlePost(user, request, url, response, params);
+ else if (request.method() == http::verb::delete_)
+ HandleDelete(user, request, url, response, params);
+ else
+ return false;
+
+ return true;
+}
+
+void ConfigStagesHandler::HandleGet(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/query");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ if (url->GetPath().size() >= 5)
+ params->Set("stage", url->GetPath()[4]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+ String stageName = HttpUtility::GetLastParameter(params, "stage");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+
+ if (!ConfigPackageUtility::ValidateStageName(stageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid stage name '" + stageName + "'.");
+
+ ArrayData results;
+
+ std::vector<std::pair<String, bool> > paths = ConfigPackageUtility::GetFiles(packageName, stageName);
+
+ String prefixPath = ConfigPackageUtility::GetPackageDir() + "/" + packageName + "/" + stageName + "/";
+
+ for (const auto& kv : paths) {
+ results.push_back(new Dictionary({
+ { "type", kv.second ? "directory" : "file" },
+ { "name", kv.first.SubStr(prefixPath.GetLength()) }
+ }));
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
+void ConfigStagesHandler::HandlePost(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/modify");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+
+ bool reload = true;
+
+ if (params->Contains("reload"))
+ reload = HttpUtility::GetLastParameter(params, "reload");
+
+ bool activate = true;
+
+ if (params->Contains("activate"))
+ activate = HttpUtility::GetLastParameter(params, "activate");
+
+ Dictionary::Ptr files = params->Get("files");
+
+ String stageName;
+
+ try {
+ if (!files)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Parameter 'files' must be specified."));
+
+ if (reload && !activate)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Parameter 'reload' must be false when 'activate' is false."));
+
+ if (m_RunningPackageUpdates.exchange(true)) {
+ return HttpUtility::SendJsonError(response, params, 423,
+ "Conflicting request, there is already an ongoing package update in progress. Please try it again later.");
+ }
+
+ auto resetPackageUpdates (Shared<Defer>::Make([]() { ConfigStagesHandler::m_RunningPackageUpdates.store(false); }));
+
+ std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
+
+ stageName = ConfigPackageUtility::CreateStage(packageName, files);
+
+ /* validate the config. on success, activate stage and reload */
+ ConfigPackageUtility::AsyncTryActivateStage(packageName, stageName, activate, reload, resetPackageUpdates);
+ } catch (const std::exception& ex) {
+ return HttpUtility::SendJsonError(response, params, 500,
+ "Stage creation failed.",
+ DiagnosticInformation(ex));
+ }
+
+
+ String responseStatus = "Created stage. ";
+
+ if (reload)
+ responseStatus += "Reload triggered.";
+ else
+ responseStatus += "Reload skipped.";
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "package", packageName },
+ { "stage", stageName },
+ { "code", 200 },
+ { "status", responseStatus }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
+void ConfigStagesHandler::HandleDelete(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/modify");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ if (url->GetPath().size() >= 5)
+ params->Set("stage", url->GetPath()[4]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+ String stageName = HttpUtility::GetLastParameter(params, "stage");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+
+ if (!ConfigPackageUtility::ValidateStageName(stageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid stage name '" + stageName + "'.");
+
+ try {
+ ConfigPackageUtility::DeleteStage(packageName, stageName);
+ } catch (const std::exception& ex) {
+ return HttpUtility::SendJsonError(response, params, 500,
+ "Failed to delete stage '" + stageName + "' in package '" + packageName + "'.",
+ DiagnosticInformation(ex));
+ }
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "code", 200 },
+ { "package", packageName },
+ { "stage", stageName },
+ { "status", "Stage deleted." }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
diff --git a/lib/remote/configstageshandler.hpp b/lib/remote/configstageshandler.hpp
new file mode 100644
index 0000000..88f248c
--- /dev/null
+++ b/lib/remote/configstageshandler.hpp
@@ -0,0 +1,56 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGSTAGESHANDLER_H
+#define CONFIGSTAGESHANDLER_H
+
+#include "remote/httphandler.hpp"
+#include <atomic>
+
+namespace icinga
+{
+
+class ConfigStagesHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigStagesHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+
+private:
+ void HandleGet(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+ void HandlePost(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+ void HandleDelete(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+
+ static std::atomic<bool> m_RunningPackageUpdates;
+};
+
+}
+
+#endif /* CONFIGSTAGESHANDLER_H */
diff --git a/lib/remote/consolehandler.cpp b/lib/remote/consolehandler.cpp
new file mode 100644
index 0000000..f5a470a
--- /dev/null
+++ b/lib/remote/consolehandler.cpp
@@ -0,0 +1,327 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configobjectslock.hpp"
+#include "remote/consolehandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "config/configcompiler.hpp"
+#include "base/configtype.hpp"
+#include "base/configwriter.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/timer.hpp"
+#include "base/namespace.hpp"
+#include "base/initialize.hpp"
+#include "base/utility.hpp"
+#include <boost/thread/once.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/console", ConsoleHandler);
+
+static std::mutex l_QueryMutex;
+static std::map<String, ApiScriptFrame> l_ApiScriptFrames;
+static Timer::Ptr l_FrameCleanupTimer;
+static std::mutex l_ApiScriptMutex;
+
+static void ScriptFrameCleanupHandler()
+{
+ std::unique_lock<std::mutex> lock(l_ApiScriptMutex);
+
+ std::vector<String> cleanup_keys;
+
+ typedef std::pair<String, ApiScriptFrame> KVPair;
+
+ for (const KVPair& kv : l_ApiScriptFrames) {
+ if (kv.second.Seen < Utility::GetTime() - 1800)
+ cleanup_keys.push_back(kv.first);
+ }
+
+ for (const String& key : cleanup_keys)
+ l_ApiScriptFrames.erase(key);
+}
+
+static void EnsureFrameCleanupTimer()
+{
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, []() {
+ l_FrameCleanupTimer = Timer::Create();
+ l_FrameCleanupTimer->OnTimerExpired.connect([](const Timer * const&) { ScriptFrameCleanupHandler(); });
+ l_FrameCleanupTimer->SetInterval(30);
+ l_FrameCleanupTimer->Start();
+ });
+}
+
+bool ConsoleHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 3)
+ return false;
+
+ if (request.method() != http::verb::post)
+ return false;
+
+ QueryDescription qd;
+
+ String methodName = url->GetPath()[2];
+
+ FilterUtility::CheckPermission(user, "console");
+
+ String session = HttpUtility::GetLastParameter(params, "session");
+
+ if (session.IsEmpty())
+ session = Utility::NewUniqueID();
+
+ String command = HttpUtility::GetLastParameter(params, "command");
+
+ bool sandboxed = HttpUtility::GetLastParameter(params, "sandboxed");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading.");
+ return true;
+ }
+
+ if (methodName == "execute-script")
+ return ExecuteScriptHelper(request, response, params, command, session, sandboxed);
+ else if (methodName == "auto-complete-script")
+ return AutocompleteScriptHelper(request, response, params, command, session, sandboxed);
+
+ HttpUtility::SendJsonError(response, params, 400, "Invalid method specified: " + methodName);
+ return true;
+}
+
+bool ConsoleHandler::ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
+{
+ namespace http = boost::beast::http;
+
+ Log(LogNotice, "Console")
+ << "Executing expression: " << command;
+
+ EnsureFrameCleanupTimer();
+
+ ApiScriptFrame& lsf = l_ApiScriptFrames[session];
+ lsf.Seen = Utility::GetTime();
+
+ if (!lsf.Locals)
+ lsf.Locals = new Dictionary();
+
+ String fileName = "<" + Convert::ToString(lsf.NextLine) + ">";
+ lsf.NextLine++;
+
+ lsf.Lines[fileName] = command;
+
+ Dictionary::Ptr resultInfo;
+ std::unique_ptr<Expression> expr;
+ Value exprResult;
+
+ try {
+ expr = ConfigCompiler::CompileText(fileName, command);
+
+ ScriptFrame frame(true);
+ frame.Locals = lsf.Locals;
+ frame.Self = lsf.Locals;
+ frame.Sandboxed = sandboxed;
+
+ exprResult = expr->Evaluate(frame);
+
+ resultInfo = new Dictionary({
+ { "code", 200 },
+ { "status", "Executed successfully." },
+ { "result", Serialize(exprResult, 0) }
+ });
+ } catch (const ScriptError& ex) {
+ DebugInfo di = ex.GetDebugInfo();
+
+ std::ostringstream msgbuf;
+
+ msgbuf << di.Path << ": " << lsf.Lines[di.Path] << "\n"
+ << String(di.Path.GetLength() + 2, ' ')
+ << String(di.FirstColumn, ' ') << String(di.LastColumn - di.FirstColumn + 1, '^') << "\n"
+ << ex.what() << "\n";
+
+ resultInfo = new Dictionary({
+ { "code", 500 },
+ { "status", String(msgbuf.str()) },
+ { "incomplete_expression", ex.IsIncompleteExpression() },
+ { "debug_info", new Dictionary({
+ { "path", di.Path },
+ { "first_line", di.FirstLine },
+ { "first_column", di.FirstColumn },
+ { "last_line", di.LastLine },
+ { "last_column", di.LastColumn }
+ }) }
+ });
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ resultInfo }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
+bool ConsoleHandler::AutocompleteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
+{
+ namespace http = boost::beast::http;
+
+ Log(LogInformation, "Console")
+ << "Auto-completing expression: " << command;
+
+ EnsureFrameCleanupTimer();
+
+ ApiScriptFrame& lsf = l_ApiScriptFrames[session];
+ lsf.Seen = Utility::GetTime();
+
+ if (!lsf.Locals)
+ lsf.Locals = new Dictionary();
+
+
+ ScriptFrame frame(true);
+ frame.Locals = lsf.Locals;
+ frame.Self = lsf.Locals;
+ frame.Sandboxed = sandboxed;
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "code", 200 },
+ { "status", "Auto-completed successfully." },
+ { "suggestions", Array::FromVector(GetAutocompletionSuggestions(command, frame)) }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
+static void AddSuggestion(std::vector<String>& matches, const String& word, const String& suggestion)
+{
+ if (suggestion.Find(word) != 0)
+ return;
+
+ matches.push_back(suggestion);
+}
+
+static void AddSuggestions(std::vector<String>& matches, const String& word, const String& pword, bool withFields, const Value& value)
+{
+ String prefix;
+
+ if (!pword.IsEmpty())
+ prefix = pword + ".";
+
+ if (value.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = value;
+
+ ObjectLock olock(dict);
+ for (const Dictionary::Pair& kv : dict) {
+ AddSuggestion(matches, word, prefix + kv.first);
+ }
+ }
+
+ if (value.IsObjectType<Namespace>()) {
+ Namespace::Ptr ns = value;
+
+ ObjectLock olock(ns);
+ for (const Namespace::Pair& kv : ns) {
+ AddSuggestion(matches, word, prefix + kv.first);
+ }
+ }
+
+ if (withFields) {
+ Type::Ptr type = value.GetReflectionType();
+
+ for (int i = 0; i < type->GetFieldCount(); i++) {
+ Field field = type->GetFieldInfo(i);
+
+ AddSuggestion(matches, word, prefix + field.Name);
+ }
+
+ while (type) {
+ Object::Ptr prototype = type->GetPrototype();
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>(prototype);
+
+ if (dict) {
+ ObjectLock olock(dict);
+ for (const Dictionary::Pair& kv : dict) {
+ AddSuggestion(matches, word, prefix + kv.first);
+ }
+ }
+
+ type = type->GetBaseType();
+ }
+ }
+}
+
+std::vector<String> ConsoleHandler::GetAutocompletionSuggestions(const String& word, ScriptFrame& frame)
+{
+ std::vector<String> matches;
+
+ for (const String& keyword : ConfigWriter::GetKeywords()) {
+ AddSuggestion(matches, word, keyword);
+ }
+
+ {
+ ObjectLock olock(frame.Locals);
+ for (const Dictionary::Pair& kv : frame.Locals) {
+ AddSuggestion(matches, word, kv.first);
+ }
+ }
+
+ {
+ ObjectLock olock(ScriptGlobal::GetGlobals());
+ for (const Namespace::Pair& kv : ScriptGlobal::GetGlobals()) {
+ AddSuggestion(matches, word, kv.first);
+ }
+ }
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+
+ AddSuggestions(matches, word, "", false, systemNS);
+ AddSuggestions(matches, word, "", true, systemNS->Get("Configuration"));
+ AddSuggestions(matches, word, "", false, ScriptGlobal::Get("Types"));
+ AddSuggestions(matches, word, "", false, ScriptGlobal::Get("Icinga"));
+
+ String::SizeType cperiod = word.RFind(".");
+
+ if (cperiod != String::NPos) {
+ String pword = word.SubStr(0, cperiod);
+
+ Value value;
+
+ try {
+ std::unique_ptr<Expression> expr = ConfigCompiler::CompileText("temp", pword);
+
+ if (expr)
+ value = expr->Evaluate(frame);
+
+ AddSuggestions(matches, word, pword, true, value);
+ } catch (...) { /* Ignore the exception */ }
+ }
+
+ return matches;
+}
diff --git a/lib/remote/consolehandler.hpp b/lib/remote/consolehandler.hpp
new file mode 100644
index 0000000..df0d77d
--- /dev/null
+++ b/lib/remote/consolehandler.hpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONSOLEHANDLER_H
+#define CONSOLEHANDLER_H
+
+#include "remote/httphandler.hpp"
+#include "base/scriptframe.hpp"
+
+namespace icinga
+{
+
+struct ApiScriptFrame
+{
+ double Seen{0};
+ int NextLine{1};
+ std::map<String, String> Lines;
+ Dictionary::Ptr Locals;
+};
+
+class ConsoleHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConsoleHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+
+ static std::vector<String> GetAutocompletionSuggestions(const String& word, ScriptFrame& frame);
+
+private:
+ static bool ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed);
+ static bool AutocompleteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed);
+
+};
+
+}
+
+#endif /* CONSOLEHANDLER_H */
diff --git a/lib/remote/createobjecthandler.cpp b/lib/remote/createobjecthandler.cpp
new file mode 100644
index 0000000..598eeec
--- /dev/null
+++ b/lib/remote/createobjecthandler.cpp
@@ -0,0 +1,155 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/createobjecthandler.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/configobjectutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/apiaction.hpp"
+#include "remote/zone.hpp"
+#include "base/configtype.hpp"
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/objects", CreateObjectHandler);
+
+bool CreateObjectHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 4)
+ return false;
+
+ if (request.method() != http::verb::put)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ FilterUtility::CheckPermission(user, "objects/create/" + type->GetName());
+
+ String name = url->GetPath()[3];
+ Array::Ptr templates = params->Get("templates");
+ Dictionary::Ptr attrs = params->Get("attrs");
+
+ /* Put created objects into the local zone if not explicitly defined.
+ * This allows additional zone members to sync the
+ * configuration at some later point.
+ */
+ Zone::Ptr localZone = Zone::GetLocalZone();
+ String localZoneName;
+
+ if (localZone) {
+ localZoneName = localZone->GetName();
+
+ if (!attrs) {
+ attrs = new Dictionary({
+ { "zone", localZoneName }
+ });
+ } else if (!attrs->Contains("zone")) {
+ attrs->Set("zone", localZoneName);
+ }
+ }
+
+ /* Sanity checks for unique groups array. */
+ if (attrs->Contains("groups")) {
+ Array::Ptr groups = attrs->Get("groups");
+
+ if (groups)
+ attrs->Set("groups", groups->Unique());
+ }
+
+ Dictionary::Ptr result1 = new Dictionary();
+ String status;
+ Array::Ptr errors = new Array();
+ Array::Ptr diagnosticInformation = new Array();
+
+ bool ignoreOnError = false;
+
+ if (params->Contains("ignore_on_error"))
+ ignoreOnError = HttpUtility::GetLastParameter(params, "ignore_on_error");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ String config;
+
+ bool verbose = false;
+
+ if (params)
+ verbose = HttpUtility::GetLastParameter(params, "verbose");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
+ return true;
+ }
+
+ /* Object creation can cause multiple errors and optionally diagnostic information.
+ * We can't use SendJsonError() here.
+ */
+ try {
+ config = ConfigObjectUtility::CreateObjectConfig(type, name, ignoreOnError, templates, attrs);
+ } catch (const std::exception& ex) {
+ errors->Add(DiagnosticInformation(ex, false));
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+
+ if (verbose)
+ result1->Set("diagnostic_information", diagnosticInformation);
+
+ result1->Set("errors", errors);
+ result1->Set("code", 500);
+ result1->Set("status", "Object could not be created.");
+
+ response.result(http::status::internal_server_error);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+ }
+
+ if (!ConfigObjectUtility::CreateObject(type, name, config, errors, diagnosticInformation)) {
+ result1->Set("errors", errors);
+ result1->Set("code", 500);
+ result1->Set("status", "Object could not be created.");
+
+ if (verbose)
+ result1->Set("diagnostic_information", diagnosticInformation);
+
+ response.result(http::status::internal_server_error);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+ }
+
+ auto *ctype = dynamic_cast<ConfigType *>(type.get());
+ ConfigObject::Ptr obj = ctype->GetObject(name);
+
+ result1->Set("code", 200);
+
+ if (obj)
+ result1->Set("status", "Object was created");
+ else if (!obj && ignoreOnError)
+ result1->Set("status", "Object was not created but 'ignore_on_error' was set to true");
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/createobjecthandler.hpp b/lib/remote/createobjecthandler.hpp
new file mode 100644
index 0000000..4bcf21b
--- /dev/null
+++ b/lib/remote/createobjecthandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CREATEOBJECTHANDLER_H
+#define CREATEOBJECTHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class CreateObjectHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CreateObjectHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* CREATEOBJECTHANDLER_H */
diff --git a/lib/remote/deleteobjecthandler.cpp b/lib/remote/deleteobjecthandler.cpp
new file mode 100644
index 0000000..a4fd98d
--- /dev/null
+++ b/lib/remote/deleteobjecthandler.cpp
@@ -0,0 +1,123 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/deleteobjecthandler.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/configobjectutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/apiaction.hpp"
+#include "config/configitem.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/objects", DeleteObjectHandler);
+
+bool DeleteObjectHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() != http::verb::delete_)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ QueryDescription qd;
+ qd.Types.insert(type->GetName());
+ qd.Permission = "objects/delete/" + type->GetName();
+
+ params->Set("type", type->GetName());
+
+ if (url->GetPath().size() >= 4) {
+ String attr = type->GetName();
+ boost::algorithm::to_lower(attr);
+ params->Set(attr, url->GetPath()[3]);
+ }
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ bool cascade = HttpUtility::GetLastParameter(params, "cascade");
+ bool verbose = HttpUtility::GetLastParameter(params, "verbose");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
+ return true;
+ }
+
+ ArrayData results;
+
+ bool success = true;
+
+ for (const ConfigObject::Ptr& obj : objs) {
+ int code;
+ String status;
+ Array::Ptr errors = new Array();
+ Array::Ptr diagnosticInformation = new Array();
+
+ if (!ConfigObjectUtility::DeleteObject(obj, cascade, errors, diagnosticInformation)) {
+ code = 500;
+ status = "Object could not be deleted.";
+ success = false;
+ } else {
+ code = 200;
+ status = "Object was deleted.";
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", type->GetName() },
+ { "name", obj->GetName() },
+ { "code", code },
+ { "status", status },
+ { "errors", errors }
+ });
+
+ if (verbose)
+ result->Set("diagnostic_information", diagnosticInformation);
+
+ results.push_back(result);
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ if (!success)
+ response.result(http::status::internal_server_error);
+ else
+ response.result(http::status::ok);
+
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
diff --git a/lib/remote/deleteobjecthandler.hpp b/lib/remote/deleteobjecthandler.hpp
new file mode 100644
index 0000000..19a46e4
--- /dev/null
+++ b/lib/remote/deleteobjecthandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DELETEOBJECTHANDLER_H
+#define DELETEOBJECTHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class DeleteObjectHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(DeleteObjectHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* DELETEOBJECTHANDLER_H */
diff --git a/lib/remote/endpoint.cpp b/lib/remote/endpoint.cpp
new file mode 100644
index 0000000..e534fc1
--- /dev/null
+++ b/lib/remote/endpoint.cpp
@@ -0,0 +1,138 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/endpoint.hpp"
+#include "remote/endpoint-ti.cpp"
+#include "remote/apilistener.hpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/zone.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Endpoint);
+
+boost::signals2::signal<void(const Endpoint::Ptr&, const JsonRpcConnection::Ptr&)> Endpoint::OnConnected;
+boost::signals2::signal<void(const Endpoint::Ptr&, const JsonRpcConnection::Ptr&)> Endpoint::OnDisconnected;
+
+void Endpoint::OnAllConfigLoaded()
+{
+ ObjectImpl<Endpoint>::OnAllConfigLoaded();
+
+ if (!m_Zone)
+ BOOST_THROW_EXCEPTION(ScriptError("Endpoint '" + GetName() +
+ "' does not belong to a zone.", GetDebugInfo()));
+}
+
+void Endpoint::SetCachedZone(const Zone::Ptr& zone)
+{
+ if (m_Zone)
+ BOOST_THROW_EXCEPTION(ScriptError("Endpoint '" + GetName()
+ + "' is in more than one zone.", GetDebugInfo()));
+
+ m_Zone = zone;
+}
+
+void Endpoint::AddClient(const JsonRpcConnection::Ptr& client)
+{
+ bool was_master = ApiListener::GetInstance()->IsMaster();
+
+ {
+ std::unique_lock<std::mutex> lock(m_ClientsLock);
+ m_Clients.insert(client);
+ }
+
+ bool is_master = ApiListener::GetInstance()->IsMaster();
+
+ if (was_master != is_master)
+ ApiListener::OnMasterChanged(is_master);
+
+ OnConnected(this, client);
+}
+
+void Endpoint::RemoveClient(const JsonRpcConnection::Ptr& client)
+{
+ bool was_master = ApiListener::GetInstance()->IsMaster();
+
+ {
+ std::unique_lock<std::mutex> lock(m_ClientsLock);
+ m_Clients.erase(client);
+
+ Log(LogWarning, "ApiListener")
+ << "Removing API client for endpoint '" << GetName() << "'. " << m_Clients.size() << " API clients left.";
+
+ SetConnecting(false);
+ }
+
+ bool is_master = ApiListener::GetInstance()->IsMaster();
+
+ if (was_master != is_master)
+ ApiListener::OnMasterChanged(is_master);
+
+ OnDisconnected(this, client);
+}
+
+std::set<JsonRpcConnection::Ptr> Endpoint::GetClients() const
+{
+ std::unique_lock<std::mutex> lock(m_ClientsLock);
+ return m_Clients;
+}
+
+Zone::Ptr Endpoint::GetZone() const
+{
+ return m_Zone;
+}
+
+bool Endpoint::GetConnected() const
+{
+ std::unique_lock<std::mutex> lock(m_ClientsLock);
+ return !m_Clients.empty();
+}
+
+Endpoint::Ptr Endpoint::GetLocalEndpoint()
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return nullptr;
+
+ return listener->GetLocalEndpoint();
+}
+
+void Endpoint::AddMessageSent(int bytes)
+{
+ double time = Utility::GetTime();
+ m_MessagesSent.InsertValue(time, 1);
+ m_BytesSent.InsertValue(time, bytes);
+ SetLastMessageSent(time);
+}
+
+void Endpoint::AddMessageReceived(int bytes)
+{
+ double time = Utility::GetTime();
+ m_MessagesReceived.InsertValue(time, 1);
+ m_BytesReceived.InsertValue(time, bytes);
+ SetLastMessageReceived(time);
+}
+
+double Endpoint::GetMessagesSentPerSecond() const
+{
+ return m_MessagesSent.CalculateRate(Utility::GetTime(), 60);
+}
+
+double Endpoint::GetMessagesReceivedPerSecond() const
+{
+ return m_MessagesReceived.CalculateRate(Utility::GetTime(), 60);
+}
+
+double Endpoint::GetBytesSentPerSecond() const
+{
+ return m_BytesSent.CalculateRate(Utility::GetTime(), 60);
+}
+
+double Endpoint::GetBytesReceivedPerSecond() const
+{
+ return m_BytesReceived.CalculateRate(Utility::GetTime(), 60);
+}
diff --git a/lib/remote/endpoint.hpp b/lib/remote/endpoint.hpp
new file mode 100644
index 0000000..d641c2c
--- /dev/null
+++ b/lib/remote/endpoint.hpp
@@ -0,0 +1,68 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ENDPOINT_H
+#define ENDPOINT_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/endpoint-ti.hpp"
+#include "base/ringbuffer.hpp"
+#include <set>
+
+namespace icinga
+{
+
+class JsonRpcConnection;
+class Zone;
+
+/**
+ * An endpoint that can be used to send and receive messages.
+ *
+ * @ingroup remote
+ */
+class Endpoint final : public ObjectImpl<Endpoint>
+{
+public:
+ DECLARE_OBJECT(Endpoint);
+ DECLARE_OBJECTNAME(Endpoint);
+
+ static boost::signals2::signal<void(const Endpoint::Ptr&, const intrusive_ptr<JsonRpcConnection>&)> OnConnected;
+ static boost::signals2::signal<void(const Endpoint::Ptr&, const intrusive_ptr<JsonRpcConnection>&)> OnDisconnected;
+
+ void AddClient(const intrusive_ptr<JsonRpcConnection>& client);
+ void RemoveClient(const intrusive_ptr<JsonRpcConnection>& client);
+ std::set<intrusive_ptr<JsonRpcConnection> > GetClients() const;
+
+ intrusive_ptr<Zone> GetZone() const;
+
+ bool GetConnected() const override;
+
+ static Endpoint::Ptr GetLocalEndpoint();
+
+ void SetCachedZone(const intrusive_ptr<Zone>& zone);
+
+ void AddMessageSent(int bytes);
+ void AddMessageReceived(int bytes);
+
+ double GetMessagesSentPerSecond() const override;
+ double GetMessagesReceivedPerSecond() const override;
+
+ double GetBytesSentPerSecond() const override;
+ double GetBytesReceivedPerSecond() const override;
+
+protected:
+ void OnAllConfigLoaded() override;
+
+private:
+ mutable std::mutex m_ClientsLock;
+ std::set<intrusive_ptr<JsonRpcConnection> > m_Clients;
+ intrusive_ptr<Zone> m_Zone;
+
+ mutable RingBuffer m_MessagesSent{60};
+ mutable RingBuffer m_MessagesReceived{60};
+ mutable RingBuffer m_BytesSent{60};
+ mutable RingBuffer m_BytesReceived{60};
+};
+
+}
+
+#endif /* ENDPOINT_H */
diff --git a/lib/remote/endpoint.ti b/lib/remote/endpoint.ti
new file mode 100644
index 0000000..78551ec
--- /dev/null
+++ b/lib/remote/endpoint.ti
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include <cstdint>
+
+library remote;
+
+namespace icinga
+{
+
+class Endpoint : ConfigObject
+{
+ load_after Zone;
+
+ [config] String host;
+ [config, required] String port {
+ default {{{ return "5665"; }}}
+ };
+ [config] double log_duration {
+ default {{{ return 86400; }}}
+ };
+
+ [state] Timestamp local_log_position;
+ [state] Timestamp remote_log_position;
+ [state] "unsigned long" icinga_version {
+ default {{{ return 0; }}}
+ };
+ [state] uint_fast64_t capabilities {
+ default {{{ return 0; }}}
+ };
+
+ [no_user_modify] bool connecting;
+ [no_user_modify] bool syncing;
+
+ [no_user_modify, no_storage] bool connected {
+ get;
+ };
+
+ Timestamp last_message_sent;
+ Timestamp last_message_received;
+
+ [no_user_modify, no_storage] double messages_sent_per_second {
+ get;
+ };
+
+ [no_user_modify, no_storage] double messages_received_per_second {
+ get;
+ };
+
+ [no_user_modify, no_storage] double bytes_sent_per_second {
+ get;
+ };
+
+ [no_user_modify, no_storage] double bytes_received_per_second {
+ get;
+ };
+};
+
+}
diff --git a/lib/remote/eventqueue.cpp b/lib/remote/eventqueue.cpp
new file mode 100644
index 0000000..d79b615
--- /dev/null
+++ b/lib/remote/eventqueue.cpp
@@ -0,0 +1,351 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/configcompiler.hpp"
+#include "remote/eventqueue.hpp"
+#include "remote/filterutility.hpp"
+#include "base/io-engine.hpp"
+#include "base/singleton.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include <boost/asio/spawn.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/date_time/posix_time/ptime.hpp>
+#include <boost/system/error_code.hpp>
+#include <chrono>
+#include <utility>
+
+using namespace icinga;
+
+EventQueue::EventQueue(String name)
+ : m_Name(std::move(name))
+{ }
+
+bool EventQueue::CanProcessEvent(const String& type) const
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_Types.find(type) != m_Types.end();
+}
+
+void EventQueue::ProcessEvent(const Dictionary::Ptr& event)
+{
+ Namespace::Ptr frameNS = new Namespace();
+ ScriptFrame frame(true, frameNS);
+ frame.Sandboxed = true;
+
+ try {
+ if (!FilterUtility::EvaluateFilter(frame, m_Filter.get(), event, "event"))
+ return;
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "EventQueue")
+ << "Error occurred while evaluating event filter for queue '" << m_Name << "': " << DiagnosticInformation(ex);
+ return;
+ }
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ typedef std::pair<void *const, std::deque<Dictionary::Ptr> > kv_pair;
+ for (kv_pair& kv : m_Events) {
+ kv.second.push_back(event);
+ }
+
+ m_CV.notify_all();
+}
+
+void EventQueue::AddClient(void *client)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ auto result = m_Events.insert(std::make_pair(client, std::deque<Dictionary::Ptr>()));
+ ASSERT(result.second);
+
+#ifndef I2_DEBUG
+ (void)result;
+#endif /* I2_DEBUG */
+}
+
+void EventQueue::RemoveClient(void *client)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ m_Events.erase(client);
+}
+
+void EventQueue::UnregisterIfUnused(const String& name, const EventQueue::Ptr& queue)
+{
+ std::unique_lock<std::mutex> lock(queue->m_Mutex);
+
+ if (queue->m_Events.empty())
+ Unregister(name);
+}
+
+void EventQueue::SetTypes(const std::set<String>& types)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Types = types;
+}
+
+void EventQueue::SetFilter(std::unique_ptr<Expression> filter)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Filter.swap(filter);
+}
+
+Dictionary::Ptr EventQueue::WaitForEvent(void *client, double timeout)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ for (;;) {
+ auto it = m_Events.find(client);
+ ASSERT(it != m_Events.end());
+
+ if (!it->second.empty()) {
+ Dictionary::Ptr result = *it->second.begin();
+ it->second.pop_front();
+ return result;
+ }
+
+ if (m_CV.wait_for(lock, std::chrono::duration<double>(timeout)) == std::cv_status::timeout)
+ return nullptr;
+ }
+}
+
+std::vector<EventQueue::Ptr> EventQueue::GetQueuesForType(const String& type)
+{
+ EventQueueRegistry::ItemMap queues = EventQueueRegistry::GetInstance()->GetItems();
+
+ std::vector<EventQueue::Ptr> availQueues;
+
+ typedef std::pair<String, EventQueue::Ptr> kv_pair;
+ for (const kv_pair& kv : queues) {
+ if (kv.second->CanProcessEvent(type))
+ availQueues.push_back(kv.second);
+ }
+
+ return availQueues;
+}
+
+EventQueue::Ptr EventQueue::GetByName(const String& name)
+{
+ return EventQueueRegistry::GetInstance()->GetItem(name);
+}
+
+void EventQueue::Register(const String& name, const EventQueue::Ptr& function)
+{
+ EventQueueRegistry::GetInstance()->Register(name, function);
+}
+
+void EventQueue::Unregister(const String& name)
+{
+ EventQueueRegistry::GetInstance()->Unregister(name);
+}
+
+EventQueueRegistry *EventQueueRegistry::GetInstance()
+{
+ return Singleton<EventQueueRegistry>::GetInstance();
+}
+
+std::mutex EventsInbox::m_FiltersMutex;
+std::map<String, EventsInbox::Filter> EventsInbox::m_Filters ({{"", EventsInbox::Filter{1, Expression::Ptr()}}});
+
+EventsRouter EventsRouter::m_Instance;
+
+EventsInbox::EventsInbox(String filter, const String& filterSource)
+ : m_Timer(IoEngine::Get().GetIoContext())
+{
+ std::unique_lock<std::mutex> lock (m_FiltersMutex);
+ m_Filter = m_Filters.find(filter);
+
+ if (m_Filter == m_Filters.end()) {
+ lock.unlock();
+
+ auto expr (ConfigCompiler::CompileText(filterSource, filter));
+
+ lock.lock();
+
+ m_Filter = m_Filters.find(filter);
+
+ if (m_Filter == m_Filters.end()) {
+ m_Filter = m_Filters.emplace(std::move(filter), Filter{1, Expression::Ptr(expr.release())}).first;
+ } else {
+ ++m_Filter->second.Refs;
+ }
+ } else {
+ ++m_Filter->second.Refs;
+ }
+}
+
+EventsInbox::~EventsInbox()
+{
+ std::unique_lock<std::mutex> lock (m_FiltersMutex);
+
+ if (!--m_Filter->second.Refs) {
+ m_Filters.erase(m_Filter);
+ }
+}
+
+const Expression::Ptr& EventsInbox::GetFilter()
+{
+ return m_Filter->second.Expr;
+}
+
+void EventsInbox::Push(Dictionary::Ptr event)
+{
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ m_Queue.emplace(std::move(event));
+ m_Timer.expires_at(boost::posix_time::neg_infin);
+}
+
+Dictionary::Ptr EventsInbox::Shift(boost::asio::yield_context yc, double timeout)
+{
+ std::unique_lock<std::mutex> lock (m_Mutex, std::defer_lock);
+
+ m_Timer.expires_at(boost::posix_time::neg_infin);
+
+ {
+ boost::system::error_code ec;
+
+ while (!lock.try_lock()) {
+ m_Timer.async_wait(yc[ec]);
+ }
+ }
+
+ if (m_Queue.empty()) {
+ m_Timer.expires_from_now(boost::posix_time::milliseconds((unsigned long)(timeout * 1000.0)));
+ lock.unlock();
+
+ {
+ boost::system::error_code ec;
+ m_Timer.async_wait(yc[ec]);
+
+ while (!lock.try_lock()) {
+ m_Timer.async_wait(yc[ec]);
+ }
+ }
+
+ if (m_Queue.empty()) {
+ return nullptr;
+ }
+ }
+
+ auto event (std::move(m_Queue.front()));
+ m_Queue.pop();
+ return event;
+}
+
+EventsSubscriber::EventsSubscriber(std::set<EventType> types, String filter, const String& filterSource)
+ : m_Types(std::move(types)), m_Inbox(new EventsInbox(std::move(filter), filterSource))
+{
+ EventsRouter::GetInstance().Subscribe(m_Types, m_Inbox);
+}
+
+EventsSubscriber::~EventsSubscriber()
+{
+ EventsRouter::GetInstance().Unsubscribe(m_Types, m_Inbox);
+}
+
+const EventsInbox::Ptr& EventsSubscriber::GetInbox()
+{
+ return m_Inbox;
+}
+
+EventsFilter::EventsFilter(std::map<Expression::Ptr, std::set<EventsInbox::Ptr>> inboxes)
+ : m_Inboxes(std::move(inboxes))
+{
+}
+
+EventsFilter::operator bool()
+{
+ return !m_Inboxes.empty();
+}
+
+void EventsFilter::Push(Dictionary::Ptr event)
+{
+ for (auto& perFilter : m_Inboxes) {
+ if (perFilter.first) {
+ ScriptFrame frame(true, new Namespace());
+ frame.Sandboxed = true;
+
+ try {
+ if (!FilterUtility::EvaluateFilter(frame, perFilter.first.get(), event, "event")) {
+ continue;
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "EventQueue")
+ << "Error occurred while evaluating event filter for queue: " << DiagnosticInformation(ex);
+ continue;
+ }
+ }
+
+ for (auto& inbox : perFilter.second) {
+ inbox->Push(event);
+ }
+ }
+}
+
+EventsRouter& EventsRouter::GetInstance()
+{
+ return m_Instance;
+}
+
+void EventsRouter::Subscribe(const std::set<EventType>& types, const EventsInbox::Ptr& inbox)
+{
+ const auto& filter (inbox->GetFilter());
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ for (auto type : types) {
+ auto perType (m_Subscribers.find(type));
+
+ if (perType == m_Subscribers.end()) {
+ perType = m_Subscribers.emplace(type, decltype(perType->second)()).first;
+ }
+
+ auto perFilter (perType->second.find(filter));
+
+ if (perFilter == perType->second.end()) {
+ perFilter = perType->second.emplace(filter, decltype(perFilter->second)()).first;
+ }
+
+ perFilter->second.emplace(inbox);
+ }
+}
+
+void EventsRouter::Unsubscribe(const std::set<EventType>& types, const EventsInbox::Ptr& inbox)
+{
+ const auto& filter (inbox->GetFilter());
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ for (auto type : types) {
+ auto perType (m_Subscribers.find(type));
+
+ if (perType != m_Subscribers.end()) {
+ auto perFilter (perType->second.find(filter));
+
+ if (perFilter != perType->second.end()) {
+ perFilter->second.erase(inbox);
+
+ if (perFilter->second.empty()) {
+ perType->second.erase(perFilter);
+ }
+ }
+
+ if (perType->second.empty()) {
+ m_Subscribers.erase(perType);
+ }
+ }
+ }
+}
+
+EventsFilter EventsRouter::GetInboxes(EventType type)
+{
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ auto perType (m_Subscribers.find(type));
+
+ if (perType == m_Subscribers.end()) {
+ return EventsFilter({});
+ }
+
+ return EventsFilter(perType->second);
+}
diff --git a/lib/remote/eventqueue.hpp b/lib/remote/eventqueue.hpp
new file mode 100644
index 0000000..32bd34a
--- /dev/null
+++ b/lib/remote/eventqueue.hpp
@@ -0,0 +1,177 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EVENTQUEUE_H
+#define EVENTQUEUE_H
+
+#include "remote/httphandler.hpp"
+#include "base/object.hpp"
+#include "config/expression.hpp"
+#include <boost/asio/deadline_timer.hpp>
+#include <boost/asio/spawn.hpp>
+#include <condition_variable>
+#include <cstddef>
+#include <cstdint>
+#include <mutex>
+#include <set>
+#include <map>
+#include <deque>
+#include <queue>
+
+namespace icinga
+{
+
+class EventQueue final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EventQueue);
+
+ EventQueue(String name);
+
+ bool CanProcessEvent(const String& type) const;
+ void ProcessEvent(const Dictionary::Ptr& event);
+ void AddClient(void *client);
+ void RemoveClient(void *client);
+
+ void SetTypes(const std::set<String>& types);
+ void SetFilter(std::unique_ptr<Expression> filter);
+
+ Dictionary::Ptr WaitForEvent(void *client, double timeout = 5);
+
+ static std::vector<EventQueue::Ptr> GetQueuesForType(const String& type);
+ static void UnregisterIfUnused(const String& name, const EventQueue::Ptr& queue);
+
+ static EventQueue::Ptr GetByName(const String& name);
+ static void Register(const String& name, const EventQueue::Ptr& function);
+ static void Unregister(const String& name);
+
+private:
+ String m_Name;
+
+ mutable std::mutex m_Mutex;
+ std::condition_variable m_CV;
+
+ std::set<String> m_Types;
+ std::unique_ptr<Expression> m_Filter;
+
+ std::map<void *, std::deque<Dictionary::Ptr> > m_Events;
+};
+
+/**
+ * A registry for API event queues.
+ *
+ * @ingroup base
+ */
+class EventQueueRegistry : public Registry<EventQueueRegistry, EventQueue::Ptr>
+{
+public:
+ static EventQueueRegistry *GetInstance();
+};
+
+enum class EventType : uint_fast8_t
+{
+ AcknowledgementCleared,
+ AcknowledgementSet,
+ CheckResult,
+ CommentAdded,
+ CommentRemoved,
+ DowntimeAdded,
+ DowntimeRemoved,
+ DowntimeStarted,
+ DowntimeTriggered,
+ Flapping,
+ Notification,
+ StateChange,
+ ObjectCreated,
+ ObjectDeleted,
+ ObjectModified
+};
+
+class EventsInbox : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EventsInbox);
+
+ EventsInbox(String filter, const String& filterSource);
+ EventsInbox(const EventsInbox&) = delete;
+ EventsInbox(EventsInbox&&) = delete;
+ EventsInbox& operator=(const EventsInbox&) = delete;
+ EventsInbox& operator=(EventsInbox&&) = delete;
+ ~EventsInbox();
+
+ const Expression::Ptr& GetFilter();
+
+ void Push(Dictionary::Ptr event);
+ Dictionary::Ptr Shift(boost::asio::yield_context yc, double timeout = 5);
+
+private:
+ struct Filter
+ {
+ std::size_t Refs;
+ Expression::Ptr Expr;
+ };
+
+ static std::mutex m_FiltersMutex;
+ static std::map<String, Filter> m_Filters;
+
+ std::mutex m_Mutex;
+ decltype(m_Filters.begin()) m_Filter;
+ std::queue<Dictionary::Ptr> m_Queue;
+ boost::asio::deadline_timer m_Timer;
+};
+
+class EventsSubscriber
+{
+public:
+ EventsSubscriber(std::set<EventType> types, String filter, const String& filterSource);
+ EventsSubscriber(const EventsSubscriber&) = delete;
+ EventsSubscriber(EventsSubscriber&&) = delete;
+ EventsSubscriber& operator=(const EventsSubscriber&) = delete;
+ EventsSubscriber& operator=(EventsSubscriber&&) = delete;
+ ~EventsSubscriber();
+
+ const EventsInbox::Ptr& GetInbox();
+
+private:
+ std::set<EventType> m_Types;
+ EventsInbox::Ptr m_Inbox;
+};
+
+class EventsFilter
+{
+public:
+ EventsFilter(std::map<Expression::Ptr, std::set<EventsInbox::Ptr>> inboxes);
+
+ operator bool();
+
+ void Push(Dictionary::Ptr event);
+
+private:
+ std::map<Expression::Ptr, std::set<EventsInbox::Ptr>> m_Inboxes;
+};
+
+class EventsRouter
+{
+public:
+ static EventsRouter& GetInstance();
+
+ void Subscribe(const std::set<EventType>& types, const EventsInbox::Ptr& inbox);
+ void Unsubscribe(const std::set<EventType>& types, const EventsInbox::Ptr& inbox);
+ EventsFilter GetInboxes(EventType type);
+
+private:
+ static EventsRouter m_Instance;
+
+ EventsRouter() = default;
+ EventsRouter(const EventsRouter&) = delete;
+ EventsRouter(EventsRouter&&) = delete;
+ EventsRouter& operator=(const EventsRouter&) = delete;
+ EventsRouter& operator=(EventsRouter&&) = delete;
+ ~EventsRouter() = default;
+
+ std::mutex m_Mutex;
+ std::map<EventType, std::map<Expression::Ptr, std::set<EventsInbox::Ptr>>> m_Subscribers;
+};
+
+}
+
+#endif /* EVENTQUEUE_H */
diff --git a/lib/remote/eventshandler.cpp b/lib/remote/eventshandler.cpp
new file mode 100644
index 0000000..e05ef22
--- /dev/null
+++ b/lib/remote/eventshandler.cpp
@@ -0,0 +1,137 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/eventshandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "config/configcompiler.hpp"
+#include "config/expression.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/objectlock.hpp"
+#include "base/json.hpp"
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/write.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <map>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/events", EventsHandler);
+
+const std::map<String, EventType> l_EventTypes ({
+ {"AcknowledgementCleared", EventType::AcknowledgementCleared},
+ {"AcknowledgementSet", EventType::AcknowledgementSet},
+ {"CheckResult", EventType::CheckResult},
+ {"CommentAdded", EventType::CommentAdded},
+ {"CommentRemoved", EventType::CommentRemoved},
+ {"DowntimeAdded", EventType::DowntimeAdded},
+ {"DowntimeRemoved", EventType::DowntimeRemoved},
+ {"DowntimeStarted", EventType::DowntimeStarted},
+ {"DowntimeTriggered", EventType::DowntimeTriggered},
+ {"Flapping", EventType::Flapping},
+ {"Notification", EventType::Notification},
+ {"StateChange", EventType::StateChange},
+ {"ObjectCreated", EventType::ObjectCreated},
+ {"ObjectDeleted", EventType::ObjectDeleted},
+ {"ObjectModified", EventType::ObjectModified}
+});
+
+const String l_ApiQuery ("<API query>");
+
+bool EventsHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace asio = boost::asio;
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 2)
+ return false;
+
+ if (request.method() != http::verb::post)
+ return false;
+
+ if (request.version() == 10) {
+ HttpUtility::SendJsonError(response, params, 400, "HTTP/1.0 not supported for event streams.");
+ return true;
+ }
+
+ Array::Ptr types = params->Get("types");
+
+ if (!types) {
+ HttpUtility::SendJsonError(response, params, 400, "'types' query parameter is required.");
+ return true;
+ }
+
+ {
+ ObjectLock olock(types);
+ for (const String& type : types) {
+ FilterUtility::CheckPermission(user, "events/" + type);
+ }
+ }
+
+ String queueName = HttpUtility::GetLastParameter(params, "queue");
+
+ if (queueName.IsEmpty()) {
+ HttpUtility::SendJsonError(response, params, 400, "'queue' query parameter is required.");
+ return true;
+ }
+
+ std::set<EventType> eventTypes;
+
+ {
+ ObjectLock olock(types);
+ for (const String& type : types) {
+ auto typeId (l_EventTypes.find(type));
+
+ if (typeId != l_EventTypes.end()) {
+ eventTypes.emplace(typeId->second);
+ }
+ }
+ }
+
+ EventsSubscriber subscriber (std::move(eventTypes), HttpUtility::GetLastParameter(params, "filter"), l_ApiQuery);
+
+ server.StartStreaming();
+
+ response.result(http::status::ok);
+ response.set(http::field::content_type, "application/json");
+
+ IoBoundWorkSlot dontLockTheIoThread (yc);
+
+ http::async_write(stream, response, yc);
+ stream.async_flush(yc);
+
+ asio::const_buffer newLine ("\n", 1);
+
+ for (;;) {
+ auto event (subscriber.GetInbox()->Shift(yc));
+
+ if (event) {
+ CpuBoundWork buildingResponse (yc);
+
+ String body = JsonEncode(event);
+
+ boost::algorithm::replace_all(body, "\n", "");
+
+ asio::const_buffer payload (body.CStr(), body.GetLength());
+
+ buildingResponse.Done();
+
+ asio::async_write(stream, payload, yc);
+ asio::async_write(stream, newLine, yc);
+ stream.async_flush(yc);
+ } else if (server.Disconnected()) {
+ return true;
+ }
+ }
+}
+
diff --git a/lib/remote/eventshandler.hpp b/lib/remote/eventshandler.hpp
new file mode 100644
index 0000000..c823415
--- /dev/null
+++ b/lib/remote/eventshandler.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EVENTSHANDLER_H
+#define EVENTSHANDLER_H
+
+#include "remote/httphandler.hpp"
+#include "remote/eventqueue.hpp"
+
+namespace icinga
+{
+
+class EventsHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EventsHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* EVENTSHANDLER_H */
diff --git a/lib/remote/filterutility.cpp b/lib/remote/filterutility.cpp
new file mode 100644
index 0000000..468b91e
--- /dev/null
+++ b/lib/remote/filterutility.cpp
@@ -0,0 +1,354 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/filterutility.hpp"
+#include "remote/httputility.hpp"
+#include "config/applyrule.hpp"
+#include "config/configcompiler.hpp"
+#include "config/expression.hpp"
+#include "base/namespace.hpp"
+#include "base/json.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <memory>
+
+using namespace icinga;
+
+Type::Ptr FilterUtility::TypeFromPluralName(const String& pluralName)
+{
+ String uname = pluralName;
+ boost::algorithm::to_lower(uname);
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ String pname = type->GetPluralName();
+ boost::algorithm::to_lower(pname);
+
+ if (uname == pname)
+ return type;
+ }
+
+ return nullptr;
+}
+
+void ConfigObjectTargetProvider::FindTargets(const String& type, const std::function<void (const Value&)>& addTarget) const
+{
+ Type::Ptr ptype = Type::GetByName(type);
+ auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (ctype) {
+ for (const ConfigObject::Ptr& object : ctype->GetObjects()) {
+ addTarget(object);
+ }
+ }
+}
+
+Value ConfigObjectTargetProvider::GetTargetByName(const String& type, const String& name) const
+{
+ ConfigObject::Ptr obj = ConfigObject::GetObject(type, name);
+
+ if (!obj)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Object does not exist."));
+
+ return obj;
+}
+
+bool ConfigObjectTargetProvider::IsValidType(const String& type) const
+{
+ Type::Ptr ptype = Type::GetByName(type);
+
+ if (!ptype)
+ return false;
+
+ return ConfigObject::TypeInstance->IsAssignableFrom(ptype);
+}
+
+String ConfigObjectTargetProvider::GetPluralName(const String& type) const
+{
+ return Type::GetByName(type)->GetPluralName();
+}
+
+bool FilterUtility::EvaluateFilter(ScriptFrame& frame, Expression *filter,
+ const Object::Ptr& target, const String& variableName)
+{
+ if (!filter)
+ return true;
+
+ Type::Ptr type = target->GetReflectionType();
+ String varName;
+
+ if (variableName.IsEmpty())
+ varName = type->GetName().ToLower();
+ else
+ varName = variableName;
+
+ Namespace::Ptr frameNS;
+
+ if (frame.Self.IsEmpty()) {
+ frameNS = new Namespace();
+ frame.Self = frameNS;
+ } else {
+ /* Enforce a namespace object for 'frame.self'. */
+ ASSERT(frame.Self.IsObjectType<Namespace>());
+
+ frameNS = frame.Self;
+
+ ASSERT(frameNS != ScriptGlobal::GetGlobals());
+ }
+
+ frameNS->Set("obj", target);
+ frameNS->Set(varName, target);
+
+ for (int fid = 0; fid < type->GetFieldCount(); fid++) {
+ Field field = type->GetFieldInfo(fid);
+
+ if ((field.Attributes & FANavigation) == 0)
+ continue;
+
+ Object::Ptr joinedObj = target->NavigateField(fid);
+
+ if (field.NavigationName)
+ frameNS->Set(field.NavigationName, joinedObj);
+ else
+ frameNS->Set(field.Name, joinedObj);
+ }
+
+ return Convert::ToBool(filter->Evaluate(frame));
+}
+
+static void FilteredAddTarget(ScriptFrame& permissionFrame, Expression *permissionFilter,
+ ScriptFrame& frame, Expression *ufilter, std::vector<Value>& result, const String& variableName, const Object::Ptr& target)
+{
+ if (FilterUtility::EvaluateFilter(permissionFrame, permissionFilter, target, variableName)) {
+ if (FilterUtility::EvaluateFilter(frame, ufilter, target, variableName)) {
+ result.emplace_back(std::move(target));
+ }
+ }
+}
+
+/**
+ * Checks whether the given API user is granted the given permission
+ *
+ * When you desire an exception to be raised when the given user doesn't have the given permission,
+ * you need to use FilterUtility::CheckPermission().
+ *
+ * @param user ApiUser pointer to the user object you want to check the permission of
+ * @param permission The actual permission you want to check the user permission against
+ * @param permissionFilter Expression pointer that is used as an output buffer for all the filter expressions of the
+ * individual permissions of the given user to be evaluated. It's up to the caller to delete
+ * this pointer when it's not needed any more.
+ *
+ * @return bool
+ */
+bool FilterUtility::HasPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* permissionFilter)
+{
+ if (permissionFilter)
+ *permissionFilter = nullptr;
+
+ if (permission.IsEmpty())
+ return true;
+
+ bool foundPermission = false;
+ String requiredPermission = permission.ToLower();
+
+ Array::Ptr permissions = user->GetPermissions();
+ if (permissions) {
+ ObjectLock olock(permissions);
+ for (const Value& item : permissions) {
+ String permission;
+ Function::Ptr filter;
+ if (item.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = item;
+ permission = dict->Get("permission");
+ filter = dict->Get("filter");
+ } else
+ permission = item;
+
+ permission = permission.ToLower();
+
+ if (!Utility::Match(permission, requiredPermission))
+ continue;
+
+ foundPermission = true;
+
+ if (filter && permissionFilter) {
+ std::vector<std::unique_ptr<Expression> > args;
+ args.emplace_back(new GetScopeExpression(ScopeThis));
+ std::unique_ptr<Expression> indexer{new IndexerExpression(std::unique_ptr<Expression>(MakeLiteral(filter)), std::unique_ptr<Expression>(MakeLiteral("call")))};
+ FunctionCallExpression *fexpr = new FunctionCallExpression(std::move(indexer), std::move(args));
+
+ if (!*permissionFilter)
+ permissionFilter->reset(fexpr);
+ else
+ *permissionFilter = std::make_unique<LogicalOrExpression>(std::move(*permissionFilter), std::unique_ptr<Expression>(fexpr));
+ }
+ }
+ }
+
+ if (!foundPermission) {
+ Log(LogWarning, "FilterUtility")
+ << "Missing permission: " << requiredPermission;
+ }
+
+ return foundPermission;
+}
+
+void FilterUtility::CheckPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* permissionFilter)
+{
+ if (!HasPermission(user, permission, permissionFilter)) {
+ BOOST_THROW_EXCEPTION(ScriptError("Missing permission: " + permission.ToLower()));
+ }
+}
+
+std::vector<Value> FilterUtility::GetFilterTargets(const QueryDescription& qd, const Dictionary::Ptr& query, const ApiUser::Ptr& user, const String& variableName)
+{
+ std::vector<Value> result;
+
+ TargetProvider::Ptr provider;
+
+ if (qd.Provider)
+ provider = qd.Provider;
+ else
+ provider = new ConfigObjectTargetProvider();
+
+ std::unique_ptr<Expression> permissionFilter;
+ CheckPermission(user, qd.Permission, &permissionFilter);
+
+ Namespace::Ptr permissionFrameNS = new Namespace();
+ ScriptFrame permissionFrame(false, permissionFrameNS);
+
+ for (const String& type : qd.Types) {
+ String attr = type;
+ boost::algorithm::to_lower(attr);
+
+ if (attr == "type")
+ attr = "name";
+
+ if (query && query->Contains(attr)) {
+ String name = HttpUtility::GetLastParameter(query, attr);
+ Object::Ptr target = provider->GetTargetByName(type, name);
+
+ if (!FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName))
+ BOOST_THROW_EXCEPTION(ScriptError("Access denied to object '" + name + "' of type '" + type + "'"));
+
+ result.emplace_back(std::move(target));
+ }
+
+ attr = provider->GetPluralName(type);
+ boost::algorithm::to_lower(attr);
+
+ if (query && query->Contains(attr)) {
+ Array::Ptr names = query->Get(attr);
+ if (names) {
+ ObjectLock olock(names);
+ for (const String& name : names) {
+ Object::Ptr target = provider->GetTargetByName(type, name);
+
+ if (!FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName))
+ BOOST_THROW_EXCEPTION(ScriptError("Access denied to object '" + name + "' of type '" + type + "'"));
+
+ result.emplace_back(std::move(target));
+ }
+ }
+ }
+ }
+
+ if ((query && query->Contains("filter")) || result.empty()) {
+ if (!query->Contains("type"))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Type must be specified when using a filter."));
+
+ String type = HttpUtility::GetLastParameter(query, "type");
+
+ if (!provider->IsValidType(type))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid type specified."));
+
+ if (qd.Types.find(type) == qd.Types.end())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid type specified for this query."));
+
+ Namespace::Ptr frameNS = new Namespace();
+ ScriptFrame frame(false, frameNS);
+ frame.Sandboxed = true;
+
+ if (query->Contains("filter")) {
+ String filter = HttpUtility::GetLastParameter(query, "filter");
+ std::unique_ptr<Expression> ufilter = ConfigCompiler::CompileText("<API query>", filter);
+ Dictionary::Ptr filter_vars = query->Get("filter_vars");
+ bool targeted = false;
+ std::vector<ConfigObject::Ptr> targets;
+
+ if (dynamic_cast<ConfigObjectTargetProvider*>(provider.get())) {
+ auto dict (dynamic_cast<DictExpression*>(ufilter.get()));
+
+ if (dict) {
+ auto& subex (dict->GetExpressions());
+
+ if (subex.size() == 1u) {
+ if (type == "Host") {
+ std::vector<const String *> targetNames;
+
+ if (ApplyRule::GetTargetHosts(subex.at(0).get(), targetNames, filter_vars)) {
+ static const auto typeHost (Type::GetByName("Host"));
+ static const auto ctypeHost (dynamic_cast<ConfigType*>(typeHost.get()));
+ targeted = true;
+
+ for (auto name : targetNames) {
+ auto target (ctypeHost->GetObject(*name));
+
+ if (target) {
+ targets.emplace_back(target);
+ }
+ }
+ }
+ } else if (type == "Service") {
+ std::vector<std::pair<const String *, const String *>> targetNames;
+
+ if (ApplyRule::GetTargetServices(subex.at(0).get(), targetNames, filter_vars)) {
+ static const auto typeService (Type::GetByName("Service"));
+ static const auto ctypeService (dynamic_cast<ConfigType*>(typeService.get()));
+ targeted = true;
+
+ for (auto name : targetNames) {
+ auto target (ctypeService->GetObject(*name.first + "!" + *name.second));
+
+ if (target) {
+ targets.emplace_back(target);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (targeted) {
+ for (auto& target : targets) {
+ if (FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName)) {
+ result.emplace_back(std::move(target));
+ }
+ }
+ } else {
+ if (filter_vars) {
+ ObjectLock olock (filter_vars);
+
+ for (auto& kv : filter_vars) {
+ frameNS->Set(kv.first, kv.second);
+ }
+ }
+
+ provider->FindTargets(type, [&permissionFrame, &permissionFilter, &frame, &ufilter, &result, variableName](const Object::Ptr& target) {
+ FilteredAddTarget(permissionFrame, permissionFilter.get(), frame, &*ufilter, result, variableName, target);
+ });
+ }
+ } else {
+ /* Ensure to pass a nullptr as filter expression.
+ * GCC 8.1.1 on F28 causes problems, see GH #6533.
+ */
+ provider->FindTargets(type, [&permissionFrame, &permissionFilter, &frame, &result, variableName](const Object::Ptr& target) {
+ FilteredAddTarget(permissionFrame, permissionFilter.get(), frame, nullptr, result, variableName, target);
+ });
+ }
+ }
+
+ return result;
+}
+
diff --git a/lib/remote/filterutility.hpp b/lib/remote/filterutility.hpp
new file mode 100644
index 0000000..7271367
--- /dev/null
+++ b/lib/remote/filterutility.hpp
@@ -0,0 +1,64 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FILTERUTILITY_H
+#define FILTERUTILITY_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/apiuser.hpp"
+#include "config/expression.hpp"
+#include "base/dictionary.hpp"
+#include "base/configobject.hpp"
+#include <set>
+
+namespace icinga
+{
+
+class TargetProvider : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TargetProvider);
+
+ virtual void FindTargets(const String& type, const std::function<void (const Value&)>& addTarget) const = 0;
+ virtual Value GetTargetByName(const String& type, const String& name) const = 0;
+ virtual bool IsValidType(const String& type) const = 0;
+ virtual String GetPluralName(const String& type) const = 0;
+};
+
+class ConfigObjectTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigObjectTargetProvider);
+
+ void FindTargets(const String& type, const std::function<void (const Value&)>& addTarget) const override;
+ Value GetTargetByName(const String& type, const String& name) const override;
+ bool IsValidType(const String& type) const override;
+ String GetPluralName(const String& type) const override;
+};
+
+struct QueryDescription
+{
+ std::set<String> Types;
+ TargetProvider::Ptr Provider;
+ String Permission;
+};
+
+/**
+ * Filter utilities.
+ *
+ * @ingroup remote
+ */
+class FilterUtility
+{
+public:
+ static Type::Ptr TypeFromPluralName(const String& pluralName);
+ static void CheckPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* filter = nullptr);
+ static bool HasPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* permissionFilter = nullptr);
+ static std::vector<Value> GetFilterTargets(const QueryDescription& qd, const Dictionary::Ptr& query,
+ const ApiUser::Ptr& user, const String& variableName = String());
+ static bool EvaluateFilter(ScriptFrame& frame, Expression *filter,
+ const Object::Ptr& target, const String& variableName = String());
+};
+
+}
+
+#endif /* FILTERUTILITY_H */
diff --git a/lib/remote/httphandler.cpp b/lib/remote/httphandler.cpp
new file mode 100644
index 0000000..afe510f
--- /dev/null
+++ b/lib/remote/httphandler.cpp
@@ -0,0 +1,129 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/logger.hpp"
+#include "remote/httphandler.hpp"
+#include "remote/httputility.hpp"
+#include "base/singleton.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/beast/http.hpp>
+
+using namespace icinga;
+
+Dictionary::Ptr HttpHandler::m_UrlTree;
+
+void HttpHandler::Register(const Url::Ptr& url, const HttpHandler::Ptr& handler)
+{
+ if (!m_UrlTree)
+ m_UrlTree = new Dictionary();
+
+ Dictionary::Ptr node = m_UrlTree;
+
+ for (const String& elem : url->GetPath()) {
+ Dictionary::Ptr children = node->Get("children");
+
+ if (!children) {
+ children = new Dictionary();
+ node->Set("children", children);
+ }
+
+ Dictionary::Ptr sub_node = children->Get(elem);
+ if (!sub_node) {
+ sub_node = new Dictionary();
+ children->Set(elem, sub_node);
+ }
+
+ node = sub_node;
+ }
+
+ Array::Ptr handlers = node->Get("handlers");
+
+ if (!handlers) {
+ handlers = new Array();
+ node->Set("handlers", handlers);
+ }
+
+ handlers->Add(handler);
+}
+
+void HttpHandler::ProcessRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ Dictionary::Ptr node = m_UrlTree;
+ std::vector<HttpHandler::Ptr> handlers;
+
+ Url::Ptr url = new Url(std::string(request.target()));
+ auto& path (url->GetPath());
+
+ for (std::vector<String>::size_type i = 0; i <= path.size(); i++) {
+ Array::Ptr current_handlers = node->Get("handlers");
+
+ if (current_handlers) {
+ ObjectLock olock(current_handlers);
+ for (const HttpHandler::Ptr& current_handler : current_handlers) {
+ handlers.push_back(current_handler);
+ }
+ }
+
+ Dictionary::Ptr children = node->Get("children");
+
+ if (!children) {
+ node.reset();
+ break;
+ }
+
+ if (i == path.size())
+ break;
+
+ node = children->Get(path[i]);
+
+ if (!node)
+ break;
+ }
+
+ std::reverse(handlers.begin(), handlers.end());
+
+ Dictionary::Ptr params;
+
+ try {
+ params = HttpUtility::FetchRequestParameters(url, request.body());
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid request body: " + DiagnosticInformation(ex, false));
+ return;
+ }
+
+ bool processed = false;
+
+ /*
+ * HandleRequest may throw a permission exception.
+ * DO NOT return a specific permission error. This
+ * allows attackers to guess from words which objects
+ * do exist.
+ */
+ try {
+ for (const HttpHandler::Ptr& handler : handlers) {
+ if (handler->HandleRequest(stream, user, request, url, response, params, yc, server)) {
+ processed = true;
+ break;
+ }
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "HttpServerConnection")
+ << "Error while processing HTTP request: " << ex.what();
+
+ processed = false;
+ }
+
+ if (!processed) {
+ HttpUtility::SendJsonError(response, params, 404, "The requested path '" + boost::algorithm::join(path, "/") +
+ "' could not be found or the request method is not valid for this path.");
+ return;
+ }
+}
+
diff --git a/lib/remote/httphandler.hpp b/lib/remote/httphandler.hpp
new file mode 100644
index 0000000..a6a7302
--- /dev/null
+++ b/lib/remote/httphandler.hpp
@@ -0,0 +1,74 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HTTPHANDLER_H
+#define HTTPHANDLER_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/url.hpp"
+#include "remote/httpserverconnection.hpp"
+#include "remote/apiuser.hpp"
+#include "base/registry.hpp"
+#include "base/tlsstream.hpp"
+#include <vector>
+#include <boost/asio/spawn.hpp>
+#include <boost/beast/http.hpp>
+
+namespace icinga
+{
+
+/**
+ * HTTP handler.
+ *
+ * @ingroup remote
+ */
+class HttpHandler : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(HttpHandler);
+
+ virtual bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) = 0;
+
+ static void Register(const Url::Ptr& url, const HttpHandler::Ptr& handler);
+ static void ProcessRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ );
+
+private:
+ static Dictionary::Ptr m_UrlTree;
+};
+
+/**
+ * Helper class for registering HTTP handlers.
+ *
+ * @ingroup remote
+ */
+class RegisterHttpHandler
+{
+public:
+ RegisterHttpHandler(const String& url, const HttpHandler& function);
+};
+
+#define REGISTER_URLHANDLER(url, klass) \
+ INITIALIZE_ONCE([]() { \
+ Url::Ptr uurl = new Url(url); \
+ HttpHandler::Ptr handler = new klass(); \
+ HttpHandler::Register(uurl, handler); \
+ })
+
+}
+
+#endif /* HTTPHANDLER_H */
diff --git a/lib/remote/httpserverconnection.cpp b/lib/remote/httpserverconnection.cpp
new file mode 100644
index 0000000..76cfd3c
--- /dev/null
+++ b/lib/remote/httpserverconnection.cpp
@@ -0,0 +1,613 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/httpserverconnection.hpp"
+#include "remote/httphandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/application.hpp"
+#include "base/base64.hpp"
+#include "base/convert.hpp"
+#include "base/configtype.hpp"
+#include "base/defer.hpp"
+#include "base/exception.hpp"
+#include "base/io-engine.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/timer.hpp"
+#include "base/tlsstream.hpp"
+#include "base/utility.hpp"
+#include <chrono>
+#include <limits>
+#include <memory>
+#include <stdexcept>
+#include <boost/asio/error.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/system/system_error.hpp>
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+auto const l_ServerHeader ("Icinga/" + Application::GetAppVersion());
+
+HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream)
+ : HttpServerConnection(identity, authenticated, stream, IoEngine::Get().GetIoContext())
+{
+}
+
+HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, boost::asio::io_context& io)
+ : m_Stream(stream), m_Seen(Utility::GetTime()), m_IoStrand(io), m_ShuttingDown(false), m_HasStartedStreaming(false),
+ m_CheckLivenessTimer(io)
+{
+ if (authenticated) {
+ m_ApiUser = ApiUser::GetByClientCN(identity);
+ }
+
+ {
+ std::ostringstream address;
+ auto endpoint (stream->lowest_layer().remote_endpoint());
+
+ address << '[' << endpoint.address() << "]:" << endpoint.port();
+
+ m_PeerAddress = address.str();
+ }
+}
+
+void HttpServerConnection::Start()
+{
+ namespace asio = boost::asio;
+
+ HttpServerConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { ProcessMessages(yc); });
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); });
+}
+
+void HttpServerConnection::Disconnect()
+{
+ namespace asio = boost::asio;
+
+ HttpServerConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
+ if (!m_ShuttingDown) {
+ m_ShuttingDown = true;
+
+ Log(LogInformation, "HttpServerConnection")
+ << "HTTP client disconnected (from " << m_PeerAddress << ")";
+
+ /*
+ * Do not swallow exceptions in a coroutine.
+ * https://github.com/Icinga/icinga2/issues/7351
+ * We must not catch `detail::forced_unwind exception` as
+ * this is used for unwinding the stack.
+ *
+ * Just use the error_code dummy here.
+ */
+ boost::system::error_code ec;
+
+ m_CheckLivenessTimer.cancel();
+
+ m_Stream->lowest_layer().cancel(ec);
+
+ m_Stream->next_layer().async_shutdown(yc[ec]);
+
+ m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both, ec);
+
+ auto listener (ApiListener::GetInstance());
+
+ if (listener) {
+ CpuBoundWork removeHttpClient (yc);
+
+ listener->RemoveHttpClient(this);
+ }
+ }
+ });
+}
+
+void HttpServerConnection::StartStreaming()
+{
+ namespace asio = boost::asio;
+
+ m_HasStartedStreaming = true;
+
+ HttpServerConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
+ if (!m_ShuttingDown) {
+ char buf[128];
+ asio::mutable_buffer readBuf (buf, 128);
+ boost::system::error_code ec;
+
+ do {
+ m_Stream->async_read_some(readBuf, yc[ec]);
+ } while (!ec);
+
+ Disconnect();
+ }
+ });
+}
+
+bool HttpServerConnection::Disconnected()
+{
+ return m_ShuttingDown;
+}
+
+static inline
+bool EnsureValidHeaders(
+ AsioTlsStream& stream,
+ boost::beast::flat_buffer& buf,
+ boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ bool& shuttingDown,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ if (shuttingDown)
+ return false;
+
+ bool httpError = false;
+ String errorMsg;
+
+ boost::system::error_code ec;
+
+ http::async_read_header(stream, buf, parser, yc[ec]);
+
+ if (ec) {
+ if (ec == boost::asio::error::operation_aborted)
+ return false;
+
+ errorMsg = ec.message();
+ httpError = true;
+ } else {
+ switch (parser.get().version()) {
+ case 10:
+ case 11:
+ break;
+ default:
+ errorMsg = "Unsupported HTTP version";
+ }
+ }
+
+ if (!errorMsg.IsEmpty() || httpError) {
+ response.result(http::status::bad_request);
+
+ if (!httpError && parser.get()[http::field::accept] == "application/json") {
+ HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
+ { "error", 400 },
+ { "status", String("Bad Request: ") + errorMsg }
+ }));
+ } else {
+ response.set(http::field::content_type, "text/html");
+ response.body() = String("<h1>Bad Request</h1><p><pre>") + errorMsg + "</pre></p>";
+ response.content_length(response.body().size());
+ }
+
+ response.set(http::field::connection, "close");
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+
+ return true;
+}
+
+static inline
+void HandleExpect100(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ if (request[http::field::expect] == "100-continue") {
+ http::response<http::string_body> response;
+
+ response.result(http::status::continue_);
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+ }
+}
+
+static inline
+bool HandleAccessControl(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ auto listener (ApiListener::GetInstance());
+
+ if (listener) {
+ auto headerAllowOrigin (listener->GetAccessControlAllowOrigin());
+
+ if (headerAllowOrigin) {
+ CpuBoundWork allowOriginHeader (yc);
+
+ auto allowedOrigins (headerAllowOrigin->ToSet<String>());
+
+ if (!allowedOrigins.empty()) {
+ auto& origin (request[http::field::origin]);
+
+ if (allowedOrigins.find(std::string(origin)) != allowedOrigins.end()) {
+ response.set(http::field::access_control_allow_origin, origin);
+ }
+
+ allowOriginHeader.Done();
+
+ response.set(http::field::access_control_allow_credentials, "true");
+
+ if (request.method() == http::verb::options && !request[http::field::access_control_request_method].empty()) {
+ response.result(http::status::ok);
+ response.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE");
+ response.set(http::field::access_control_allow_headers, "Authorization, Content-Type, X-HTTP-Method-Override");
+ response.body() = "Preflight OK";
+ response.content_length(response.body().size());
+ response.set(http::field::connection, "close");
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+static inline
+bool EnsureAcceptHeader(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ if (request.method() != http::verb::get && request[http::field::accept] != "application/json") {
+ response.result(http::status::bad_request);
+ response.set(http::field::content_type, "text/html");
+ response.body() = "<h1>Accept header is missing or not set to 'application/json'.</h1>";
+ response.content_length(response.body().size());
+ response.set(http::field::connection, "close");
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+
+ return true;
+}
+
+static inline
+bool EnsureAuthenticatedUser(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ if (!authenticatedUser) {
+ Log(LogWarning, "HttpServerConnection")
+ << "Unauthorized request: " << request.method_string() << ' ' << request.target();
+
+ response.result(http::status::unauthorized);
+ response.set(http::field::www_authenticate, "Basic realm=\"Icinga 2\"");
+ response.set(http::field::connection, "close");
+
+ if (request[http::field::accept] == "application/json") {
+ HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
+ { "error", 401 },
+ { "status", "Unauthorized. Please check your user credentials." }
+ }));
+ } else {
+ response.set(http::field::content_type, "text/html");
+ response.body() = "<h1>Unauthorized. Please check your user credentials.</h1>";
+ response.content_length(response.body().size());
+ }
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+
+ return true;
+}
+
+static inline
+bool EnsureValidBody(
+ AsioTlsStream& stream,
+ boost::beast::flat_buffer& buf,
+ boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ bool& shuttingDown,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ {
+ size_t maxSize = 1024 * 1024;
+ Array::Ptr permissions = authenticatedUser->GetPermissions();
+
+ if (permissions) {
+ CpuBoundWork evalPermissions (yc);
+
+ ObjectLock olock(permissions);
+
+ for (const Value& permissionInfo : permissions) {
+ String permission;
+
+ if (permissionInfo.IsObjectType<Dictionary>()) {
+ permission = static_cast<Dictionary::Ptr>(permissionInfo)->Get("permission");
+ } else {
+ permission = permissionInfo;
+ }
+
+ static std::vector<std::pair<String, size_t>> specialContentLengthLimits {
+ { "config/modify", 512 * 1024 * 1024 }
+ };
+
+ for (const auto& limitInfo : specialContentLengthLimits) {
+ if (limitInfo.second <= maxSize) {
+ continue;
+ }
+
+ if (Utility::Match(permission, limitInfo.first)) {
+ maxSize = limitInfo.second;
+ }
+ }
+ }
+ }
+
+ parser.body_limit(maxSize);
+ }
+
+ if (shuttingDown)
+ return false;
+
+ boost::system::error_code ec;
+
+ http::async_read(stream, buf, parser, yc[ec]);
+
+ if (ec) {
+ if (ec == boost::asio::error::operation_aborted)
+ return false;
+
+ /**
+ * Unfortunately there's no way to tell an HTTP protocol error
+ * from an error on a lower layer:
+ *
+ * <https://github.com/boostorg/beast/issues/643>
+ */
+
+ response.result(http::status::bad_request);
+
+ if (parser.get()[http::field::accept] == "application/json") {
+ HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
+ { "error", 400 },
+ { "status", String("Bad Request: ") + ec.message() }
+ }));
+ } else {
+ response.set(http::field::content_type, "text/html");
+ response.body() = String("<h1>Bad Request</h1><p><pre>") + ec.message() + "</pre></p>";
+ response.content_length(response.body().size());
+ }
+
+ response.set(http::field::connection, "close");
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+
+ return true;
+}
+
+static inline
+bool ProcessRequest(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ HttpServerConnection& server,
+ bool& hasStartedStreaming,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ try {
+ CpuBoundWork handlingRequest (yc);
+
+ HttpHandler::ProcessRequest(stream, authenticatedUser, request, response, yc, server);
+ } catch (const std::exception& ex) {
+ if (hasStartedStreaming) {
+ return false;
+ }
+
+ auto sysErr (dynamic_cast<const boost::system::system_error*>(&ex));
+
+ if (sysErr && sysErr->code() == boost::asio::error::operation_aborted) {
+ throw;
+ }
+
+ http::response<http::string_body> response;
+
+ HttpUtility::SendJsonError(response, nullptr, 500, "Unhandled exception" , DiagnosticInformation(ex));
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return true;
+ }
+
+ if (hasStartedStreaming) {
+ return false;
+ }
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return true;
+}
+
+void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
+{
+ namespace beast = boost::beast;
+ namespace http = beast::http;
+ namespace ch = std::chrono;
+
+ try {
+ /* Do not reset the buffer in the state machine.
+ * EnsureValidHeaders already reads from the stream into the buffer,
+ * EnsureValidBody continues. ProcessRequest() actually handles the request
+ * and needs the full buffer.
+ */
+ beast::flat_buffer buf;
+
+ for (;;) {
+ m_Seen = Utility::GetTime();
+
+ http::parser<true, http::string_body> parser;
+ http::response<http::string_body> response;
+
+ parser.header_limit(1024 * 1024);
+ parser.body_limit(-1);
+
+ response.set(http::field::server, l_ServerHeader);
+
+ if (!EnsureValidHeaders(*m_Stream, buf, parser, response, m_ShuttingDown, yc)) {
+ break;
+ }
+
+ m_Seen = Utility::GetTime();
+ auto start (ch::steady_clock::now());
+
+ auto& request (parser.get());
+
+ {
+ auto method (http::string_to_verb(request["X-Http-Method-Override"]));
+
+ if (method != http::verb::unknown) {
+ request.method(method);
+ }
+ }
+
+ HandleExpect100(*m_Stream, request, yc);
+
+ auto authenticatedUser (m_ApiUser);
+
+ if (!authenticatedUser) {
+ CpuBoundWork fetchingAuthenticatedUser (yc);
+
+ authenticatedUser = ApiUser::GetByAuthHeader(std::string(request[http::field::authorization]));
+ }
+
+ Log logMsg (LogInformation, "HttpServerConnection");
+
+ logMsg << "Request " << request.method_string() << ' ' << request.target()
+ << " (from " << m_PeerAddress
+ << "), user: " << (authenticatedUser ? authenticatedUser->GetName() : "<unauthenticated>")
+ << ", agent: " << request[http::field::user_agent]; //operator[] - Returns the value for a field, or "" if it does not exist.
+
+ Defer addRespCode ([&response, start, &logMsg]() {
+ logMsg << ", status: " << response.result() << ") took "
+ << ch::duration_cast<ch::milliseconds>(ch::steady_clock::now() - start).count() << "ms.";
+ });
+
+ if (!HandleAccessControl(*m_Stream, request, response, yc)) {
+ break;
+ }
+
+ if (!EnsureAcceptHeader(*m_Stream, request, response, yc)) {
+ break;
+ }
+
+ if (!EnsureAuthenticatedUser(*m_Stream, request, authenticatedUser, response, yc)) {
+ break;
+ }
+
+ if (!EnsureValidBody(*m_Stream, buf, parser, authenticatedUser, response, m_ShuttingDown, yc)) {
+ break;
+ }
+
+ m_Seen = std::numeric_limits<decltype(m_Seen)>::max();
+
+ if (!ProcessRequest(*m_Stream, request, authenticatedUser, response, *this, m_HasStartedStreaming, yc)) {
+ break;
+ }
+
+ if (request.version() != 11 || request[http::field::connection] == "close") {
+ break;
+ }
+ }
+ } catch (const std::exception& ex) {
+ if (!m_ShuttingDown) {
+ Log(LogCritical, "HttpServerConnection")
+ << "Unhandled exception while processing HTTP request: " << ex.what();
+ }
+ }
+
+ Disconnect();
+}
+
+void HttpServerConnection::CheckLiveness(boost::asio::yield_context yc)
+{
+ boost::system::error_code ec;
+
+ for (;;) {
+ m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(5));
+ m_CheckLivenessTimer.async_wait(yc[ec]);
+
+ if (m_ShuttingDown) {
+ break;
+ }
+
+ if (m_Seen < Utility::GetTime() - 10) {
+ Log(LogInformation, "HttpServerConnection")
+ << "No messages for HTTP connection have been received in the last 10 seconds.";
+
+ Disconnect();
+ break;
+ }
+ }
+}
diff --git a/lib/remote/httpserverconnection.hpp b/lib/remote/httpserverconnection.hpp
new file mode 100644
index 0000000..9c812e5
--- /dev/null
+++ b/lib/remote/httpserverconnection.hpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HTTPSERVERCONNECTION_H
+#define HTTPSERVERCONNECTION_H
+
+#include "remote/apiuser.hpp"
+#include "base/string.hpp"
+#include "base/tlsstream.hpp"
+#include <memory>
+#include <boost/asio/deadline_timer.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/io_context_strand.hpp>
+#include <boost/asio/spawn.hpp>
+
+namespace icinga
+{
+
+/**
+ * An API client connection.
+ *
+ * @ingroup remote
+ */
+class HttpServerConnection final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(HttpServerConnection);
+
+ HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream);
+
+ void Start();
+ void Disconnect();
+ void StartStreaming();
+
+ bool Disconnected();
+
+private:
+ ApiUser::Ptr m_ApiUser;
+ Shared<AsioTlsStream>::Ptr m_Stream;
+ double m_Seen;
+ String m_PeerAddress;
+ boost::asio::io_context::strand m_IoStrand;
+ bool m_ShuttingDown;
+ bool m_HasStartedStreaming;
+ boost::asio::deadline_timer m_CheckLivenessTimer;
+
+ HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, boost::asio::io_context& io);
+
+ void ProcessMessages(boost::asio::yield_context yc);
+ void CheckLiveness(boost::asio::yield_context yc);
+};
+
+}
+
+#endif /* HTTPSERVERCONNECTION_H */
diff --git a/lib/remote/httputility.cpp b/lib/remote/httputility.cpp
new file mode 100644
index 0000000..a2142e5
--- /dev/null
+++ b/lib/remote/httputility.cpp
@@ -0,0 +1,80 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/httputility.hpp"
+#include "remote/url.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include <map>
+#include <string>
+#include <vector>
+#include <boost/beast/http.hpp>
+
+using namespace icinga;
+
+Dictionary::Ptr HttpUtility::FetchRequestParameters(const Url::Ptr& url, const std::string& body)
+{
+ Dictionary::Ptr result;
+
+ if (!body.empty()) {
+ Log(LogDebug, "HttpUtility")
+ << "Request body: '" << body << '\'';
+
+ result = JsonDecode(body);
+ }
+
+ if (!result)
+ result = new Dictionary();
+
+ std::map<String, std::vector<String>> query;
+ for (const auto& kv : url->GetQuery()) {
+ query[kv.first].emplace_back(kv.second);
+ }
+
+ for (auto& kv : query) {
+ result->Set(kv.first, Array::FromVector(kv.second));
+ }
+
+ return result;
+}
+
+Value HttpUtility::GetLastParameter(const Dictionary::Ptr& params, const String& key)
+{
+ Value varr = params->Get(key);
+
+ if (!varr.IsObjectType<Array>())
+ return varr;
+
+ Array::Ptr arr = varr;
+
+ if (arr->GetLength() == 0)
+ return Empty;
+ else
+ return arr->Get(arr->GetLength() - 1);
+}
+
+void HttpUtility::SendJsonBody(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const Value& val)
+{
+ namespace http = boost::beast::http;
+
+ response.set(http::field::content_type, "application/json");
+ response.body() = JsonEncode(val, params && GetLastParameter(params, "pretty"));
+ response.content_length(response.body().size());
+}
+
+void HttpUtility::SendJsonError(boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, int code, const String& info, const String& diagnosticInformation)
+{
+ Dictionary::Ptr result = new Dictionary({ { "error", code } });
+
+ if (!info.IsEmpty()) {
+ result->Set("status", info);
+ }
+
+ if (params && HttpUtility::GetLastParameter(params, "verbose") && !diagnosticInformation.IsEmpty()) {
+ result->Set("diagnostic_information", diagnosticInformation);
+ }
+
+ response.result(code);
+
+ HttpUtility::SendJsonBody(response, params, result);
+}
diff --git a/lib/remote/httputility.hpp b/lib/remote/httputility.hpp
new file mode 100644
index 0000000..6465b4a
--- /dev/null
+++ b/lib/remote/httputility.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HTTPUTILITY_H
+#define HTTPUTILITY_H
+
+#include "remote/url.hpp"
+#include "base/dictionary.hpp"
+#include <boost/beast/http.hpp>
+#include <string>
+
+namespace icinga
+{
+
+/**
+ * Helper functions.
+ *
+ * @ingroup remote
+ */
+class HttpUtility
+{
+
+public:
+ static Dictionary::Ptr FetchRequestParameters(const Url::Ptr& url, const std::string& body);
+ static Value GetLastParameter(const Dictionary::Ptr& params, const String& key);
+
+ static void SendJsonBody(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const Value& val);
+ static void SendJsonError(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const int code,
+ const String& verbose = String(), const String& diagnosticInformation = String());
+};
+
+}
+
+#endif /* HTTPUTILITY_H */
diff --git a/lib/remote/i2-remote.hpp b/lib/remote/i2-remote.hpp
new file mode 100644
index 0000000..5755bef
--- /dev/null
+++ b/lib/remote/i2-remote.hpp
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2REMOTE_H
+#define I2REMOTE_H
+
+/**
+ * @defgroup remote Remote library
+ *
+ * The Icinga library implements remote cluster functionality.
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2REMOTE_H */
diff --git a/lib/remote/infohandler.cpp b/lib/remote/infohandler.cpp
new file mode 100644
index 0000000..80ebba7
--- /dev/null
+++ b/lib/remote/infohandler.cpp
@@ -0,0 +1,100 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/infohandler.hpp"
+#include "remote/httputility.hpp"
+#include "base/application.hpp"
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/", InfoHandler);
+
+bool InfoHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 2)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ if (url->GetPath().empty()) {
+ response.result(http::status::found);
+ response.set(http::field::location, "/v1");
+ return true;
+ }
+
+ if (url->GetPath()[0] != "v1" || url->GetPath().size() != 1)
+ return false;
+
+ response.result(http::status::ok);
+
+ std::vector<String> permInfo;
+ Array::Ptr permissions = user->GetPermissions();
+
+ if (permissions) {
+ ObjectLock olock(permissions);
+ for (const Value& permission : permissions) {
+ String name;
+ bool hasFilter = false;
+ if (permission.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dpermission = permission;
+ name = dpermission->Get("permission");
+ hasFilter = dpermission->Contains("filter");
+ } else
+ name = permission;
+
+ if (hasFilter)
+ name += " (filtered)";
+
+ permInfo.emplace_back(std::move(name));
+ }
+ }
+
+ if (request[http::field::accept] == "application/json") {
+ Dictionary::Ptr result1 = new Dictionary({
+ { "user", user->GetName() },
+ { "permissions", Array::FromVector(permInfo) },
+ { "version", Application::GetAppVersion() },
+ { "info", "More information about API requests is available in the documentation at https://icinga.com/docs/icinga2/latest/" }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ HttpUtility::SendJsonBody(response, params, result);
+ } else {
+ response.set(http::field::content_type, "text/html");
+
+ String body = "<html><head><title>Icinga 2</title></head><h1>Hello from Icinga 2 (Version: " + Application::GetAppVersion() + ")!</h1>";
+ body += "<p>You are authenticated as <b>" + user->GetName() + "</b>. ";
+
+ if (!permInfo.empty()) {
+ body += "Your user has the following permissions:</p> <ul>";
+
+ for (const String& perm : permInfo) {
+ body += "<li>" + perm + "</li>";
+ }
+
+ body += "</ul>";
+ } else
+ body += "Your user does not have any permissions.</p>";
+
+ body += R"(<p>More information about API requests is available in the <a href="https://icinga.com/docs/icinga2/latest/" target="_blank">documentation</a>.</p></html>)";
+ response.body() = body;
+ response.content_length(response.body().size());
+ }
+
+ return true;
+}
+
diff --git a/lib/remote/infohandler.hpp b/lib/remote/infohandler.hpp
new file mode 100644
index 0000000..e1fe983
--- /dev/null
+++ b/lib/remote/infohandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef INFOHANDLER_H
+#define INFOHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class InfoHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(InfoHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* INFOHANDLER_H */
diff --git a/lib/remote/jsonrpc.cpp b/lib/remote/jsonrpc.cpp
new file mode 100644
index 0000000..d4d3d3c
--- /dev/null
+++ b/lib/remote/jsonrpc.cpp
@@ -0,0 +1,157 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpc.hpp"
+#include "base/netstring.hpp"
+#include "base/json.hpp"
+#include "base/console.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/convert.hpp"
+#include "base/tlsstream.hpp"
+#include <iostream>
+#include <memory>
+#include <utility>
+#include <boost/asio/spawn.hpp>
+
+using namespace icinga;
+
+#ifdef I2_DEBUG
+/**
+ * Determine whether the developer wants to see raw JSON messages.
+ *
+ * @return Internal.DebugJsonRpc boolean
+ */
+static bool GetDebugJsonRpcCached()
+{
+ static int debugJsonRpc = -1;
+
+ if (debugJsonRpc != -1)
+ return debugJsonRpc;
+
+ debugJsonRpc = false;
+
+ Namespace::Ptr internal = ScriptGlobal::Get("Internal", &Empty);
+
+ if (!internal)
+ return false;
+
+ Value vdebug;
+
+ if (!internal->Get("DebugJsonRpc", &vdebug))
+ return false;
+
+ debugJsonRpc = Convert::ToLong(vdebug);
+
+ return debugJsonRpc;
+}
+#endif /* I2_DEBUG */
+
+/**
+ * Sends a message to the connected peer and returns the bytes sent.
+ *
+ * @param message The message.
+ *
+ * @return The amount of bytes sent.
+ */
+size_t JsonRpc::SendMessage(const Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message)
+{
+ String json = JsonEncode(message);
+
+#ifdef I2_DEBUG
+ if (GetDebugJsonRpcCached())
+ std::cerr << ConsoleColorTag(Console_ForegroundBlue) << ">> " << json << ConsoleColorTag(Console_Normal) << "\n";
+#endif /* I2_DEBUG */
+
+ return NetString::WriteStringToStream(stream, json);
+}
+
+/**
+ * Sends a message to the connected peer and returns the bytes sent.
+ *
+ * @param message The message.
+ *
+ * @return The amount of bytes sent.
+ */
+size_t JsonRpc::SendMessage(const Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc)
+{
+ return JsonRpc::SendRawMessage(stream, JsonEncode(message), yc);
+}
+
+ /**
+ * Sends a raw message to the connected peer.
+ *
+ * @param stream ASIO TLS Stream
+ * @param json message
+ * @param yc Yield context required for ASIO
+ *
+ * @return bytes sent
+ */
+size_t JsonRpc::SendRawMessage(const Shared<AsioTlsStream>::Ptr& stream, const String& json, boost::asio::yield_context yc)
+{
+#ifdef I2_DEBUG
+ if (GetDebugJsonRpcCached())
+ std::cerr << ConsoleColorTag(Console_ForegroundBlue) << ">> " << json << ConsoleColorTag(Console_Normal) << "\n";
+#endif /* I2_DEBUG */
+
+ return NetString::WriteStringToStream(stream, json, yc);
+}
+
+/**
+ * Reads a message from the connected peer.
+ *
+ * @param stream ASIO TLS Stream
+ * @param maxMessageLength maximum size of bytes read.
+ *
+ * @return A JSON string
+ */
+
+String JsonRpc::ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, ssize_t maxMessageLength)
+{
+ String jsonString = NetString::ReadStringFromStream(stream, maxMessageLength);
+
+#ifdef I2_DEBUG
+ if (GetDebugJsonRpcCached())
+ std::cerr << ConsoleColorTag(Console_ForegroundBlue) << "<< " << jsonString << ConsoleColorTag(Console_Normal) << "\n";
+#endif /* I2_DEBUG */
+
+ return jsonString;
+}
+
+/**
+ * Reads a message from the connected peer.
+ *
+ * @param stream ASIO TLS Stream
+ * @param yc Yield Context for ASIO
+ * @param maxMessageLength maximum size of bytes read.
+ *
+ * @return A JSON string
+ */
+String JsonRpc::ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, boost::asio::yield_context yc, ssize_t maxMessageLength)
+{
+ String jsonString = NetString::ReadStringFromStream(stream, yc, maxMessageLength);
+
+#ifdef I2_DEBUG
+ if (GetDebugJsonRpcCached())
+ std::cerr << ConsoleColorTag(Console_ForegroundBlue) << "<< " << jsonString << ConsoleColorTag(Console_Normal) << "\n";
+#endif /* I2_DEBUG */
+
+ return jsonString;
+}
+
+/**
+ * Decode message, enforce a Dictionary
+ *
+ * @param message JSON string
+ *
+ * @return Dictionary ptr
+ */
+Dictionary::Ptr JsonRpc::DecodeMessage(const String& message)
+{
+ Value value = JsonDecode(message);
+
+ if (!value.IsObjectType<Dictionary>()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("JSON-RPC"
+ " message must be a dictionary."));
+ }
+
+ return value;
+}
diff --git a/lib/remote/jsonrpc.hpp b/lib/remote/jsonrpc.hpp
new file mode 100644
index 0000000..3f3cdec
--- /dev/null
+++ b/lib/remote/jsonrpc.hpp
@@ -0,0 +1,39 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef JSONRPC_H
+#define JSONRPC_H
+
+#include "base/stream.hpp"
+#include "base/dictionary.hpp"
+#include "base/tlsstream.hpp"
+#include "remote/i2-remote.hpp"
+#include <memory>
+#include <boost/asio/spawn.hpp>
+
+namespace icinga
+{
+
+/**
+ * A JSON-RPC connection.
+ *
+ * @ingroup remote
+ */
+class JsonRpc
+{
+public:
+ static size_t SendMessage(const Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message);
+ static size_t SendMessage(const Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc);
+ static size_t SendRawMessage(const Shared<AsioTlsStream>::Ptr& stream, const String& json, boost::asio::yield_context yc);
+
+ static String ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, ssize_t maxMessageLength = -1);
+ static String ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, boost::asio::yield_context yc, ssize_t maxMessageLength = -1);
+
+ static Dictionary::Ptr DecodeMessage(const String& message);
+
+private:
+ JsonRpc();
+};
+
+}
+
+#endif /* JSONRPC_H */
diff --git a/lib/remote/jsonrpcconnection-heartbeat.cpp b/lib/remote/jsonrpcconnection-heartbeat.cpp
new file mode 100644
index 0000000..2474688
--- /dev/null
+++ b/lib/remote/jsonrpcconnection-heartbeat.cpp
@@ -0,0 +1,48 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/messageorigin.hpp"
+#include "remote/apifunction.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include <boost/asio/spawn.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/system/system_error.hpp>
+
+using namespace icinga;
+
+REGISTER_APIFUNCTION(Heartbeat, event, &JsonRpcConnection::HeartbeatAPIHandler);
+
+/**
+ * We still send a heartbeat without timeout here
+ * to keep the m_Seen variable up to date. This is to keep the
+ * cluster connection alive when there isn't much going on.
+ */
+
+void JsonRpcConnection::HandleAndWriteHeartbeats(boost::asio::yield_context yc)
+{
+ boost::system::error_code ec;
+
+ for (;;) {
+ m_HeartbeatTimer.expires_from_now(boost::posix_time::seconds(20));
+ m_HeartbeatTimer.async_wait(yc[ec]);
+
+ if (m_ShuttingDown) {
+ break;
+ }
+
+ SendMessageInternal(new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "event::Heartbeat" },
+ { "params", new Dictionary() }
+ }));
+ }
+}
+
+Value JsonRpcConnection::HeartbeatAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ return Empty;
+}
+
diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp
new file mode 100644
index 0000000..340e12b
--- /dev/null
+++ b/lib/remote/jsonrpcconnection-pki.cpp
@@ -0,0 +1,439 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/atomic-file.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include <boost/thread/once.hpp>
+#include <boost/regex.hpp>
+#include <fstream>
+#include <openssl/asn1.h>
+#include <openssl/ssl.h>
+#include <openssl/x509.h>
+
+using namespace icinga;
+
+static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
+static Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+REGISTER_APIFUNCTION(UpdateCertificate, pki, &UpdateCertificateHandler);
+
+Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ String certText = params->Get("cert_request");
+
+ std::shared_ptr<X509> cert;
+
+ Dictionary::Ptr result = new Dictionary();
+ auto& tlsConn (origin->FromClient->GetStream()->next_layer());
+
+ /* Use the presented client certificate if not provided. */
+ if (certText.IsEmpty()) {
+ cert = tlsConn.GetPeerCertificate();
+ } else {
+ cert = StringToCertificate(certText);
+ }
+
+ if (!cert) {
+ Log(LogWarning, "JsonRpcConnection") << "No certificate or CSR received";
+
+ result->Set("status_code", 1);
+ result->Set("error", "No certificate or CSR received.");
+
+ return result;
+ }
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+ std::shared_ptr<X509> cacert = GetX509Certificate(listener->GetDefaultCaPath());
+
+ String cn = GetCertificateCN(cert);
+
+ bool signedByCA = false;
+
+ {
+ Log logmsg(LogInformation, "JsonRpcConnection");
+ logmsg << "Received certificate request for CN '" << cn << "'";
+
+ try {
+ signedByCA = VerifyCertificate(cacert, cert, listener->GetCrlPath());
+ if (!signedByCA) {
+ logmsg << " not";
+ }
+ logmsg << " signed by our CA.";
+ } catch (const std::exception &ex) {
+ logmsg << " which couldn't be verified";
+
+ if (const unsigned long *openssl_code = boost::get_error_info<errinfo_openssl_error>(ex)) {
+ logmsg << ": " << X509_verify_cert_error_string(long(*openssl_code)) << " (code " << *openssl_code << ")";
+ } else {
+ logmsg << ".";
+ }
+ }
+ }
+
+ std::shared_ptr<X509> parsedRequestorCA;
+ X509* requestorCA = nullptr;
+
+ if (signedByCA) {
+ bool uptodate = IsCertUptodate(cert);
+
+ if (uptodate) {
+ // Even if the leaf is up-to-date, the root may expire soon.
+ // In a regular setup where Icinga manages the PKI, there is only one CA.
+ // Icinga includes it in handshakes, let's see whether the peer needs a fresh one...
+
+ if (cn == origin->FromClient->GetIdentity()) {
+ auto chain (SSL_get_peer_cert_chain(tlsConn.native_handle()));
+
+ if (chain) {
+ auto len (sk_X509_num(chain));
+
+ for (int i = 0; i < len; ++i) {
+ auto link (sk_X509_value(chain, i));
+
+ if (!X509_NAME_cmp(X509_get_subject_name(link), X509_get_issuer_name(link))) {
+ requestorCA = link;
+ }
+ }
+ }
+ } else {
+ Value requestorCaStr;
+
+ if (params->Get("requestor_ca", &requestorCaStr)) {
+ parsedRequestorCA = StringToCertificate(requestorCaStr);
+ requestorCA = parsedRequestorCA.get();
+ }
+ }
+
+ if (requestorCA && !IsCaUptodate(requestorCA)) {
+ int days;
+
+ if (ASN1_TIME_diff(&days, nullptr, X509_get_notAfter(requestorCA), X509_get_notAfter(cacert.get())) && days > 0) {
+ uptodate = false;
+ }
+ }
+ }
+
+ if (uptodate) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "The certificates for CN '" << cn << "' and its root CA are valid and uptodate. Skipping automated renewal.";
+ result->Set("status_code", 1);
+ result->Set("error", "The certificates for CN '" + cn + "' and its root CA are valid and uptodate. Skipping automated renewal.");
+ return result;
+ }
+ }
+
+ unsigned int n;
+ unsigned char digest[EVP_MAX_MD_SIZE];
+
+ if (!X509_digest(cert.get(), EVP_sha256(), digest, &n)) {
+ result->Set("status_code", 1);
+ result->Set("error", "Could not calculate fingerprint for the X509 certificate for CN '" + cn + "'.");
+
+ Log(LogWarning, "JsonRpcConnection")
+ << "Could not calculate fingerprint for the X509 certificate requested for CN '"
+ << cn << "'.";
+
+ return result;
+ }
+
+ char certFingerprint[EVP_MAX_MD_SIZE*2+1];
+ for (unsigned int i = 0; i < n; i++)
+ sprintf(certFingerprint + 2 * i, "%02x", digest[i]);
+
+ result->Set("fingerprint_request", certFingerprint);
+
+ String requestDir = ApiListener::GetCertificateRequestsDir();
+ String requestPath = requestDir + "/" + certFingerprint + ".json";
+
+ result->Set("ca", CertificateToString(cacert));
+
+ JsonRpcConnection::Ptr client = origin->FromClient;
+
+ /* If we already have a signed certificate request, send it to the client. */
+ if (Utility::PathExists(requestPath)) {
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
+
+ String certResponse = request->Get("cert_response");
+
+ if (!certResponse.IsEmpty()) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Sending certificate response for CN '" << cn
+ << "' to endpoint '" << client->GetIdentity() << "'.";
+
+ result->Set("cert", certResponse);
+ result->Set("status_code", 0);
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "pki::UpdateCertificate" },
+ { "params", result }
+ });
+ client->SendMessage(message);
+
+ return result;
+ }
+ } else if (Utility::PathExists(requestDir + "/" + certFingerprint + ".removed")) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Certificate for CN " << cn << " has been removed. Ignoring signing request.";
+ result->Set("status_code", 1);
+ result->Set("error", "Ticket for CN " + cn + " declined by administrator.");
+ return result;
+ }
+
+ std::shared_ptr<X509> newcert;
+ Dictionary::Ptr message;
+ String ticket;
+
+ /* Check whether we are a signing instance or we
+ * must delay the signing request.
+ */
+ if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
+ goto delayed_request;
+
+ if (!signedByCA) {
+ String salt = listener->GetTicketSalt();
+
+ ticket = params->Get("ticket");
+
+ // Auto-signing is disabled: Client did not include a ticket in its request.
+ if (ticket.IsEmpty()) {
+ Log(LogNotice, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn
+ << "': No ticket included, skipping auto-signing and waiting for on-demand signing approval.";
+
+ goto delayed_request;
+ }
+
+ // Auto-signing is disabled: no TicketSalt
+ if (salt.IsEmpty()) {
+ Log(LogNotice, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn
+ << "': This instance is the signing master for the Icinga CA."
+ << " The 'ticket_salt' attribute in the 'api' feature is not set."
+ << " Not signing the request. Please check the docs.";
+
+ goto delayed_request;
+ }
+
+ String realTicket = PBKDF2_SHA1(cn, salt, 50000);
+
+ Log(LogDebug, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn << "': Comparing received ticket '"
+ << ticket << "' with calculated ticket '" << realTicket << "'.";
+
+ if (!Utility::ComparePasswords(ticket, realTicket)) {
+ Log(LogWarning, "JsonRpcConnection")
+ << "Ticket '" << ticket << "' for CN '" << cn << "' is invalid.";
+
+ result->Set("status_code", 1);
+ result->Set("error", "Invalid ticket for CN '" + cn + "'.");
+ return result;
+ }
+ }
+
+ newcert = listener->RenewCert(cert);
+
+ if (!newcert) {
+ goto delayed_request;
+ }
+
+ /* Send the signed certificate update. */
+ Log(LogInformation, "JsonRpcConnection")
+ << "Sending certificate response for CN '" << cn << "' to endpoint '"
+ << client->GetIdentity() << "'" << (!ticket.IsEmpty() ? " (auto-signing ticket)" : "" ) << ".";
+
+ result->Set("cert", CertificateToString(newcert));
+
+ result->Set("status_code", 0);
+
+ message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "pki::UpdateCertificate" },
+ { "params", result }
+ });
+ client->SendMessage(message);
+
+ return result;
+
+delayed_request:
+ /* Send a delayed certificate signing request. */
+ Utility::MkDirP(requestDir, 0700);
+
+ Dictionary::Ptr request = new Dictionary({
+ { "cert_request", CertificateToString(cert) },
+ { "ticket", params->Get("ticket") }
+ });
+
+ if (requestorCA) {
+ request->Set("requestor_ca", CertificateToString(requestorCA));
+ }
+
+ Utility::SaveJsonFile(requestPath, 0600, request);
+
+ JsonRpcConnection::SendCertificateRequest(nullptr, origin, requestPath);
+
+ result->Set("status_code", 2);
+ result->Set("error", "Certificate request for CN '" + cn + "' is pending. Waiting for approval from the parent Icinga instance.");
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn << "' is pending. Waiting for approval.";
+
+ if (origin) {
+ auto client (origin->FromClient);
+
+ if (client && !client->GetEndpoint()) {
+ client->Disconnect();
+ }
+ }
+
+ return result;
+}
+
+void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const MessageOrigin::Ptr& origin, const String& path)
+{
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "pki::RequestCertificate");
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ message->Set("params", params);
+
+ /* Path is empty if this is our own request. */
+ if (path.IsEmpty()) {
+ {
+ Log msg (LogInformation, "JsonRpcConnection");
+ msg << "Requesting new certificate for this Icinga instance";
+
+ if (aclient) {
+ msg << " from endpoint '" << aclient->GetIdentity() << "'";
+ }
+
+ msg << ".";
+ }
+
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+
+ std::ifstream fp(ticketPath.CStr());
+ String ticket((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+ fp.close();
+
+ params->Set("ticket", ticket);
+ } else {
+ Dictionary::Ptr request = Utility::LoadJsonFile(path);
+
+ if (request->Contains("cert_response"))
+ return;
+
+ request->CopyTo(params);
+ }
+
+ /* Send the request to a) the connected client
+ * or b) the local zone and all parents.
+ */
+ if (aclient)
+ aclient->SendMessage(message);
+ else
+ listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
+}
+
+Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ if (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'update certificate' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ String ca = params->Get("ca");
+ String cert = params->Get("cert");
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ std::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetDefaultCertPath());
+ std::shared_ptr<X509> newCert = StringToCertificate(cert);
+
+ String cn = GetCertificateCN(newCert);
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Received certificate update message for CN '" << cn << "'";
+
+ /* Check if this is a certificate update for a subordinate instance. */
+ std::shared_ptr<EVP_PKEY> oldKey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(oldCert.get()), EVP_PKEY_free);
+ std::shared_ptr<EVP_PKEY> newKey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(newCert.get()), EVP_PKEY_free);
+
+ if (X509_NAME_cmp(X509_get_subject_name(oldCert.get()), X509_get_subject_name(newCert.get())) != 0 ||
+ EVP_PKEY_cmp(oldKey.get(), newKey.get()) != 1) {
+ String certFingerprint = params->Get("fingerprint_request");
+
+ /* Validate the fingerprint format. */
+ boost::regex expr("^[0-9a-f]+$");
+
+ if (!boost::regex_match(certFingerprint.GetData(), expr)) {
+ Log(LogWarning, "JsonRpcConnection")
+ << "Endpoint '" << origin->FromClient->GetIdentity() << "' sent an invalid certificate fingerprint: '"
+ << certFingerprint << "' for CN '" << cn << "'.";
+ return Empty;
+ }
+
+ String requestDir = ApiListener::GetCertificateRequestsDir();
+ String requestPath = requestDir + "/" + certFingerprint + ".json";
+
+ /* Save the received signed certificate request to disk. */
+ if (Utility::PathExists(requestPath)) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Saved certificate update for CN '" << cn << "'";
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
+ request->Set("cert_response", cert);
+ Utility::SaveJsonFile(requestPath, 0644, request);
+ }
+
+ return Empty;
+ }
+
+ /* Update CA certificate. */
+ String caPath = listener->GetDefaultCaPath();
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating CA certificate in '" << caPath << "'.";
+
+ AtomicFile::Write(caPath, 0644, ca);
+
+ /* Update signed certificate. */
+ String certPath = listener->GetDefaultCertPath();
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";
+
+ AtomicFile::Write(certPath, 0644, cert);
+
+ /* Remove ticket for successful signing request. */
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+
+ Utility::Remove(ticketPath);
+
+ /* Update the certificates at runtime and reconnect all endpoints. */
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating the client certificate for CN '" << cn << "' at runtime and reconnecting the endpoints.";
+
+ listener->UpdateSSLContext();
+
+ return Empty;
+}
diff --git a/lib/remote/jsonrpcconnection.cpp b/lib/remote/jsonrpcconnection.cpp
new file mode 100644
index 0000000..3bae3ca
--- /dev/null
+++ b/lib/remote/jsonrpcconnection.cpp
@@ -0,0 +1,388 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/defer.hpp"
+#include "base/configtype.hpp"
+#include "base/io-engine.hpp"
+#include "base/json.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include "base/tlsstream.hpp"
+#include <memory>
+#include <utility>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/system/system_error.hpp>
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler);
+
+static RingBuffer l_TaskStats (15 * 60);
+
+JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated,
+ const Shared<AsioTlsStream>::Ptr& stream, ConnectionRole role)
+ : JsonRpcConnection(identity, authenticated, stream, role, IoEngine::Get().GetIoContext())
+{
+}
+
+JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated,
+ const Shared<AsioTlsStream>::Ptr& stream, ConnectionRole role, boost::asio::io_context& io)
+ : m_Identity(identity), m_Authenticated(authenticated), m_Stream(stream), m_Role(role),
+ m_Timestamp(Utility::GetTime()), m_Seen(Utility::GetTime()), m_NextHeartbeat(0), m_IoStrand(io),
+ m_OutgoingMessagesQueued(io), m_WriterDone(io), m_ShuttingDown(false),
+ m_CheckLivenessTimer(io), m_HeartbeatTimer(io)
+{
+ if (authenticated)
+ m_Endpoint = Endpoint::GetByName(identity);
+}
+
+void JsonRpcConnection::Start()
+{
+ namespace asio = boost::asio;
+
+ JsonRpcConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { HandleIncomingMessages(yc); });
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { WriteOutgoingMessages(yc); });
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { HandleAndWriteHeartbeats(yc); });
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); });
+}
+
+void JsonRpcConnection::HandleIncomingMessages(boost::asio::yield_context yc)
+{
+ m_Stream->next_layer().SetSeen(&m_Seen);
+
+ for (;;) {
+ String message;
+
+ try {
+ message = JsonRpc::ReadMessage(m_Stream, yc, m_Endpoint ? -1 : 1024 * 1024);
+ } catch (const std::exception& ex) {
+ Log(m_ShuttingDown ? LogDebug : LogNotice, "JsonRpcConnection")
+ << "Error while reading JSON-RPC message for identity '" << m_Identity
+ << "': " << DiagnosticInformation(ex);
+
+ break;
+ }
+
+ m_Seen = Utility::GetTime();
+
+ try {
+ CpuBoundWork handleMessage (yc);
+
+ MessageHandler(message);
+ } catch (const std::exception& ex) {
+ Log(m_ShuttingDown ? LogDebug : LogWarning, "JsonRpcConnection")
+ << "Error while processing JSON-RPC message for identity '" << m_Identity
+ << "': " << DiagnosticInformation(ex);
+
+ break;
+ }
+
+ CpuBoundWork taskStats (yc);
+
+ l_TaskStats.InsertValue(Utility::GetTime(), 1);
+ }
+
+ Disconnect();
+}
+
+void JsonRpcConnection::WriteOutgoingMessages(boost::asio::yield_context yc)
+{
+ Defer signalWriterDone ([this]() { m_WriterDone.Set(); });
+
+ do {
+ m_OutgoingMessagesQueued.Wait(yc);
+
+ auto queue (std::move(m_OutgoingMessagesQueue));
+
+ m_OutgoingMessagesQueue.clear();
+ m_OutgoingMessagesQueued.Clear();
+
+ if (!queue.empty()) {
+ try {
+ for (auto& message : queue) {
+ size_t bytesSent = JsonRpc::SendRawMessage(m_Stream, message, yc);
+
+ if (m_Endpoint) {
+ m_Endpoint->AddMessageSent(bytesSent);
+ }
+ }
+
+ m_Stream->async_flush(yc);
+ } catch (const std::exception& ex) {
+ Log(m_ShuttingDown ? LogDebug : LogWarning, "JsonRpcConnection")
+ << "Error while sending JSON-RPC message for identity '"
+ << m_Identity << "'\n" << DiagnosticInformation(ex);
+
+ break;
+ }
+ }
+ } while (!m_ShuttingDown);
+
+ Disconnect();
+}
+
+double JsonRpcConnection::GetTimestamp() const
+{
+ return m_Timestamp;
+}
+
+String JsonRpcConnection::GetIdentity() const
+{
+ return m_Identity;
+}
+
+bool JsonRpcConnection::IsAuthenticated() const
+{
+ return m_Authenticated;
+}
+
+Endpoint::Ptr JsonRpcConnection::GetEndpoint() const
+{
+ return m_Endpoint;
+}
+
+Shared<AsioTlsStream>::Ptr JsonRpcConnection::GetStream() const
+{
+ return m_Stream;
+}
+
+ConnectionRole JsonRpcConnection::GetRole() const
+{
+ return m_Role;
+}
+
+void JsonRpcConnection::SendMessage(const Dictionary::Ptr& message)
+{
+ Ptr keepAlive (this);
+
+ m_IoStrand.post([this, keepAlive, message]() { SendMessageInternal(message); });
+}
+
+void JsonRpcConnection::SendRawMessage(const String& message)
+{
+ Ptr keepAlive (this);
+
+ m_IoStrand.post([this, keepAlive, message]() {
+ m_OutgoingMessagesQueue.emplace_back(message);
+ m_OutgoingMessagesQueued.Set();
+ });
+}
+
+void JsonRpcConnection::SendMessageInternal(const Dictionary::Ptr& message)
+{
+ m_OutgoingMessagesQueue.emplace_back(JsonEncode(message));
+ m_OutgoingMessagesQueued.Set();
+}
+
+void JsonRpcConnection::Disconnect()
+{
+ namespace asio = boost::asio;
+
+ JsonRpcConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
+ if (!m_ShuttingDown) {
+ m_ShuttingDown = true;
+
+ Log(LogWarning, "JsonRpcConnection")
+ << "API client disconnected for identity '" << m_Identity << "'";
+
+ {
+ CpuBoundWork removeClient (yc);
+
+ if (m_Endpoint) {
+ m_Endpoint->RemoveClient(this);
+ } else {
+ ApiListener::GetInstance()->RemoveAnonymousClient(this);
+ }
+ }
+
+ m_OutgoingMessagesQueued.Set();
+
+ m_WriterDone.Wait(yc);
+
+ /*
+ * Do not swallow exceptions in a coroutine.
+ * https://github.com/Icinga/icinga2/issues/7351
+ * We must not catch `detail::forced_unwind exception` as
+ * this is used for unwinding the stack.
+ *
+ * Just use the error_code dummy here.
+ */
+ boost::system::error_code ec;
+
+ m_CheckLivenessTimer.cancel();
+ m_HeartbeatTimer.cancel();
+
+ m_Stream->lowest_layer().cancel(ec);
+
+ Timeout::Ptr shutdownTimeout (new Timeout(
+ m_IoStrand.context(),
+ m_IoStrand,
+ boost::posix_time::seconds(10),
+ [this, keepAlive](asio::yield_context yc) {
+ boost::system::error_code ec;
+ m_Stream->lowest_layer().cancel(ec);
+ }
+ ));
+
+ m_Stream->next_layer().async_shutdown(yc[ec]);
+
+ shutdownTimeout->Cancel();
+
+ m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both, ec);
+ }
+ });
+}
+
+void JsonRpcConnection::MessageHandler(const String& jsonString)
+{
+ Dictionary::Ptr message = JsonRpc::DecodeMessage(jsonString);
+
+ if (m_Endpoint && message->Contains("ts")) {
+ double ts = message->Get("ts");
+
+ /* ignore old messages */
+ if (ts < m_Endpoint->GetRemoteLogPosition())
+ return;
+
+ m_Endpoint->SetRemoteLogPosition(ts);
+ }
+
+ MessageOrigin::Ptr origin = new MessageOrigin();
+ origin->FromClient = this;
+
+ if (m_Endpoint) {
+ if (m_Endpoint->GetZone() != Zone::GetLocalZone())
+ origin->FromZone = m_Endpoint->GetZone();
+ else
+ origin->FromZone = Zone::GetByName(message->Get("originZone"));
+
+ m_Endpoint->AddMessageReceived(jsonString.GetLength());
+ }
+
+ Value vmethod;
+
+ if (!message->Get("method", &vmethod)) {
+ Value vid;
+
+ if (!message->Get("id", &vid))
+ return;
+
+ Log(LogWarning, "JsonRpcConnection",
+ "We received a JSON-RPC response message. This should never happen because we're only ever sending notifications.");
+
+ return;
+ }
+
+ String method = vmethod;
+
+ Log(LogNotice, "JsonRpcConnection")
+ << "Received '" << method << "' message from identity '" << m_Identity << "'.";
+
+ Dictionary::Ptr resultMessage = new Dictionary();
+
+ try {
+ ApiFunction::Ptr afunc = ApiFunction::GetByName(method);
+
+ if (!afunc) {
+ Log(LogNotice, "JsonRpcConnection")
+ << "Call to non-existent function '" << method << "' from endpoint '" << m_Identity << "'.";
+ } else {
+ Dictionary::Ptr params = message->Get("params");
+ if (params)
+ resultMessage->Set("result", afunc->Invoke(origin, params));
+ else
+ resultMessage->Set("result", Empty);
+ }
+ } catch (const std::exception& ex) {
+ /* TODO: Add a user readable error message for the remote caller */
+ String diagInfo = DiagnosticInformation(ex);
+ resultMessage->Set("error", diagInfo);
+ Log(LogWarning, "JsonRpcConnection")
+ << "Error while processing message for identity '" << m_Identity << "'\n" << diagInfo;
+ }
+
+ if (message->Contains("id")) {
+ resultMessage->Set("jsonrpc", "2.0");
+ resultMessage->Set("id", message->Get("id"));
+
+ SendMessageInternal(resultMessage);
+ }
+}
+
+Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ double log_position = params->Get("log_position");
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint)
+ return Empty;
+
+ if (log_position > endpoint->GetLocalLogPosition())
+ endpoint->SetLocalLogPosition(log_position);
+
+ return Empty;
+}
+
+void JsonRpcConnection::CheckLiveness(boost::asio::yield_context yc)
+{
+ boost::system::error_code ec;
+
+ if (!m_Authenticated) {
+ /* Anonymous connections are normally only used for requesting a certificate and are closed after this request
+ * is received. However, the request is only sent if the child has successfully verified the certificate of its
+ * parent so that it is an authenticated connection from its perspective. In case this verification fails, both
+ * ends view it as an anonymous connection and never actually use it but attempt a reconnect after 10 seconds
+ * leaking the connection. Therefore close it after a timeout.
+ */
+
+ m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(10));
+ m_CheckLivenessTimer.async_wait(yc[ec]);
+
+ if (m_ShuttingDown) {
+ return;
+ }
+
+ auto remote (m_Stream->lowest_layer().remote_endpoint());
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Closing anonymous connection [" << remote.address() << "]:" << remote.port() << " after 10 seconds.";
+
+ Disconnect();
+ } else {
+ for (;;) {
+ m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(30));
+ m_CheckLivenessTimer.async_wait(yc[ec]);
+
+ if (m_ShuttingDown) {
+ break;
+ }
+
+ if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "No messages for identity '" << m_Identity << "' have been received in the last 60 seconds.";
+
+ Disconnect();
+ break;
+ }
+ }
+ }
+}
+
+double JsonRpcConnection::GetWorkQueueRate()
+{
+ return l_TaskStats.UpdateAndGetValues(Utility::GetTime(), 60) / 60.0;
+}
diff --git a/lib/remote/jsonrpcconnection.hpp b/lib/remote/jsonrpcconnection.hpp
new file mode 100644
index 0000000..591ddcb
--- /dev/null
+++ b/lib/remote/jsonrpcconnection.hpp
@@ -0,0 +1,100 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef JSONRPCCONNECTION_H
+#define JSONRPCCONNECTION_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/endpoint.hpp"
+#include "base/io-engine.hpp"
+#include "base/tlsstream.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include <memory>
+#include <vector>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/io_context_strand.hpp>
+#include <boost/asio/spawn.hpp>
+
+namespace icinga
+{
+
+enum ClientRole
+{
+ ClientInbound,
+ ClientOutbound
+};
+
+enum ClientType
+{
+ ClientJsonRpc,
+ ClientHttp
+};
+
+class MessageOrigin;
+
+/**
+ * An API client connection.
+ *
+ * @ingroup remote
+ */
+class JsonRpcConnection final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(JsonRpcConnection);
+
+ JsonRpcConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, ConnectionRole role);
+
+ void Start();
+
+ double GetTimestamp() const;
+ String GetIdentity() const;
+ bool IsAuthenticated() const;
+ Endpoint::Ptr GetEndpoint() const;
+ Shared<AsioTlsStream>::Ptr GetStream() const;
+ ConnectionRole GetRole() const;
+
+ void Disconnect();
+
+ void SendMessage(const Dictionary::Ptr& request);
+ void SendRawMessage(const String& request);
+
+ static Value HeartbeatAPIHandler(const intrusive_ptr<MessageOrigin>& origin, const Dictionary::Ptr& params);
+
+ static double GetWorkQueueRate();
+
+ static void SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const intrusive_ptr<MessageOrigin>& origin, const String& path);
+
+private:
+ String m_Identity;
+ bool m_Authenticated;
+ Endpoint::Ptr m_Endpoint;
+ Shared<AsioTlsStream>::Ptr m_Stream;
+ ConnectionRole m_Role;
+ double m_Timestamp;
+ double m_Seen;
+ double m_NextHeartbeat;
+ boost::asio::io_context::strand m_IoStrand;
+ std::vector<String> m_OutgoingMessagesQueue;
+ AsioConditionVariable m_OutgoingMessagesQueued;
+ AsioConditionVariable m_WriterDone;
+ bool m_ShuttingDown;
+ boost::asio::deadline_timer m_CheckLivenessTimer, m_HeartbeatTimer;
+
+ JsonRpcConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, ConnectionRole role, boost::asio::io_context& io);
+
+ void HandleIncomingMessages(boost::asio::yield_context yc);
+ void WriteOutgoingMessages(boost::asio::yield_context yc);
+ void HandleAndWriteHeartbeats(boost::asio::yield_context yc);
+ void CheckLiveness(boost::asio::yield_context yc);
+
+ bool ProcessMessage();
+ void MessageHandler(const String& jsonString);
+
+ void CertificateRequestResponseHandler(const Dictionary::Ptr& message);
+
+ void SendMessageInternal(const Dictionary::Ptr& request);
+};
+
+}
+
+#endif /* JSONRPCCONNECTION_H */
diff --git a/lib/remote/messageorigin.cpp b/lib/remote/messageorigin.cpp
new file mode 100644
index 0000000..7de0ca7
--- /dev/null
+++ b/lib/remote/messageorigin.cpp
@@ -0,0 +1,10 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/messageorigin.hpp"
+
+using namespace icinga;
+
+bool MessageOrigin::IsLocal() const
+{
+ return !FromClient;
+}
diff --git a/lib/remote/messageorigin.hpp b/lib/remote/messageorigin.hpp
new file mode 100644
index 0000000..8a91ecc
--- /dev/null
+++ b/lib/remote/messageorigin.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MESSAGEORIGIN_H
+#define MESSAGEORIGIN_H
+
+#include "remote/zone.hpp"
+#include "remote/jsonrpcconnection.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup remote
+ */
+class MessageOrigin final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(MessageOrigin);
+
+ Zone::Ptr FromZone;
+ JsonRpcConnection::Ptr FromClient;
+
+ bool IsLocal() const;
+};
+
+}
+
+#endif /* MESSAGEORIGIN_H */
diff --git a/lib/remote/modifyobjecthandler.cpp b/lib/remote/modifyobjecthandler.cpp
new file mode 100644
index 0000000..d6fa98b
--- /dev/null
+++ b/lib/remote/modifyobjecthandler.cpp
@@ -0,0 +1,168 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/modifyobjecthandler.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/apiaction.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/objects", ModifyObjectHandler);
+
+bool ModifyObjectHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() != http::verb::post)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ QueryDescription qd;
+ qd.Types.insert(type->GetName());
+ qd.Permission = "objects/modify/" + type->GetName();
+
+ params->Set("type", type->GetName());
+
+ if (url->GetPath().size() >= 4) {
+ String attr = type->GetName();
+ boost::algorithm::to_lower(attr);
+ params->Set(attr, url->GetPath()[3]);
+ }
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ Value attrsVal = params->Get("attrs");
+
+ if (attrsVal.GetReflectionType() != Dictionary::TypeInstance && attrsVal.GetType() != ValueEmpty) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'attrs' attribute specified. Dictionary type is required."
+ "Or is this a POST query and you missed adding a 'X-HTTP-Method-Override: GET' header?");
+ return true;
+ }
+
+ Dictionary::Ptr attrs = attrsVal;
+
+ Value restoreAttrsVal = params->Get("restore_attrs");
+
+ if (restoreAttrsVal.GetReflectionType() != Array::TypeInstance && restoreAttrsVal.GetType() != ValueEmpty) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'restore_attrs' attribute specified. Array type is required.");
+ return true;
+ }
+
+ Array::Ptr restoreAttrs = restoreAttrsVal;
+
+ if (!(attrs || restoreAttrs)) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Missing both 'attrs' and 'restore_attrs'. "
+ "Or is this a POST query and you missed adding a 'X-HTTP-Method-Override: GET' header?");
+ return true;
+ }
+
+ bool verbose = false;
+
+ if (params)
+ verbose = HttpUtility::GetLastParameter(params, "verbose");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
+ return true;
+ }
+
+ ArrayData results;
+
+ for (const ConfigObject::Ptr& obj : objs) {
+ Dictionary::Ptr result1 = new Dictionary();
+
+ result1->Set("type", type->GetName());
+ result1->Set("name", obj->GetName());
+
+ String key;
+
+ try {
+ if (restoreAttrs) {
+ ObjectLock oLock (restoreAttrs);
+
+ for (auto& attr : restoreAttrs) {
+ key = attr;
+ obj->RestoreAttribute(key);
+ }
+ }
+ } catch (const std::exception& ex) {
+ result1->Set("code", 500);
+ result1->Set("status", "Attribute '" + key + "' could not be restored: " + DiagnosticInformation(ex, false));
+
+ if (verbose)
+ result1->Set("diagnostic_information", DiagnosticInformation(ex));
+
+ results.push_back(std::move(result1));
+ continue;
+ }
+
+ try {
+ if (attrs) {
+ ObjectLock olock(attrs);
+ for (const Dictionary::Pair& kv : attrs) {
+ key = kv.first;
+ obj->ModifyAttribute(kv.first, kv.second);
+ }
+ }
+ } catch (const std::exception& ex) {
+ result1->Set("code", 500);
+ result1->Set("status", "Attribute '" + key + "' could not be set: " + DiagnosticInformation(ex, false));
+
+ if (verbose)
+ result1->Set("diagnostic_information", DiagnosticInformation(ex));
+
+ results.push_back(std::move(result1));
+ continue;
+ }
+
+ result1->Set("code", 200);
+ result1->Set("status", "Attributes updated.");
+
+ results.push_back(std::move(result1));
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/modifyobjecthandler.hpp b/lib/remote/modifyobjecthandler.hpp
new file mode 100644
index 0000000..f469301
--- /dev/null
+++ b/lib/remote/modifyobjecthandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MODIFYOBJECTHANDLER_H
+#define MODIFYOBJECTHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ModifyObjectHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ModifyObjectHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* MODIFYOBJECTHANDLER_H */
diff --git a/lib/remote/objectqueryhandler.cpp b/lib/remote/objectqueryhandler.cpp
new file mode 100644
index 0000000..ad73030
--- /dev/null
+++ b/lib/remote/objectqueryhandler.cpp
@@ -0,0 +1,330 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/objectqueryhandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/serializer.hpp"
+#include "base/dependencygraph.hpp"
+#include "base/configtype.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <set>
+#include <unordered_map>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/objects", ObjectQueryHandler);
+
+Dictionary::Ptr ObjectQueryHandler::SerializeObjectAttrs(const Object::Ptr& object,
+ const String& attrPrefix, const Array::Ptr& attrs, bool isJoin, bool allAttrs)
+{
+ Type::Ptr type = object->GetReflectionType();
+
+ std::vector<int> fids;
+
+ if (isJoin && attrs) {
+ ObjectLock olock(attrs);
+ for (const String& attr : attrs) {
+ if (attr == attrPrefix) {
+ allAttrs = true;
+ break;
+ }
+ }
+ }
+
+ if (!isJoin && (!attrs || attrs->GetLength() == 0))
+ allAttrs = true;
+
+ if (allAttrs) {
+ for (int fid = 0; fid < type->GetFieldCount(); fid++) {
+ fids.push_back(fid);
+ }
+ } else if (attrs) {
+ ObjectLock olock(attrs);
+ for (const String& attr : attrs) {
+ String userAttr;
+
+ if (isJoin) {
+ String::SizeType dpos = attr.FindFirstOf(".");
+ if (dpos == String::NPos)
+ continue;
+
+ String userJoinAttr = attr.SubStr(0, dpos);
+ if (userJoinAttr != attrPrefix)
+ continue;
+
+ userAttr = attr.SubStr(dpos + 1);
+ } else
+ userAttr = attr;
+
+ int fid = type->GetFieldId(userAttr);
+
+ if (fid < 0)
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid field specified: " + userAttr));
+
+ fids.push_back(fid);
+ }
+ }
+
+ DictionaryData resultAttrs;
+ resultAttrs.reserve(fids.size());
+
+ for (int fid : fids) {
+ Field field = type->GetFieldInfo(fid);
+
+ Value val = object->GetField(fid);
+
+ /* hide attributes which shouldn't be user-visible */
+ if (field.Attributes & FANoUserView)
+ continue;
+
+ /* hide internal navigation fields */
+ if (field.Attributes & FANavigation && !(field.Attributes & (FAConfig | FAState)))
+ continue;
+
+ Value sval = Serialize(val, FAConfig | FAState);
+ resultAttrs.emplace_back(field.Name, sval);
+ }
+
+ return new Dictionary(std::move(resultAttrs));
+}
+
+bool ObjectQueryHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ QueryDescription qd;
+ qd.Types.insert(type->GetName());
+ qd.Permission = "objects/query/" + type->GetName();
+
+ Array::Ptr uattrs, ujoins, umetas;
+
+ try {
+ uattrs = params->Get("attrs");
+ } catch (const std::exception&) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'attrs' attribute specified. Array type is required.");
+ return true;
+ }
+
+ try {
+ ujoins = params->Get("joins");
+ } catch (const std::exception&) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'joins' attribute specified. Array type is required.");
+ return true;
+ }
+
+ try {
+ umetas = params->Get("meta");
+ } catch (const std::exception&) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'meta' attribute specified. Array type is required.");
+ return true;
+ }
+
+ bool allJoins = HttpUtility::GetLastParameter(params, "all_joins");
+
+ params->Set("type", type->GetName());
+
+ if (url->GetPath().size() >= 4) {
+ String attr = type->GetName();
+ boost::algorithm::to_lower(attr);
+ params->Set(attr, url->GetPath()[3]);
+ }
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ ArrayData results;
+ results.reserve(objs.size());
+
+ std::set<String> joinAttrs;
+ std::set<String> userJoinAttrs;
+
+ if (ujoins) {
+ ObjectLock olock(ujoins);
+ for (const String& ujoin : ujoins) {
+ userJoinAttrs.insert(ujoin.SubStr(0, ujoin.FindFirstOf(".")));
+ }
+ }
+
+ for (int fid = 0; fid < type->GetFieldCount(); fid++) {
+ Field field = type->GetFieldInfo(fid);
+
+ if (!(field.Attributes & FANavigation))
+ continue;
+
+ if (!allJoins && userJoinAttrs.find(field.NavigationName) == userJoinAttrs.end())
+ continue;
+
+ joinAttrs.insert(field.Name);
+ }
+
+ std::unordered_map<Type*, std::pair<bool, std::unique_ptr<Expression>>> typePermissions;
+ std::unordered_map<Object*, bool> objectAccessAllowed;
+
+ for (const ConfigObject::Ptr& obj : objs) {
+ DictionaryData result1{
+ { "name", obj->GetName() },
+ { "type", obj->GetReflectionType()->GetName() }
+ };
+
+ DictionaryData metaAttrs;
+
+ if (umetas) {
+ ObjectLock olock(umetas);
+ for (const String& meta : umetas) {
+ if (meta == "used_by") {
+ Array::Ptr used_by = new Array();
+ metaAttrs.emplace_back("used_by", used_by);
+
+ for (const Object::Ptr& pobj : DependencyGraph::GetParents((obj)))
+ {
+ ConfigObject::Ptr configObj = dynamic_pointer_cast<ConfigObject>(pobj);
+
+ if (!configObj)
+ continue;
+
+ used_by->Add(new Dictionary({
+ { "type", configObj->GetReflectionType()->GetName() },
+ { "name", configObj->GetName() }
+ }));
+ }
+ } else if (meta == "location") {
+ metaAttrs.emplace_back("location", obj->GetSourceLocation());
+ } else {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid field specified for meta: " + meta);
+ return true;
+ }
+ }
+ }
+
+ result1.emplace_back("meta", new Dictionary(std::move(metaAttrs)));
+
+ try {
+ result1.emplace_back("attrs", SerializeObjectAttrs(obj, String(), uattrs, false, false));
+ } catch (const ScriptError& ex) {
+ HttpUtility::SendJsonError(response, params, 400, ex.what());
+ return true;
+ }
+
+ DictionaryData joins;
+
+ for (const String& joinAttr : joinAttrs) {
+ Object::Ptr joinedObj;
+ int fid = type->GetFieldId(joinAttr);
+
+ if (fid < 0) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid field specified for join: " + joinAttr);
+ return true;
+ }
+
+ Field field = type->GetFieldInfo(fid);
+
+ if (!(field.Attributes & FANavigation)) {
+ HttpUtility::SendJsonError(response, params, 400, "Not a joinable field: " + joinAttr);
+ return true;
+ }
+
+ joinedObj = obj->NavigateField(fid);
+
+ if (!joinedObj)
+ continue;
+
+ Type::Ptr reflectionType = joinedObj->GetReflectionType();
+ auto it = typePermissions.find(reflectionType.get());
+ bool granted;
+
+ if (it == typePermissions.end()) {
+ String permission = "objects/query/" + reflectionType->GetName();
+
+ std::unique_ptr<Expression> permissionFilter;
+ granted = FilterUtility::HasPermission(user, permission, &permissionFilter);
+
+ it = typePermissions.insert({reflectionType.get(), std::make_pair(granted, std::move(permissionFilter))}).first;
+ }
+
+ granted = it->second.first;
+ const std::unique_ptr<Expression>& permissionFilter = it->second.second;
+
+ if (!granted) {
+ // Not authorized
+ continue;
+ }
+
+ auto relation = objectAccessAllowed.find(joinedObj.get());
+ bool accessAllowed;
+
+ if (relation == objectAccessAllowed.end()) {
+ ScriptFrame permissionFrame(false, new Namespace());
+
+ try {
+ accessAllowed = FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), joinedObj);
+ } catch (const ScriptError& err) {
+ accessAllowed = false;
+ }
+
+ objectAccessAllowed.insert({joinedObj.get(), accessAllowed});
+ } else {
+ accessAllowed = relation->second;
+ }
+
+ if (!accessAllowed) {
+ // Access denied
+ continue;
+ }
+
+ String prefix = field.NavigationName;
+
+ try {
+ joins.emplace_back(prefix, SerializeObjectAttrs(joinedObj, prefix, ujoins, true, allJoins));
+ } catch (const ScriptError& ex) {
+ HttpUtility::SendJsonError(response, params, 400, ex.what());
+ return true;
+ }
+ }
+
+ result1.emplace_back("joins", new Dictionary(std::move(joins)));
+
+ results.push_back(new Dictionary(std::move(result1)));
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/objectqueryhandler.hpp b/lib/remote/objectqueryhandler.hpp
new file mode 100644
index 0000000..691b2cf
--- /dev/null
+++ b/lib/remote/objectqueryhandler.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTQUERYHANDLER_H
+#define OBJECTQUERYHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ObjectQueryHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ObjectQueryHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+
+private:
+ static Dictionary::Ptr SerializeObjectAttrs(const Object::Ptr& object, const String& attrPrefix,
+ const Array::Ptr& attrs, bool isJoin, bool allAttrs);
+};
+
+}
+
+#endif /* OBJECTQUERYHANDLER_H */
diff --git a/lib/remote/pkiutility.cpp b/lib/remote/pkiutility.cpp
new file mode 100644
index 0000000..00d6525
--- /dev/null
+++ b/lib/remote/pkiutility.cpp
@@ -0,0 +1,452 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/pkiutility.hpp"
+#include "remote/apilistener.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/tlsutility.hpp"
+#include "base/console.hpp"
+#include "base/tlsstream.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/json.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include "remote/jsonrpc.hpp"
+#include <fstream>
+#include <iostream>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/filesystem/path.hpp>
+
+using namespace icinga;
+
+int PkiUtility::NewCa()
+{
+ String caDir = ApiListener::GetCaDir();
+ String caCertFile = caDir + "/ca.crt";
+ String caKeyFile = caDir + "/ca.key";
+
+ if (Utility::PathExists(caCertFile) && Utility::PathExists(caKeyFile)) {
+ Log(LogWarning, "cli")
+ << "CA files '" << caCertFile << "' and '" << caKeyFile << "' already exist.";
+ return 1;
+ }
+
+ Utility::MkDirP(caDir, 0700);
+
+ MakeX509CSR("Icinga CA", caKeyFile, String(), caCertFile, true);
+
+ return 0;
+}
+
+int PkiUtility::NewCert(const String& cn, const String& keyfile, const String& csrfile, const String& certfile)
+{
+ try {
+ MakeX509CSR(cn, keyfile, csrfile, certfile);
+ } catch(std::exception&) {
+ return 1;
+ }
+
+ return 0;
+}
+
+int PkiUtility::SignCsr(const String& csrfile, const String& certfile)
+{
+ char errbuf[256];
+
+ InitializeOpenSSL();
+
+ BIO *csrbio = BIO_new_file(csrfile.CStr(), "r");
+ X509_REQ *req = PEM_read_bio_X509_REQ(csrbio, nullptr, nullptr, nullptr);
+
+ if (!req) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Could not read X509 certificate request from '" << csrfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ return 1;
+ }
+
+ BIO_free(csrbio);
+
+ std::shared_ptr<EVP_PKEY> pubkey = std::shared_ptr<EVP_PKEY>(X509_REQ_get_pubkey(req), EVP_PKEY_free);
+ std::shared_ptr<X509> cert = CreateCertIcingaCA(pubkey.get(), X509_REQ_get_subject_name(req));
+
+ X509_REQ_free(req);
+
+ WriteCert(cert, certfile);
+
+ return 0;
+}
+
+std::shared_ptr<X509> PkiUtility::FetchCert(const String& host, const String& port)
+{
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "pki")
+ << "Cannot make SSL context.";
+ Log(LogDebug, "pki")
+ << "Cannot make SSL context:\n" << DiagnosticInformation(ex);
+ return std::shared_ptr<X509>();
+ }
+
+ auto stream (Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, host));
+
+ try {
+ Connect(stream->lowest_layer(), host, port);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "pki")
+ << "Cannot connect to host '" << host << "' on port '" << port << "'";
+ Log(LogDebug, "pki")
+ << "Cannot connect to host '" << host << "' on port '" << port << "':\n" << DiagnosticInformation(ex);
+ return std::shared_ptr<X509>();
+ }
+
+ auto& sslConn (stream->next_layer());
+
+ try {
+ sslConn.handshake(sslConn.client);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "pki")
+ << "Client TLS handshake failed. (" << ex.what() << ")";
+ return std::shared_ptr<X509>();
+ }
+
+ Defer shutdown ([&sslConn]() { sslConn.shutdown(); });
+
+ return sslConn.GetPeerCertificate();
+}
+
+int PkiUtility::WriteCert(const std::shared_ptr<X509>& cert, const String& trustedfile)
+{
+ std::ofstream fpcert;
+ fpcert.open(trustedfile.CStr());
+ fpcert << CertificateToString(cert);
+ fpcert.close();
+
+ if (fpcert.fail()) {
+ Log(LogCritical, "pki")
+ << "Could not write certificate to file '" << trustedfile << "'.";
+ return 1;
+ }
+
+ Log(LogInformation, "pki")
+ << "Writing certificate to file '" << trustedfile << "'.";
+
+ return 0;
+}
+
+int PkiUtility::GenTicket(const String& cn, const String& salt, std::ostream& ticketfp)
+{
+ ticketfp << PBKDF2_SHA1(cn, salt, 50000) << "\n";
+
+ return 0;
+}
+
+int PkiUtility::RequestCertificate(const String& host, const String& port, const String& keyfile,
+ const String& certfile, const String& cafile, const std::shared_ptr<X509>& trustedCert, const String& ticket)
+{
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext(certfile, keyfile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot make SSL context for cert path: '" << certfile << "' key path: '" << keyfile << "' ca path: '" << cafile << "'.";
+ Log(LogDebug, "cli")
+ << "Cannot make SSL context for cert path: '" << certfile << "' key path: '" << keyfile << "' ca path: '" << cafile << "':\n" << DiagnosticInformation(ex);
+ return 1;
+ }
+
+ auto stream (Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, host));
+
+ try {
+ Connect(stream->lowest_layer(), host, port);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot connect to host '" << host << "' on port '" << port << "'";
+ Log(LogDebug, "cli")
+ << "Cannot connect to host '" << host << "' on port '" << port << "':\n" << DiagnosticInformation(ex);
+ return 1;
+ }
+
+ auto& sslConn (stream->next_layer());
+
+ try {
+ sslConn.handshake(sslConn.client);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Client TLS handshake failed: " << DiagnosticInformation(ex, false);
+ return 1;
+ }
+
+ Defer shutdown ([&sslConn]() { sslConn.shutdown(); });
+
+ auto peerCert (sslConn.GetPeerCertificate());
+
+ if (X509_cmp(peerCert.get(), trustedCert.get())) {
+ Log(LogCritical, "cli", "Peer certificate does not match trusted certificate.");
+ return 1;
+ }
+
+ Dictionary::Ptr params = new Dictionary({
+ { "ticket", String(ticket) }
+ });
+
+ String msgid = Utility::NewUniqueID();
+
+ Dictionary::Ptr request = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "id", msgid },
+ { "method", "pki::RequestCertificate" },
+ { "params", params }
+ });
+
+ Dictionary::Ptr response;
+
+ try {
+ JsonRpc::SendMessage(stream, request);
+ stream->flush();
+
+ for (;;) {
+ response = JsonRpc::DecodeMessage(JsonRpc::ReadMessage(stream));
+
+ if (response && response->Contains("error")) {
+ Log(LogCritical, "cli", "Could not fetch valid response. Please check the master log (notice or debug).");
+#ifdef I2_DEBUG
+ /* we shouldn't expose master errors to the user in production environments */
+ Log(LogCritical, "cli", response->Get("error"));
+#endif /* I2_DEBUG */
+ return 1;
+ }
+
+ if (response && (response->Get("id") != msgid))
+ continue;
+
+ break;
+ }
+ } catch (...) {
+ Log(LogCritical, "cli", "Could not fetch valid response. Please check the master log.");
+ return 1;
+ }
+
+ if (!response) {
+ Log(LogCritical, "cli", "Could not fetch valid response. Please check the master log.");
+ return 1;
+ }
+
+ Dictionary::Ptr result = response->Get("result");
+
+ if (result->Contains("ca")) {
+ try {
+ StringToCertificate(result->Get("ca"));
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Could not write CA file: " << DiagnosticInformation(ex, false);
+ return 1;
+ }
+
+ Log(LogInformation, "cli")
+ << "Writing CA certificate to file '" << cafile << "'.";
+
+ std::ofstream fpca;
+ fpca.open(cafile.CStr());
+ fpca << result->Get("ca");
+ fpca.close();
+
+ if (fpca.fail()) {
+ Log(LogCritical, "cli")
+ << "Could not open CA certificate file '" << cafile << "' for writing.";
+ return 1;
+ }
+ }
+
+ if (result->Contains("error")) {
+ LogSeverity severity;
+
+ Value vstatus;
+
+ if (!result->Get("status_code", &vstatus))
+ vstatus = 1;
+
+ int status = vstatus;
+
+ if (status == 1)
+ severity = LogCritical;
+ else {
+ severity = LogInformation;
+ Log(severity, "cli", "!!!!!!");
+ }
+
+ Log(severity, "cli")
+ << "!!! " << result->Get("error");
+
+ if (status == 1)
+ return 1;
+ else {
+ Log(severity, "cli", "!!!!!!");
+ return 0;
+ }
+ }
+
+ try {
+ StringToCertificate(result->Get("cert"));
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Could not write certificate file: " << DiagnosticInformation(ex, false);
+ return 1;
+ }
+
+ Log(LogInformation, "cli")
+ << "Writing signed certificate to file '" << certfile << "'.";
+
+ std::ofstream fpcert;
+ fpcert.open(certfile.CStr());
+ fpcert << result->Get("cert");
+ fpcert.close();
+
+ if (fpcert.fail()) {
+ Log(LogCritical, "cli")
+ << "Could not write certificate to file '" << certfile << "'.";
+ return 1;
+ }
+
+ return 0;
+}
+
+String PkiUtility::GetCertificateInformation(const std::shared_ptr<X509>& cert) {
+ BIO *out = BIO_new(BIO_s_mem());
+ String pre;
+
+ pre = "\n Version: " + Convert::ToString(GetCertificateVersion(cert));
+ BIO_write(out, pre.CStr(), pre.GetLength());
+
+ pre = "\n Subject: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ X509_NAME_print_ex(out, X509_get_subject_name(cert.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
+
+ pre = "\n Issuer: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ X509_NAME_print_ex(out, X509_get_issuer_name(cert.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
+
+ pre = "\n Valid From: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ ASN1_TIME_print(out, X509_get_notBefore(cert.get()));
+
+ pre = "\n Valid Until: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ ASN1_TIME_print(out, X509_get_notAfter(cert.get()));
+
+ pre = "\n Serial: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ ASN1_INTEGER *asn1_serial = X509_get_serialNumber(cert.get());
+ for (int i = 0; i < asn1_serial->length; i++) {
+ BIO_printf(out, "%02x%c", asn1_serial->data[i], ((i + 1 == asn1_serial->length) ? '\n' : ':'));
+ }
+
+ pre = "\n Signature Algorithm: " + GetSignatureAlgorithm(cert);
+ BIO_write(out, pre.CStr(), pre.GetLength());
+
+ pre = "\n Subject Alt Names: " + GetSubjectAltNames(cert)->Join(" ");
+ BIO_write(out, pre.CStr(), pre.GetLength());
+
+ pre = "\n Fingerprint: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ unsigned char md[EVP_MAX_MD_SIZE];
+ unsigned int diglen;
+ X509_digest(cert.get(), EVP_sha256(), md, &diglen);
+
+ char *data;
+ long length = BIO_get_mem_data(out, &data);
+
+ std::stringstream info;
+ info << String(data, data + length);
+
+ BIO_free(out);
+
+ for (unsigned int i = 0; i < diglen; i++) {
+ info << std::setfill('0') << std::setw(2) << std::uppercase
+ << std::hex << static_cast<int>(md[i]) << ' ';
+ }
+ info << '\n';
+
+ return info.str();
+}
+
+static void CollectRequestHandler(const Dictionary::Ptr& requests, const String& requestFile)
+{
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestFile);
+
+ if (!request)
+ return;
+
+ Dictionary::Ptr result = new Dictionary();
+
+ namespace fs = boost::filesystem;
+ fs::path file(requestFile.Begin(), requestFile.End());
+ String fingerprint = file.stem().string();
+
+ String certRequestText = request->Get("cert_request");
+ result->Set("cert_request", certRequestText);
+
+ Value vcertResponseText;
+
+ if (request->Get("cert_response", &vcertResponseText)) {
+ String certResponseText = vcertResponseText;
+ result->Set("cert_response", certResponseText);
+ }
+
+ std::shared_ptr<X509> certRequest = StringToCertificate(certRequestText);
+
+/* XXX (requires OpenSSL >= 1.0.0)
+ time_t now;
+ time(&now);
+ ASN1_TIME *tm = ASN1_TIME_adj(nullptr, now, 0, 0);
+
+ int day, sec;
+ ASN1_TIME_diff(&day, &sec, tm, X509_get_notBefore(certRequest.get()));
+
+ result->Set("timestamp", static_cast<double>(now) + day * 24 * 60 * 60 + sec); */
+
+ BIO *out = BIO_new(BIO_s_mem());
+ ASN1_TIME_print(out, X509_get_notBefore(certRequest.get()));
+
+ char *data;
+ long length;
+ length = BIO_get_mem_data(out, &data);
+
+ result->Set("timestamp", String(data, data + length));
+ BIO_free(out);
+
+ out = BIO_new(BIO_s_mem());
+ X509_NAME_print_ex(out, X509_get_subject_name(certRequest.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
+
+ length = BIO_get_mem_data(out, &data);
+
+ result->Set("subject", String(data, data + length));
+ BIO_free(out);
+
+ requests->Set(fingerprint, result);
+}
+
+Dictionary::Ptr PkiUtility::GetCertificateRequests(bool removed)
+{
+ Dictionary::Ptr requests = new Dictionary();
+
+ String requestDir = ApiListener::GetCertificateRequestsDir();
+ String ext = "json";
+
+ if (removed)
+ ext = "removed";
+
+ if (Utility::PathExists(requestDir))
+ Utility::Glob(requestDir + "/*." + ext, [requests](const String& requestFile) { CollectRequestHandler(requests, requestFile); }, GlobFile);
+
+ return requests;
+}
+
diff --git a/lib/remote/pkiutility.hpp b/lib/remote/pkiutility.hpp
new file mode 100644
index 0000000..50d47e0
--- /dev/null
+++ b/lib/remote/pkiutility.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKIUTILITY_H
+#define PKIUTILITY_H
+
+#include "remote/i2-remote.hpp"
+#include "base/exception.hpp"
+#include "base/dictionary.hpp"
+#include "base/string.hpp"
+#include <openssl/x509v3.h>
+#include <memory>
+
+namespace icinga
+{
+
+/**
+ * @ingroup remote
+ */
+class PkiUtility
+{
+public:
+ static int NewCa();
+ static int NewCert(const String& cn, const String& keyfile, const String& csrfile, const String& certfile);
+ static int SignCsr(const String& csrfile, const String& certfile);
+ static std::shared_ptr<X509> FetchCert(const String& host, const String& port);
+ static int WriteCert(const std::shared_ptr<X509>& cert, const String& trustedfile);
+ static int GenTicket(const String& cn, const String& salt, std::ostream& ticketfp);
+ static int RequestCertificate(const String& host, const String& port, const String& keyfile,
+ const String& certfile, const String& cafile, const std::shared_ptr<X509>& trustedcert,
+ const String& ticket = String());
+ static String GetCertificateInformation(const std::shared_ptr<X509>& certificate);
+ static Dictionary::Ptr GetCertificateRequests(bool removed = false);
+
+private:
+ PkiUtility();
+
+};
+
+}
+
+#endif /* PKIUTILITY_H */
diff --git a/lib/remote/statushandler.cpp b/lib/remote/statushandler.cpp
new file mode 100644
index 0000000..1f3f618
--- /dev/null
+++ b/lib/remote/statushandler.cpp
@@ -0,0 +1,120 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/statushandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/serializer.hpp"
+#include "base/statsfunction.hpp"
+#include "base/namespace.hpp"
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/status", StatusHandler);
+
+class StatusTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(StatusTargetProvider);
+
+ void FindTargets(const String& type,
+ const std::function<void (const Value&)>& addTarget) const override
+ {
+ Namespace::Ptr statsFunctions = ScriptGlobal::Get("StatsFunctions", &Empty);
+
+ if (statsFunctions) {
+ ObjectLock olock(statsFunctions);
+
+ for (const Namespace::Pair& kv : statsFunctions)
+ addTarget(GetTargetByName("Status", kv.first));
+ }
+ }
+
+ Value GetTargetByName(const String& type, const String& name) const override
+ {
+ Namespace::Ptr statsFunctions = ScriptGlobal::Get("StatsFunctions", &Empty);
+
+ if (!statsFunctions)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("No status functions are available."));
+
+ Value vfunc;
+
+ if (!statsFunctions->Get(name, &vfunc))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid status function name."));
+
+ Function::Ptr func = vfunc;
+
+ if (!func)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid status function name."));
+
+ Dictionary::Ptr status = new Dictionary();
+ Array::Ptr perfdata = new Array();
+ func->Invoke({ status, perfdata });
+
+ return new Dictionary({
+ { "name", name },
+ { "status", status },
+ { "perfdata", Serialize(perfdata, FAState) }
+ });
+ }
+
+ bool IsValidType(const String& type) const override
+ {
+ return type == "Status";
+ }
+
+ String GetPluralName(const String& type) const override
+ {
+ return "statuses";
+ }
+};
+
+bool StatusHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 3)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ QueryDescription qd;
+ qd.Types.insert("Status");
+ qd.Provider = new StatusTargetProvider();
+ qd.Permission = "status/query";
+
+ params->Set("type", "Status");
+
+ if (url->GetPath().size() >= 3)
+ params->Set("status", url->GetPath()[2]);
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(objs)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
diff --git a/lib/remote/statushandler.hpp b/lib/remote/statushandler.hpp
new file mode 100644
index 0000000..c722ab3
--- /dev/null
+++ b/lib/remote/statushandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STATUSHANDLER_H
+#define STATUSHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class StatusHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(StatusHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* STATUSHANDLER_H */
diff --git a/lib/remote/templatequeryhandler.cpp b/lib/remote/templatequeryhandler.cpp
new file mode 100644
index 0000000..e70dafb
--- /dev/null
+++ b/lib/remote/templatequeryhandler.cpp
@@ -0,0 +1,136 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/templatequeryhandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "config/configitem.hpp"
+#include "base/configtype.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/logger.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/templates", TemplateQueryHandler);
+
+class TemplateTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TemplateTargetProvider);
+
+ static Dictionary::Ptr GetTargetForTemplate(const ConfigItem::Ptr& item)
+ {
+ DebugInfo di = item->GetDebugInfo();
+
+ return new Dictionary({
+ { "name", item->GetName() },
+ { "type", item->GetType()->GetName() },
+ { "location", new Dictionary({
+ { "path", di.Path },
+ { "first_line", di.FirstLine },
+ { "first_column", di.FirstColumn },
+ { "last_line", di.LastLine },
+ { "last_column", di.LastColumn }
+ }) }
+ });
+ }
+
+ void FindTargets(const String& type,
+ const std::function<void (const Value&)>& addTarget) const override
+ {
+ Type::Ptr ptype = Type::GetByName(type);
+
+ for (const ConfigItem::Ptr& item : ConfigItem::GetItems(ptype)) {
+ if (item->IsAbstract())
+ addTarget(GetTargetForTemplate(item));
+ }
+ }
+
+ Value GetTargetByName(const String& type, const String& name) const override
+ {
+ Type::Ptr ptype = Type::GetByName(type);
+
+ ConfigItem::Ptr item = ConfigItem::GetByTypeAndName(ptype, name);
+
+ if (!item || !item->IsAbstract())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Template does not exist."));
+
+ return GetTargetForTemplate(item);
+ }
+
+ bool IsValidType(const String& type) const override
+ {
+ Type::Ptr ptype = Type::GetByName(type);
+
+ if (!ptype)
+ return false;
+
+ return ConfigObject::TypeInstance->IsAssignableFrom(ptype);
+ }
+
+ String GetPluralName(const String& type) const override
+ {
+ return Type::GetByName(type)->GetPluralName();
+ }
+};
+
+bool TemplateQueryHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ QueryDescription qd;
+ qd.Types.insert(type->GetName());
+ qd.Permission = "templates/query/" + type->GetName();
+ qd.Provider = new TemplateTargetProvider();
+
+ params->Set("type", type->GetName());
+
+ if (url->GetPath().size() >= 4) {
+ String attr = type->GetName();
+ boost::algorithm::to_lower(attr);
+ params->Set(attr, url->GetPath()[3]);
+ }
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user, "tmpl");
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No templates found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(objs)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/templatequeryhandler.hpp b/lib/remote/templatequeryhandler.hpp
new file mode 100644
index 0000000..503bc85
--- /dev/null
+++ b/lib/remote/templatequeryhandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TEMPLATEQUERYHANDLER_H
+#define TEMPLATEQUERYHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class TemplateQueryHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TemplateQueryHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* TEMPLATEQUERYHANDLER_H */
diff --git a/lib/remote/typequeryhandler.cpp b/lib/remote/typequeryhandler.cpp
new file mode 100644
index 0000000..4e82653
--- /dev/null
+++ b/lib/remote/typequeryhandler.cpp
@@ -0,0 +1,156 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/typequeryhandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/configtype.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/logger.hpp"
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/types", TypeQueryHandler);
+
+class TypeTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TypeTargetProvider);
+
+ void FindTargets(const String& type,
+ const std::function<void (const Value&)>& addTarget) const override
+ {
+ for (const Type::Ptr& target : Type::GetAllTypes()) {
+ addTarget(target);
+ }
+ }
+
+ Value GetTargetByName(const String& type, const String& name) const override
+ {
+ Type::Ptr ptype = Type::GetByName(name);
+
+ if (!ptype)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Type does not exist."));
+
+ return ptype;
+ }
+
+ bool IsValidType(const String& type) const override
+ {
+ return type == "Type";
+ }
+
+ String GetPluralName(const String& type) const override
+ {
+ return "types";
+ }
+};
+
+bool TypeQueryHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 3)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ QueryDescription qd;
+ qd.Types.insert("Type");
+ qd.Permission = "types";
+ qd.Provider = new TypeTargetProvider();
+
+ if (params->Contains("type"))
+ params->Set("name", params->Get("type"));
+
+ params->Set("type", "Type");
+
+ if (url->GetPath().size() >= 3)
+ params->Set("name", url->GetPath()[2]);
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ ArrayData results;
+
+ for (const Type::Ptr& obj : objs) {
+ Dictionary::Ptr result1 = new Dictionary();
+ results.push_back(result1);
+
+ Dictionary::Ptr resultAttrs = new Dictionary();
+ result1->Set("name", obj->GetName());
+ result1->Set("plural_name", obj->GetPluralName());
+ if (obj->GetBaseType())
+ result1->Set("base", obj->GetBaseType()->GetName());
+ result1->Set("abstract", obj->IsAbstract());
+ result1->Set("fields", resultAttrs);
+
+ Dictionary::Ptr prototype = dynamic_pointer_cast<Dictionary>(obj->GetPrototype());
+ Array::Ptr prototypeKeys = new Array();
+ result1->Set("prototype_keys", prototypeKeys);
+
+ if (prototype) {
+ ObjectLock olock(prototype);
+ for (const Dictionary::Pair& kv : prototype) {
+ prototypeKeys->Add(kv.first);
+ }
+ }
+
+ int baseFieldCount = 0;
+
+ if (obj->GetBaseType())
+ baseFieldCount = obj->GetBaseType()->GetFieldCount();
+
+ for (int fid = baseFieldCount; fid < obj->GetFieldCount(); fid++) {
+ Field field = obj->GetFieldInfo(fid);
+
+ Dictionary::Ptr fieldInfo = new Dictionary();
+ resultAttrs->Set(field.Name, fieldInfo);
+
+ fieldInfo->Set("id", fid);
+ fieldInfo->Set("type", field.TypeName);
+ if (field.RefTypeName)
+ fieldInfo->Set("ref_type", field.RefTypeName);
+ if (field.Attributes & FANavigation)
+ fieldInfo->Set("navigation_name", field.NavigationName);
+ fieldInfo->Set("array_rank", field.ArrayRank);
+
+ fieldInfo->Set("attributes", new Dictionary({
+ { "config", static_cast<bool>(field.Attributes & FAConfig) },
+ { "state", static_cast<bool>(field.Attributes & FAState) },
+ { "required", static_cast<bool>(field.Attributes & FARequired) },
+ { "navigation", static_cast<bool>(field.Attributes & FANavigation) },
+ { "no_user_modify", static_cast<bool>(field.Attributes & FANoUserModify) },
+ { "no_user_view", static_cast<bool>(field.Attributes & FANoUserView) },
+ { "deprecated", static_cast<bool>(field.Attributes & FADeprecated) }
+ }));
+ }
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/typequeryhandler.hpp b/lib/remote/typequeryhandler.hpp
new file mode 100644
index 0000000..5489cb2
--- /dev/null
+++ b/lib/remote/typequeryhandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TYPEQUERYHANDLER_H
+#define TYPEQUERYHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class TypeQueryHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TypeQueryHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* TYPEQUERYHANDLER_H */
diff --git a/lib/remote/url-characters.hpp b/lib/remote/url-characters.hpp
new file mode 100644
index 0000000..3cc4921
--- /dev/null
+++ b/lib/remote/url-characters.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef URL_CHARACTERS_H
+#define URL_CHARACTERS_H
+
+#define ALPHA "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+#define NUMERIC "0123456789"
+
+#define UNRESERVED ALPHA NUMERIC "-._~" "%"
+#define GEN_DELIMS ":/?#[]@"
+#define SUB_DELIMS "!$&'()*+,;="
+#define PCHAR UNRESERVED SUB_DELIMS ":@"
+#define PCHAR_ENCODE UNRESERVED ":@"
+
+#define ACSCHEME ALPHA NUMERIC ".-+"
+
+//authority = [ userinfo "@" ] host [ ":" port ]
+#define ACUSERINFO UNRESERVED SUB_DELIMS
+#define ACHOST UNRESERVED SUB_DELIMS
+#define ACPORT NUMERIC
+
+#define ACPATHSEGMENT PCHAR
+#define ACPATHSEGMENT_ENCODE PCHAR_ENCODE
+#define ACQUERY PCHAR "/?"
+#define ACQUERY_ENCODE PCHAR_ENCODE "/?"
+#define ACFRAGMENT PCHAR "/?"
+#define ACFRAGMENT_ENCODE PCHAR_ENCODE "/?"
+
+#endif /* URL_CHARACTERS_H */
diff --git a/lib/remote/url.cpp b/lib/remote/url.cpp
new file mode 100644
index 0000000..e87628e
--- /dev/null
+++ b/lib/remote/url.cpp
@@ -0,0 +1,363 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/array.hpp"
+#include "base/utility.hpp"
+#include "base/objectlock.hpp"
+#include "remote/url.hpp"
+#include "remote/url-characters.hpp"
+#include <boost/tokenizer.hpp>
+
+using namespace icinga;
+
+Url::Url(const String& base_url)
+{
+ String url = base_url;
+
+ if (url.GetLength() == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Empty URL."));
+
+ size_t pHelper = String::NPos;
+ if (url[0] != '/')
+ pHelper = url.Find(":");
+
+ if (pHelper != String::NPos) {
+ if (!ParseScheme(url.SubStr(0, pHelper)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Scheme."));
+ url = url.SubStr(pHelper + 1);
+ }
+
+ if (*url.Begin() != '/')
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL: '/' expected after scheme."));
+
+ if (url.GetLength() == 1) {
+ return;
+ }
+
+ if (*(url.Begin() + 1) == '/') {
+ pHelper = url.Find("/", 2);
+
+ if (pHelper == String::NPos)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL: Missing '/' after authority."));
+
+ if (!ParseAuthority(url.SubStr(0, pHelper)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Authority"));
+
+ url = url.SubStr(pHelper);
+ }
+
+ if (*url.Begin() == '/') {
+ pHelper = url.FindFirstOf("#?");
+ if (!ParsePath(url.SubStr(1, pHelper - 1)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Path"));
+
+ if (pHelper != String::NPos)
+ url = url.SubStr(pHelper);
+ } else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL: Missing path."));
+
+ if (*url.Begin() == '?') {
+ pHelper = url.Find("#");
+ if (!ParseQuery(url.SubStr(1, pHelper - 1)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Query"));
+
+ if (pHelper != String::NPos)
+ url = url.SubStr(pHelper);
+ }
+
+ if (*url.Begin() == '#') {
+ if (!ParseFragment(url.SubStr(1)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Fragment"));
+ }
+}
+
+String Url::GetScheme() const
+{
+ return m_Scheme;
+}
+
+String Url::GetAuthority() const
+{
+ if (m_Host.IsEmpty())
+ return "";
+
+ String auth;
+ if (!m_Username.IsEmpty()) {
+ auth = m_Username;
+ if (!m_Password.IsEmpty())
+ auth += ":" + m_Password;
+ auth += "@";
+ }
+
+ auth += m_Host;
+
+ if (!m_Port.IsEmpty())
+ auth += ":" + m_Port;
+
+ return auth;
+}
+
+String Url::GetUsername() const
+{
+ return m_Username;
+}
+
+String Url::GetPassword() const
+{
+ return m_Password;
+}
+
+String Url::GetHost() const
+{
+ return m_Host;
+}
+
+String Url::GetPort() const
+{
+ return m_Port;
+}
+
+const std::vector<String>& Url::GetPath() const
+{
+ return m_Path;
+}
+
+const std::vector<std::pair<String, String>>& Url::GetQuery() const
+{
+ return m_Query;
+}
+
+String Url::GetFragment() const
+{
+ return m_Fragment;
+}
+
+void Url::SetScheme(const String& scheme)
+{
+ m_Scheme = scheme;
+}
+
+void Url::SetUsername(const String& username)
+{
+ m_Username = username;
+}
+
+void Url::SetPassword(const String& password)
+{
+ m_Password = password;
+}
+
+void Url::SetHost(const String& host)
+{
+ m_Host = host;
+}
+
+void Url::SetPort(const String& port)
+{
+ m_Port = port;
+}
+
+void Url::SetPath(const std::vector<String>& path)
+{
+ m_Path = path;
+}
+
+void Url::SetQuery(const std::vector<std::pair<String, String>>& query)
+{
+ m_Query = query;
+}
+
+void Url::SetArrayFormatUseBrackets(bool useBrackets)
+{
+ m_ArrayFormatUseBrackets = useBrackets;
+}
+
+void Url::AddQueryElement(const String& name, const String& value)
+{
+ m_Query.emplace_back(name, value);
+}
+
+void Url::SetFragment(const String& fragment) {
+ m_Fragment = fragment;
+}
+
+String Url::Format(bool onlyPathAndQuery, bool printCredentials) const
+{
+ String url;
+
+ if (!onlyPathAndQuery) {
+ if (!m_Scheme.IsEmpty())
+ url += m_Scheme + ":";
+
+ if (printCredentials && !GetAuthority().IsEmpty())
+ url += "//" + GetAuthority();
+ else if (!GetHost().IsEmpty())
+ url += "//" + GetHost() + (!GetPort().IsEmpty() ? ":" + GetPort() : "");
+ }
+
+ if (m_Path.empty())
+ url += "/";
+ else {
+ for (const String& segment : m_Path) {
+ url += "/";
+ url += Utility::EscapeString(segment, ACPATHSEGMENT_ENCODE, false);
+ }
+ }
+
+ String param;
+ if (!m_Query.empty()) {
+ typedef std::pair<String, std::vector<String> > kv_pair;
+
+ for (const auto& kv : m_Query) {
+ String key = Utility::EscapeString(kv.first, ACQUERY_ENCODE, false);
+ if (param.IsEmpty())
+ param = "?";
+ else
+ param += "&";
+
+ param += key;
+ param += kv.second.IsEmpty() ?
+ String() : "=" + Utility::EscapeString(kv.second, ACQUERY_ENCODE, false);
+ }
+ }
+
+ url += param;
+
+ if (!m_Fragment.IsEmpty())
+ url += "#" + Utility::EscapeString(m_Fragment, ACFRAGMENT_ENCODE, false);
+
+ return url;
+}
+
+bool Url::ParseScheme(const String& scheme)
+{
+ m_Scheme = scheme;
+
+ if (scheme.FindFirstOf(ALPHA) != 0)
+ return false;
+
+ return (ValidateToken(scheme, ACSCHEME));
+}
+
+bool Url::ParseAuthority(const String& authority)
+{
+ String auth = authority.SubStr(2);
+ size_t pos = auth.Find("@");
+ if (pos != String::NPos && pos != 0) {
+ if (!Url::ParseUserinfo(auth.SubStr(0, pos)))
+ return false;
+ auth = auth.SubStr(pos+1);
+ }
+
+ pos = auth.Find(":");
+ if (pos != String::NPos) {
+ if (pos == 0 || pos == auth.GetLength() - 1 || !Url::ParsePort(auth.SubStr(pos+1)))
+ return false;
+ }
+
+ m_Host = auth.SubStr(0, pos);
+ return ValidateToken(m_Host, ACHOST);
+}
+
+bool Url::ParseUserinfo(const String& userinfo)
+{
+ size_t pos = userinfo.Find(":");
+ m_Username = userinfo.SubStr(0, pos);
+ if (!ValidateToken(m_Username, ACUSERINFO))
+ return false;
+ m_Username = Utility::UnescapeString(m_Username);
+ if (pos != String::NPos && pos != userinfo.GetLength() - 1) {
+ m_Password = userinfo.SubStr(pos+1);
+ if (!ValidateToken(m_Username, ACUSERINFO))
+ return false;
+ m_Password = Utility::UnescapeString(m_Password);
+ } else
+ m_Password = "";
+
+ return true;
+}
+
+bool Url::ParsePort(const String& port)
+{
+ m_Port = Utility::UnescapeString(port);
+ if (!ValidateToken(m_Port, ACPORT))
+ return false;
+ return true;
+}
+
+bool Url::ParsePath(const String& path)
+{
+ const std::string& pathStr = path;
+ boost::char_separator<char> sep("/");
+ boost::tokenizer<boost::char_separator<char> > tokens(pathStr, sep);
+
+ for (const String& token : tokens) {
+ if (token.IsEmpty())
+ continue;
+
+ if (!ValidateToken(token, ACPATHSEGMENT))
+ return false;
+
+ m_Path.emplace_back(Utility::UnescapeString(token));
+ }
+
+ return true;
+}
+
+bool Url::ParseQuery(const String& query)
+{
+ /* Tokenizer does not like String AT ALL */
+ const std::string& queryStr = query;
+ boost::char_separator<char> sep("&");
+ boost::tokenizer<boost::char_separator<char> > tokens(queryStr, sep);
+
+ for (const String& token : tokens) {
+ size_t pHelper = token.Find("=");
+
+ if (pHelper == 0)
+ // /?foo=bar&=bar == invalid
+ return false;
+
+ String key = token.SubStr(0, pHelper);
+ String value = Empty;
+
+ if (pHelper != String::NPos && pHelper != token.GetLength() - 1)
+ value = token.SubStr(pHelper+1);
+
+ if (!ValidateToken(value, ACQUERY))
+ return false;
+
+ value = Utility::UnescapeString(value);
+
+ pHelper = key.Find("[]");
+
+ if (pHelper == 0 || (pHelper != String::NPos && pHelper != key.GetLength()-2))
+ return false;
+
+ key = key.SubStr(0, pHelper);
+
+ if (!ValidateToken(key, ACQUERY))
+ return false;
+
+ m_Query.emplace_back(Utility::UnescapeString(key), std::move(value));
+ }
+
+ return true;
+}
+
+bool Url::ParseFragment(const String& fragment)
+{
+ m_Fragment = Utility::UnescapeString(fragment);
+
+ return ValidateToken(fragment, ACFRAGMENT);
+}
+
+bool Url::ValidateToken(const String& token, const String& symbols)
+{
+ for (const char ch : token) {
+ if (symbols.FindFirstOf(ch) == String::NPos)
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/lib/remote/url.hpp b/lib/remote/url.hpp
new file mode 100644
index 0000000..6012b2f
--- /dev/null
+++ b/lib/remote/url.hpp
@@ -0,0 +1,78 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef URL_H
+#define URL_H
+
+#include "remote/i2-remote.hpp"
+#include "base/object.hpp"
+#include "base/string.hpp"
+#include "base/array.hpp"
+#include "base/value.hpp"
+#include <map>
+#include <utility>
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * A url class to use with the API
+ *
+ * @ingroup base
+ */
+class Url final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Url);
+
+ Url() = default;
+ Url(const String& url);
+
+ String Format(bool onlyPathAndQuery = false, bool printCredentials = false) const;
+
+ String GetScheme() const;
+ String GetAuthority() const;
+ String GetUsername() const;
+ String GetPassword() const;
+ String GetHost() const;
+ String GetPort() const;
+ const std::vector<String>& GetPath() const;
+ const std::vector<std::pair<String, String>>& GetQuery() const;
+ String GetFragment() const;
+
+ void SetScheme(const String& scheme);
+ void SetUsername(const String& username);
+ void SetPassword(const String& password);
+ void SetHost(const String& host);
+ void SetPort(const String& port);
+ void SetPath(const std::vector<String>& path);
+ void SetQuery(const std::vector<std::pair<String, String>>& query);
+ void SetArrayFormatUseBrackets(bool useBrackets = true);
+
+ void AddQueryElement(const String& name, const String& query);
+ void SetFragment(const String& fragment);
+
+private:
+ String m_Scheme;
+ String m_Username;
+ String m_Password;
+ String m_Host;
+ String m_Port;
+ std::vector<String> m_Path;
+ std::vector<std::pair<String, String>> m_Query;
+ bool m_ArrayFormatUseBrackets;
+ String m_Fragment;
+
+ bool ParseScheme(const String& scheme);
+ bool ParseAuthority(const String& authority);
+ bool ParseUserinfo(const String& userinfo);
+ bool ParsePort(const String& port);
+ bool ParsePath(const String& path);
+ bool ParseQuery(const String& query);
+ bool ParseFragment(const String& fragment);
+
+ static bool ValidateToken(const String& token, const String& symbols);
+};
+
+}
+#endif /* URL_H */
diff --git a/lib/remote/variablequeryhandler.cpp b/lib/remote/variablequeryhandler.cpp
new file mode 100644
index 0000000..50c0e78
--- /dev/null
+++ b/lib/remote/variablequeryhandler.cpp
@@ -0,0 +1,121 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/variablequeryhandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/configtype.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/namespace.hpp"
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/variables", VariableQueryHandler);
+
+class VariableTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(VariableTargetProvider);
+
+ static Dictionary::Ptr GetTargetForVar(const String& name, const Value& value)
+ {
+ return new Dictionary({
+ { "name", name },
+ { "type", value.GetReflectionType()->GetName() },
+ { "value", value }
+ });
+ }
+
+ void FindTargets(const String& type,
+ const std::function<void (const Value&)>& addTarget) const override
+ {
+ {
+ Namespace::Ptr globals = ScriptGlobal::GetGlobals();
+ ObjectLock olock(globals);
+ for (const Namespace::Pair& kv : globals) {
+ addTarget(GetTargetForVar(kv.first, kv.second.Val));
+ }
+ }
+ }
+
+ Value GetTargetByName(const String& type, const String& name) const override
+ {
+ return GetTargetForVar(name, ScriptGlobal::Get(name));
+ }
+
+ bool IsValidType(const String& type) const override
+ {
+ return type == "Variable";
+ }
+
+ String GetPluralName(const String& type) const override
+ {
+ return "variables";
+ }
+};
+
+bool VariableQueryHandler::HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 3)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ QueryDescription qd;
+ qd.Types.insert("Variable");
+ qd.Permission = "variables";
+ qd.Provider = new VariableTargetProvider();
+
+ params->Set("type", "Variable");
+
+ if (url->GetPath().size() >= 3)
+ params->Set("variable", url->GetPath()[2]);
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user, "variable");
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No variables found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ ArrayData results;
+
+ for (const Dictionary::Ptr& var : objs) {
+ if (var->Get("name") == "TicketSalt")
+ continue;
+
+ results.emplace_back(new Dictionary({
+ { "name", var->Get("name") },
+ { "type", var->Get("type") },
+ { "value", Serialize(var->Get("value"), 0) }
+ }));
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
diff --git a/lib/remote/variablequeryhandler.hpp b/lib/remote/variablequeryhandler.hpp
new file mode 100644
index 0000000..48e73be
--- /dev/null
+++ b/lib/remote/variablequeryhandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VARIABLEQUERYHANDLER_H
+#define VARIABLEQUERYHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class VariableQueryHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(VariableQueryHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* VARIABLEQUERYHANDLER_H */
diff --git a/lib/remote/zone.cpp b/lib/remote/zone.cpp
new file mode 100644
index 0000000..5ae1468
--- /dev/null
+++ b/lib/remote/zone.cpp
@@ -0,0 +1,154 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/zone.hpp"
+#include "remote/zone-ti.cpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Zone);
+
+void Zone::OnAllConfigLoaded()
+{
+ ObjectImpl<Zone>::OnAllConfigLoaded();
+
+ m_Parent = Zone::GetByName(GetParentRaw());
+
+ if (m_Parent && m_Parent->IsGlobal())
+ BOOST_THROW_EXCEPTION(ScriptError("Zone '" + GetName() + "' can not have a global zone as parent.", GetDebugInfo()));
+
+ Zone::Ptr zone = m_Parent;
+ int levels = 0;
+
+ Array::Ptr endpoints = GetEndpointsRaw();
+
+ if (endpoints) {
+ ObjectLock olock(endpoints);
+ for (const String& endpoint : endpoints) {
+ Endpoint::Ptr ep = Endpoint::GetByName(endpoint);
+
+ if (ep)
+ ep->SetCachedZone(this);
+ }
+ }
+
+ while (zone) {
+ m_AllParents.push_back(zone);
+
+ zone = Zone::GetByName(zone->GetParentRaw());
+ levels++;
+
+ if (levels > 32)
+ BOOST_THROW_EXCEPTION(ScriptError("Infinite recursion detected while resolving zone graph. Check your zone hierarchy.", GetDebugInfo()));
+ }
+}
+
+Zone::Ptr Zone::GetParent() const
+{
+ return m_Parent;
+}
+
+std::set<Endpoint::Ptr> Zone::GetEndpoints() const
+{
+ std::set<Endpoint::Ptr> result;
+
+ Array::Ptr endpoints = GetEndpointsRaw();
+
+ if (endpoints) {
+ ObjectLock olock(endpoints);
+
+ for (const String& name : endpoints) {
+ Endpoint::Ptr endpoint = Endpoint::GetByName(name);
+
+ if (!endpoint)
+ continue;
+
+ result.insert(endpoint);
+ }
+ }
+
+ return result;
+}
+
+std::vector<Zone::Ptr> Zone::GetAllParentsRaw() const
+{
+ return m_AllParents;
+}
+
+Array::Ptr Zone::GetAllParents() const
+{
+ auto result (new Array);
+
+ for (auto& parent : m_AllParents)
+ result->Add(parent->GetName());
+
+ return result;
+}
+
+bool Zone::CanAccessObject(const ConfigObject::Ptr& object)
+{
+ Zone::Ptr object_zone;
+
+ if (object->GetReflectionType() == Zone::TypeInstance)
+ object_zone = static_pointer_cast<Zone>(object);
+ else
+ object_zone = static_pointer_cast<Zone>(object->GetZone());
+
+ if (!object_zone)
+ object_zone = Zone::GetLocalZone();
+
+ if (object_zone->GetGlobal())
+ return true;
+
+ return object_zone->IsChildOf(this);
+}
+
+bool Zone::IsChildOf(const Zone::Ptr& zone)
+{
+ Zone::Ptr azone = this;
+
+ while (azone) {
+ if (azone == zone)
+ return true;
+
+ azone = azone->GetParent();
+ }
+
+ return false;
+}
+
+bool Zone::IsGlobal() const
+{
+ return GetGlobal();
+}
+
+bool Zone::IsSingleInstance() const
+{
+ Array::Ptr endpoints = GetEndpointsRaw();
+ return !endpoints || endpoints->GetLength() < 2;
+}
+
+Zone::Ptr Zone::GetLocalZone()
+{
+ Endpoint::Ptr local = Endpoint::GetLocalEndpoint();
+
+ if (!local)
+ return nullptr;
+
+ return local->GetZone();
+}
+
+void Zone::ValidateEndpointsRaw(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Zone>::ValidateEndpointsRaw(lvalue, utils);
+
+ if (lvalue() && lvalue()->GetLength() > 2) {
+ Log(LogWarning, "Zone")
+ << "The Zone object '" << GetName() << "' has more than two endpoints."
+ << " Due to a known issue this type of configuration is strongly"
+ << " discouraged and may cause Icinga to use excessive amounts of CPU time.";
+ }
+}
diff --git a/lib/remote/zone.hpp b/lib/remote/zone.hpp
new file mode 100644
index 0000000..897b18e
--- /dev/null
+++ b/lib/remote/zone.hpp
@@ -0,0 +1,46 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ZONE_H
+#define ZONE_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/zone-ti.hpp"
+#include "remote/endpoint.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup remote
+ */
+class Zone final : public ObjectImpl<Zone>
+{
+public:
+ DECLARE_OBJECT(Zone);
+ DECLARE_OBJECTNAME(Zone);
+
+ void OnAllConfigLoaded() override;
+
+ Zone::Ptr GetParent() const;
+ std::set<Endpoint::Ptr> GetEndpoints() const;
+ std::vector<Zone::Ptr> GetAllParentsRaw() const;
+ Array::Ptr GetAllParents() const override;
+
+ bool CanAccessObject(const ConfigObject::Ptr& object);
+ bool IsChildOf(const Zone::Ptr& zone);
+ bool IsGlobal() const;
+ bool IsSingleInstance() const;
+
+ static Zone::Ptr GetLocalZone();
+
+protected:
+ void ValidateEndpointsRaw(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ Zone::Ptr m_Parent;
+ std::vector<Zone::Ptr> m_AllParents;
+};
+
+}
+
+#endif /* ZONE_H */
diff --git a/lib/remote/zone.ti b/lib/remote/zone.ti
new file mode 100644
index 0000000..25f6a64
--- /dev/null
+++ b/lib/remote/zone.ti
@@ -0,0 +1,25 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library remote;
+
+namespace icinga
+{
+
+class Zone : ConfigObject
+{
+ [config, no_user_modify, navigation] name(Zone) parent (ParentRaw) {
+ navigate {{{
+ return Zone::GetByName(GetParentRaw());
+ }}}
+ };
+
+ [config] array(name(Endpoint)) endpoints (EndpointsRaw);
+ [config] bool global;
+ [no_user_modify, no_storage] array(Value) all_parents {
+ get;
+ };
+};
+
+}