From 0915b3ef56dfac3113cce55a59a5765dc94976be Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 14:34:54 +0200 Subject: Adding upstream version 2.13.6. Signed-off-by: Daniel Baumann --- lib/CMakeLists.txt | 60 + lib/base/CMakeLists.txt | 149 ++ lib/base/application-environment.cpp | 17 + lib/base/application-version.cpp | 17 + lib/base/application.cpp | 1227 ++++++++++++ lib/base/application.hpp | 160 ++ lib/base/application.ti | 14 + lib/base/array-script.cpp | 260 +++ lib/base/array.cpp | 380 ++++ lib/base/array.hpp | 117 ++ lib/base/atomic-file.cpp | 116 ++ lib/base/atomic-file.hpp | 34 + lib/base/atomic.hpp | 91 + lib/base/base64.cpp | 53 + lib/base/base64.hpp | 25 + lib/base/boolean-script.cpp | 26 + lib/base/boolean.cpp | 9 + lib/base/boolean.hpp | 27 + lib/base/bulker.hpp | 119 ++ lib/base/configobject-script.cpp | 36 + lib/base/configobject.cpp | 701 +++++++ lib/base/configobject.hpp | 101 + lib/base/configobject.ti | 93 + lib/base/configtype.cpp | 76 + lib/base/configtype.hpp | 63 + lib/base/configuration.cpp | 377 ++++ lib/base/configuration.hpp | 156 ++ lib/base/configuration.ti | 164 ++ lib/base/configwriter.cpp | 260 +++ lib/base/configwriter.hpp | 67 + lib/base/console.cpp | 203 ++ lib/base/console.hpp | 91 + lib/base/context.cpp | 56 + lib/base/context.hpp | 50 + lib/base/convert.cpp | 46 + lib/base/convert.hpp | 84 + lib/base/datetime-script.cpp | 28 + lib/base/datetime.cpp | 58 + lib/base/datetime.hpp | 40 + lib/base/datetime.ti | 15 + lib/base/debug.hpp | 49 + lib/base/debuginfo.cpp | 98 + lib/base/debuginfo.hpp | 36 + lib/base/defer.hpp | 54 + lib/base/dependencygraph.cpp | 50 + lib/base/dependencygraph.hpp | 34 + lib/base/dictionary-script.cpp | 119 ++ lib/base/dictionary.cpp | 303 +++ lib/base/dictionary.hpp | 88 + lib/base/exception.cpp | 507 +++++ lib/base/exception.hpp | 166 ++ lib/base/fifo.cpp | 124 ++ lib/base/fifo.hpp | 48 + lib/base/filelogger.cpp | 59 + lib/base/filelogger.hpp | 33 + lib/base/filelogger.ti | 17 + lib/base/function-script.cpp | 50 + lib/base/function.cpp | 37 + lib/base/function.hpp | 89 + lib/base/function.ti | 18 + lib/base/functionwrapper.hpp | 149 ++ lib/base/i2-base.hpp | 79 + lib/base/initialize.cpp | 13 + lib/base/initialize.hpp | 29 + lib/base/io-engine.cpp | 154 ++ lib/base/io-engine.hpp | 216 ++ lib/base/json-script.cpp | 29 + lib/base/json.cpp | 516 +++++ lib/base/json.hpp | 19 + lib/base/lazy-init.hpp | 72 + lib/base/library.cpp | 68 + lib/base/library.hpp | 41 + lib/base/loader.cpp | 35 + lib/base/loader.hpp | 55 + lib/base/logger.cpp | 326 ++++ lib/base/logger.hpp | 149 ++ lib/base/logger.ti | 17 + lib/base/math-script.cpp | 185 ++ lib/base/namespace-script.cpp | 84 + lib/base/namespace.cpp | 224 +++ lib/base/namespace.hpp | 110 ++ lib/base/netstring.cpp | 334 ++++ lib/base/netstring.hpp | 43 + lib/base/networkstream.cpp | 81 + lib/base/networkstream.hpp | 39 + lib/base/number-script.cpp | 25 + lib/base/number.cpp | 9 + lib/base/number.hpp | 27 + lib/base/object-packer.cpp | 246 +++ lib/base/object-packer.hpp | 18 + lib/base/object-script.cpp | 45 + lib/base/object.cpp | 275 +++ lib/base/object.hpp | 225 +++ lib/base/objectlock.cpp | 55 + lib/base/objectlock.hpp | 32 + lib/base/objecttype.cpp | 57 + lib/base/objecttype.hpp | 29 + lib/base/perfdatavalue.cpp | 395 ++++ lib/base/perfdatavalue.hpp | 38 + lib/base/perfdatavalue.ti | 20 + lib/base/primitivetype.cpp | 64 + lib/base/primitivetype.hpp | 62 + lib/base/process.cpp | 1208 ++++++++++++ lib/base/process.hpp | 117 ++ lib/base/reference-script.cpp | 35 + lib/base/reference.cpp | 38 + lib/base/reference.hpp | 40 + lib/base/registry.hpp | 121 ++ lib/base/ringbuffer.cpp | 91 + lib/base/ringbuffer.hpp | 45 + lib/base/scriptframe.cpp | 130 ++ lib/base/scriptframe.hpp | 42 + lib/base/scriptglobal.cpp | 117 ++ lib/base/scriptglobal.hpp | 35 + lib/base/scriptutils.cpp | 570 ++++++ lib/base/scriptutils.hpp | 54 + lib/base/serializer.cpp | 287 +++ lib/base/serializer.hpp | 33 + lib/base/shared-object.hpp | 73 + lib/base/shared.hpp | 101 + lib/base/singleton.hpp | 29 + lib/base/socket.cpp | 430 ++++ lib/base/socket.hpp | 66 + lib/base/stacktrace.cpp | 43 + lib/base/stacktrace.hpp | 31 + lib/base/statsfunction.hpp | 17 + lib/base/stdiostream.cpp | 57 + lib/base/stdiostream.hpp | 36 + lib/base/stream.cpp | 149 ++ lib/base/stream.hpp | 133 ++ lib/base/streamlogger.cpp | 119 ++ lib/base/streamlogger.hpp | 47 + lib/base/streamlogger.ti | 14 + lib/base/string-script.cpp | 138 ++ lib/base/string.cpp | 468 +++++ lib/base/string.hpp | 207 ++ lib/base/sysloglogger.cpp | 136 ++ lib/base/sysloglogger.hpp | 41 + lib/base/sysloglogger.ti | 19 + lib/base/tcpsocket.cpp | 211 ++ lib/base/tcpsocket.hpp | 96 + lib/base/threadpool.cpp | 36 + lib/base/threadpool.hpp | 98 + lib/base/timer.cpp | 321 +++ lib/base/timer.hpp | 60 + lib/base/tlsstream.cpp | 71 + lib/base/tlsstream.hpp | 129 ++ lib/base/tlsutility.cpp | 1053 ++++++++++ lib/base/tlsutility.hpp | 85 + lib/base/type.cpp | 208 ++ lib/base/type.hpp | 147 ++ lib/base/typetype-script.cpp | 31 + lib/base/unix.hpp | 49 + lib/base/unixsocket.cpp | 53 + lib/base/unixsocket.hpp | 32 + lib/base/utility.cpp | 2023 +++++++++++++++++++ lib/base/utility.hpp | 202 ++ lib/base/value-operators.cpp | 719 +++++++ lib/base/value.cpp | 264 +++ lib/base/value.hpp | 251 +++ lib/base/win32.hpp | 35 + lib/base/windowseventloglogger-provider.mc | 5 + lib/base/windowseventloglogger.cpp | 83 + lib/base/windowseventloglogger.hpp | 37 + lib/base/windowseventloglogger.ti | 15 + lib/base/workqueue.cpp | 318 +++ lib/base/workqueue.hpp | 154 ++ lib/checker/CMakeLists.txt | 34 + lib/checker/checkercomponent.cpp | 348 ++++ lib/checker/checkercomponent.hpp | 99 + lib/checker/checkercomponent.ti | 18 + lib/cli/CMakeLists.txt | 49 + lib/cli/apisetupcommand.cpp | 59 + lib/cli/apisetupcommand.hpp | 31 + lib/cli/apisetuputility.cpp | 207 ++ lib/cli/apisetuputility.hpp | 39 + lib/cli/calistcommand.cpp | 89 + lib/cli/calistcommand.hpp | 33 + lib/cli/caremovecommand.cpp | 93 + lib/cli/caremovecommand.hpp | 30 + lib/cli/carestorecommand.cpp | 88 + lib/cli/carestorecommand.hpp | 30 + lib/cli/casigncommand.cpp | 108 + lib/cli/casigncommand.hpp | 30 + lib/cli/clicommand.cpp | 373 ++++ lib/cli/clicommand.hpp | 79 + lib/cli/consolecommand.cpp | 723 +++++++ lib/cli/consolecommand.hpp | 61 + lib/cli/daemoncommand.cpp | 882 +++++++++ lib/cli/daemoncommand.hpp | 31 + lib/cli/daemonutility.cpp | 263 +++ lib/cli/daemonutility.hpp | 27 + lib/cli/editline.hpp | 19 + lib/cli/featuredisablecommand.cpp | 55 + lib/cli/featuredisablecommand.hpp | 33 + lib/cli/featureenablecommand.cpp | 50 + lib/cli/featureenablecommand.hpp | 32 + lib/cli/featurelistcommand.cpp | 34 + lib/cli/featurelistcommand.hpp | 28 + lib/cli/featureutility.cpp | 243 +++ lib/cli/featureutility.hpp | 42 + lib/cli/i2-cli.hpp | 14 + lib/cli/internalsignalcommand.cpp | 67 + lib/cli/internalsignalcommand.hpp | 33 + lib/cli/nodesetupcommand.cpp | 568 ++++++ lib/cli/nodesetupcommand.hpp | 36 + lib/cli/nodeutility.cpp | 390 ++++ lib/cli/nodeutility.hpp | 49 + lib/cli/nodewizardcommand.cpp | 824 ++++++++ lib/cli/nodewizardcommand.hpp | 36 + lib/cli/objectlistcommand.cpp | 122 ++ lib/cli/objectlistcommand.hpp | 36 + lib/cli/objectlistutility.cpp | 155 ++ lib/cli/objectlistutility.hpp | 34 + lib/cli/pkinewcacommand.cpp | 29 + lib/cli/pkinewcacommand.hpp | 29 + lib/cli/pkinewcertcommand.cpp | 66 + lib/cli/pkinewcertcommand.hpp | 32 + lib/cli/pkirequestcommand.cpp | 93 + lib/cli/pkirequestcommand.hpp | 32 + lib/cli/pkisavecertcommand.cpp | 89 + lib/cli/pkisavecertcommand.hpp | 32 + lib/cli/pkisigncsrcommand.cpp | 56 + lib/cli/pkisigncsrcommand.hpp | 32 + lib/cli/pkiticketcommand.cpp | 55 + lib/cli/pkiticketcommand.hpp | 31 + lib/cli/pkiverifycommand.cpp | 226 +++ lib/cli/pkiverifycommand.hpp | 32 + lib/cli/variablegetcommand.cpp | 75 + lib/cli/variablegetcommand.hpp | 34 + lib/cli/variablelistcommand.cpp | 52 + lib/cli/variablelistcommand.hpp | 34 + lib/cli/variableutility.cpp | 76 + lib/cli/variableutility.hpp | 31 + lib/compat/CMakeLists.txt | 47 + lib/compat/checkresultreader.cpp | 166 ++ lib/compat/checkresultreader.hpp | 38 + lib/compat/checkresultreader.ti | 20 + lib/compat/compatlogger.cpp | 612 ++++++ lib/compat/compatlogger.hpp | 60 + lib/compat/compatlogger.ti | 23 + lib/compat/externalcommandlistener.cpp | 150 ++ lib/compat/externalcommandlistener.hpp | 41 + lib/compat/externalcommandlistener.ti | 20 + lib/compat/statusdatawriter.cpp | 897 +++++++++ lib/compat/statusdatawriter.hpp | 89 + lib/compat/statusdatawriter.ti | 26 + lib/config/CMakeLists.txt | 47 + lib/config/activationcontext.cpp | 61 + lib/config/activationcontext.hpp | 46 + lib/config/applyrule-targeted.cpp | 254 +++ lib/config/applyrule.cpp | 199 ++ lib/config/applyrule.hpp | 115 ++ lib/config/config_lexer.ll | 253 +++ lib/config/config_parser.yy | 1243 ++++++++++++ lib/config/configcompiler.cpp | 364 ++++ lib/config/configcompiler.hpp | 161 ++ lib/config/configcompilercontext.cpp | 68 + lib/config/configcompilercontext.hpp | 37 + lib/config/configfragment.hpp | 26 + lib/config/configitem.cpp | 820 ++++++++ lib/config/configitem.hpp | 106 + lib/config/configitembuilder.cpp | 120 ++ lib/config/configitembuilder.hpp | 58 + lib/config/expression.cpp | 1078 ++++++++++ lib/config/expression.hpp | 981 ++++++++++ lib/config/i2-config.hpp | 16 + lib/config/objectrule.cpp | 18 + lib/config/objectrule.hpp | 33 + lib/config/vmops.hpp | 274 +++ lib/db_ido/CMakeLists.txt | 40 + lib/db_ido/commanddbobject.cpp | 31 + lib/db_ido/commanddbobject.hpp | 30 + lib/db_ido/db_ido-itl.conf | 19 + lib/db_ido/dbconnection.cpp | 582 ++++++ lib/db_ido/dbconnection.hpp | 138 ++ lib/db_ido/dbconnection.ti | 82 + lib/db_ido/dbevents.cpp | 1884 ++++++++++++++++++ lib/db_ido/dbevents.hpp | 128 ++ lib/db_ido/dbobject.cpp | 430 ++++ lib/db_ido/dbobject.hpp | 112 ++ lib/db_ido/dbquery.cpp | 52 + lib/db_ido/dbquery.hpp | 72 + lib/db_ido/dbreference.cpp | 19 + lib/db_ido/dbreference.hpp | 30 + lib/db_ido/dbtype.cpp | 141 ++ lib/db_ido/dbtype.hpp | 90 + lib/db_ido/dbvalue.cpp | 69 + lib/db_ido/dbvalue.hpp | 52 + lib/db_ido/endpointdbobject.cpp | 91 + lib/db_ido/endpointdbobject.hpp | 37 + lib/db_ido/hostdbobject.cpp | 423 ++++ lib/db_ido/hostdbobject.hpp | 38 + lib/db_ido/hostgroupdbobject.cpp | 33 + lib/db_ido/hostgroupdbobject.hpp | 34 + lib/db_ido/i2-db_ido.hpp | 14 + lib/db_ido/idochecktask.cpp | 198 ++ lib/db_ido/idochecktask.hpp | 29 + lib/db_ido/servicedbobject.cpp | 359 ++++ lib/db_ido/servicedbobject.hpp | 41 + lib/db_ido/servicegroupdbobject.cpp | 32 + lib/db_ido/servicegroupdbobject.hpp | 31 + lib/db_ido/timeperioddbobject.cpp | 85 + lib/db_ido/timeperioddbobject.hpp | 33 + lib/db_ido/userdbobject.cpp | 161 ++ lib/db_ido/userdbobject.hpp | 35 + lib/db_ido/usergroupdbobject.cpp | 30 + lib/db_ido/usergroupdbobject.hpp | 31 + lib/db_ido/zonedbobject.cpp | 38 + lib/db_ido/zonedbobject.hpp | 31 + lib/db_ido_mysql/CMakeLists.txt | 41 + lib/db_ido_mysql/idomysqlconnection.cpp | 1268 ++++++++++++ lib/db_ido_mysql/idomysqlconnection.hpp | 114 ++ lib/db_ido_mysql/idomysqlconnection.ti | 42 + lib/db_ido_mysql/schema/mysql.sql | 1666 ++++++++++++++++ lib/db_ido_mysql/schema/upgrade/2.0.2.sql | 20 + lib/db_ido_mysql/schema/upgrade/2.1.0.sql | 17 + lib/db_ido_mysql/schema/upgrade/2.11.0.sql | 89 + lib/db_ido_mysql/schema/upgrade/2.12.7.sql | 15 + lib/db_ido_mysql/schema/upgrade/2.13.0.sql | 23 + lib/db_ido_mysql/schema/upgrade/2.13.3.sql | 15 + lib/db_ido_mysql/schema/upgrade/2.2.0.sql | 23 + lib/db_ido_mysql/schema/upgrade/2.3.0.sql | 26 + lib/db_ido_mysql/schema/upgrade/2.4.0.sql | 75 + lib/db_ido_mysql/schema/upgrade/2.5.0.sql | 103 + lib/db_ido_mysql/schema/upgrade/2.6.0.sql | 151 ++ lib/db_ido_mysql/schema/upgrade/2.8.0.sql | 81 + lib/db_ido_mysql/schema/upgrade/2.8.1.sql | 67 + lib/db_ido_pgsql/CMakeLists.txt | 41 + lib/db_ido_pgsql/idopgsqlconnection.cpp | 1028 ++++++++++ lib/db_ido_pgsql/idopgsqlconnection.hpp | 99 + lib/db_ido_pgsql/idopgsqlconnection.ti | 39 + lib/db_ido_pgsql/schema/pgsql.sql | 1733 ++++++++++++++++ lib/db_ido_pgsql/schema/upgrade/2.0.2.sql | 17 + lib/db_ido_pgsql/schema/upgrade/2.1.0.sql | 17 + lib/db_ido_pgsql/schema/upgrade/2.2.0.sql | 21 + lib/db_ido_pgsql/schema/upgrade/2.3.0.sql | 26 + lib/db_ido_pgsql/schema/upgrade/2.4.0.sql | 185 ++ lib/db_ido_pgsql/schema/upgrade/2.5.0.sql | 85 + lib/db_ido_pgsql/schema/upgrade/2.6.0.sql | 161 ++ lib/db_ido_pgsql/schema/upgrade/2.8.0.sql | 32 + lib/db_ido_pgsql/schema/upgrade/2.8.1.sql | 19 + lib/icinga/CMakeLists.txt | 75 + lib/icinga/apiactions.cpp | 898 +++++++++ lib/icinga/apiactions.hpp | 42 + lib/icinga/apievents.cpp | 438 +++++ lib/icinga/apievents.hpp | 51 + lib/icinga/checkable-check.cpp | 705 +++++++ lib/icinga/checkable-comment.cpp | 70 + lib/icinga/checkable-dependency.cpp | 158 ++ lib/icinga/checkable-downtime.cpp | 64 + lib/icinga/checkable-event.cpp | 81 + lib/icinga/checkable-flapping.cpp | 114 ++ lib/icinga/checkable-notification.cpp | 334 ++++ lib/icinga/checkable-script.cpp | 28 + lib/icinga/checkable.cpp | 322 +++ lib/icinga/checkable.hpp | 256 +++ lib/icinga/checkable.ti | 192 ++ lib/icinga/checkcommand.cpp | 22 + lib/icinga/checkcommand.hpp | 32 + lib/icinga/checkcommand.ti | 14 + lib/icinga/checkresult.cpp | 34 + lib/icinga/checkresult.hpp | 28 + lib/icinga/checkresult.ti | 72 + lib/icinga/cib.cpp | 346 ++++ lib/icinga/cib.hpp | 91 + lib/icinga/clusterevents-check.cpp | 379 ++++ lib/icinga/clusterevents.cpp | 1436 ++++++++++++++ lib/icinga/clusterevents.hpp | 96 + lib/icinga/command.cpp | 68 + lib/icinga/command.hpp | 30 + lib/icinga/command.ti | 54 + lib/icinga/comment.cpp | 258 +++ lib/icinga/comment.hpp | 59 + lib/icinga/comment.ti | 80 + lib/icinga/compatutility.cpp | 302 +++ lib/icinga/compatutility.hpp | 56 + lib/icinga/customvarobject.cpp | 49 + lib/icinga/customvarobject.hpp | 31 + lib/icinga/customvarobject.ti | 15 + lib/icinga/dependency-apply.cpp | 163 ++ lib/icinga/dependency.cpp | 213 ++ lib/icinga/dependency.hpp | 59 + lib/icinga/dependency.ti | 99 + lib/icinga/downtime.cpp | 546 ++++++ lib/icinga/downtime.hpp | 92 + lib/icinga/downtime.ti | 82 + lib/icinga/eventcommand.cpp | 20 + lib/icinga/eventcommand.hpp | 32 + lib/icinga/eventcommand.ti | 15 + lib/icinga/externalcommandprocessor.cpp | 2281 ++++++++++++++++++++++ lib/icinga/externalcommandprocessor.hpp | 169 ++ lib/icinga/host.cpp | 330 ++++ lib/icinga/host.hpp | 71 + lib/icinga/host.ti | 48 + lib/icinga/hostgroup.cpp | 108 + lib/icinga/hostgroup.hpp | 43 + lib/icinga/hostgroup.ti | 28 + lib/icinga/i2-icinga.hpp | 15 + lib/icinga/icinga-itl.conf | 15 + lib/icinga/icingaapplication.cpp | 327 ++++ lib/icinga/icingaapplication.hpp | 52 + lib/icinga/icingaapplication.ti | 41 + lib/icinga/legacytimeperiod.cpp | 644 ++++++ lib/icinga/legacytimeperiod.hpp | 45 + lib/icinga/macroprocessor.cpp | 556 ++++++ lib/icinga/macroprocessor.hpp | 61 + lib/icinga/macroresolver.hpp | 31 + lib/icinga/notification-apply.cpp | 163 ++ lib/icinga/notification.cpp | 777 ++++++++ lib/icinga/notification.hpp | 133 ++ lib/icinga/notification.ti | 107 + lib/icinga/notificationcommand.cpp | 27 + lib/icinga/notificationcommand.hpp | 36 + lib/icinga/notificationcommand.ti | 14 + lib/icinga/objectutils.cpp | 55 + lib/icinga/objectutils.hpp | 29 + lib/icinga/pluginutility.cpp | 217 ++ lib/icinga/pluginutility.hpp | 42 + lib/icinga/scheduleddowntime-apply.cpp | 161 ++ lib/icinga/scheduleddowntime.cpp | 393 ++++ lib/icinga/scheduleddowntime.hpp | 60 + lib/icinga/scheduleddowntime.ti | 76 + lib/icinga/service-apply.cpp | 135 ++ lib/icinga/service.cpp | 287 +++ lib/icinga/service.hpp | 65 + lib/icinga/service.ti | 71 + lib/icinga/servicegroup.cpp | 111 ++ lib/icinga/servicegroup.hpp | 43 + lib/icinga/servicegroup.ti | 28 + lib/icinga/timeperiod.cpp | 399 ++++ lib/icinga/timeperiod.hpp | 50 + lib/icinga/timeperiod.ti | 47 + lib/icinga/user.cpp | 103 + lib/icinga/user.hpp | 44 + lib/icinga/user.ti | 47 + lib/icinga/usergroup.cpp | 128 ++ lib/icinga/usergroup.hpp | 49 + lib/icinga/usergroup.ti | 25 + lib/icingadb/CMakeLists.txt | 32 + lib/icingadb/icingadb-itl.conf | 24 + lib/icingadb/icingadb-objects.cpp | 2932 ++++++++++++++++++++++++++++ lib/icingadb/icingadb-stats.cpp | 54 + lib/icingadb/icingadb-utility.cpp | 313 +++ lib/icingadb/icingadb.cpp | 309 +++ lib/icingadb/icingadb.hpp | 241 +++ lib/icingadb/icingadb.ti | 63 + lib/icingadb/icingadbchecktask.cpp | 514 +++++ lib/icingadb/icingadbchecktask.hpp | 29 + lib/icingadb/redisconnection.cpp | 773 ++++++++ lib/icingadb/redisconnection.hpp | 678 +++++++ lib/livestatus/CMakeLists.txt | 65 + lib/livestatus/aggregator.cpp | 18 + lib/livestatus/aggregator.hpp | 44 + lib/livestatus/andfilter.cpp | 15 + lib/livestatus/andfilter.hpp | 26 + lib/livestatus/attributefilter.cpp | 121 ++ lib/livestatus/attributefilter.hpp | 33 + lib/livestatus/avgaggregator.cpp | 38 + lib/livestatus/avgaggregator.hpp | 42 + lib/livestatus/column.cpp | 21 + lib/livestatus/column.hpp | 37 + lib/livestatus/combinerfilter.cpp | 10 + lib/livestatus/combinerfilter.hpp | 31 + lib/livestatus/commandstable.cpp | 142 ++ lib/livestatus/commandstable.hpp | 41 + lib/livestatus/commentstable.cpp | 178 ++ lib/livestatus/commentstable.hpp | 49 + lib/livestatus/contactgroupstable.cpp | 74 + lib/livestatus/contactgroupstable.hpp | 39 + lib/livestatus/contactstable.cpp | 278 +++ lib/livestatus/contactstable.hpp | 50 + lib/livestatus/countaggregator.cpp | 30 + lib/livestatus/countaggregator.hpp | 37 + lib/livestatus/downtimestable.cpp | 168 ++ lib/livestatus/downtimestable.hpp | 51 + lib/livestatus/endpointstable.cpp | 109 ++ lib/livestatus/endpointstable.hpp | 41 + lib/livestatus/filter.hpp | 28 + lib/livestatus/historytable.hpp | 24 + lib/livestatus/hostgroupstable.cpp | 473 +++++ lib/livestatus/hostgroupstable.hpp | 61 + lib/livestatus/hoststable.cpp | 1522 +++++++++++++++ lib/livestatus/hoststable.hpp | 133 ++ lib/livestatus/i2-livestatus.hpp | 14 + lib/livestatus/invavgaggregator.cpp | 38 + lib/livestatus/invavgaggregator.hpp | 42 + lib/livestatus/invsumaggregator.cpp | 37 + lib/livestatus/invsumaggregator.hpp | 41 + lib/livestatus/livestatuslistener.cpp | 211 ++ lib/livestatus/livestatuslistener.hpp | 47 + lib/livestatus/livestatuslistener.ti | 31 + lib/livestatus/livestatuslogutility.cpp | 321 +++ lib/livestatus/livestatuslogutility.hpp | 60 + lib/livestatus/livestatusquery.cpp | 648 ++++++ lib/livestatus/livestatusquery.hpp | 90 + lib/livestatus/logtable.cpp | 229 +++ lib/livestatus/logtable.hpp | 65 + lib/livestatus/maxaggregator.cpp | 38 + lib/livestatus/maxaggregator.hpp | 41 + lib/livestatus/minaggregator.cpp | 45 + lib/livestatus/minaggregator.hpp | 42 + lib/livestatus/negatefilter.cpp | 14 + lib/livestatus/negatefilter.hpp | 31 + lib/livestatus/orfilter.cpp | 18 + lib/livestatus/orfilter.hpp | 26 + lib/livestatus/servicegroupstable.cpp | 323 +++ lib/livestatus/servicegroupstable.hpp | 54 + lib/livestatus/servicestable.cpp | 1205 ++++++++++++ lib/livestatus/servicestable.hpp | 115 ++ lib/livestatus/statehisttable.cpp | 466 +++++ lib/livestatus/statehisttable.hpp | 75 + lib/livestatus/statustable.cpp | 269 +++ lib/livestatus/statustable.hpp | 61 + lib/livestatus/stdaggregator.cpp | 40 + lib/livestatus/stdaggregator.hpp | 43 + lib/livestatus/sumaggregator.cpp | 37 + lib/livestatus/sumaggregator.hpp | 41 + lib/livestatus/table.cpp | 165 ++ lib/livestatus/table.hpp | 73 + lib/livestatus/timeperiodstable.cpp | 58 + lib/livestatus/timeperiodstable.hpp | 39 + lib/livestatus/zonestable.cpp | 92 + lib/livestatus/zonestable.hpp | 40 + lib/methods/CMakeLists.txt | 33 + lib/methods/clusterchecktask.cpp | 117 ++ lib/methods/clusterchecktask.hpp | 29 + lib/methods/clusterzonechecktask.cpp | 211 ++ lib/methods/clusterzonechecktask.hpp | 28 + lib/methods/dummychecktask.cpp | 77 + lib/methods/dummychecktask.hpp | 30 + lib/methods/exceptionchecktask.cpp | 41 + lib/methods/exceptionchecktask.hpp | 29 + lib/methods/i2-methods.hpp | 15 + lib/methods/icingachecktask.cpp | 211 ++ lib/methods/icingachecktask.hpp | 29 + lib/methods/methods-itl.conf | 85 + lib/methods/nullchecktask.cpp | 50 + lib/methods/nullchecktask.hpp | 30 + lib/methods/nulleventtask.cpp | 26 + lib/methods/nulleventtask.hpp | 30 + lib/methods/pluginchecktask.cpp | 91 + lib/methods/pluginchecktask.hpp | 33 + lib/methods/plugineventtask.cpp | 63 + lib/methods/plugineventtask.hpp | 33 + lib/methods/pluginnotificationtask.cpp | 80 + lib/methods/pluginnotificationtask.hpp | 36 + lib/methods/randomchecktask.cpp | 65 + lib/methods/randomchecktask.hpp | 29 + lib/methods/sleepchecktask.cpp | 69 + lib/methods/sleepchecktask.hpp | 30 + lib/methods/timeperiodtask.cpp | 35 + lib/methods/timeperiodtask.hpp | 28 + lib/mysql_shim/CMakeLists.txt | 31 + lib/mysql_shim/mysql_shim.def | 3 + lib/mysql_shim/mysqlinterface.cpp | 119 ++ lib/mysql_shim/mysqlinterface.hpp | 65 + lib/notification/CMakeLists.txt | 34 + lib/notification/notificationcomponent.cpp | 269 +++ lib/notification/notificationcomponent.hpp | 38 + lib/notification/notificationcomponent.ti | 19 + lib/perfdata/CMakeLists.txt | 74 + lib/perfdata/elasticsearchwriter.cpp | 686 +++++++ lib/perfdata/elasticsearchwriter.hpp | 65 + lib/perfdata/elasticsearchwriter.ti | 50 + lib/perfdata/gelfwriter.cpp | 538 +++++ lib/perfdata/gelfwriter.hpp | 70 + lib/perfdata/gelfwriter.ti | 45 + lib/perfdata/graphitewriter.cpp | 515 +++++ lib/perfdata/graphitewriter.hpp | 69 + lib/perfdata/graphitewriter.ti | 38 + lib/perfdata/influxdb2writer.cpp | 44 + lib/perfdata/influxdb2writer.hpp | 33 + lib/perfdata/influxdb2writer.ti | 19 + lib/perfdata/influxdbcommonwriter.cpp | 596 ++++++ lib/perfdata/influxdbcommonwriter.hpp | 101 + lib/perfdata/influxdbcommonwriter.ti | 88 + lib/perfdata/influxdbwriter.cpp | 56 + lib/perfdata/influxdbwriter.hpp | 31 + lib/perfdata/influxdbwriter.ti | 35 + lib/perfdata/opentsdbwriter.cpp | 526 +++++ lib/perfdata/opentsdbwriter.hpp | 62 + lib/perfdata/opentsdbwriter.ti | 55 + lib/perfdata/perfdatawriter.cpp | 202 ++ lib/perfdata/perfdatawriter.hpp | 53 + lib/perfdata/perfdatawriter.ti | 61 + lib/pgsql_shim/CMakeLists.txt | 32 + lib/pgsql_shim/pgsql_shim.def | 3 + lib/pgsql_shim/pgsqlinterface.cpp | 108 + lib/pgsql_shim/pgsqlinterface.hpp | 61 + lib/remote/CMakeLists.txt | 66 + lib/remote/actionshandler.cpp | 139 ++ lib/remote/actionshandler.hpp | 32 + lib/remote/apiaction.cpp | 40 + lib/remote/apiaction.hpp | 69 + lib/remote/apifunction.cpp | 35 + lib/remote/apifunction.hpp | 59 + lib/remote/apilistener-authority.cpp | 84 + lib/remote/apilistener-configsync.cpp | 474 +++++ lib/remote/apilistener-filesync.cpp | 887 +++++++++ lib/remote/apilistener.cpp | 1929 ++++++++++++++++++ lib/remote/apilistener.hpp | 263 +++ lib/remote/apilistener.ti | 66 + lib/remote/apiuser.cpp | 55 + lib/remote/apiuser.hpp | 27 + lib/remote/apiuser.ti | 31 + lib/remote/configfileshandler.cpp | 94 + lib/remote/configfileshandler.hpp | 30 + lib/remote/configobjectutility.cpp | 377 ++++ lib/remote/configobjectutility.hpp | 46 + lib/remote/configpackageshandler.cpp | 179 ++ lib/remote/configpackageshandler.hpp | 54 + lib/remote/configpackageutility.cpp | 413 ++++ lib/remote/configpackageutility.hpp | 73 + lib/remote/configstageshandler.cpp | 225 +++ lib/remote/configstageshandler.hpp | 56 + lib/remote/consolehandler.cpp | 319 +++ lib/remote/consolehandler.hpp | 50 + lib/remote/createobjecthandler.cpp | 147 ++ lib/remote/createobjecthandler.hpp | 30 + lib/remote/deleteobjecthandler.cpp | 115 ++ lib/remote/deleteobjecthandler.hpp | 30 + lib/remote/endpoint.cpp | 138 ++ lib/remote/endpoint.hpp | 68 + lib/remote/endpoint.ti | 59 + lib/remote/eventqueue.cpp | 351 ++++ lib/remote/eventqueue.hpp | 177 ++ lib/remote/eventshandler.cpp | 137 ++ lib/remote/eventshandler.hpp | 31 + lib/remote/filterutility.cpp | 297 +++ lib/remote/filterutility.hpp | 64 + lib/remote/httphandler.cpp | 129 ++ lib/remote/httphandler.hpp | 74 + lib/remote/httpserverconnection.cpp | 609 ++++++ lib/remote/httpserverconnection.hpp | 54 + lib/remote/httputility.cpp | 80 + lib/remote/httputility.hpp | 33 + lib/remote/i2-remote.hpp | 14 + lib/remote/infohandler.cpp | 100 + lib/remote/infohandler.hpp | 30 + lib/remote/jsonrpc.cpp | 157 ++ lib/remote/jsonrpc.hpp | 39 + lib/remote/jsonrpcconnection-heartbeat.cpp | 48 + lib/remote/jsonrpcconnection-pki.cpp | 402 ++++ lib/remote/jsonrpcconnection.cpp | 388 ++++ lib/remote/jsonrpcconnection.hpp | 100 + lib/remote/messageorigin.cpp | 10 + lib/remote/messageorigin.hpp | 28 + lib/remote/modifyobjecthandler.cpp | 120 ++ lib/remote/modifyobjecthandler.hpp | 30 + lib/remote/objectqueryhandler.cpp | 332 ++++ lib/remote/objectqueryhandler.hpp | 34 + lib/remote/pkiutility.cpp | 452 +++++ lib/remote/pkiutility.hpp | 41 + lib/remote/statushandler.cpp | 120 ++ lib/remote/statushandler.hpp | 30 + lib/remote/templatequeryhandler.cpp | 136 ++ lib/remote/templatequeryhandler.hpp | 30 + lib/remote/typequeryhandler.cpp | 156 ++ lib/remote/typequeryhandler.hpp | 30 + lib/remote/url-characters.hpp | 29 + lib/remote/url.cpp | 363 ++++ lib/remote/url.hpp | 78 + lib/remote/variablequeryhandler.cpp | 118 ++ lib/remote/variablequeryhandler.hpp | 30 + lib/remote/zone.cpp | 154 ++ lib/remote/zone.hpp | 46 + lib/remote/zone.ti | 25 + 668 files changed, 105834 insertions(+) create mode 100644 lib/CMakeLists.txt create mode 100644 lib/base/CMakeLists.txt create mode 100644 lib/base/application-environment.cpp create mode 100644 lib/base/application-version.cpp create mode 100644 lib/base/application.cpp create mode 100644 lib/base/application.hpp create mode 100644 lib/base/application.ti create mode 100644 lib/base/array-script.cpp create mode 100644 lib/base/array.cpp create mode 100644 lib/base/array.hpp create mode 100644 lib/base/atomic-file.cpp create mode 100644 lib/base/atomic-file.hpp create mode 100644 lib/base/atomic.hpp create mode 100644 lib/base/base64.cpp create mode 100644 lib/base/base64.hpp create mode 100644 lib/base/boolean-script.cpp create mode 100644 lib/base/boolean.cpp create mode 100644 lib/base/boolean.hpp create mode 100644 lib/base/bulker.hpp create mode 100644 lib/base/configobject-script.cpp create mode 100644 lib/base/configobject.cpp create mode 100644 lib/base/configobject.hpp create mode 100644 lib/base/configobject.ti create mode 100644 lib/base/configtype.cpp create mode 100644 lib/base/configtype.hpp create mode 100644 lib/base/configuration.cpp create mode 100644 lib/base/configuration.hpp create mode 100644 lib/base/configuration.ti create mode 100644 lib/base/configwriter.cpp create mode 100644 lib/base/configwriter.hpp create mode 100644 lib/base/console.cpp create mode 100644 lib/base/console.hpp create mode 100644 lib/base/context.cpp create mode 100644 lib/base/context.hpp create mode 100644 lib/base/convert.cpp create mode 100644 lib/base/convert.hpp create mode 100644 lib/base/datetime-script.cpp create mode 100644 lib/base/datetime.cpp create mode 100644 lib/base/datetime.hpp create mode 100644 lib/base/datetime.ti create mode 100644 lib/base/debug.hpp create mode 100644 lib/base/debuginfo.cpp create mode 100644 lib/base/debuginfo.hpp create mode 100644 lib/base/defer.hpp create mode 100644 lib/base/dependencygraph.cpp create mode 100644 lib/base/dependencygraph.hpp create mode 100644 lib/base/dictionary-script.cpp create mode 100644 lib/base/dictionary.cpp create mode 100644 lib/base/dictionary.hpp create mode 100644 lib/base/exception.cpp create mode 100644 lib/base/exception.hpp create mode 100644 lib/base/fifo.cpp create mode 100644 lib/base/fifo.hpp create mode 100644 lib/base/filelogger.cpp create mode 100644 lib/base/filelogger.hpp create mode 100644 lib/base/filelogger.ti create mode 100644 lib/base/function-script.cpp create mode 100644 lib/base/function.cpp create mode 100644 lib/base/function.hpp create mode 100644 lib/base/function.ti create mode 100644 lib/base/functionwrapper.hpp create mode 100644 lib/base/i2-base.hpp create mode 100644 lib/base/initialize.cpp create mode 100644 lib/base/initialize.hpp create mode 100644 lib/base/io-engine.cpp create mode 100644 lib/base/io-engine.hpp create mode 100644 lib/base/json-script.cpp create mode 100644 lib/base/json.cpp create mode 100644 lib/base/json.hpp create mode 100644 lib/base/lazy-init.hpp create mode 100644 lib/base/library.cpp create mode 100644 lib/base/library.hpp create mode 100644 lib/base/loader.cpp create mode 100644 lib/base/loader.hpp create mode 100644 lib/base/logger.cpp create mode 100644 lib/base/logger.hpp create mode 100644 lib/base/logger.ti create mode 100644 lib/base/math-script.cpp create mode 100644 lib/base/namespace-script.cpp create mode 100644 lib/base/namespace.cpp create mode 100644 lib/base/namespace.hpp create mode 100644 lib/base/netstring.cpp create mode 100644 lib/base/netstring.hpp create mode 100644 lib/base/networkstream.cpp create mode 100644 lib/base/networkstream.hpp create mode 100644 lib/base/number-script.cpp create mode 100644 lib/base/number.cpp create mode 100644 lib/base/number.hpp create mode 100644 lib/base/object-packer.cpp create mode 100644 lib/base/object-packer.hpp create mode 100644 lib/base/object-script.cpp create mode 100644 lib/base/object.cpp create mode 100644 lib/base/object.hpp create mode 100644 lib/base/objectlock.cpp create mode 100644 lib/base/objectlock.hpp create mode 100644 lib/base/objecttype.cpp create mode 100644 lib/base/objecttype.hpp create mode 100644 lib/base/perfdatavalue.cpp create mode 100644 lib/base/perfdatavalue.hpp create mode 100644 lib/base/perfdatavalue.ti create mode 100644 lib/base/primitivetype.cpp create mode 100644 lib/base/primitivetype.hpp create mode 100644 lib/base/process.cpp create mode 100644 lib/base/process.hpp create mode 100644 lib/base/reference-script.cpp create mode 100644 lib/base/reference.cpp create mode 100644 lib/base/reference.hpp create mode 100644 lib/base/registry.hpp create mode 100644 lib/base/ringbuffer.cpp create mode 100644 lib/base/ringbuffer.hpp create mode 100644 lib/base/scriptframe.cpp create mode 100644 lib/base/scriptframe.hpp create mode 100644 lib/base/scriptglobal.cpp create mode 100644 lib/base/scriptglobal.hpp create mode 100644 lib/base/scriptutils.cpp create mode 100644 lib/base/scriptutils.hpp create mode 100644 lib/base/serializer.cpp create mode 100644 lib/base/serializer.hpp create mode 100644 lib/base/shared-object.hpp create mode 100644 lib/base/shared.hpp create mode 100644 lib/base/singleton.hpp create mode 100644 lib/base/socket.cpp create mode 100644 lib/base/socket.hpp create mode 100644 lib/base/stacktrace.cpp create mode 100644 lib/base/stacktrace.hpp create mode 100644 lib/base/statsfunction.hpp create mode 100644 lib/base/stdiostream.cpp create mode 100644 lib/base/stdiostream.hpp create mode 100644 lib/base/stream.cpp create mode 100644 lib/base/stream.hpp create mode 100644 lib/base/streamlogger.cpp create mode 100644 lib/base/streamlogger.hpp create mode 100644 lib/base/streamlogger.ti create mode 100644 lib/base/string-script.cpp create mode 100644 lib/base/string.cpp create mode 100644 lib/base/string.hpp create mode 100644 lib/base/sysloglogger.cpp create mode 100644 lib/base/sysloglogger.hpp create mode 100644 lib/base/sysloglogger.ti create mode 100644 lib/base/tcpsocket.cpp create mode 100644 lib/base/tcpsocket.hpp create mode 100644 lib/base/threadpool.cpp create mode 100644 lib/base/threadpool.hpp create mode 100644 lib/base/timer.cpp create mode 100644 lib/base/timer.hpp create mode 100644 lib/base/tlsstream.cpp create mode 100644 lib/base/tlsstream.hpp create mode 100644 lib/base/tlsutility.cpp create mode 100644 lib/base/tlsutility.hpp create mode 100644 lib/base/type.cpp create mode 100644 lib/base/type.hpp create mode 100644 lib/base/typetype-script.cpp create mode 100644 lib/base/unix.hpp create mode 100644 lib/base/unixsocket.cpp create mode 100644 lib/base/unixsocket.hpp create mode 100644 lib/base/utility.cpp create mode 100644 lib/base/utility.hpp create mode 100644 lib/base/value-operators.cpp create mode 100644 lib/base/value.cpp create mode 100644 lib/base/value.hpp create mode 100644 lib/base/win32.hpp create mode 100644 lib/base/windowseventloglogger-provider.mc create mode 100644 lib/base/windowseventloglogger.cpp create mode 100644 lib/base/windowseventloglogger.hpp create mode 100644 lib/base/windowseventloglogger.ti create mode 100644 lib/base/workqueue.cpp create mode 100644 lib/base/workqueue.hpp create mode 100644 lib/checker/CMakeLists.txt create mode 100644 lib/checker/checkercomponent.cpp create mode 100644 lib/checker/checkercomponent.hpp create mode 100644 lib/checker/checkercomponent.ti create mode 100644 lib/cli/CMakeLists.txt create mode 100644 lib/cli/apisetupcommand.cpp create mode 100644 lib/cli/apisetupcommand.hpp create mode 100644 lib/cli/apisetuputility.cpp create mode 100644 lib/cli/apisetuputility.hpp create mode 100644 lib/cli/calistcommand.cpp create mode 100644 lib/cli/calistcommand.hpp create mode 100644 lib/cli/caremovecommand.cpp create mode 100644 lib/cli/caremovecommand.hpp create mode 100644 lib/cli/carestorecommand.cpp create mode 100644 lib/cli/carestorecommand.hpp create mode 100644 lib/cli/casigncommand.cpp create mode 100644 lib/cli/casigncommand.hpp create mode 100644 lib/cli/clicommand.cpp create mode 100644 lib/cli/clicommand.hpp create mode 100644 lib/cli/consolecommand.cpp create mode 100644 lib/cli/consolecommand.hpp create mode 100644 lib/cli/daemoncommand.cpp create mode 100644 lib/cli/daemoncommand.hpp create mode 100644 lib/cli/daemonutility.cpp create mode 100644 lib/cli/daemonutility.hpp create mode 100644 lib/cli/editline.hpp create mode 100644 lib/cli/featuredisablecommand.cpp create mode 100644 lib/cli/featuredisablecommand.hpp create mode 100644 lib/cli/featureenablecommand.cpp create mode 100644 lib/cli/featureenablecommand.hpp create mode 100644 lib/cli/featurelistcommand.cpp create mode 100644 lib/cli/featurelistcommand.hpp create mode 100644 lib/cli/featureutility.cpp create mode 100644 lib/cli/featureutility.hpp create mode 100644 lib/cli/i2-cli.hpp create mode 100644 lib/cli/internalsignalcommand.cpp create mode 100644 lib/cli/internalsignalcommand.hpp create mode 100644 lib/cli/nodesetupcommand.cpp create mode 100644 lib/cli/nodesetupcommand.hpp create mode 100644 lib/cli/nodeutility.cpp create mode 100644 lib/cli/nodeutility.hpp create mode 100644 lib/cli/nodewizardcommand.cpp create mode 100644 lib/cli/nodewizardcommand.hpp create mode 100644 lib/cli/objectlistcommand.cpp create mode 100644 lib/cli/objectlistcommand.hpp create mode 100644 lib/cli/objectlistutility.cpp create mode 100644 lib/cli/objectlistutility.hpp create mode 100644 lib/cli/pkinewcacommand.cpp create mode 100644 lib/cli/pkinewcacommand.hpp create mode 100644 lib/cli/pkinewcertcommand.cpp create mode 100644 lib/cli/pkinewcertcommand.hpp create mode 100644 lib/cli/pkirequestcommand.cpp create mode 100644 lib/cli/pkirequestcommand.hpp create mode 100644 lib/cli/pkisavecertcommand.cpp create mode 100644 lib/cli/pkisavecertcommand.hpp create mode 100644 lib/cli/pkisigncsrcommand.cpp create mode 100644 lib/cli/pkisigncsrcommand.hpp create mode 100644 lib/cli/pkiticketcommand.cpp create mode 100644 lib/cli/pkiticketcommand.hpp create mode 100644 lib/cli/pkiverifycommand.cpp create mode 100644 lib/cli/pkiverifycommand.hpp create mode 100644 lib/cli/variablegetcommand.cpp create mode 100644 lib/cli/variablegetcommand.hpp create mode 100644 lib/cli/variablelistcommand.cpp create mode 100644 lib/cli/variablelistcommand.hpp create mode 100644 lib/cli/variableutility.cpp create mode 100644 lib/cli/variableutility.hpp create mode 100644 lib/compat/CMakeLists.txt create mode 100644 lib/compat/checkresultreader.cpp create mode 100644 lib/compat/checkresultreader.hpp create mode 100644 lib/compat/checkresultreader.ti create mode 100644 lib/compat/compatlogger.cpp create mode 100644 lib/compat/compatlogger.hpp create mode 100644 lib/compat/compatlogger.ti create mode 100644 lib/compat/externalcommandlistener.cpp create mode 100644 lib/compat/externalcommandlistener.hpp create mode 100644 lib/compat/externalcommandlistener.ti create mode 100644 lib/compat/statusdatawriter.cpp create mode 100644 lib/compat/statusdatawriter.hpp create mode 100644 lib/compat/statusdatawriter.ti create mode 100644 lib/config/CMakeLists.txt create mode 100644 lib/config/activationcontext.cpp create mode 100644 lib/config/activationcontext.hpp create mode 100644 lib/config/applyrule-targeted.cpp create mode 100644 lib/config/applyrule.cpp create mode 100644 lib/config/applyrule.hpp create mode 100644 lib/config/config_lexer.ll create mode 100644 lib/config/config_parser.yy create mode 100644 lib/config/configcompiler.cpp create mode 100644 lib/config/configcompiler.hpp create mode 100644 lib/config/configcompilercontext.cpp create mode 100644 lib/config/configcompilercontext.hpp create mode 100644 lib/config/configfragment.hpp create mode 100644 lib/config/configitem.cpp create mode 100644 lib/config/configitem.hpp create mode 100644 lib/config/configitembuilder.cpp create mode 100644 lib/config/configitembuilder.hpp create mode 100644 lib/config/expression.cpp create mode 100644 lib/config/expression.hpp create mode 100644 lib/config/i2-config.hpp create mode 100644 lib/config/objectrule.cpp create mode 100644 lib/config/objectrule.hpp create mode 100644 lib/config/vmops.hpp create mode 100644 lib/db_ido/CMakeLists.txt create mode 100644 lib/db_ido/commanddbobject.cpp create mode 100644 lib/db_ido/commanddbobject.hpp create mode 100644 lib/db_ido/db_ido-itl.conf create mode 100644 lib/db_ido/dbconnection.cpp create mode 100644 lib/db_ido/dbconnection.hpp create mode 100644 lib/db_ido/dbconnection.ti create mode 100644 lib/db_ido/dbevents.cpp create mode 100644 lib/db_ido/dbevents.hpp create mode 100644 lib/db_ido/dbobject.cpp create mode 100644 lib/db_ido/dbobject.hpp create mode 100644 lib/db_ido/dbquery.cpp create mode 100644 lib/db_ido/dbquery.hpp create mode 100644 lib/db_ido/dbreference.cpp create mode 100644 lib/db_ido/dbreference.hpp create mode 100644 lib/db_ido/dbtype.cpp create mode 100644 lib/db_ido/dbtype.hpp create mode 100644 lib/db_ido/dbvalue.cpp create mode 100644 lib/db_ido/dbvalue.hpp create mode 100644 lib/db_ido/endpointdbobject.cpp create mode 100644 lib/db_ido/endpointdbobject.hpp create mode 100644 lib/db_ido/hostdbobject.cpp create mode 100644 lib/db_ido/hostdbobject.hpp create mode 100644 lib/db_ido/hostgroupdbobject.cpp create mode 100644 lib/db_ido/hostgroupdbobject.hpp create mode 100644 lib/db_ido/i2-db_ido.hpp create mode 100644 lib/db_ido/idochecktask.cpp create mode 100644 lib/db_ido/idochecktask.hpp create mode 100644 lib/db_ido/servicedbobject.cpp create mode 100644 lib/db_ido/servicedbobject.hpp create mode 100644 lib/db_ido/servicegroupdbobject.cpp create mode 100644 lib/db_ido/servicegroupdbobject.hpp create mode 100644 lib/db_ido/timeperioddbobject.cpp create mode 100644 lib/db_ido/timeperioddbobject.hpp create mode 100644 lib/db_ido/userdbobject.cpp create mode 100644 lib/db_ido/userdbobject.hpp create mode 100644 lib/db_ido/usergroupdbobject.cpp create mode 100644 lib/db_ido/usergroupdbobject.hpp create mode 100644 lib/db_ido/zonedbobject.cpp create mode 100644 lib/db_ido/zonedbobject.hpp create mode 100644 lib/db_ido_mysql/CMakeLists.txt create mode 100644 lib/db_ido_mysql/idomysqlconnection.cpp create mode 100644 lib/db_ido_mysql/idomysqlconnection.hpp create mode 100644 lib/db_ido_mysql/idomysqlconnection.ti create mode 100644 lib/db_ido_mysql/schema/mysql.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.0.2.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.1.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.11.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.12.7.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.13.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.13.3.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.2.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.3.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.4.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.5.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.6.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.8.0.sql create mode 100644 lib/db_ido_mysql/schema/upgrade/2.8.1.sql create mode 100644 lib/db_ido_pgsql/CMakeLists.txt create mode 100644 lib/db_ido_pgsql/idopgsqlconnection.cpp create mode 100644 lib/db_ido_pgsql/idopgsqlconnection.hpp create mode 100644 lib/db_ido_pgsql/idopgsqlconnection.ti create mode 100644 lib/db_ido_pgsql/schema/pgsql.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.0.2.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.1.0.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.2.0.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.3.0.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.4.0.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.5.0.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.6.0.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.8.0.sql create mode 100644 lib/db_ido_pgsql/schema/upgrade/2.8.1.sql create mode 100644 lib/icinga/CMakeLists.txt create mode 100644 lib/icinga/apiactions.cpp create mode 100644 lib/icinga/apiactions.hpp create mode 100644 lib/icinga/apievents.cpp create mode 100644 lib/icinga/apievents.hpp create mode 100644 lib/icinga/checkable-check.cpp create mode 100644 lib/icinga/checkable-comment.cpp create mode 100644 lib/icinga/checkable-dependency.cpp create mode 100644 lib/icinga/checkable-downtime.cpp create mode 100644 lib/icinga/checkable-event.cpp create mode 100644 lib/icinga/checkable-flapping.cpp create mode 100644 lib/icinga/checkable-notification.cpp create mode 100644 lib/icinga/checkable-script.cpp create mode 100644 lib/icinga/checkable.cpp create mode 100644 lib/icinga/checkable.hpp create mode 100644 lib/icinga/checkable.ti create mode 100644 lib/icinga/checkcommand.cpp create mode 100644 lib/icinga/checkcommand.hpp create mode 100644 lib/icinga/checkcommand.ti create mode 100644 lib/icinga/checkresult.cpp create mode 100644 lib/icinga/checkresult.hpp create mode 100644 lib/icinga/checkresult.ti create mode 100644 lib/icinga/cib.cpp create mode 100644 lib/icinga/cib.hpp create mode 100644 lib/icinga/clusterevents-check.cpp create mode 100644 lib/icinga/clusterevents.cpp create mode 100644 lib/icinga/clusterevents.hpp create mode 100644 lib/icinga/command.cpp create mode 100644 lib/icinga/command.hpp create mode 100644 lib/icinga/command.ti create mode 100644 lib/icinga/comment.cpp create mode 100644 lib/icinga/comment.hpp create mode 100644 lib/icinga/comment.ti create mode 100644 lib/icinga/compatutility.cpp create mode 100644 lib/icinga/compatutility.hpp create mode 100644 lib/icinga/customvarobject.cpp create mode 100644 lib/icinga/customvarobject.hpp create mode 100644 lib/icinga/customvarobject.ti create mode 100644 lib/icinga/dependency-apply.cpp create mode 100644 lib/icinga/dependency.cpp create mode 100644 lib/icinga/dependency.hpp create mode 100644 lib/icinga/dependency.ti create mode 100644 lib/icinga/downtime.cpp create mode 100644 lib/icinga/downtime.hpp create mode 100644 lib/icinga/downtime.ti create mode 100644 lib/icinga/eventcommand.cpp create mode 100644 lib/icinga/eventcommand.hpp create mode 100644 lib/icinga/eventcommand.ti create mode 100644 lib/icinga/externalcommandprocessor.cpp create mode 100644 lib/icinga/externalcommandprocessor.hpp create mode 100644 lib/icinga/host.cpp create mode 100644 lib/icinga/host.hpp create mode 100644 lib/icinga/host.ti create mode 100644 lib/icinga/hostgroup.cpp create mode 100644 lib/icinga/hostgroup.hpp create mode 100644 lib/icinga/hostgroup.ti create mode 100644 lib/icinga/i2-icinga.hpp create mode 100644 lib/icinga/icinga-itl.conf create mode 100644 lib/icinga/icingaapplication.cpp create mode 100644 lib/icinga/icingaapplication.hpp create mode 100644 lib/icinga/icingaapplication.ti create mode 100644 lib/icinga/legacytimeperiod.cpp create mode 100644 lib/icinga/legacytimeperiod.hpp create mode 100644 lib/icinga/macroprocessor.cpp create mode 100644 lib/icinga/macroprocessor.hpp create mode 100644 lib/icinga/macroresolver.hpp create mode 100644 lib/icinga/notification-apply.cpp create mode 100644 lib/icinga/notification.cpp create mode 100644 lib/icinga/notification.hpp create mode 100644 lib/icinga/notification.ti create mode 100644 lib/icinga/notificationcommand.cpp create mode 100644 lib/icinga/notificationcommand.hpp create mode 100644 lib/icinga/notificationcommand.ti create mode 100644 lib/icinga/objectutils.cpp create mode 100644 lib/icinga/objectutils.hpp create mode 100644 lib/icinga/pluginutility.cpp create mode 100644 lib/icinga/pluginutility.hpp create mode 100644 lib/icinga/scheduleddowntime-apply.cpp create mode 100644 lib/icinga/scheduleddowntime.cpp create mode 100644 lib/icinga/scheduleddowntime.hpp create mode 100644 lib/icinga/scheduleddowntime.ti create mode 100644 lib/icinga/service-apply.cpp create mode 100644 lib/icinga/service.cpp create mode 100644 lib/icinga/service.hpp create mode 100644 lib/icinga/service.ti create mode 100644 lib/icinga/servicegroup.cpp create mode 100644 lib/icinga/servicegroup.hpp create mode 100644 lib/icinga/servicegroup.ti create mode 100644 lib/icinga/timeperiod.cpp create mode 100644 lib/icinga/timeperiod.hpp create mode 100644 lib/icinga/timeperiod.ti create mode 100644 lib/icinga/user.cpp create mode 100644 lib/icinga/user.hpp create mode 100644 lib/icinga/user.ti create mode 100644 lib/icinga/usergroup.cpp create mode 100644 lib/icinga/usergroup.hpp create mode 100644 lib/icinga/usergroup.ti create mode 100644 lib/icingadb/CMakeLists.txt create mode 100644 lib/icingadb/icingadb-itl.conf create mode 100644 lib/icingadb/icingadb-objects.cpp create mode 100644 lib/icingadb/icingadb-stats.cpp create mode 100644 lib/icingadb/icingadb-utility.cpp create mode 100644 lib/icingadb/icingadb.cpp create mode 100644 lib/icingadb/icingadb.hpp create mode 100644 lib/icingadb/icingadb.ti create mode 100644 lib/icingadb/icingadbchecktask.cpp create mode 100644 lib/icingadb/icingadbchecktask.hpp create mode 100644 lib/icingadb/redisconnection.cpp create mode 100644 lib/icingadb/redisconnection.hpp create mode 100644 lib/livestatus/CMakeLists.txt create mode 100644 lib/livestatus/aggregator.cpp create mode 100644 lib/livestatus/aggregator.hpp create mode 100644 lib/livestatus/andfilter.cpp create mode 100644 lib/livestatus/andfilter.hpp create mode 100644 lib/livestatus/attributefilter.cpp create mode 100644 lib/livestatus/attributefilter.hpp create mode 100644 lib/livestatus/avgaggregator.cpp create mode 100644 lib/livestatus/avgaggregator.hpp create mode 100644 lib/livestatus/column.cpp create mode 100644 lib/livestatus/column.hpp create mode 100644 lib/livestatus/combinerfilter.cpp create mode 100644 lib/livestatus/combinerfilter.hpp create mode 100644 lib/livestatus/commandstable.cpp create mode 100644 lib/livestatus/commandstable.hpp create mode 100644 lib/livestatus/commentstable.cpp create mode 100644 lib/livestatus/commentstable.hpp create mode 100644 lib/livestatus/contactgroupstable.cpp create mode 100644 lib/livestatus/contactgroupstable.hpp create mode 100644 lib/livestatus/contactstable.cpp create mode 100644 lib/livestatus/contactstable.hpp create mode 100644 lib/livestatus/countaggregator.cpp create mode 100644 lib/livestatus/countaggregator.hpp create mode 100644 lib/livestatus/downtimestable.cpp create mode 100644 lib/livestatus/downtimestable.hpp create mode 100644 lib/livestatus/endpointstable.cpp create mode 100644 lib/livestatus/endpointstable.hpp create mode 100644 lib/livestatus/filter.hpp create mode 100644 lib/livestatus/historytable.hpp create mode 100644 lib/livestatus/hostgroupstable.cpp create mode 100644 lib/livestatus/hostgroupstable.hpp create mode 100644 lib/livestatus/hoststable.cpp create mode 100644 lib/livestatus/hoststable.hpp create mode 100644 lib/livestatus/i2-livestatus.hpp create mode 100644 lib/livestatus/invavgaggregator.cpp create mode 100644 lib/livestatus/invavgaggregator.hpp create mode 100644 lib/livestatus/invsumaggregator.cpp create mode 100644 lib/livestatus/invsumaggregator.hpp create mode 100644 lib/livestatus/livestatuslistener.cpp create mode 100644 lib/livestatus/livestatuslistener.hpp create mode 100644 lib/livestatus/livestatuslistener.ti create mode 100644 lib/livestatus/livestatuslogutility.cpp create mode 100644 lib/livestatus/livestatuslogutility.hpp create mode 100644 lib/livestatus/livestatusquery.cpp create mode 100644 lib/livestatus/livestatusquery.hpp create mode 100644 lib/livestatus/logtable.cpp create mode 100644 lib/livestatus/logtable.hpp create mode 100644 lib/livestatus/maxaggregator.cpp create mode 100644 lib/livestatus/maxaggregator.hpp create mode 100644 lib/livestatus/minaggregator.cpp create mode 100644 lib/livestatus/minaggregator.hpp create mode 100644 lib/livestatus/negatefilter.cpp create mode 100644 lib/livestatus/negatefilter.hpp create mode 100644 lib/livestatus/orfilter.cpp create mode 100644 lib/livestatus/orfilter.hpp create mode 100644 lib/livestatus/servicegroupstable.cpp create mode 100644 lib/livestatus/servicegroupstable.hpp create mode 100644 lib/livestatus/servicestable.cpp create mode 100644 lib/livestatus/servicestable.hpp create mode 100644 lib/livestatus/statehisttable.cpp create mode 100644 lib/livestatus/statehisttable.hpp create mode 100644 lib/livestatus/statustable.cpp create mode 100644 lib/livestatus/statustable.hpp create mode 100644 lib/livestatus/stdaggregator.cpp create mode 100644 lib/livestatus/stdaggregator.hpp create mode 100644 lib/livestatus/sumaggregator.cpp create mode 100644 lib/livestatus/sumaggregator.hpp create mode 100644 lib/livestatus/table.cpp create mode 100644 lib/livestatus/table.hpp create mode 100644 lib/livestatus/timeperiodstable.cpp create mode 100644 lib/livestatus/timeperiodstable.hpp create mode 100644 lib/livestatus/zonestable.cpp create mode 100644 lib/livestatus/zonestable.hpp create mode 100644 lib/methods/CMakeLists.txt create mode 100644 lib/methods/clusterchecktask.cpp create mode 100644 lib/methods/clusterchecktask.hpp create mode 100644 lib/methods/clusterzonechecktask.cpp create mode 100644 lib/methods/clusterzonechecktask.hpp create mode 100644 lib/methods/dummychecktask.cpp create mode 100644 lib/methods/dummychecktask.hpp create mode 100644 lib/methods/exceptionchecktask.cpp create mode 100644 lib/methods/exceptionchecktask.hpp create mode 100644 lib/methods/i2-methods.hpp create mode 100644 lib/methods/icingachecktask.cpp create mode 100644 lib/methods/icingachecktask.hpp create mode 100644 lib/methods/methods-itl.conf create mode 100644 lib/methods/nullchecktask.cpp create mode 100644 lib/methods/nullchecktask.hpp create mode 100644 lib/methods/nulleventtask.cpp create mode 100644 lib/methods/nulleventtask.hpp create mode 100644 lib/methods/pluginchecktask.cpp create mode 100644 lib/methods/pluginchecktask.hpp create mode 100644 lib/methods/plugineventtask.cpp create mode 100644 lib/methods/plugineventtask.hpp create mode 100644 lib/methods/pluginnotificationtask.cpp create mode 100644 lib/methods/pluginnotificationtask.hpp create mode 100644 lib/methods/randomchecktask.cpp create mode 100644 lib/methods/randomchecktask.hpp create mode 100644 lib/methods/sleepchecktask.cpp create mode 100644 lib/methods/sleepchecktask.hpp create mode 100644 lib/methods/timeperiodtask.cpp create mode 100644 lib/methods/timeperiodtask.hpp create mode 100644 lib/mysql_shim/CMakeLists.txt create mode 100644 lib/mysql_shim/mysql_shim.def create mode 100644 lib/mysql_shim/mysqlinterface.cpp create mode 100644 lib/mysql_shim/mysqlinterface.hpp create mode 100644 lib/notification/CMakeLists.txt create mode 100644 lib/notification/notificationcomponent.cpp create mode 100644 lib/notification/notificationcomponent.hpp create mode 100644 lib/notification/notificationcomponent.ti create mode 100644 lib/perfdata/CMakeLists.txt create mode 100644 lib/perfdata/elasticsearchwriter.cpp create mode 100644 lib/perfdata/elasticsearchwriter.hpp create mode 100644 lib/perfdata/elasticsearchwriter.ti create mode 100644 lib/perfdata/gelfwriter.cpp create mode 100644 lib/perfdata/gelfwriter.hpp create mode 100644 lib/perfdata/gelfwriter.ti create mode 100644 lib/perfdata/graphitewriter.cpp create mode 100644 lib/perfdata/graphitewriter.hpp create mode 100644 lib/perfdata/graphitewriter.ti create mode 100644 lib/perfdata/influxdb2writer.cpp create mode 100644 lib/perfdata/influxdb2writer.hpp create mode 100644 lib/perfdata/influxdb2writer.ti create mode 100644 lib/perfdata/influxdbcommonwriter.cpp create mode 100644 lib/perfdata/influxdbcommonwriter.hpp create mode 100644 lib/perfdata/influxdbcommonwriter.ti create mode 100644 lib/perfdata/influxdbwriter.cpp create mode 100644 lib/perfdata/influxdbwriter.hpp create mode 100644 lib/perfdata/influxdbwriter.ti create mode 100644 lib/perfdata/opentsdbwriter.cpp create mode 100644 lib/perfdata/opentsdbwriter.hpp create mode 100644 lib/perfdata/opentsdbwriter.ti create mode 100644 lib/perfdata/perfdatawriter.cpp create mode 100644 lib/perfdata/perfdatawriter.hpp create mode 100644 lib/perfdata/perfdatawriter.ti create mode 100644 lib/pgsql_shim/CMakeLists.txt create mode 100644 lib/pgsql_shim/pgsql_shim.def create mode 100644 lib/pgsql_shim/pgsqlinterface.cpp create mode 100644 lib/pgsql_shim/pgsqlinterface.hpp create mode 100644 lib/remote/CMakeLists.txt create mode 100644 lib/remote/actionshandler.cpp create mode 100644 lib/remote/actionshandler.hpp create mode 100644 lib/remote/apiaction.cpp create mode 100644 lib/remote/apiaction.hpp create mode 100644 lib/remote/apifunction.cpp create mode 100644 lib/remote/apifunction.hpp create mode 100644 lib/remote/apilistener-authority.cpp create mode 100644 lib/remote/apilistener-configsync.cpp create mode 100644 lib/remote/apilistener-filesync.cpp create mode 100644 lib/remote/apilistener.cpp create mode 100644 lib/remote/apilistener.hpp create mode 100644 lib/remote/apilistener.ti create mode 100644 lib/remote/apiuser.cpp create mode 100644 lib/remote/apiuser.hpp create mode 100644 lib/remote/apiuser.ti create mode 100644 lib/remote/configfileshandler.cpp create mode 100644 lib/remote/configfileshandler.hpp create mode 100644 lib/remote/configobjectutility.cpp create mode 100644 lib/remote/configobjectutility.hpp create mode 100644 lib/remote/configpackageshandler.cpp create mode 100644 lib/remote/configpackageshandler.hpp create mode 100644 lib/remote/configpackageutility.cpp create mode 100644 lib/remote/configpackageutility.hpp create mode 100644 lib/remote/configstageshandler.cpp create mode 100644 lib/remote/configstageshandler.hpp create mode 100644 lib/remote/consolehandler.cpp create mode 100644 lib/remote/consolehandler.hpp create mode 100644 lib/remote/createobjecthandler.cpp create mode 100644 lib/remote/createobjecthandler.hpp create mode 100644 lib/remote/deleteobjecthandler.cpp create mode 100644 lib/remote/deleteobjecthandler.hpp create mode 100644 lib/remote/endpoint.cpp create mode 100644 lib/remote/endpoint.hpp create mode 100644 lib/remote/endpoint.ti create mode 100644 lib/remote/eventqueue.cpp create mode 100644 lib/remote/eventqueue.hpp create mode 100644 lib/remote/eventshandler.cpp create mode 100644 lib/remote/eventshandler.hpp create mode 100644 lib/remote/filterutility.cpp create mode 100644 lib/remote/filterutility.hpp create mode 100644 lib/remote/httphandler.cpp create mode 100644 lib/remote/httphandler.hpp create mode 100644 lib/remote/httpserverconnection.cpp create mode 100644 lib/remote/httpserverconnection.hpp create mode 100644 lib/remote/httputility.cpp create mode 100644 lib/remote/httputility.hpp create mode 100644 lib/remote/i2-remote.hpp create mode 100644 lib/remote/infohandler.cpp create mode 100644 lib/remote/infohandler.hpp create mode 100644 lib/remote/jsonrpc.cpp create mode 100644 lib/remote/jsonrpc.hpp create mode 100644 lib/remote/jsonrpcconnection-heartbeat.cpp create mode 100644 lib/remote/jsonrpcconnection-pki.cpp create mode 100644 lib/remote/jsonrpcconnection.cpp create mode 100644 lib/remote/jsonrpcconnection.hpp create mode 100644 lib/remote/messageorigin.cpp create mode 100644 lib/remote/messageorigin.hpp create mode 100644 lib/remote/modifyobjecthandler.cpp create mode 100644 lib/remote/modifyobjecthandler.hpp create mode 100644 lib/remote/objectqueryhandler.cpp create mode 100644 lib/remote/objectqueryhandler.hpp create mode 100644 lib/remote/pkiutility.cpp create mode 100644 lib/remote/pkiutility.hpp create mode 100644 lib/remote/statushandler.cpp create mode 100644 lib/remote/statushandler.hpp create mode 100644 lib/remote/templatequeryhandler.cpp create mode 100644 lib/remote/templatequeryhandler.hpp create mode 100644 lib/remote/typequeryhandler.cpp create mode 100644 lib/remote/typequeryhandler.hpp create mode 100644 lib/remote/url-characters.hpp create mode 100644 lib/remote/url.cpp create mode 100644 lib/remote/url.hpp create mode 100644 lib/remote/variablequeryhandler.cpp create mode 100644 lib/remote/variablequeryhandler.hpp create mode 100644 lib/remote/zone.cpp create mode 100644 lib/remote/zone.hpp create mode 100644 lib/remote/zone.ti (limited to 'lib') 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..f872a69 --- /dev/null +++ b/lib/base/CMakeLists.txt @@ -0,0 +1,149 @@ +# 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(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 + 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-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 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}) +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..dc032f4 --- /dev/null +++ b/lib/base/application.cpp @@ -0,0 +1,1227 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __linux__ +#include +#endif /* __linux__ */ +#ifdef _WIN32 +#include +#else /* _WIN32 */ +#include +#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 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; +double Application::m_LastReloadFailed; + +#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::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(); +#ifdef I2_DEBUG + exit(rc); +#else /* I2_DEBUG */ + _exit(rc); // Yay, our static destructors are pretty much beyond repair at this point. +#endif /* I2_DEBUG */ +} + +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(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 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(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(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; + 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; + 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() +{ + return m_LastReloadFailed; +} + +void Application::SetLastReloadFailed(double ts) +{ + m_LastReloadFailed = ts; +} + +void Application::ValidateName(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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..53d0b0c --- /dev/null +++ b/lib/base/application.hpp @@ -0,0 +1,160 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef APPLICATION_H +#define APPLICATION_H + +#include "base/i2-base.hpp" +#include "base/application-ti.hpp" +#include "base/logger.hpp" +#include "base/configuration.hpp" +#include + +namespace icinga +{ + +class ThreadPool; + +/** + * Abstract base class for applications. + * + * @ingroup base + */ +class Application : public ObjectImpl { +public: + DECLARE_OBJECT(Application); + + static boost::signals2::signal 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& 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; + static double m_LastReloadFailed; + +#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(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(vframe->Self); + REQUIRE_NOT_NULL(self); + self->Set(index, value); +} + +static Value ArrayGet(int index) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Array::Ptr self = static_cast(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(vframe->Self); + REQUIRE_NOT_NULL(self); + self->Add(value); +} + +static void ArrayRemove(int index) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Array::Ptr self = static_cast(vframe->Self); + REQUIRE_NOT_NULL(self); + self->Remove(index); +} + +static bool ArrayContains(const Value& value) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Array::Ptr self = static_cast(vframe->Self); + REQUIRE_NOT_NULL(self); + return self->Contains(value); +} + +static void ArrayClear() +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Array::Ptr self = static_cast(vframe->Self); + REQUIRE_NOT_NULL(self); + self->Clear(); +} + +static Array::Ptr ArraySort(const std::vector& args) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Array::Ptr self = static_cast(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(vframe->Self); + REQUIRE_NOT_NULL(self); + return self->ShallowClone(); +} + +static Value ArrayJoin(const Value& separator) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Array::Ptr self = static_cast(vframe->Self); + REQUIRE_NOT_NULL(self); + return self->Join(separator); +} + +static Array::Ptr ArrayReverse() +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Array::Ptr self = static_cast(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(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(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(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(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(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(vframe->Self); + REQUIRE_NOT_NULL(self); + return self->Unique(); +} + +static void ArrayFreeze() +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Array::Ptr self = static_cast(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; + +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 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(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 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(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(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 +#include +#include + +namespace icinga +{ + +typedef std::vector 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::iterator Iterator; + + typedef std::vector::size_type SizeType; + + Array() = default; + Array(const ArrayData& other); + Array(ArrayData&& other); + Array(std::initializer_list 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 + static Array::Ptr FromVector(const std::vector& v) + { + Array::Ptr result = new Array(); + ObjectLock olock(result); + std::copy(v.begin(), v.end(), std::back_inserter(result->m_Data)); + return result; + } + + template + std::set ToSet() + { + ObjectLock olock(this); + return std::set(Begin(), End()); + } + + template + static Array::Ptr FromSet(const std::set& 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 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; + +#endif /* ARRAY_H */ diff --git a/lib/base/atomic-file.cpp b/lib/base/atomic-file.cpp new file mode 100644 index 0000000..361a3d2 --- /dev/null +++ b/lib/base/atomic-file.cpp @@ -0,0 +1,116 @@ +/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */ + +#include "base/atomic-file.hpp" +#include "base/exception.hpp" +#include "base/utility.hpp" +#include + +#ifdef _WIN32 +# include +# include +#else /* _WIN32 */ +# include +# include +#endif /* _WIN32 */ + +using namespace icinga; + +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..5ad79d9 --- /dev/null +++ b/lib/base/atomic-file.hpp @@ -0,0 +1,34 @@ +/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */ + +#ifndef ATOMIC_FILE_H +#define ATOMIC_FILE_H + +#include "base/string.hpp" +#include +#include + +namespace icinga +{ + +/** + * Atomically replaces a file's content. + * + * @ingroup base + */ +class AtomicFile : public boost::iostreams::stream +{ +public: + AtomicFile(String path, int mode); + ~AtomicFile(); + + 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 +#include +#include +#include + +namespace icinga +{ + +/** + * Extends std::atomic with an atomic constructor. + * + * @ingroup base + */ +template +class Atomic : public std::atomic { +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-like interface that locks using a mutex. + * + * In contrast to std::atomic, Locked is also valid for types that are not trivially copyable. + * In case T is trivially copyable, std::atomic is almost certainly the better choice. + * + * @ingroup base + */ +template +class Locked +{ +public: + inline T load() const + { + std::unique_lock lock(m_Mutex); + + return m_Value; + } + + inline void store(T desired) + { + std::unique_lock lock(m_Mutex); + + m_Value = std::move(desired); + } + +private: + mutable std::mutex m_Mutex; + T m_Value; +}; + +/** + * Type alias for std::atomic if possible, otherwise Locked is used as a fallback. + * + * @ingroup base + */ +template +using AtomicOrLocked = +#if defined(__GNUC__) && __GNUC__ < 5 + // GCC does not implement std::is_trivially_copyable until version 5. + typename std::conditional::value || std::is_pointer::value, std::atomic, Locked>::type; +#else /* defined(__GNUC__) && __GNUC__ < 5 */ + typename std::conditional::value, std::atomic, Locked>::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 +#include +#include +#include + +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(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 +#include +#include +#include +#include +#include +#include + +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 Bulker +{ +private: + typedef std::chrono::steady_clock Clock; + +public: + typedef std::vector 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 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 m_Bulks; + TimePoint m_NextConsumption; +}; + +template +void Bulker::ProduceOne(T needle) +{ + std::unique_lock 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 +typename Bulker::Container Bulker::ConsumeMany() +{ + std::unique_lock 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 +typename Bulker::SizeType Bulker::Size() +{ + std::unique_lock 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..144afa4 --- /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 +#include +#include +#include + +using namespace icinga; + +REGISTER_TYPE_WITH_PROTOTYPE(ConfigObject, ConfigObject::GetPrototype()); + +boost::signals2::signal 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(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 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::size_type i = 1; i < tokens.size() - 1; i++) { + if (!current.IsObjectType()) + 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, ¤t)) { + current = new Dictionary(); + dict->Set(key, current); + } + } + + if (!current.IsObjectType()) + 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::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::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{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 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::size_type i = 1; i < tokens.size() - 1; i++) { + if (!current.IsObjectType()) + 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()) + 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 restoredAttrs; + + { + ObjectLock olock(original_attributes); + for (const auto& kv : original_attributes) { + std::vector originalTokens = String(kv.first).Split("."); + + if (tokens.size() > originalTokens.size()) + continue; + + bool match = true; + for (std::vector::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::size_type i = tokens.size() - 1; i < originalTokens.size() - 1; i++) { + dict = currentSub; + currentSub = dict->Get(originalTokens[i]); + + if (!currentSub.IsObjectType()) { + 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::Ptr type = static_pointer_cast >(GetReflectionType()); + type->RegisterObject(this); +} + +void ConfigObject::Unregister() +{ + ASSERT(!OwnsLock()); + + TypeImpl::Ptr type = static_pointer_cast >(GetReflectionType()); + type->UnregisterObject(this); +} + +void ConfigObject::Start(bool runtimeCreated) +{ + ObjectImpl::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::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(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(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(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 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(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& callback) +{ + for (const Type::Ptr& type : Type::GetAllTypes()) { + auto *dtype = dynamic_cast(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 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::size_type i = 1; i < tokens.size() - 1; i++) { + if (!current.IsObjectType()) + 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()) + 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(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 + +namespace icinga +{ + +class ConfigType; + +/** + * A dynamic object that can be instantiated from the configuration file. + * + * @ingroup base + */ +class ConfigObject : public ObjectImpl +{ +public: + DECLARE_OBJECT(ConfigObject); + + static boost::signals2::signal 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 + static intrusive_ptr GetObject(const String& name) + { + typedef TypeImpl ObjType; + auto *ptype = static_cast(T::TypeInstance.get()); + return static_pointer_cast(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& 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 GetByName(const String& name) \ + { \ + return ConfigObject::GetObject(name); \ + } + +} + +#endif /* CONFIGOBJECT_H */ diff --git a/lib/base/configobject.ti b/lib/base/configobject.ti new file mode 100644 index 0000000..19b7cde --- /dev/null +++ b/lib/base/configobject.ti @@ -0,0 +1,93 @@ +/* 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 +{ +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] 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; }}} + }; +}; + +} diff --git a/lib/base/configtype.cpp b/lib/base/configtype.cpp new file mode 100644 index 0000000..81f2c93 --- /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::unique_lock 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 lock(m_Mutex); + + auto it = m_ObjectMap.find(name); + + if (it != m_ObjectMap.end()) { + if (it->second == object) + return; + + auto *type = dynamic_cast(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 lock(m_Mutex); + + m_ObjectMap.erase(name); + m_ObjectVector.erase(std::remove(m_ObjectVector.begin(), m_ObjectVector.end(), object), m_ObjectVector.end()); + } +} + +std::vector ConfigType::GetObjects() const +{ + std::unique_lock lock(m_Mutex); + return m_ObjectVector; +} + +std::vector ConfigType::GetObjectsHelper(Type *type) +{ + return static_cast *>(type)->GetObjects(); +} + +int ConfigType::GetObjectCount() const +{ + std::unique_lock 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..5008cd7 --- /dev/null +++ b/lib/base/configtype.hpp @@ -0,0 +1,63 @@ +/* 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 + +namespace icinga +{ + +class ConfigObject; + +class ConfigType +{ +public: + virtual ~ConfigType(); + + intrusive_ptr GetObject(const String& name) const; + + void RegisterObject(const intrusive_ptr& object); + void UnregisterObject(const intrusive_ptr& object); + + std::vector > GetObjects() const; + + template + static TypeImpl *Get() + { + typedef TypeImpl ObjType; + return static_cast(T::TypeInstance.get()); + } + + template + static std::vector > GetObjectsByType() + { + std::vector > objects = GetObjectsHelper(T::TypeInstance.get()); + std::vector > result; + result.reserve(objects.size()); +for (const auto& object : objects) { + result.push_back(static_pointer_cast(object)); + } + return result; + } + + int GetObjectCount() const; + +private: + typedef std::map > ObjectMap; + typedef std::vector > ObjectVector; + + mutable std::mutex m_Mutex; + ObjectMap m_ObjectMap; + ObjectVector m_ObjectVector; + + static std::vector > GetObjectsHelper(Type *type); +}; + +} + +#endif /* CONFIGTYPE_H */ diff --git a/lib/base/configuration.cpp b/lib/base/configuration.cpp new file mode 100644 index 0000000..93996ba --- /dev/null +++ b/lib/base/configuration.cpp @@ -0,0 +1,377 @@ +/* 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{static_cast(std::thread::hardware_concurrency())}; +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 +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); +} + +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..5609065 --- /dev/null +++ b/lib/base/configuration.hpp @@ -0,0 +1,156 @@ +/* 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 +{ +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 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 +#include +#include +#include + +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 tokens = kv.first.Split("."); + + EmitIdentifier(fp, tokens[0], true); + + for (std::vector::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()) + EmitArray(fp, indentLevel, val); + else if (val.IsObjectType()) + EmitScope(fp, indentLevel, val); + else if (val.IsObjectType()) + EmitIdentifier(fp, static_cast(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 keywords; + static std::mutex mutex; + + { + std::unique_lock lock(mutex); + if (keywords.empty()) { + const std::vector& 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& ConfigWriter::GetKeywords() +{ + static std::vector keywords; + static std::mutex mutex; + std::unique_lock 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 + +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& 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 + +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 + +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..2cac7a8 --- /dev/null +++ b/lib/base/context.cpp @@ -0,0 +1,56 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "base/context.hpp" +#include +#include + +using namespace icinga; + +static boost::thread_specific_ptr > l_Frames; + +ContextFrame::ContextFrame(const String& message) +{ + GetFrames().push_front(message); +} + +ContextFrame::~ContextFrame() +{ + GetFrames().pop_front(); +} + +std::list& ContextFrame::GetFrames() +{ + if (!l_Frames.get()) + l_Frames.reset(new std::list()); + + return *l_Frames; +} + +ContextTrace::ContextTrace() + : m_Frames(ContextFrame::GetFrames()) +{ } + +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..0e4b92a --- /dev/null +++ b/lib/base/context.hpp @@ -0,0 +1,50 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef CONTEXT_H +#define CONTEXT_H + +#include "base/i2-base.hpp" +#include "base/string.hpp" +#include + +namespace icinga +{ + +class ContextTrace +{ +public: + ContextTrace(); + + void Print(std::ostream& fp) const; + + size_t GetLength() const; + +private: + std::list m_Frames; +}; + +std::ostream& operator<<(std::ostream& stream, const ContextTrace& trace); + +/** + * A context frame. + * + * @ingroup base + */ +class ContextFrame +{ +public: + ContextFrame(const String& message); + ~ContextFrame(); + +private: + static std::list& 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(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 +#include + +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()) + return static_cast(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 + +namespace icinga +{ + +/** + * Utility class for converting types. + * + * @ingroup base + */ +class Convert +{ +public: + template + static long ToLong(const T& val) + { + try { + return boost::lexical_cast(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 + static double ToDouble(const T& val) + { + try { + return boost::lexical_cast(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(val); + } + + static double ToDouble(const Value& val) + { + return val; + } + + static bool ToBool(const Value& val) + { + return val.ToBool(); + } + + template + static String ToString(const T& val) + { + return boost::lexical_cast(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(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& 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 + +namespace icinga +{ + +/** + * A date/time value. + * + * @ingroup base + */ +class DateTime final : public ObjectImpl +{ +public: + DECLARE_OBJECT(DateTime); + + DateTime(double value); + DateTime(const std::vector& 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 + +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 +#include + +namespace icinga +{ + +/** + * An action to be executed at end of scope. + * + * @ingroup base + */ +class Defer +{ +public: + inline + Defer(std::function 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 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 > DependencyGraph::m_Dependencies; + +void DependencyGraph::AddDependency(Object *parent, Object *child) +{ + std::unique_lock lock(m_Mutex); + m_Dependencies[child][parent]++; +} + +void DependencyGraph::RemoveDependency(Object *parent, Object *child) +{ + std::unique_lock 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 DependencyGraph::GetParents(const Object::Ptr& child) +{ + std::vector objects; + + std::unique_lock lock(m_Mutex); + auto it = m_Dependencies.find(child.get()); + + if (it != m_Dependencies.end()) { + typedef std::pair 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 +#include + +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 GetParents(const Object::Ptr& child); + +private: + DependencyGraph(); + + static std::mutex m_Mutex; + static std::map > 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(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(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(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(vframe->Self); + REQUIRE_NOT_NULL(self); + self->Remove(key); +} + +static void DictionaryClear() +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Dictionary::Ptr self = static_cast(vframe->Self); + REQUIRE_NOT_NULL(self); + self->Clear(); +} + +static bool DictionaryContains(const String& key) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Dictionary::Ptr self = static_cast(vframe->Self); + REQUIRE_NOT_NULL(self); + return self->Contains(key); +} + +static Dictionary::Ptr DictionaryShallowClone() +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Dictionary::Ptr self = static_cast(vframe->Self); + REQUIRE_NOT_NULL(self); + return self->ShallowClone(); +} + +static Array::Ptr DictionaryKeys() +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Dictionary::Ptr self = static_cast(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(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(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..435df31 --- /dev/null +++ b/lib/base/dictionary.cpp @@ -0,0 +1,303 @@ +/* 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 + +using namespace icinga; + +template class std::map; + +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 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 +{ + ObjectLock olock(this); + + 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 +{ + ObjectLock olock(this); + + auto it = m_Data.find(key); + + if (it == m_Data.end()) + return false; + + *result = it->second; + return true; +} + +/** + * 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); + + 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 +{ + ObjectLock olock(this); + + 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 +{ + ObjectLock olock(this); + + 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. + * @param overrideFrozen Whether to allow modifying frozen dictionaries. + */ +void Dictionary::Remove(Dictionary::Iterator it, bool overrideFrozen) +{ + ASSERT(OwnsLock()); + + if (m_Frozen && !overrideFrozen) + 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. + * @param overrideFrozen Whether to allow modifying frozen dictionaries. + */ +void Dictionary::Remove(const String& key, bool overrideFrozen) +{ + ObjectLock olock(this); + + if (m_Frozen && !overrideFrozen) + 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. + * + * @param overrideFrozen Whether to allow modifying frozen dictionaries. + */ +void Dictionary::Clear(bool overrideFrozen) +{ + ObjectLock olock(this); + + if (m_Frozen && !overrideFrozen) + BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionary must not be modified.")); + + m_Data.clear(); +} + +void Dictionary::CopyTo(const Dictionary::Ptr& dest) const +{ + ObjectLock olock(this); + + 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; + + { + ObjectLock olock(this); + + 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 Dictionary::GetKeys() const +{ + ObjectLock olock(this); + + std::vector 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(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(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..2278687 --- /dev/null +++ b/lib/base/dictionary.hpp @@ -0,0 +1,88 @@ +/* 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 +#include +#include + +namespace icinga +{ + +typedef std::vector > 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::iterator Iterator; + + typedef std::map::size_type SizeType; + + typedef std::map::value_type Pair; + + Dictionary() = default; + Dictionary(const DictionaryData& other); + Dictionary(DictionaryData&& other); + Dictionary(std::initializer_list init); + + Value Get(const String& key) const; + bool Get(const String& key, Value *result) 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, bool overrideFrozen = false); + + void Remove(Iterator it, bool overrideFrozen = false); + + void Clear(bool overrideFrozen = false); + + void CopyTo(const Dictionary::Ptr& dest) const; + Dictionary::Ptr ShallowClone() const; + + std::vector 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 m_Data; /**< The data for the dictionary. */ + bool m_Frozen{false}; +}; + +Dictionary::Iterator begin(const Dictionary::Ptr& x); +Dictionary::Iterator end(const Dictionary::Ptr& x); + +} + +extern template class std::map; + +#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 +#include + +#ifdef _WIN32 +# include "base/utility.hpp" +#endif /* _WIN32 */ + +#ifdef HAVE_CXXABI_H +# include +#endif /* HAVE_CXXABI_H */ + +using namespace icinga; + +static boost::thread_specific_ptr l_LastExceptionStack; +static boost::thread_specific_ptr 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(src); + const auto *dstInfo = static_cast(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 l_LastExceptionObj; +static boost::thread_specific_ptr l_LastExceptionPvtInfo; + +typedef void (*DestCallback)(void *); +static boost::thread_specific_ptr 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(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(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(*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(*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(&ex); + if (win32_err) { + message = to_string(*win32_err); + } +#endif /* _WIN32 */ + + const auto *vex = dynamic_cast(&ex); + + if (message.IsEmpty()) + result << boost::diagnostic_information(ex) << "\n"; + else + result << "Error: " << message << "\n"; + + const auto *dex = dynamic_cast(&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(&ex); + const auto *pex = dynamic_cast(&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(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(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(*this); + + if (func) + msgbuf << "Function call '" << *func << "'"; + else + msgbuf << "Function call"; + + const std::string *fname = boost::get_error_info(*this); + + if (fname) + msgbuf << " for file '" << *fname << "'"; + + msgbuf << " failed"; + + const int *errnum = boost::get_error_info(*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& 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 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(e); + + if (func) { + msgbuf << "Function call '" << *func << "'"; + } else { + msgbuf << "Function call"; + } + + const std::string *fname = boost::get_error_info(e); + + if (fname) { + msgbuf << " for file '" << *fname << "'"; + } + + msgbuf << " failed"; + + const int *errnum = boost::get_error_info(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 +#include +#include +#include +#include +#include + +#ifdef _WIN32 +# include +#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& attributePath, const String& message); + ~ValidationError() throw() override; + + const char *what() const throw() override; + + ConfigObject::Ptr GetObject() const; + std::vector GetAttributePath() const; + String GetMessage() const; + + void SetDebugHint(const Dictionary::Ptr& dhint); + Dictionary::Ptr GetDebugHint() const; + +private: + ConfigObject::Ptr m_Object; + std::vector 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 StackTraceErrorInfo; + +std::string to_string(const StackTraceErrorInfo&); + +typedef boost::error_info 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 errinfo_win32_error; + +std::string to_string(const errinfo_win32_error& e); +#endif /* _WIN32 */ + +struct errinfo_getaddrinfo_error_; +typedef boost::error_info errinfo_getaddrinfo_error; + +std::string to_string(const errinfo_getaddrinfo_error& e); + +struct errinfo_message_; +typedef boost::error_info 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(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 + +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()) { + 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::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 +{ +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& 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(vframe->Self); + REQUIRE_NOT_NULL(self); + + std::vector 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(vframe->Self); + REQUIRE_NOT_NULL(self); + + std::vector uargs; + + { + ObjectLock olock(args); + uargs = std::vector(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& 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& arguments) +{ + ScriptFrame frame(false); + return m_Callback(arguments); +} + +Value Function::InvokeThis(const Value& otherThis, const std::vector& arguments) +{ + ScriptFrame frame(false, otherThis); + return m_Callback(arguments); +} + +Object::Ptr Function::Clone() const +{ + return const_cast(this); +} diff --git a/lib/base/function.hpp b/lib/base/function.hpp new file mode 100644 index 0000000..e0067d2 --- /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 + +namespace icinga +{ + +/** + * A script function that can be used to execute a script task. + * + * @ingroup base + */ +class Function final : public ObjectImpl +{ +public: + DECLARE_OBJECT(Function); + + typedef std::function& arguments)> Callback; + + template + Function(const String& name, F function, const std::vector& args = std::vector(), + bool side_effect_free = false, bool deprecated = false) + : Function(name, WrapFunction(function), args, side_effect_free, deprecated) + { } + + Value Invoke(const std::vector& arguments = std::vector()); + Value InvokeThis(const Value& otherThis, const std::vector& arguments = std::vector()); + + 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& 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->SetAttribute(#name, new ConstEmbeddedNamespaceValue(sf)); \ + }, 10) + +#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->SetAttribute(#name, new ConstEmbeddedNamespaceValue(sf)); \ + }, 10) + +#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->SetAttribute(#name, new EmbeddedNamespaceValue(sf)); \ + }, 10) + +#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, new EmbeddedNamespaceValue(sf)); \ + }, 10) + +} + +#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 +#include +#include +#include +#include + +namespace icinga +{ + +template +typename std::enable_if< + std::is_class::value && + std::is_same::type, Value>::value && + boost::function_types::function_arity::value == 2, + std::function&)>>::type +WrapFunction(FuncType function) +{ + static_assert(std::is_same, 1>::type, const std::vector&>::value, "Argument type must be const std::vector"); + return function; +} + +inline std::function&)> WrapFunction(void (*function)(const std::vector&)) +{ + return [function](const std::vector& arguments) { + function(arguments); + return Empty; + }; +} + +template +std::function&)> WrapFunction(Return (*function)(const std::vector&)) +{ + return [function](const std::vector& values) -> Value { return function(values); }; +} + +template +struct indices { + using next = indices; +}; + +template +struct build_indices { + using type = typename build_indices::type::next; +}; + +template <> +struct build_indices<0> { + using type = indices<>; +}; + +template +using BuildIndices = typename build_indices::type; + +struct UnpackCaller +{ +private: + template + auto Invoke(FuncType f, const std::vector& args, indices) -> decltype(f(args[I]...)) + { + return f(args[I]...); + } + +public: + template + auto operator() (FuncType f, const std::vector& args) -> decltype(Invoke(f, args, BuildIndices{})) + { + return Invoke(f, args, BuildIndices{}); + } +}; + +template +struct FunctionWrapper +{ + static Value Invoke(FuncType function, const std::vector& arguments) + { + return UnpackCaller().operator()(function, arguments); + } +}; + +template +struct FunctionWrapper +{ + static Value Invoke(FuncType function, const std::vector& arguments) + { + UnpackCaller().operator()(function, arguments); + return Empty; + } +}; + +template +typename std::enable_if< + std::is_function::type>::value && !std::is_same&)>::value, + std::function&)>>::type +WrapFunction(FuncType function) +{ + return [function](const std::vector& arguments) { + constexpr size_t arity = boost::function_types::function_arity::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()(*static_cast(nullptr), std::vector())); + + return FunctionWrapper::Invoke(function, arguments); + }; +} + +template +typename std::enable_if< + std::is_class::value && + !(std::is_same::type, Value>::value && + boost::function_types::function_arity::value == 2), + std::function&)>>::type +WrapFunction(FuncType function) +{ + static_assert(!std::is_same, 1>::type, const std::vector&>::value, "Argument type must be const std::vector"); + + using FuncTypeInvoker = decltype(&FuncType::operator()); + + return [function](const std::vector& arguments) { + constexpr size_t arity = boost::function_types::function_arity::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()(*static_cast(nullptr), std::vector())); + + return FunctionWrapper::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: + * + * + * 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. + * + * 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. + * + */ + +/** + * @defgroup base Base class library + * + * The base class library implements commonly-used functionality like + * event handling for sockets and timers. + */ + +#include + +#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 +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#if defined(__APPLE__) && defined(__MACH__) +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +#define BOOST_BIND_NO_PLACEHOLDERS + +#include + +#endif /* I2BASE_H */ diff --git a/lib/base/initialize.cpp b/lib/base/initialize.cpp new file mode 100644 index 0000000..23373cc --- /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(void (*func)(), int 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..0629abe --- /dev/null +++ b/lib/base/initialize.hpp @@ -0,0 +1,29 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef INITIALIZE_H +#define INITIALIZE_H + +#include "base/i2-base.hpp" + +namespace icinga +{ + +#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(void (*func)(), int priority = 0); + +#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..c00bd78 --- /dev/null +++ b/lib/base/io-engine.cpp @@ -0,0 +1,154 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "base/exception.hpp" +#include "base/io-engine.hpp" +#include "base/lazy-init.hpp" +#include "base/logger.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +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> IoEngine::m_Instance ([]() { return std::unique_ptr(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(std::thread::hardware_concurrency() * 2u)), m_AlreadyExpiredTimer(m_IoContext) +{ + m_AlreadyExpiredTimer.expires_at(boost::posix_time::neg_infin); + m_CpuBoundSemaphore.store(std::thread::hardware_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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 + 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> m_Instance; + + boost::asio::io_context m_IoContext; + boost::asio::executor_work_guard m_KeepAlive; + std::vector 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 + 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 m_Cancelled; +}; + +} + +#endif /* IO_ENGINE_H */ diff --git a/lib/base/json-script.cpp b/lib/base/json-script.cpp new file mode 100644 index 0000000..54d6631 --- /dev/null +++ b/lib/base/json-script.cpp @@ -0,0 +1,29 @@ +/* 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([]() { + auto jsonNSBehavior = new ConstNamespaceBehavior(); + Namespace::Ptr jsonNS = new Namespace(jsonNSBehavior); + + /* Methods */ + jsonNS->Set("encode", new Function("Json#encode", JsonEncodeShim, { "value" }, true)); + jsonNS->Set("decode", new Function("Json#decode", JsonDecode, { "value" }, true)); + + jsonNSBehavior->Freeze(); + + Namespace::Ptr systemNS = ScriptGlobal::Get("System"); + systemNS->SetAttribute("Json", new ConstEmbeddedNamespaceValue(jsonNS)); +}); diff --git a/lib/base/json.cpp b/lib/base/json.cpp new file mode 100644 index 0000000..6937f0a --- /dev/null +++ b/lib/base/json.cpp @@ -0,0 +1,516 @@ +/* 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 +#include +#include +#include +#include +#include +#include + +using namespace icinga; + +class JsonSax : public nlohmann::json_sax +{ +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 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> 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 +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 m_Result; + String m_CurrentKey; + std::stack> m_CurrentSubtree; + + void AppendChar(char c); + + template + void AppendChars(Iterator begin, Iterator end); + + void AppendJson(nlohmann::json json); + + void BeforeItem(); + + void FinishContainer(char terminator); +}; + +template +void Encode(JsonEncoder& stateMachine, const Value& value); + +template +inline +void EncodeNamespace(JsonEncoder& 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->Get()); + } + + stateMachine.EndObject(); +} + +template +inline +void EncodeDictionary(JsonEncoder& 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 +inline +void EncodeArray(JsonEncoder& stateMachine, const Array::Ptr& arr) +{ + stateMachine.StartArray(); + + ObjectLock olock(arr); + for (const Value& value : arr) { + Encode(stateMachine, value); + } + + stateMachine.EndArray(); +} + +template +void Encode(JsonEncoder& stateMachine, const Value& value) +{ + switch (value.GetType()) { + case ValueNumber: + stateMachine.NumberFloat(value.Get()); + break; + + case ValueBoolean: + stateMachine.Boolean(value.ToBool()); + break; + + case ValueString: + stateMachine.Strng(Utility::ValidateUTF8(value.Get())); + break; + + case ValueObject: + { + const Object::Ptr& obj = value.Get(); + + { + Namespace::Ptr ns = dynamic_pointer_cast(obj); + if (ns) { + EncodeNamespace(stateMachine, ns); + break; + } + } + + { + Dictionary::Ptr dict = dynamic_pointer_cast(obj); + if (dict) { + EncodeDictionary(stateMachine, dict); + break; + } + } + + { + Array::Ptr arr = dynamic_pointer_cast(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 stateMachine; + + Encode(stateMachine, value); + + return stateMachine.GetResult() + "\n"; + } else { + JsonEncoder 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::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 +inline +void JsonEncoder::Null() +{ + BeforeItem(); + AppendChars((const char*)l_Null, (const char*)l_Null + 4); +} + +template +inline +void JsonEncoder::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 +inline +void JsonEncoder::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 +inline +void JsonEncoder::Strng(String value) +{ + BeforeItem(); + AppendJson(std::move(value)); +} + +template +inline +void JsonEncoder::StartObject() +{ + BeforeItem(); + AppendChar('{'); + + m_CurrentSubtree.push(2); +} + +template +inline +void JsonEncoder::Key(String value) +{ + m_CurrentKey = std::move(value); +} + +template +inline +void JsonEncoder::EndObject() +{ + FinishContainer('}'); +} + +template +inline +void JsonEncoder::StartArray() +{ + BeforeItem(); + AppendChar('['); + + m_CurrentSubtree.push(0); +} + +template +inline +void JsonEncoder::EndArray() +{ + FinishContainer(']'); +} + +template +inline +String JsonEncoder::GetResult() +{ + return String(m_Result.begin(), m_Result.end()); +} + +template +inline +void JsonEncoder::AppendChar(char c) +{ + m_Result.emplace_back(c); +} + +template +template +inline +void JsonEncoder::AppendChars(Iterator begin, Iterator end) +{ + m_Result.insert(m_Result.end(), begin, end); +} + +template +inline +void JsonEncoder::AppendJson(nlohmann::json json) +{ + nlohmann::detail::serializer(nlohmann::detail::output_adapter(m_Result), ' ').dump(std::move(json), prettyPrint, true, 0); +} + +template +inline +void JsonEncoder::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 +inline +void JsonEncoder::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 +#include +#include +#include + +namespace icinga +{ + +/** + * Lazy object initialization abstraction inspired from + * . + * + * @ingroup base + */ +template +class LazyInit +{ +public: + inline + LazyInit(std::function 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 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 m_Initializer; + std::mutex m_Mutex; + std::atomic 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 + +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 + T GetSymbolAddress(const String& name) const + { + static_assert(!std::is_same::value, "T must not be void *"); + + return reinterpret_cast(GetSymbolAddress(name)); + } + +private: + std::shared_ptr m_Handle; +}; + +} + +#endif /* LIBRARY_H */ diff --git a/lib/base/loader.cpp b/lib/base/loader.cpp new file mode 100644 index 0000000..e969f37 --- /dev/null +++ b/lib/base/loader.cpp @@ -0,0 +1,35 @@ +/* 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::GetDeferredInitializers() +{ + static boost::thread_specific_ptr > initializers; + return initializers; +} + +void Loader::ExecuteDeferredInitializers() +{ + if (!GetDeferredInitializers().get()) + return; + + while (!GetDeferredInitializers().get()->empty()) { + DeferredInitializer initializer = GetDeferredInitializers().get()->top(); + GetDeferredInitializers().get()->pop(); + initializer(); + } +} + +void Loader::AddDeferredInitializer(const std::function& callback, int priority) +{ + if (!GetDeferredInitializers().get()) + GetDeferredInitializers().reset(new std::priority_queue()); + + GetDeferredInitializers().get()->push(DeferredInitializer(callback, priority)); +} + diff --git a/lib/base/loader.hpp b/lib/base/loader.hpp new file mode 100644 index 0000000..704a32a --- /dev/null +++ b/lib/base/loader.hpp @@ -0,0 +1,55 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef LOADER_H +#define LOADER_H + +#include "base/i2-base.hpp" +#include "base/string.hpp" +#include +#include + +namespace icinga +{ + +struct DeferredInitializer +{ +public: + DeferredInitializer(std::function callback, int 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 m_Callback; + int m_Priority; +}; + +/** + * Loader helper functions. + * + * @ingroup base + */ +class Loader +{ +public: + static void AddDeferredInitializer(const std::function& callback, int priority = 0); + static void ExecuteDeferredInitializers(); + +private: + Loader(); + + static boost::thread_specific_ptr >& GetDeferredInitializers(); +}; + +} + +#endif /* LOADER_H */ diff --git a/lib/base/logger.cpp b/lib/base/logger.cpp new file mode 100644 index 0000000..0be7279 --- /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 +#include +#include + +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::m_Loggers; +std::mutex Logger::m_Mutex; +bool Logger::m_ConsoleLogEnabled = true; +std::atomic Logger::m_EarlyLoggingEnabled (true); +bool Logger::m_TimestampEnabled = true; +LogSeverity Logger::m_ConsoleLogSeverity = LogInformation; +std::mutex Logger::m_UpdateMinLogSeverityMutex; +Atomic Logger::m_MinLogSeverity (LogDebug); + +INITIALIZE_ONCE([]() { + ScriptGlobal::Set("System.LogDebug", LogDebug, true); + ScriptGlobal::Set("System.LogNotice", LogNotice, true); + ScriptGlobal::Set("System.LogInformation", LogInformation, true); + ScriptGlobal::Set("System.LogWarning", LogWarning, true); + ScriptGlobal::Set("System.LogCritical", LogCritical, true); +}); + +/** + * Constructor for the Logger class. + */ +void Logger::Start(bool runtimeCreated) +{ + ObjectImpl::Start(runtimeCreated); + + { + std::unique_lock lock(m_Mutex); + m_Loggers.insert(this); + } + + UpdateMinLogSeverity(); +} + +void Logger::Stop(bool runtimeRemoved) +{ + { + std::unique_lock lock(m_Mutex); + m_Loggers.erase(this); + } + + UpdateMinLogSeverity(); + + ObjectImpl::Stop(runtimeRemoved); +} + +std::set Logger::GetLoggers() +{ + std::unique_lock 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::SetSeverity(value, suppress_events, cookie); + + UpdateMinLogSeverity(); +} + +void Logger::ValidateSeverity(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::ValidateSeverity(lvalue, utils); + + try { + StringToSeverity(lvalue()); + } catch (...) { + BOOST_THROW_EXCEPTION(ValidationError(this, { "severity" }, "Invalid severity specified: " + lvalue())); + } +} + +void Logger::UpdateMinLogSeverity() +{ + std::unique_lock 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 +#include + +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 +{ +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 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& 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 m_Loggers; + static bool m_ConsoleLogEnabled; + static std::atomic m_EarlyLoggingEnabled; + static bool m_TimestampEnabled; + static LogSeverity m_ConsoleLogSeverity; + static std::mutex m_UpdateMinLogSeverityMutex; + static Atomic 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 + 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..0060513 --- /dev/null +++ b/lib/base/math-script.cpp @@ -0,0 +1,185 @@ +/* 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 +#include + +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& 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& 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([]() { + auto mathNSBehavior = new ConstNamespaceBehavior(); + Namespace::Ptr mathNS = new Namespace(mathNSBehavior); + + /* 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)); + + mathNSBehavior->Freeze(); + + Namespace::Ptr systemNS = ScriptGlobal::Get("System"); + systemNS->SetAttribute("Math", new ConstEmbeddedNamespaceValue(mathNS)); +}); diff --git a/lib/base/namespace-script.cpp b/lib/base/namespace-script.cpp new file mode 100644 index 0000000..29d23a0 --- /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(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(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(vframe->Self); + REQUIRE_NOT_NULL(self); + self->Remove(key); +} + +static bool NamespaceContains(const String& key) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Namespace::Ptr self = static_cast(vframe->Self); + REQUIRE_NOT_NULL(self); + return self->Contains(key); +} + +static Array::Ptr NamespaceKeys() +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Namespace::Ptr self = static_cast(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(vframe->Self); + REQUIRE_NOT_NULL(self); + + ArrayData values; + ObjectLock olock(self); + for (const Namespace::Pair& kv : self) { + values.push_back(kv.second->Get()); + } + 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..33cca7d --- /dev/null +++ b/lib/base/namespace.cpp @@ -0,0 +1,224 @@ +/* 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 + +using namespace icinga; + +template class std::map >; + +REGISTER_PRIMITIVE_TYPE(Namespace, Object, Namespace::GetPrototype()); + +Namespace::Namespace(NamespaceBehavior *behavior) + : m_Behavior(std::unique_ptr(behavior)) +{ } + +Value Namespace::Get(const String& field) const +{ + ObjectLock olock(this); + + Value value; + if (!GetOwnField(field, &value)) + BOOST_THROW_EXCEPTION(ScriptError("Namespace does not contain field '" + field + "'")); + return value; +} + +bool Namespace::Get(const String& field, Value *value) const +{ + ObjectLock olock(this); + + auto nsVal = GetAttribute(field); + + if (!nsVal) + return false; + + *value = nsVal->Get(DebugInfo()); + return true; +} + +void Namespace::Set(const String& field, const Value& value, bool overrideFrozen) +{ + ObjectLock olock(this); + + return SetFieldByName(field, value, overrideFrozen, DebugInfo()); +} + +bool Namespace::Contains(const String& field) const +{ + ObjectLock olock(this); + + return HasOwnField(field); +} + +void Namespace::Remove(const String& field, bool overrideFrozen) +{ + ObjectLock olock(this); + + m_Behavior->Remove(this, field, overrideFrozen); +} + +void Namespace::RemoveAttribute(const String& field) +{ + ObjectLock olock(this); + + Namespace::Iterator it; + it = m_Data.find(field); + + if (it == m_Data.end()) + return; + + m_Data.erase(it); +} + +NamespaceValue::Ptr Namespace::GetAttribute(const String& key) const +{ + ObjectLock olock(this); + + auto it = m_Data.find(key); + + if (it == m_Data.end()) + return nullptr; + + return it->second; +} + +void Namespace::SetAttribute(const String& key, const NamespaceValue::Ptr& nsVal) +{ + ObjectLock olock(this); + + m_Data[key] = nsVal; +} + +Value Namespace::GetFieldByName(const String& field, bool, const DebugInfo& debugInfo) const +{ + ObjectLock olock(this); + + auto nsVal = GetAttribute(field); + + if (nsVal) + return nsVal->Get(debugInfo); + else + return GetPrototypeField(const_cast(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) +{ + ObjectLock olock(this); + + auto nsVal = GetAttribute(field); + + if (!nsVal) + m_Behavior->Register(this, field, value, overrideFrozen, debugInfo); + else + nsVal->Set(value, overrideFrozen, debugInfo); +} + +bool Namespace::HasOwnField(const String& field) const +{ + ObjectLock olock(this); + + return GetAttribute(field) != nullptr; +} + +bool Namespace::GetOwnField(const String& field, Value *result) const +{ + ObjectLock olock(this); + + auto nsVal = GetAttribute(field); + + if (!nsVal) + return false; + + *result = nsVal->Get(DebugInfo()); + return true; +} + +EmbeddedNamespaceValue::EmbeddedNamespaceValue(const Value& value) + : m_Value(value) +{ } + +Value EmbeddedNamespaceValue::Get(const DebugInfo& debugInfo) const +{ + return m_Value; +} + +void EmbeddedNamespaceValue::Set(const Value& value, bool, const DebugInfo&) +{ + m_Value = value; +} + +void ConstEmbeddedNamespaceValue::Set(const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) +{ + if (!overrideFrozen) + BOOST_THROW_EXCEPTION(ScriptError("Constant must not be modified.", debugInfo)); + + EmbeddedNamespaceValue::Set(value, overrideFrozen, debugInfo); +} + +void NamespaceBehavior::Register(const Namespace::Ptr& ns, const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) const +{ + ns->SetAttribute(field, new EmbeddedNamespaceValue(value)); +} + +void NamespaceBehavior::Remove(const Namespace::Ptr& ns, const String& field, bool overrideFrozen) +{ + if (!overrideFrozen) { + auto attr = ns->GetAttribute(field); + + if (dynamic_pointer_cast(attr)) + BOOST_THROW_EXCEPTION(ScriptError("Constants must not be removed.")); + } + + ns->RemoveAttribute(field); +} + +void ConstNamespaceBehavior::Register(const Namespace::Ptr& ns, const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) const +{ + if (m_Frozen && !overrideFrozen) + BOOST_THROW_EXCEPTION(ScriptError("Namespace is read-only and must not be modified.", debugInfo)); + + ns->SetAttribute(field, new ConstEmbeddedNamespaceValue(value)); +} + +void ConstNamespaceBehavior::Remove(const Namespace::Ptr& ns, const String& field, bool overrideFrozen) +{ + if (m_Frozen && !overrideFrozen) + BOOST_THROW_EXCEPTION(ScriptError("Namespace is read-only and must not be modified.")); + + NamespaceBehavior::Remove(ns, field, overrideFrozen); +} + +void ConstNamespaceBehavior::Freeze() +{ + m_Frozen = true; +} + +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..310dba7 --- /dev/null +++ b/lib/base/namespace.hpp @@ -0,0 +1,110 @@ +/* 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 +#include +#include + +namespace icinga +{ + +struct NamespaceValue : public SharedObject +{ + DECLARE_PTR_TYPEDEFS(NamespaceValue); + + virtual Value Get(const DebugInfo& debugInfo = DebugInfo()) const = 0; + virtual void Set(const Value& value, bool overrideFrozen, const DebugInfo& debugInfo = DebugInfo()) = 0; +}; + +struct EmbeddedNamespaceValue : public NamespaceValue +{ + EmbeddedNamespaceValue(const Value& value); + + Value Get(const DebugInfo& debugInfo) const override; + void Set(const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) override; + +private: + Value m_Value; +}; + +struct ConstEmbeddedNamespaceValue : public EmbeddedNamespaceValue +{ + using EmbeddedNamespaceValue::EmbeddedNamespaceValue; + + void Set(const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) override; +}; + +class Namespace; + +struct NamespaceBehavior +{ + virtual void Register(const boost::intrusive_ptr& ns, const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) const; + virtual void Remove(const boost::intrusive_ptr& ns, const String& field, bool overrideFrozen); +}; + +struct ConstNamespaceBehavior : public NamespaceBehavior +{ + void Register(const boost::intrusive_ptr& ns, const String& field, const Value& value, bool overrideFrozen, const DebugInfo& debugInfo) const override; + void Remove(const boost::intrusive_ptr& ns, const String& field, bool overrideFrozen) override; + void Freeze(); + +private: + bool m_Frozen; +}; + +/** + * A namespace. + * + * @ingroup base + */ +class Namespace final : public Object +{ +public: + DECLARE_OBJECT(Namespace); + + typedef std::map::iterator Iterator; + + typedef std::map::value_type Pair; + + Namespace(NamespaceBehavior *behavior = new NamespaceBehavior); + + Value Get(const String& field) const; + bool Get(const String& field, Value *value) const; + void Set(const String& field, const Value& value, bool overrideFrozen = false); + bool Contains(const String& field) const; + void Remove(const String& field, bool overrideFrozen = false); + + NamespaceValue::Ptr GetAttribute(const String& field) const; + void SetAttribute(const String& field, const NamespaceValue::Ptr& nsVal); + void RemoveAttribute(const String& field); + + Iterator Begin(); + Iterator End(); + + 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::map m_Data; + std::unique_ptr m_Behavior; +}; + +Namespace::Iterator begin(const Namespace::Ptr& x); +Namespace::Iterator end(const Namespace::Ptr& x); + +} + +extern template class std::map >; + +#endif /* NAMESPACE_H */ diff --git a/lib/base/netstring.cpp b/lib/base/netstring.cpp new file mode 100644 index 0000000..fb8098a --- /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 +#include +#include +#include +#include +#include +#include +#include + +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::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 std::move(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::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 std::move(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::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::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 +#include + +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::Ptr& stream, ssize_t maxMessageLength = -1); + static String ReadStringFromStream(const Shared::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::Ptr& stream, const String& message); + static size_t WriteStringToStream(const Shared::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 +#include +#include +#include +#include + +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(), builder); + break; + + case ValueNumber: + builder += '\3'; + PackFloat64BE(value.Get(), builder); + break; + + case ValueBoolean: + builder += (value.ToBool() ? '\2' : '\1'); + break; + + case ValueEmpty: + builder += '\0'; + break; + + case ValueObject: + { + const Object::Ptr& obj = value.Get(); + + Dictionary::Ptr dict = dynamic_pointer_cast(obj); + if (dict) { + PackDictionary(dict, builder); + break; + } + + Array::Ptr arr = dynamic_pointer_cast(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(vframe->Self); + REQUIRE_NOT_NULL(self); + return self->ToString(); +} + +static void ObjectNotifyAttribute(const String& attribute) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Object::Ptr self = static_cast(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(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..0643903 --- /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 +#include +#include + +using namespace icinga; + +DEFINE_TYPE_INSTANCE(Object); + +#ifdef I2_LEAK_DEBUG +static std::mutex l_ObjectCountLock; +static std::map 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(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& 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 lock(l_ObjectCountLock); + String typeName = Utility::GetTypeName(typeid(*object)); + l_ObjectCounts[typeName]++; +} + +void icinga::TypeRemoveObject(Object *object) +{ + std::unique_lock lock(l_ObjectCountLock); + String typeName = Utility::GetTypeName(typeid(*object)); + l_ObjectCounts[typeName]--; +} + +static void TypeInfoTimerHandler() +{ + std::unique_lock lock(l_ObjectCountLock); + + typedef std::map::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 = new Timer(); + 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& args) +{ + if (!args.empty()) + BOOST_THROW_EXCEPTION(std::invalid_argument("Constructor does not take any arguments.")); +} + +void icinga::RequireNotNullInternal(const intrusive_ptr& 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 +#include +#include +#include +#include +#include +#include + +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 Ptr + +#define IMPL_TYPE_LOOKUP_SUPER() \ + +#define IMPL_TYPE_LOOKUP() \ + static intrusive_ptr TypeInstance; \ + virtual intrusive_ptr 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, const char *description); + +void DefaultObjectFactoryCheckArgs(const std::vector& args); + +template +intrusive_ptr DefaultObjectFactory(const std::vector& args) +{ + DefaultObjectFactoryCheckArgs(args); + + return new T(); +} + +template +intrusive_ptr DefaultObjectFactoryVA(const std::vector& args) +{ + return new T(args); +} + +typedef intrusive_ptr (*ObjectFactory)(const std::vector&); + +template +struct TypeHelper +{ +}; + +template +struct TypeHelper +{ + static ObjectFactory GetFactory() + { + return DefaultObjectFactory; + } +}; + +template +struct TypeHelper +{ + static ObjectFactory GetFactory() + { + return DefaultObjectFactoryVA; + } +}; + +template +struct Lazy +{ + using Accessor = std::function; + + explicit Lazy(T value) + : m_Cached(true), m_Value(value) + { } + + explicit Lazy(Accessor accessor) + : m_Accessor(accessor) + { } + + template + explicit Lazy(const Lazy& other) + { + if (other.m_Cached) { + m_Accessor = Accessor(); + m_Value = static_cast(other.m_Value); + m_Cached = true; + } else { + auto accessor = other.m_Accessor; + m_Accessor = [accessor]() { return static_cast(accessor()); }; + m_Cached = false; + } + } + + template + operator Lazy() const + { + if (m_Cached) + return Lazy(static_cast(m_Value)); + else { + Accessor accessor = m_Accessor; + return Lazy(static_cast::Accessor>([accessor]() { return static_cast(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 + 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 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& 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 TypeInstance; + +private: + Object(const Object& other) = delete; + Object& operator=(const Object& rhs) = delete; + + std::atomic m_References; + mutable std::recursive_mutex m_Mutex; + +#ifdef I2_DEBUG + mutable std::atomic 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 +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 + +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..277f990 --- /dev/null +++ b/lib/base/objectlock.hpp @@ -0,0 +1,32 @@ +/* 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(); + + 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..7ab0f2a --- /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 + +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; +}, 20); + +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; +} + 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..6268ac1 --- /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 +#include +#include +#include +#include +#include + +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 UoMs; +typedef std::unordered_multimap 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 std::move(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 std::move(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 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 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& tokens, std::vector::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 +{ +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& tokens, + std::vector::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..ae7454e --- /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); \ + }, 15) + +#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; \ + }, 15); \ + DEFINE_TYPE_INSTANCE(type) + +#define REGISTER_PRIMITIVE_TYPE(type, base, prototype) \ + REGISTER_PRIMITIVE_TYPE_FACTORY(type, base, prototype, DefaultObjectFactory) + +#define REGISTER_PRIMITIVE_TYPE_VA(type, base, prototype) \ + REGISTER_PRIMITIVE_TYPE_FACTORY(type, base, prototype, DefaultObjectFactoryVA) + +#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..7aa7972 --- /dev/null +++ b/lib/base/process.cpp @@ -0,0 +1,1208 @@ +/* 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 +#include +#include +#include + +#ifndef _WIN32 +# include +# include +# include + +# ifndef __APPLE__ +extern char **environ; +# else /* __APPLE__ */ +# include +# define environ (*_NSGetEnviron()) +# endif /* __APPLE__ */ +#endif /* _WIN32 */ + +using namespace icinga; + +#define IOTHREADS 4 + +static std::mutex l_ProcessMutex[IOTHREADS]; +static std::map l_Processes[IOTHREADS]; +#ifdef _WIN32 +static HANDLE l_Events[IOTHREADS]; +#else /* _WIN32 */ +static int l_EventFDs[IOTHREADS][2]; +static std::map 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); + } + + _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& 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 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 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 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 args; +#endif /* _WIN32 */ + + if (command.IsObjectType()) { + 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 lock(l_ProcessMutex[tid]); + + count = 1 + l_Processes[tid].size(); +#ifdef _WIN32 + handles = reinterpret_cast(realloc(handles, sizeof(HANDLE) * count)); + fhandles = reinterpret_cast(realloc(fhandles, sizeof(HANDLE) * count)); + + fhandles[0] = l_Events[tid]; + +#else /* _WIN32 */ + pfds = reinterpret_cast(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 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(timeout)); +#else /* _WIN32 */ + int rc = poll(pfds, count, timeout); + + if (rc < 0) + continue; +#endif /* _WIN32 */ + + now = Utility::GetTime(); + + { + std::unique_lock 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& 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(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(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(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(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(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(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 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 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 << ""; + + 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 << ""; + 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 << ""; + output = output + outputbuf.str(); + exitcode = 128; + } else { + exitcode = 128; + } +#endif /* _WIN32 */ + + { + std::lock_guard 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(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 +#include +#include +#include +#include +#include + +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 Arguments; + typedef pid_t ProcessHandle; + typedef int ConsoleHandle; +#endif /* _WIN32 */ + + static const std::deque::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& callback = std::function()); + + 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 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(vframe->Self); + REQUIRE_NOT_NULL(self); + self->Set(value); +} + +static Value ReferenceGet() +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + Reference::Ptr self = static_cast(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 +#include +#include + +namespace icinga +{ + +/** + * A registry. + * + * @ingroup base + */ +template +class Registry +{ +public: + typedef std::map ItemMap; + + void RegisterIfNew(const String& name, const T& item) + { + std::unique_lock 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 lock(m_Mutex); + + RegisterInternal(name, item, lock); + } + + void Unregister(const String& name) + { + size_t erased; + + { + std::unique_lock lock(m_Mutex); + erased = m_Items.erase(name); + } + + if (erased > 0) + OnUnregistered(name); + } + + void Clear() + { + typename Registry::ItemMap items; + + { + std::unique_lock lock(m_Mutex); + items = m_Items; + } + + for (const auto& kv : items) { + OnUnregistered(kv.first); + } + + { + std::unique_lock lock(m_Mutex); + m_Items.clear(); + } + } + + T GetItem(const String& name) const + { + std::unique_lock 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 lock(m_Mutex); + + return m_Items; /* Makes a copy of the map. */ + } + + boost::signals2::signal OnRegistered; + boost::signals2::signal OnUnregistered; + +private: + mutable std::mutex m_Mutex; + typename Registry::ItemMap m_Items; + + void RegisterInternal(const String& name, const T& item, std::unique_lock& 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 + +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 lock(m_Mutex); + return m_Slots.size(); +} + +void RingBuffer::InsertValue(RingBuffer::SizeType tv, int num) +{ + std::unique_lock 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 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 lock(m_Mutex); + + int sum = UpdateAndGetValuesUnlocked(tv, span); + return sum / static_cast(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 +#include + +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::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 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..8369e55 --- /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" + +using namespace icinga; + +boost::thread_specific_ptr > ScriptFrame::m_ScriptFrames; + +static auto l_InternalNSBehavior = new ConstNamespaceBehavior(); + +/* 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(); + + auto systemNSBehavior = new ConstNamespaceBehavior(); + systemNSBehavior->Freeze(); + Namespace::Ptr systemNS = new Namespace(systemNSBehavior); + globalNS->SetAttribute("System", new ConstEmbeddedNamespaceValue(systemNS)); + + systemNS->SetAttribute("Configuration", new EmbeddedNamespaceValue(new Configuration())); + + auto typesNSBehavior = new ConstNamespaceBehavior(); + typesNSBehavior->Freeze(); + Namespace::Ptr typesNS = new Namespace(typesNSBehavior); + globalNS->SetAttribute("Types", new ConstEmbeddedNamespaceValue(typesNS)); + + auto statsNSBehavior = new ConstNamespaceBehavior(); + statsNSBehavior->Freeze(); + Namespace::Ptr statsNS = new Namespace(statsNSBehavior); + globalNS->SetAttribute("StatsFunctions", new ConstEmbeddedNamespaceValue(statsNS)); + + Namespace::Ptr internalNS = new Namespace(l_InternalNSBehavior); + globalNS->SetAttribute("Internal", new ConstEmbeddedNamespaceValue(internalNS)); +}, 1000); + +INITIALIZE_ONCE_WITH_PRIORITY([]() { + l_InternalNSBehavior->Freeze(); +}, 0); + +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 *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 *frames = m_ScriptFrames.get(); + + ASSERT(!frames->empty()); + return frames->top(); +} + +ScriptFrame *ScriptFrame::PopFrame() +{ + std::stack *frames = m_ScriptFrames.get(); + + ASSERT(!frames->empty()); + + ScriptFrame *frame = frames->top(); + frames->pop(); + + return frame; +} + +void ScriptFrame::PushFrame(ScriptFrame *frame) +{ + std::stack *frames = m_ScriptFrames.get(); + + if (!frames) { + frames = new std::stack(); + 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 +#include + +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 > 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..545c097 --- /dev/null +++ b/lib/base/scriptglobal.cpp @@ -0,0 +1,117 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#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 + +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, bool overrideFrozen) +{ + std::vector 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::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->SetFieldByName(tokens[tokens.size() - 1], value, overrideFrozen, DebugInfo()); + } +} + +void ScriptGlobal::SetConst(const String& name, const Value& value) +{ + GetGlobals()->SetAttribute(name, new ConstEmbeddedNamespaceValue(value)); +} + +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 << "'"; + + std::fstream fp; + String tempFilename = Utility::CreateTempFile(filename + ".XXXXXX", 0600, fp); + + if (!fp) + BOOST_THROW_EXCEPTION(std::runtime_error("Could not open '" + tempFilename + "' file")); + + StdioStream::Ptr sfp = new StdioStream(&fp, false); + + ObjectLock olock(m_Globals); + for (const Namespace::Pair& kv : m_Globals) { + Value value = kv.second->Get(); + + 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.close(); + + Utility::RenameFile(tempFilename, filename); +} + diff --git a/lib/base/scriptglobal.hpp b/lib/base/scriptglobal.hpp new file mode 100644 index 0000000..c31cdcd --- /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, bool overrideFrozen = false); + 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..b24cdbd --- /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 +#include +#include +#ifdef _WIN32 +#include +#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, true); + ScriptGlobal::Set("System.MatchAny", MatchAny, true); + + ScriptGlobal::Set("System.GlobFile", GlobFile, true); + ScriptGlobal::Set("System.GlobDirectory", GlobDirectory, true); +} + +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& 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()) + BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionaries are not supported by regex().")); + + MatchType mode; + + if (args.size() > 2) + mode = static_cast(static_cast(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& 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()) + BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionaries are not supported by match().")); + + MatchType mode; + + if (args.size() > 2) + mode = static_cast(static_cast(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& 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()) + BOOST_THROW_EXCEPTION(std::invalid_argument("Dictionaries are not supported by cidr_match().")); + + MatchType mode; + + if (args.size() > 2) + mode = static_cast(static_cast(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::Ptr dict = value; + return dict->GetLength(); + } else if (value.IsObjectType()) { + 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& arguments) +{ + std::set 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& 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::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& 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(arguments[0]); + severity = static_cast(sval); + facility = arguments[1]; + message = arguments[2]; + } + + if (message.IsString() || (!message.IsObjectType() && !message.IsObjectType())) + ::Log(severity, facility, message); + else + ::Log(severity, facility, JsonEncode(message)); +} + +Array::Ptr ScriptUtils::Range(const std::vector& 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(obj); + + if (dict) { + ObjectLock olock(dict); + for (const Dictionary::Pair& kv : dict) { + result.push_back(kv.first); + } + } + + Namespace::Ptr ns = dynamic_pointer_cast(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()) + 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()) + ptype = vtype; + else + ptype = Type::GetByName(vtype); + + auto *ctype = dynamic_cast(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(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(object.get()); +} + +Value ScriptUtils::Glob(const std::vector& 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 paths; + Utility::Glob(pathSpec, [&paths](const String& path) { paths.push_back(path); }, type); + + return Array::FromVector(paths); +} + +Value ScriptUtils::GlobRecursive(const std::vector& 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 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& args); + static bool Match(const std::vector& args); + static bool CidrMatch(const std::vector& args); + static double Len(const Value& value); + static Array::Ptr Union(const std::vector& arguments); + static Array::Ptr Intersection(const std::vector& arguments); + static void Log(const std::vector& arguments); + static Array::Ptr Range(const std::vector& 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& args); + static Value GlobRecursive(const std::vector& 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..c081c44 --- /dev/null +++ b/lib/base/serializer.cpp @@ -0,0 +1,287 @@ +/* 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 +#include + +using namespace icinga; + +struct SerializeStackEntry +{ + String Name; + Value Val; +}; + +CircularReferenceError::CircularReferenceError(String message, std::vector path) + : m_Message(message), m_Path(path) +{ } + +const char *CircularReferenceError::what(void) const throw() +{ + return m_Message.CStr(); +} + +std::vector CircularReferenceError::GetPath() const +{ + return m_Path; +} + +struct SerializeStack +{ + std::deque 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 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); + +static Array::Ptr SerializeArray(const Array::Ptr& input, int attributeTypes, SerializeStack& stack) +{ + ArrayData result; + + result.reserve(input->GetLength()); + + ObjectLock olock(input); + + int index = 0; + + for (const Value& value : input) { + stack.Push(Convert::ToString(index), value); + result.emplace_back(SerializeInternal(value, attributeTypes, stack)); + stack.Pop(); + index++; + } + + return new Array(std::move(result)); +} + +static Dictionary::Ptr SerializeDictionary(const Dictionary::Ptr& input, int attributeTypes, SerializeStack& stack) +{ + DictionaryData result; + + result.reserve(input->GetLength()); + + ObjectLock olock(input); + + for (const Dictionary::Pair& kv : input) { + stack.Push(kv.first, kv.second); + result.emplace_back(kv.first, SerializeInternal(kv.second, attributeTypes, stack)); + stack.Pop(); + } + + return new Dictionary(std::move(result)); +} + +static Dictionary::Ptr SerializeNamespace(const Namespace::Ptr& input, int attributeTypes, SerializeStack& stack) +{ + DictionaryData result; + + ObjectLock olock(input); + + for (const Namespace::Pair& kv : input) { + Value val = kv.second->Get(); + stack.Push(kv.first, val); + result.emplace_back(kv.first, Serialize(val, attributeTypes)); + stack.Pop(); + } + + return new Dictionary(std::move(result)); +} + +static Object::Ptr SerializeObject(const Object::Ptr& input, int attributeTypes, SerializeStack& stack) +{ + Type::Ptr type = input->GetReflectionType(); + + if (!type) + return nullptr; + + DictionaryData fields; + 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); + fields.emplace_back(field.Name, SerializeInternal(value, attributeTypes, stack)); + stack.Pop(); + } + + fields.emplace_back("type", type->GetName()); + + return 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()); + + 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) +{ + if (!value.IsObject()) + return value; + + Object::Ptr input = value; + + Array::Ptr array = dynamic_pointer_cast(input); + + if (array) + return SerializeArray(array, attributeTypes, stack); + + Dictionary::Ptr dict = dynamic_pointer_cast(input); + + if (dict) + return SerializeDictionary(dict, attributeTypes, stack); + + Namespace::Ptr ns = dynamic_pointer_cast(input); + + if (ns) + return SerializeNamespace(ns, attributeTypes, stack); + + return SerializeObject(input, attributeTypes, stack); +} + +Value icinga::Serialize(const Value& value, int attributeTypes) +{ + SerializeStack stack; + return SerializeInternal(value, attributeTypes, stack); +} + +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(input); + + if (array) + return DeserializeArray(array, safe_mode, attributeTypes); + + Dictionary::Ptr dict = dynamic_pointer_cast(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..77b31ce --- /dev/null +++ b/lib/base/serializer.hpp @@ -0,0 +1,33 @@ +/* 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 path); + + const char *what(void) const throw() final; + std::vector GetPath() const; + +private: + String m_Message; + std::vector m_Path; +}; + +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-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 + +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 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 +#include +#include + +namespace icinga +{ + +template +class Shared; + +template +inline void intrusive_ptr_add_ref(Shared *object) +{ + object->m_References.fetch_add(1); +} + +template +inline void intrusive_ptr_release(Shared *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 Shared : public T +{ + friend void intrusive_ptr_add_ref<>(Shared *object); + friend void intrusive_ptr_release<>(Shared *object); + +public: + typedef boost::intrusive_ptr Ptr; + + /** + * Like std::make_shared, but for this class. + * + * @param args Constructor arguments + * + * @return Ptr + */ + template + static inline + Ptr Make(Args&&... args) + { + return new Shared(std::forward(args)...); + } + + inline Shared(const Shared& origin) : Shared((const T&)origin) + { + } + + inline Shared(Shared&& origin) : Shared((T&&)origin) + { + } + + template + inline Shared(Args&&... args) : T(std::forward(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 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 +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 +#include +#include +#include +#include + +#ifndef _WIN32 +# include +#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& 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 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 Socket::GetClientAddressDetails() +{ + std::unique_lock 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 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 Socket::GetPeerAddressDetails() +{ + std::unique_lock 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 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 + +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 GetClientAddressDetails(); + String GetClientAddress(); + std::pair 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 GetDetailsFromSockaddr(sockaddr *address, socklen_t len); + static String GetHumanReadableAddress(const std::pair& 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 +#include "base/stacktrace.hpp" +#include +#include +#include + +#ifdef HAVE_BACKTRACE_SYMBOLS +# include +#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 `` instead of the implementation + * provided by Boost. + */ + + const boost::stacktrace::stacktrace &stack = f.m_Stack; + +#ifdef ICINGA2_STACKTRACE_USE_BACKTRACE_SYMBOLS + std::vector addrs; + addrs.reserve(stack.size()); + std::transform(stack.begin(), stack.end(), std::back_inserter(addrs), [](const boost::stacktrace::frame &f) { + return const_cast(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 + +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(buffer), size); + return m_InnerStream->gcount(); +} + +void StdioStream::Write(const void *buffer, size_t size) +{ + ObjectLock olock(this); + + m_InnerStream->write(static_cast(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 +#include + +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 +#include + +using namespace icinga; + +void Stream::RegisterDataHandler(const std::function& 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 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 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 lock(m_Mutex); + + return m_CV.wait_for(lock, ch::duration(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 +#include +#include + +namespace icinga +{ + +class String; +class Stream; + +enum ConnectionRole +{ + RoleClient, + RoleServer +}; + +struct StreamReadContext +{ + ~StreamReadContext() + { + free(Buffer); + } + + bool FillFromStream(const intrusive_ptr& 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& handler); + + StreamReadStatus ReadLine(String *line, StreamReadContext& context, bool may_wait = false); + +protected: + void SignalDataAvailable(); + +private: + boost::signals2::signal 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..66bd1dc --- /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 + +using namespace icinga; + +REGISTER_TYPE(StreamLogger); + +std::mutex StreamLogger::m_Mutex; + +void StreamLogger::Stop(bool runtimeRemoved) +{ + ObjectImpl::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(); + + 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 = new Timer(); + 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 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 + +namespace icinga +{ + +/** + * A logger that logs to an iostream. + * + * @ingroup base + */ +class StreamLogger : public ObjectImpl +{ +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 + +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& args) +{ + ScriptFrame *vframe = ScriptFrame::GetCurrentFrame(); + String self = vframe->Self; + + if (args.empty()) + BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments")); + + if (static_cast(args[0]) < 0 || static_cast(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 tokens = self.Split(delims.CStr()); + + return Array::FromVector(tokens); +} + +static int StringFind(const std::vector& 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(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..e4e5c27 --- /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 +#include +#include +#include + +using namespace icinga; + +template class std::vector; + +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()); + else + *this = static_cast(other); + + return *this; +} + +String& String::operator+=(const Value& rhs) +{ + m_Data += static_cast(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::string_view. + * + * This allows using String as the value for HTTP headers in boost::beast::http::basic_fields::set. + * + * @return A boost::string_view representing this string. + */ +String::operator boost::string_view() const +{ + return boost::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::Split(const char *separators) const +{ + std::vector 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::operator()(const String& s) const noexcept +{ + return std::hash{}(s.GetData()); +} diff --git a/lib/base/string.hpp b/lib/base/string.hpp new file mode 100644 index 0000000..10ddaf9 --- /dev/null +++ b/lib/base/string.hpp @@ -0,0 +1,207 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef STRING_H +#define STRING_H + +#include "base/i2-base.hpp" +#include "base/object.hpp" +#include +#include +#include +#include +#include + +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 + 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::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 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 + 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 +{ + std::size_t operator()(const icinga::String& s) const noexcept; +}; + +extern template class std::vector; + +namespace boost +{ + +template<> +struct range_mutable_iterator +{ + typedef icinga::String::Iterator type; +}; + +template<> +struct range_const_iterator +{ + 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..44babc3 --- /dev/null +++ b/lib/base/sysloglogger.cpp @@ -0,0 +1,136 @@ +/* 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" + +using namespace icinga; + +REGISTER_TYPE(SyslogLogger); + +REGISTER_STATSFUNCTION(SyslogLogger, &SyslogLogger::StatsFunc); + +INITIALIZE_ONCE(&SyslogLogger::StaticInitialize); + +std::map SyslogLogger::m_FacilityMap; + +void SyslogLogger::StaticInitialize() +{ + ScriptGlobal::Set("System.FacilityAuth", "LOG_AUTH", true); + ScriptGlobal::Set("System.FacilityAuthPriv", "LOG_AUTHPRIV", true); + ScriptGlobal::Set("System.FacilityCron", "LOG_CRON", true); + ScriptGlobal::Set("System.FacilityDaemon", "LOG_DAEMON", true); + ScriptGlobal::Set("System.FacilityFtp", "LOG_FTP", true); + ScriptGlobal::Set("System.FacilityKern", "LOG_KERN", true); + ScriptGlobal::Set("System.FacilityLocal0", "LOG_LOCAL0", true); + ScriptGlobal::Set("System.FacilityLocal1", "LOG_LOCAL1", true); + ScriptGlobal::Set("System.FacilityLocal2", "LOG_LOCAL2", true); + ScriptGlobal::Set("System.FacilityLocal3", "LOG_LOCAL3", true); + ScriptGlobal::Set("System.FacilityLocal4", "LOG_LOCAL4", true); + ScriptGlobal::Set("System.FacilityLocal5", "LOG_LOCAL5", true); + ScriptGlobal::Set("System.FacilityLocal6", "LOG_LOCAL6", true); + ScriptGlobal::Set("System.FacilityLocal7", "LOG_LOCAL7", true); + ScriptGlobal::Set("System.FacilityLpr", "LOG_LPR", true); + ScriptGlobal::Set("System.FacilityMail", "LOG_MAIL", true); + ScriptGlobal::Set("System.FacilityNews", "LOG_NEWS", true); + ScriptGlobal::Set("System.FacilitySyslog", "LOG_SYSLOG", true); + ScriptGlobal::Set("System.FacilityUser", "LOG_USER", true); + ScriptGlobal::Set("System.FacilityUucp", "LOG_UUCP", true); + + m_FacilityMap["LOG_AUTH"] = LOG_AUTH; + m_FacilityMap["LOG_AUTHPRIV"] = LOG_AUTHPRIV; + m_FacilityMap["LOG_CRON"] = LOG_CRON; + m_FacilityMap["LOG_DAEMON"] = LOG_DAEMON; + m_FacilityMap["LOG_FTP"] = 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; +} + +void SyslogLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&) +{ + DictionaryData nodes; + + for (const SyslogLogger::Ptr& sysloglogger : ConfigType::GetObjectsByType()) { + nodes.emplace_back(sysloglogger->GetName(), 1); //add more stats + } + + status->Set("sysloglogger", new Dictionary(std::move(nodes))); +} + +void SyslogLogger::OnConfigLoaded() +{ + ObjectImpl::OnConfigLoaded(); + + String facilityString = GetFacility(); + + auto it = m_FacilityMap.find(facilityString); + + if (it != m_FacilityMap.end()) + m_Facility = it->second; + else + m_Facility = Convert::ToLong(facilityString); +} + +void SyslogLogger::ValidateFacility(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::ValidateFacility(lvalue, utils); + + if (m_FacilityMap.find(lvalue()) == m_FacilityMap.end()) { + try { + Convert::ToLong(lvalue()); + } catch (const std::exception&) { + 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) +{ + int severity; + switch (entry.Severity) { + case LogDebug: + severity = LOG_DEBUG; + break; + case LogNotice: + severity = LOG_NOTICE; + break; + case LogWarning: + severity = LOG_WARNING; + break; + case LogCritical: + severity = LOG_CRIT; + break; + case LogInformation: + default: + severity = LOG_INFO; + break; + } + + syslog(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..168c5d9 --- /dev/null +++ b/lib/base/sysloglogger.hpp @@ -0,0 +1,41 @@ +/* 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 +{ + +/** + * A logger that logs to syslog. + * + * @ingroup base + */ +class SyslogLogger final : public ObjectImpl +{ +public: + DECLARE_OBJECT(SyslogLogger); + DECLARE_OBJECTNAME(SyslogLogger); + + static void StaticInitialize(); + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + + void OnConfigLoaded() override; + void ValidateFacility(const Lazy& lvalue, const ValidationUtils& utils) override; + +protected: + static std::map m_FacilityMap; + 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 +#include +#include + +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(&optFalse), sizeof(optFalse)); + + const int optTrue = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&optTrue), sizeof(optTrue)); +#ifdef SO_REUSEPORT + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&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(&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..e0f5022 --- /dev/null +++ b/lib/base/tcpsocket.hpp @@ -0,0 +1,96 @@ +/* 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 +#include + +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 +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&) { + if (++current == result.end()) { + throw; + } + + if (socket.is_open()) { + socket.close(); + } + } + } +} + +template +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&) { + if (++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..26787ab --- /dev/null +++ b/lib/base/threadpool.cpp @@ -0,0 +1,36 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "base/threadpool.hpp" +#include + +using namespace icinga; + +ThreadPool::ThreadPool(size_t threads) + : m_Threads(threads), m_Pending(0) +{ + Start(); +} + +ThreadPool::~ThreadPool() +{ + Stop(); +} + +void ThreadPool::Start() +{ + boost::unique_lock lock (m_Mutex); + + if (!m_Pool) { + m_Pool = decltype(m_Pool)(new boost::asio::thread_pool(m_Threads)); + } +} + +void ThreadPool::Stop() +{ + boost::unique_lock lock (m_Mutex); + + if (m_Pool) { + m_Pool->join(); + m_Pool = nullptr; + } +} diff --git a/lib/base/threadpool.hpp b/lib/base/threadpool.hpp new file mode 100644 index 0000000..af351cd --- /dev/null +++ b/lib/base/threadpool.hpp @@ -0,0 +1,98 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef THREADPOOL_H +#define THREADPOOL_H + +#include "base/atomic.hpp" +#include "base/exception.hpp" +#include "base/logger.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace icinga +{ + +enum SchedulerPolicy +{ + DefaultScheduler, + LowLatencyScheduler +}; + +/** + * A thread pool. + * + * @ingroup base + */ +class ThreadPool +{ +public: + typedef std::function WorkFunction; + + ThreadPool(size_t threads = std::thread::hardware_concurrency() * 2u); + ~ThreadPool(); + + void Start(); + void Stop(); + + /** + * 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 + bool Post(T callback, SchedulerPolicy) + { + boost::shared_lock 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 m_Pool; + size_t m_Threads; + Atomic m_Pending; +}; + +} + +#endif /* THREADPOOL_H */ diff --git a/lib/base/timer.cpp b/lib/base/timer.cpp new file mode 100644 index 0000000..973888e --- /dev/null +++ b/lib/base/timer.cpp @@ -0,0 +1,321 @@ +/* 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 +#include +#include +#include +#include +#include +#include + +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::ordered_non_unique > + > +> 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); + +/** + * Destructor for the Timer class. + */ +Timer::~Timer() +{ + Stop(true); +} + +void Timer::Initialize() +{ + std::unique_lock lock(l_TimerMutex); + + if (l_AliveTimers > 0) { + InitializeThread(); + } +} + +void Timer::Uninitialize() +{ + std::unique_lock 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 lock(l_TimerMutex); + m_Interval = interval; +} + +/** + * Retrieves the interval for this timer. + * + * @returns The interval. + */ +double Timer::GetInterval() const +{ + std::unique_lock lock(l_TimerMutex); + return m_Interval; +} + +/** + * Registers the timer and starts processing events for it. + */ +void Timer::Start() +{ + { + std::unique_lock lock(l_TimerMutex); + m_Started = true; + + if (++l_AliveTimers == 1) { + InitializeThread(); + } + } + + InternalReschedule(false); +} + +/** + * Unregisters the timer and stops processing events for it. + */ +void Timer::Stop(bool wait) +{ + if (l_StopTimerThread) + return; + + std::unique_lock 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); +} + +/** + * 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::InternalReschedule(bool completed, double next) +{ + std::unique_lock lock(l_TimerMutex); + + 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 lock(l_TimerMutex); + return m_Next; +} + +/** + * Adjusts all 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 lock(l_TimerMutex); + + double now = Utility::GetTime(); + + typedef boost::multi_index::nth_index::type TimerView; + TimerView& idx = boost::get<1>(l_Timers); + + std::vector timers; + + for (Timer *timer : idx) { + 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"); + + for (;;) { + std::unique_lock lock(l_TimerMutex); + + typedef boost::multi_index::nth_index::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 = *it; + + ch::time_point> next (ch::duration(timer->m_Next)); + + if (next - ch::system_clock::now() > ch::duration(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); + + timer->m_Running = true; + + lock.unlock(); + + /* Asynchronously call the timer. */ + Utility::QueueAsyncCallback([timer]() { timer->Call(); }); + } +} diff --git a/lib/base/timer.hpp b/lib/base/timer.hpp new file mode 100644 index 0000000..2088a66 --- /dev/null +++ b/lib/base/timer.hpp @@ -0,0 +1,60 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef TIMER_H +#define TIMER_H + +#include "base/i2-base.hpp" +#include "base/object.hpp" +#include + +namespace icinga { + +class TimerHolder; + +/** + * A timer that periodically triggers an event. + * + * @ingroup base + */ +class Timer final : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(Timer); + + ~Timer() override; + + 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 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. */ + + void Call(); + void InternalReschedule(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 +#include +#include +#include +#include +#include +#include +#include + +using namespace icinga; + +bool UnbufferedAsioTlsStream::IsVerifyOK() const +{ + return m_VerifyOK; +} + +String UnbufferedAsioTlsStream::GetVerifyError() const +{ + return m_VerifyError; +} + +std::shared_ptr UnbufferedAsioTlsStream::GetPeerCertificate() +{ + return std::shared_ptr(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 +#include +#include +#include +#include +#include +#include +#include +#include + +namespace icinga +{ + +template +class SeenStream : public ARS +{ +public: + template + SeenStream(Args&&... args) : ARS(std::forward(args)...) + { + m_Seen.store(nullptr); + } + + template + auto async_read_some(Args&&... args) -> decltype(((ARS*)nullptr)->async_read_some(std::forward(args)...)) + { + { + auto seen (m_Seen.load()); + + if (seen) { + *seen = Utility::GetTime(); + } + } + + return ((ARS*)this)->async_read_some(std::forward(args)...); + } + + inline void SetSeen(double* seen) + { + m_Seen.store(seen); + } + +private: + std::atomic m_Seen; +}; + +struct UnbufferedAsioTlsStreamParams +{ + boost::asio::io_context& IoContext; + boost::asio::ssl::context& SslContext; + const String& Hostname; +}; + +typedef SeenStream> 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 GetPeerCertificate(); + + template + inline + auto async_handshake(handshake_type type, Args&&... args) -> decltype(((AsioTcpTlsStream*)nullptr)->async_handshake(type, std::forward(args)...)) + { + BeforeHandshake(type); + + return AsioTcpTlsStream::async_handshake(type, std::forward(args)...); + } + + template + inline + auto handshake(handshake_type type, Args&&... args) -> decltype(((AsioTcpTlsStream*)nullptr)->handshake(type, std::forward(args)...)) + { + BeforeHandshake(type); + + return AsioTcpTlsStream::handshake(type, std::forward(args)...); + } + +private: + bool m_VerifyOK; + String m_VerifyError; + String m_Hostname; + + void BeforeHandshake(handshake_type type); +}; + +class AsioTlsStream : public boost::asio::buffered_stream +{ +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 AsioTcpStream; +typedef std::pair::Ptr, Shared::Ptr> OptionalTlsStream; + +} + +#endif /* TLSSTREAM_H */ diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp new file mode 100644 index 0000000..e022621 --- /dev/null +++ b/lib/base/tlsutility.cpp @@ -0,0 +1,1053 @@ +/* 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 +#include +#include +#include + +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::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; + + 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 (!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::Ptr MakeAsioSslContext(const String& pubkey, const String& privkey, const String& cakey) +{ + namespace ssl = boost::asio::ssl; + + InitializeOpenSSL(); + + auto context (Shared::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::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::Ptr SetupSslContext(String certPath, String keyPath, + String caPath, String crlPath, String cipherList, String protocolmin, DebugInfo di) +{ + namespace ssl = boost::asio::ssl; + + Shared::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 std::move(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::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::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& 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 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(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(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 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(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(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(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 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(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(san.CStr())); + if (subjectAltNameExt) { + X509_add_ext(cert, subjectAltNameExt, -1); + X509_EXTENSION_free(subjectAltNameExt); + } + } + + X509_sign(cert, cakey, EVP_sha256()); + + return std::shared_ptr(cert, X509_free); +} + +String GetIcingaCADir() +{ + return Configuration::DataDir + "/ca"; +} + +std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject) +{ + char errbuf[256]; + + String cadir = GetIcingaCADir(); + + String cakeyfile = cadir + "/ca.key"; + + RSA *rsa; + + BIO *cakeybio = BIO_new_file(const_cast(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(); + } + + 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(); + } + + BIO_free(cakeybio); + + String cacertfile = cadir + "/ca.crt"; + + std::shared_ptr 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, false); +} + +std::shared_ptr CreateCertIcingaCA(const std::shared_ptr& cert) +{ + std::shared_ptr pkey = std::shared_ptr(X509_get_pubkey(cert.get()), EVP_PKEY_free); + return CreateCertIcingaCA(pkey.get(), X509_get_subject_name(cert.get())); +} + +bool IsCertUptodate(const std::shared_ptr& cert) +{ + time_t now; + time(&now); + + /* 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 */ + time_t renewalStart = now + RENEW_THRESHOLD; + + return X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1; +} + +String CertificateToString(const std::shared_ptr& cert) +{ + BIO *mem = BIO_new(BIO_s_mem()); + PEM_write_bio_X509(mem, cert.get()); + + char *data; + long len = BIO_get_mem_data(mem, &data); + + String result = String(data, data + len); + + BIO_free(mem); + + return result; +} + +std::shared_ptr 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(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(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(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(digest), reinterpret_cast(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 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 &caCertificate, const std::shared_ptr &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& 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& cert) +{ + return X509_get_version(cert.get()) + 1; +} + +String GetSignatureAlgorithm(const std::shared_ptr& 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& 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..3b8a89c --- /dev/null +++ b/lib/base/tlsutility.hpp @@ -0,0 +1,85 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace icinga +{ + +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:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384: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::Ptr MakeAsioSslContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String()); +void AddCRLToSSLContext(const Shared::Ptr& context, const String& crlPath); +void AddCRLToSSLContext(X509_STORE *x509_store, const String& crlPath); +void SetCipherListToSSLContext(const Shared::Ptr& context, const String& cipherList); +void SetTlsProtocolminToSSLContext(const Shared::Ptr& context, const String& tlsProtocolmin); +int ResolveTlsProtocolVersion(const std::string& version); + +Shared::Ptr SetupSslContext(String certPath, String keyPath, + String caPath, String crlPath, String cipherList, String protocolmin, DebugInfo di); + +String GetCertificateCN(const std::shared_ptr& certificate); +std::shared_ptr 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 CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca); + +String GetIcingaCADir(); +String CertificateToString(const std::shared_ptr& cert); + +std::shared_ptr StringToCertificate(const String& cert); +std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject); +std::shared_ptr CreateCertIcingaCA(const std::shared_ptr& cert); +bool IsCertUptodate(const std::shared_ptr& 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& caCertificate, const std::shared_ptr& certificate, const String& crlFile); +bool IsCa(const std::shared_ptr& cacert); +int GetCertificateVersion(const std::shared_ptr& cert); +String GetSignatureAlgorithm(const std::shared_ptr& cert); +Array::Ptr GetSubjectAltNames(const std::shared_ptr& cert); + +class openssl_error : virtual public std::exception, virtual public boost::exception { }; + +struct errinfo_openssl_error_; +typedef boost::error_info errinfo_openssl_error; + +} + +#endif /* TLSUTILITY_H */ diff --git a/lib/base/type.cpp b/lib/base/type.cpp new file mode 100644 index 0000000..493833d --- /dev/null +++ b/lib/base/type.cpp @@ -0,0 +1,208 @@ +/* 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; + +/* 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); +}, 20); + +String Type::ToString() const +{ + return "type '" + GetName() + "'"; +} + +void Type::Register(const Type::Ptr& type) +{ + ScriptGlobal::Set("Types." + type->GetName(), type, true); +} + +Type::Ptr Type::GetByName(const String& name) +{ + Namespace::Ptr typesNS = ScriptGlobal::Get("Types", &Empty); + + if (!typesNS) + return nullptr; + + Value ptype; + if (!typesNS->Get(name, &ptype)) + return nullptr; + + if (!ptype.IsObjectType()) + return nullptr; + + return ptype; +} + +std::vector Type::GetAllTypes() +{ + std::vector types; + + Namespace::Ptr typesNS = ScriptGlobal::Get("Types", &Empty); + + if (typesNS) { + ObjectLock olock(typesNS); + + for (const Namespace::Pair& kv : typesNS) { + Value value = kv.second->Get(); + + if (value.IsObjectType()) + 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& 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.")); +} + +std::vector Type::GetLoadDependencies() const +{ + return std::vector(); +} + +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..2bf54cc --- /dev/null +++ b/lib/base/type.hpp @@ -0,0 +1,147 @@ +/* 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 + +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& 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 GetAllTypes(); + + void SetField(int id, const Value& value, bool suppress_events = false, const Value& cookie = Empty) override; + Value GetField(int id) const override; + + virtual std::vector GetLoadDependencies() const; + virtual int GetActivationPriority() const; + + typedef std::function 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 +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::TypeInstance = t; \ + icinga::Type::Register(t); \ + }, 10); \ + DEFINE_TYPE_INSTANCE(type) + +#define REGISTER_TYPE_WITH_PROTOTYPE(type, prototype) \ + INITIALIZE_ONCE_WITH_PRIORITY([]() { \ + icinga::Type::Ptr t = new TypeImpl(); \ + t->SetPrototype(prototype); \ + type::TypeInstance = t; \ + icinga::Type::Register(t); \ + }, 10); \ + 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(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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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..a2930a0 --- /dev/null +++ b/lib/base/utility.cpp @@ -0,0 +1,2023 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __FreeBSD__ +# include +#endif /* __FreeBSD__ */ + +#ifdef HAVE_CXXABI_H +# include +#endif /* HAVE_CXXABI_H */ + +#ifndef _WIN32 +# include +# include +# include +# include +# include +# include +#endif /* _WIN32 */ + +#ifdef _WIN32 +# include +# include +# include +# include +# include +#endif /*_WIN32*/ + +using namespace icinga; + +boost::thread_specific_ptr Utility::m_ThreadName; +boost::thread_specific_ptr 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(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:) */ + 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(boost::uuids::random_generator()()); +} + +#ifdef _WIN32 +static bool GlobHelper(const String& pathSpec, int type, std::vector& files, std::vector& 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& callback, int type) +{ + std::vector files, dirs; + +#ifdef _WIN32 + std::vector tokens = pathSpec.Split("\\/"); + + String part1; + + for (std::vector::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::size_type k = i + 1; k < tokens.size(); k++) { + if (!part2.IsEmpty()) + part2 += "/"; + + part2 += tokens[k]; + } + + std::vector 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& callback, int type) +{ + std::vector 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& except, std::function onClose) +{ +#if defined(__linux__) || defined(__APPLE__) + namespace fs = boost::filesystem; + + std::set 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(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& callback, SchedulerPolicy policy) +{ + Application::GetTP().Post(callback, policy); +} + +String Utility::NaturalJoin(const std::vector& tokens) +{ + String result; + + for (std::vector::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 tokens; + String result; + + if (duration >= 86400) { + int days = duration / 86400; + tokens.emplace_back(Convert::ToString(days) + (days != 1 ? " days" : " day")); + duration = static_cast(duration) % 86400; + } + + if (duration >= 3600) { + int hours = duration / 3600; + tokens.emplace_back(Convert::ToString(hours) + (hours != 1 ? " hours" : " hour")); + duration = static_cast(duration) % 3600; + } + + if (duration >= 60) { + int minutes = duration / 60; + tokens.emplace_back(Convert::ToString(minutes) + (minutes != 1 ? " minutes" : " minute")); + duration = static_cast(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 tokensv1 = v1.Split("."); + std::vector tokensv2 = v2.Split("."); + + for (std::vector::size_type i = 0; i < tokensv2.size() - tokensv1.size(); i++) + tokensv1.emplace_back("0"); + + for (std::vector::size_type i = 0; i < tokensv1.size() - tokensv2.size(); i++) + tokensv2.emplace_back("0"); + + for (std::vector::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(path)); +} + +Value Utility::LoadJsonFile(const String& path) +{ + std::ifstream fp; + fp.open(path.CStr()); + + String json((std::istreambuf_iterator(fp)), std::istreambuf_iterator()); + + 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) +{ + namespace fs = boost::filesystem; + + std::fstream fp; + String tempFilename = Utility::CreateTempFile(path + ".XXXXXX", mode, fp); + + fp.exceptions(std::ofstream::failbit | std::ofstream::badbit); + fp << JsonEncode(value); + fp.close(); + + RenameFile(tempFilename, path); +} + +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)); +} + +String Utility::CreateTempFile(const String& path, int mode, std::fstream& fp) +{ + std::vector targetPath(path.Begin(), path.End()); + targetPath.push_back('\0'); + + int fd; +#ifndef _WIN32 + fd = mkstemp(&targetPath[0]); +#else /* _WIN32 */ + fd = MksTemp(&targetPath[0]); +#endif /*_WIN32*/ + + if (fd < 0) { + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("mkstemp") + << boost::errinfo_errno(errno) + << boost::errinfo_file_name(path)); + } + + try { + fp.open(&targetPath[0], std::ios_base::trunc | std::ios_base::out); + } catch (const std::fstream::failure&) { + close(fd); + throw; + } + + close(fd); + + String resultPath = String(targetPath.begin(), targetPath.end() - 1); + + if (chmod(resultPath.CStr(), mode) < 0) { + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("chmod") + << boost::errinfo_errno(errno) + << boost::errinfo_file_name(resultPath)); + } + + return resultPath; +} + +#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..6f9d03d --- /dev/null +++ b/lib/base/utility.hpp @@ -0,0 +1,202 @@ +/* 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 +#include +#include +#include +#include + +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& callback, int type = GlobFile | GlobDirectory); + static bool GlobRecursive(const String& path, const String& pattern, const std::function& 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& callback, SchedulerPolicy policy = DefaultScheduler); + + static String NaturalJoin(const std::vector& 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& except, std::function 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); + + static String CreateTempFile(const String& path, int mode, std::fstream& fp); + +#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 + 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 m_ThreadName; + static boost::thread_specific_ptr 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..69974d0 --- /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 + +using namespace icinga; + +Value::operator double() const +{ + const double *value = boost::get(&m_Value); + + if (value) + return *value; + + const bool *fvalue = boost::get(&m_Value); + + if (fvalue) + return *fvalue; + + if (IsEmpty()) + return 0; + + try { + return boost::lexical_cast(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(m_Value)); + case ValueBoolean: + if (boost::get(m_Value)) + return "true"; + else + return "false"; + case ValueString: + return boost::get(m_Value); + case ValueObject: + object = boost::get(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(value); + else + stream << static_cast(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(*this) == rhs; +} + +bool Value::operator!=(const char *rhs) const +{ + return !(*this == rhs); +} + +bool Value::operator==(const String& rhs) const +{ + return static_cast(*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() == rhs.Get(); + else if ((IsBoolean() || IsNumber()) && (rhs.IsBoolean() || rhs.IsNumber()) && !(IsEmpty() && rhs.IsEmpty())) + return static_cast(*this) == static_cast(rhs); + + if (IsString() && rhs.IsString()) + return Get() == rhs.Get(); + else if ((IsString() || IsEmpty()) && (rhs.IsString() || rhs.IsEmpty()) && !(IsEmpty() && rhs.IsEmpty())) + return static_cast(*this) == static_cast(rhs); + + if (IsEmpty() != rhs.IsEmpty()) + return false; + + if (IsEmpty()) + return true; + + if (IsObject() != rhs.IsObject()) + return false; + + if (IsObject()) { + if (IsObjectType() && rhs.IsObjectType()) { + DateTime::Ptr dt1 = *this; + DateTime::Ptr dt2 = rhs; + + return dt1->GetValue() == dt2->GetValue(); + } + + if (IsObjectType() && rhs.IsObjectType()) { + 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() == rhs.Get(); + } + + 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(lhs) + static_cast(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(lhs) + static_cast(rhs); + else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) + return static_cast(lhs) + static_cast(rhs); + else if (lhs.IsObjectType() && rhs.IsNumber()) + return new DateTime(Convert::ToDateTimeValue(lhs) + rhs); + else if ((lhs.IsObjectType() || lhs.IsEmpty()) && (rhs.IsObjectType() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) { + Array::Ptr result = new Array(); + if (!lhs.IsEmpty()) + static_cast(lhs)->CopyTo(result); + if (!rhs.IsEmpty()) + static_cast(rhs)->CopyTo(result); + return result; + } else if ((lhs.IsObjectType() || lhs.IsEmpty()) && (rhs.IsObjectType() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) { + Dictionary::Ptr result = new Dictionary(); + if (!lhs.IsEmpty()) + static_cast(lhs)->CopyTo(result); + if (!rhs.IsEmpty()) + static_cast(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(lhs) - static_cast(rhs); + else if (lhs.IsObjectType() && rhs.IsNumber()) + return new DateTime(Convert::ToDateTimeValue(lhs) - rhs); + else if (lhs.IsObjectType() && rhs.IsObjectType()) + return Convert::ToDateTimeValue(lhs) - Convert::ToDateTimeValue(rhs); + else if ((lhs.IsObjectType() || lhs.IsEmpty()) && (rhs.IsObjectType() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) + return new DateTime(Convert::ToDateTimeValue(lhs) - Convert::ToDateTimeValue(rhs)); + else if ((lhs.IsObjectType() || lhs.IsEmpty()) && (rhs.IsObjectType() || 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(lhs) * static_cast(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(rhs) == 0) + BOOST_THROW_EXCEPTION(std::invalid_argument("Right-hand side argument for operator / is 0.")); + + return static_cast(lhs) / static_cast(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(rhs) == 0) + BOOST_THROW_EXCEPTION(std::invalid_argument("Right-hand side argument for operator % is 0.")); + + return static_cast(lhs) % static_cast(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(lhs) ^ static_cast(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(lhs) & static_cast(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(lhs) | static_cast(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(lhs) << static_cast(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(lhs) >> static_cast(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(lhs) < static_cast(rhs); + else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) + return static_cast(lhs) < static_cast(rhs); + else if ((lhs.IsObjectType() || lhs.IsEmpty()) && (rhs.IsObjectType() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) + return Convert::ToDateTimeValue(lhs) < Convert::ToDateTimeValue(rhs); + else if (lhs.IsObjectType() && rhs.IsObjectType()) { + 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(lhs) > static_cast(rhs); + else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) + return static_cast(lhs) > static_cast(rhs); + else if ((lhs.IsObjectType() || lhs.IsEmpty()) && (rhs.IsObjectType() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) + return Convert::ToDateTimeValue(lhs) > Convert::ToDateTimeValue(rhs); + else if (lhs.IsObjectType() && rhs.IsObjectType()) { + 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(lhs) <= static_cast(rhs); + else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) + return static_cast(lhs) <= static_cast(rhs); + else if ((lhs.IsObjectType() || lhs.IsEmpty()) && (rhs.IsObjectType() || rhs.IsEmpty()) && !(lhs.IsEmpty() && 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(lhs) >= static_cast(rhs); + else if ((lhs.IsNumber() || lhs.IsEmpty()) && (rhs.IsNumber() || rhs.IsEmpty()) && !(lhs.IsEmpty() && rhs.IsEmpty())) + return static_cast(lhs) >= static_cast(rhs); + else if ((lhs.IsObjectType() || lhs.IsEmpty()) && (rhs.IsObjectType() || rhs.IsEmpty()) && !(lhs.IsEmpty() && 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; +template const double& Value::Get() const; +template const bool& Value::Get() const; +template const String& Value::Get() const; +template const Object::Ptr& Value::Get() 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& 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(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(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(boost::get(m_Value)); + + case ValueBoolean: + return boost::get(m_Value); + + case ValueString: + return !boost::get(m_Value).IsEmpty(); + + case ValueObject: + if (IsObjectType()) { + Dictionary::Ptr dictionary = *this; + return dictionary->GetLength() > 0; + } else if (IsObjectType()) { + 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(m_Value)->GetReflectionType(); + if (!t) { + if (IsObjectType()) + return "Array"; + else if (IsObjectType()) + 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(m_Value)->GetReflectionType(); + default: + return nullptr; + } +} + +Value Value::Clone() const +{ + if (IsObject()) + return static_cast(*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 +#include +#include + +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& value); + + template + Value(const intrusive_ptr& value) + : Value(static_pointer_cast(value)) + { + static_assert(!std::is_same::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 + operator intrusive_ptr() const + { + if (IsEmpty() && !IsString()) + return intrusive_ptr(); + + if (!IsObject()) + BOOST_THROW_EXCEPTION(std::runtime_error("Cannot convert value of type '" + GetTypeName() + "' to an object.")); + + const auto& object = Get(); + + ASSERT(object); + + intrusive_ptr tobject = dynamic_pointer_cast(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 + bool IsObjectType() const + { + if (!IsObject()) + return false; + + return dynamic_cast(Get().get()); + } + + ValueType GetType() const; + + void Swap(Value& other); + + String GetTypeName() const; + + Type::Ptr GetReflectionType() const; + + Value Clone() const; + + template + const T& Get() const + { + return boost::get(m_Value); + } + +private: + boost::variant m_Value; +}; + +extern template const double& Value::Get() const; +extern template const bool& Value::Get() const; +extern template const String& Value::Get() const; +extern template const Object::Ptr& Value::Get() 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; + +#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 +#include +#include +#include +#include + +#include + +#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 + +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()) { + 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 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 +{ +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..47d560e --- /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 +#include + +using namespace icinga; + +std::atomic WorkQueue::m_NextID(1); +boost::thread_specific_ptr 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 = new Timer(); + 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 WorkQueue::AcquireLock() +{ + return std::unique_lock(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& lock, std::function&& 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&& 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 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 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 WorkQueue::GetExceptions() const +{ + std::unique_lock lock(m_Mutex); + + return m_Exceptions; +} + +void WorkQueue::ReportExceptions(const String& facility, bool verbose) const +{ + std::vector 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 lock(m_Mutex); + + return m_Tasks.size(); +} + +void WorkQueue::StatusTimerHandler() +{ + std::unique_lock 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 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 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 +#include +#include +#include +#include +#include +#include + +namespace icinga +{ + +enum WorkQueuePriority +{ + PriorityLow = 0, + PriorityNormal = 1, + PriorityHigh = 2, + PriorityImmediate = 4 +}; + +using TaskFunction = std::function; + +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 ExceptionCallback; + + WorkQueue(size_t maxItems = 0, int threadCount = 1, LogSeverity statsLogLevel = LogInformation); + ~WorkQueue(); + + void SetName(const String& name); + String GetName() const; + + std::unique_lock AcquireLock(); + void EnqueueUnlocked(std::unique_lock& lock, TaskFunction&& function, WorkQueuePriority priority = PriorityNormal); + void Enqueue(TaskFunction&& function, WorkQueuePriority priority = PriorityNormal, + bool allowInterleaved = false); + void Join(bool stop = false); + + template + void ParallelFor(const VectorType& items, const FuncType& func) + { + ParallelFor(items, true, func); + } + + template + 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 GetExceptions() const; + void ReportExceptions(const String& facility, bool verbose = false) const; + +protected: + void IncreaseTaskCount(); + +private: + int m_ID; + String m_Name; + static std::atomic 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 > m_Tasks; + int m_NextTaskID{0}; + ExceptionCallback m_ExceptionCallback; + std::vector 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..aeaf2d1 --- /dev/null +++ b/lib/checker/checkercomponent.cpp @@ -0,0 +1,348 @@ +/* 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 + +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()) { + 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::Start(runtimeCreated); + + Log(LogInformation, "CheckerComponent") + << "'" << GetName() << "' started."; + + + m_Thread = std::thread([this]() { CheckThreadProc(); }); + + m_ResultTimer = new Timer(); + m_ResultTimer->SetInterval(5); + m_ResultTimer->OnTimerExpired.connect([this](const Timer * const&) { ResultTimerHandler(); }); + m_ResultTimer->Start(); +} + +void CheckerComponent::Stop(bool runtimeRemoved) +{ + { + std::unique_lock lock(m_Mutex); + m_Stopped = true; + m_CV.notify_all(); + } + + m_ResultTimer->Stop(); + m_Thread.join(); + + Log(LogInformation, "CheckerComponent") + << "'" << GetName() << "' stopped."; + + ObjectImpl::Stop(runtimeRemoved); +} + +void CheckerComponent::CheckThreadProc() +{ + Utility::SetThreadName("Check Scheduler"); + IcingaApplication::Ptr icingaApp = IcingaApplication::GetInstance(); + + std::unique_lock lock(m_Mutex); + + for (;;) { + typedef boost::multi_index::nth_index::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(wait)); + + continue; + } + + Checkable::Ptr checkable = csi.Object; + + m_IdleCheckables.erase(checkable); + + bool forced = checkable->GetForceNextCheck(); + bool check = true; + + if (!forced) { + if (!checkable->IsReachable(DependencyCheckExecution)) { + Log(LogNotice, "CheckerComponent") + << "Skipping check for object '" << checkable->GetName() << "': Dependency failed."; + check = false; + } + + 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; + } + } + + /* 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(); + + 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 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 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(object); + + if (!checkable) + return; + + Zone::Ptr zone = Zone::GetByName(checkable->GetZoneName()); + bool same_zone = (!zone || Zone::GetLocalZone() == zone); + + { + std::unique_lock 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 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::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 lock(m_Mutex); + + return m_IdleCheckables.size(); +} + +unsigned long CheckerComponent::GetPendingCheckables() +{ + std::unique_lock 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 +#include +#include +#include +#include +#include + +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 +{ +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::ordered_non_unique + > + > 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 + +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(), "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& ap) const +{ + String cn; + + if (vm.count("cn")) { + cn = vm["cn"].as(); + } 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& 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..21583af --- /dev/null +++ b/lib/cli/apisetuputility.cpp @@ -0,0 +1,207 @@ +/* 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/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 +#include +#include +#include +#include +#include +#include + +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); + + std::fstream fp; + String tempFilename = Utility::CreateTempFile(apiUsersPath + ".XXXXXX", 0644, fp); + + 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.close(); + + Utility::RenameFile(tempFilename, apiUsersPath); + + 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 + +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 + +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& 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& 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& 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 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& 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& 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 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& 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& 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 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 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& 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 +#include +#include +#include +#include + +using namespace icinga; +namespace po = boost::program_options; + +std::vector icinga::GetBashCompletionSuggestions(const String& type, const String& word) +{ + std::vector 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 icinga::GetFieldCompletionSuggestions(const Type::Ptr& type, const String& word) +{ + std::vector 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, CLICommand::Ptr>& CLICommand::GetRegistry() +{ + static std::map, CLICommand::Ptr> registry; + return registry; +} + +CLICommand::Ptr CLICommand::GetByName(const std::vector& name) +{ + std::unique_lock lock(GetRegistryMutex()); + + auto it = GetRegistry().find(name); + + if (it == GetRegistry().end()) + return nullptr; + + return it->second; +} + +void CLICommand::Register(const std::vector& name, const CLICommand::Ptr& function) +{ + std::unique_lock lock(GetRegistryMutex()); + GetRegistry()[name] = function; +} + +void CLICommand::Unregister(const std::vector& name) +{ + std::unique_lock lock(GetRegistryMutex()); + GetRegistry().erase(name); +} + +std::vector CLICommand::GetArgumentSuggestions(const String& argument, const String& word) const +{ + return std::vector(); +} + +std::vector CLICommand::GetPositionalSuggestions(const String& word) const +{ + return std::vector(); +} + +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 lock(GetRegistryMutex()); + + typedef std::map, CLICommand::Ptr>::value_type CLIKeyValue; + + std::vector best_match; + int arg_end = 0; + bool tried_command = false; + + for (const CLIKeyValue& kv : GetRegistry()) { + const std::vector& vname = kv.first; + + std::vector::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 lock(GetRegistryMutex()); + + typedef std::map, CLICommand::Ptr>::value_type CLIKeyValue; + + std::vector best_match; + int arg_begin = 0; + CLICommand::Ptr command; + + for (const CLIKeyValue& kv : GetRegistry()) { + const std::vector& vname = kv.first; + + arg_begin = 0; + + std::vector::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(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(best_match.size()) && !command) + return; + } else + std::cout << "Supported commands: " << std::endl; + + for (const CLIKeyValue& kv : GetRegistry()) { + const std::vector& vname = kv.first; + + if (vname.size() < best_match.size() || kv.second->IsHidden()) + continue; + + bool match = true; + + for (std::vector::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(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& 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 +#include + +namespace icinga +{ + +std::vector GetBashCompletionSuggestions(const String& type, const String& word); +std::vector 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(*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& ap) const = 0; + virtual std::vector GetArgumentSuggestions(const String& argument, const String& word) const; + virtual std::vector GetPositionalSuggestions(const String& word) const; + + static CLICommand::Ptr GetByName(const std::vector& name); + static void Register(const std::vector& name, const CLICommand::Ptr& command); + static void Unregister(const std::vector& 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, CLICommand::Ptr>& GetRegistry(); +}; + +#define REGISTER_CLICOMMAND(name, klass) \ + INITIALIZE_ONCE([]() { \ + std::vector 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..b54966e --- /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 +#include "base/exception.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#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::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 expr; + + try { + ScriptFrame frame(true); + expr = ConfigCompiler::CompileText("", 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 expr; + + try { + ScriptFrame frame(true); + frame.Locals = new Dictionary({ + { "arg", value } + }); + expr = ConfigCompiler::CompileText("", 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 expr; + + try { + ScriptFrame frame(true); + frame.Locals = new Dictionary({ + { "arg", object } + }); + expr = ConfigCompiler::CompileText("", 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 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(), "connect to an Icinga 2 instance") + ("eval,e", po::value(), "evaluate expression and terminate") + ("file,r", po::value(), "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 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(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& 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(); + + 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(); + else if (vm.count("file")) { + commandFileName = vm["file"].as(); + + try { + std::ifstream fp(commandFileName.CStr()); + fp.exceptions(std::ifstream::failbit | std::ifstream::badbit); + command = String(std::istreambuf_iterator(fp), std::istreambuf_iterator()); + } 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 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 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(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 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::Ptr ConsoleCommand::Connect() +{ + Shared::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::Ptr stream = Shared::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 std::move(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 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 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 + + +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& 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::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..fb1dd02 --- /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/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 +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else /* _WIN32 */ +#include +#include +#include +#include +#endif /* _WIN32 */ + +#ifdef HAVE_SYSTEMD +#include +#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 >(), "parse a configuration file") + ("no-config,z", "start without a configuration file") + ("validate,C", "exit after validating the configuration") + ("errorlog,e", po::value(), "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 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 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 */ + +/** + * 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& 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 attachment."; + + Utility::Sleep(delay); + } +#endif /* I2_DEBUG */ + + Log(LogInformation, "cli", "Loading configuration file(s)."); + NotifyStatus("Loading configuration file(s)..."); + + { + std::vector newItems; + + if (!DaemonUtility::LoadConfigFiles(configs, newItems, Configuration::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 possible states of a seamless worker being started by StartUnixWorker(). + */ +enum class UnixWorkerState : uint_fast8_t +{ + Pending, + LoadedConfig, + Failed +}; + +// The signals to block temporarily in StartUnixWorker(). +static const sigset_t l_UnixWorkerSignals = ([]() -> sigset_t { + sigset_t s; + + (void)sigemptyset(&s); + (void)sigaddset(&s, SIGCHLD); + (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 l_CurrentlyStartingUnixWorkerPid (-1); + +// The state of the seamless worker currently being started by StartUnixWorker() +static Atomic l_CurrentlyStartingUnixWorkerState (UnixWorkerState::Pending); + +// The last temination signal we received +static Atomic l_TermSignal (-1); + +// Whether someone requested to re-load config (and we didn't handle that request, yet) +static Atomic l_RequestedReload (false); + +// Whether someone requested to re-open logs (and we didn't handle that request, yet) +static Atomic 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_CurrentlyStartingUnixWorkerState.load() == UnixWorkerState::Pending + && (info->si_pid == 0 || info->si_pid == l_CurrentlyStartingUnixWorkerPid.load()) ) { + // The seamless worker currently being started by StartUnixWorker() successfully loaded its config + l_CurrentlyStartingUnixWorkerState.store(UnixWorkerState::LoadedConfig); + } + break; + case SIGCHLD: + if (l_CurrentlyStartingUnixWorkerState.load() == UnixWorkerState::Pending + && (info->si_pid == 0 || info->si_pid == l_CurrentlyStartingUnixWorkerPid.load()) ) { + // The seamless worker currently being started by StartUnixWorker() failed + l_CurrentlyStartingUnixWorkerState.store(UnixWorkerState::Failed); + } + 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 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& 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 and SIGCHLD handlers 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(SIGCHLD, &sa, nullptr); + (void)sigaction(SIGUSR1, &sa, nullptr); + (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 */ + + switch (l_CurrentlyStartingUnixWorkerState.load()) { + case UnixWorkerState::LoadedConfig: + Log(LogNotice, "cli") + << "Worker process successfully loaded its config"; + break; + case UnixWorkerState::Failed: + Log(LogNotice, "cli") + << "Worker process couldn't load its config"; + + while (waitpid(pid, nullptr, 0) == -1 && errno == EINTR) { +#ifdef HAVE_SYSTEMD + NotifyWatchdog(); +#endif /* HAVE_SYSTEMD */ + } + pid = -2; + break; + default: + Utility::Sleep(0.2); + continue; + } + + break; + } + + // Reset flags for the next time + l_CurrentlyStartingUnixWorkerPid.store(-1); + l_CurrentlyStartingUnixWorkerState.store(UnixWorkerState::Pending); + + 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& 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 configs; + if (vm.count("config") > 0) + configs = vm["config"].as >(); + 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("validate")) { + Log(LogInformation, "cli", "Loading configuration file(s)."); + + std::vector newItems; + + if (!DaemonUtility::LoadConfigFiles(configs, newItems, Configuration::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(); + + 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(SIGCHLD, &sa, nullptr); + (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(); + + // 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 */ + + pid_t nextWorker = StartUnixWorker(configs); + + switch (nextWorker) { + case -1: + break; + case -2: + Log(LogCritical, "Application", "Found error in config: reloading aborted"); + break; + default: + Log(LogInformation, "Application") + << "Reload done, old process shutting down. Child process with PID '" << nextWorker << "' is taking over."; + + NotifyStatus("Shutting down old instance..."); + + (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 GetArgumentSuggestions(const String& argument, const String& word) const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& ap) const override; +}; + +} + +#endif /* DAEMONCOMMAND_H */ diff --git a/lib/cli/daemonutility.cpp b/lib/cli/daemonutility.cpp new file mode 100644 index 0000000..4c862ab --- /dev/null +++ b/lib/cli/daemonutility.cpp @@ -0,0 +1,263 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "cli/daemonutility.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 + +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 > 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 > 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 expr = ConfigCompiler::CompileFile(packagePath + "/include.conf", + String(), packageName); + + if (!ExecuteExpression(&*expr)) + success = false; + } +} + +bool DaemonUtility::ValidateConfigFiles(const std::vector& configs, const String& objectsFile) +{ + bool success; + + Namespace::Ptr systemNS = ScriptGlobal::Get("System"); + VERIFY(systemNS); + + if (!objectsFile.IsEmpty()) + ConfigCompilerContext::GetInstance()->OpenObjectsFile(objectsFile); + + if (!configs.empty()) { + for (const String& configPath : configs) { + try { + std::unique_ptr 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 (!systemNS->Contains("ZonesStageVarDir")) { + String zonesEtcDir = Configuration::ZonesDir; + if (!zonesEtcDir.IsEmpty() && Utility::PathExists(zonesEtcDir)) { + std::set 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 (systemNS->Contains("ZonesStageVarDir")) { + zonesVarDir = systemNS->Get("ZonesStageVarDir"); + + Log(LogNotice, "DaemonUtility") + << "Overriding zones var directory with '" << zonesVarDir << "' for cluster config sync staging."; + } + + + if (Utility::PathExists(zonesVarDir)) { + std::set 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& configs, + std::vector& newItems, + const String& objectsFile, const String& varsfile) +{ + ActivationScope ascope; + + if (!DaemonUtility::ValidateConfigFiles(configs, objectsFile)) { + ConfigCompilerContext::GetInstance()->CancelObjectsFile(); + return false; + } + + WorkQueue upq(25000, Configuration::Concurrency); + upq.SetName("DaemonUtility::LoadConfigFiles"); + bool result = ConfigItem::CommitItems(ascope.GetContext(), upq, newItems); + + if (!result) { + ConfigCompilerContext::GetInstance()->CancelObjectsFile(); + return false; + } + + ConfigCompilerContext::GetInstance()->FinishObjectsFile(); + + try { + ScriptGlobal::WriteToFile(varsfile); + } catch (const std::exception& ex) { + Log(LogCritical, "cli", "Could not write vars file: " + DiagnosticInformation(ex, false)); + Application::Exit(1); + } + + 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 + +namespace icinga +{ + +/** + * @ingroup cli + */ +class DaemonUtility +{ +public: + static bool ValidateConfigFiles(const std::vector& configs, const String& objectsFile = String()); + static bool LoadConfigFiles(const std::vector& configs, std::vector& 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 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& 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 GetPositionalSuggestions(const String& word) const override; + ImpersonationLevel GetImpersonationLevel() const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& 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 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& 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 GetPositionalSuggestions(const String& word) const override; + ImpersonationLevel GetImpersonationLevel() const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& 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 +#include + +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& 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& 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 +#include +#include +#include + +using namespace icinga; + +String FeatureUtility::GetFeaturesAvailablePath() +{ + return Configuration::ConfigDir + "/features-available"; +} + +String FeatureUtility::GetFeaturesEnabledPath() +{ + return Configuration::ConfigDir + "/features-enabled"; +} + +std::vector FeatureUtility::GetFieldCompletionSuggestions(const String& word, bool enable) +{ + std::vector cache; + std::vector 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& 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 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& 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 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 disabled_features; + std::vector 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& features, bool get_disabled) +{ + /* request all disabled features */ + if (get_disabled) { + /* disable = available-enabled */ + String available_pattern = GetFeaturesAvailablePath() + "/*.conf"; + std::vector available; + Utility::Glob(available_pattern, [&available](const String& featureFile) { CollectFeatures(featureFile, available); }, GlobFile); + + String enabled_pattern = GetFeaturesEnabledPath() + "/*.conf"; + std::vector 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 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& 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 +#include + +namespace icinga +{ + +/** + * @ingroup cli + */ +class FeatureUtility +{ +public: + static String GetFeaturesAvailablePath(); + static String GetFeaturesEnabledPath(); + + static std::vector GetFieldCompletionSuggestions(const String& word, bool enable); + + static int EnableFeatures(const std::vector& features); + static int DisableFeatures(const std::vector& features); + static int ListFeatures(std::ostream& os = std::cout); + + static bool GetFeatures(std::vector& 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& 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 + +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(), "Target PID") + ("sig,s", po::value(), "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& ap) const +{ +#ifndef _WIN32 + String signal = vm["sig"].as(); + + /* Thank POSIX */ + if (signal == "SIGKILL") + return kill(vm["pid"].as(), SIGKILL); + if (signal == "SIGINT") + return kill(vm["pid"].as(), SIGINT); + if (signal == "SIGCHLD") + return kill(vm["pid"].as(), SIGCHLD); + if (signal == "SIGHUP") + return kill(vm["pid"].as(), 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& ap) const override; + +}; + +} + +#endif /* INTERNALSIGNALCOMMAND_H */ diff --git a/lib/cli/nodesetupcommand.cpp b/lib/cli/nodesetupcommand.cpp new file mode 100644 index 0000000..688ff97 --- /dev/null +++ b/lib/cli/nodesetupcommand.cpp @@ -0,0 +1,568 @@ +/* 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/logger.hpp" +#include "base/console.hpp" +#include "base/application.hpp" +#include "base/tlsutility.hpp" +#include "base/scriptglobal.hpp" +#include "base/exception.hpp" +#include +#include + +#include +#include +#include + +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(), "The name of the local zone") + ("endpoint", po::value >(), "Connect to remote endpoint; syntax: cn[,host,port]") + ("parent_host", po::value(), "The name of the parent host for auto-signing the csr; syntax: host[,port]") + ("parent_zone", po::value(), "The name of the parent zone") + ("listen", po::value(), "Listen on host,port") + ("ticket", po::value(), "Generated ticket number for this request (optional)") + ("trustedcert", po::value(), "Trusted parent certificate file as connection verification (received via 'pki save-cert')") + ("cn", po::value(), "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 >(), "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(), "DEPRECATED: The name of the master zone") + ("master_host", po::value(), "DEPRECATED: The name of the master host for auto-signing the csr; syntax: host[,port]"); +} + +std::vector 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& 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& 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(); + + /* 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(); + + /* 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 globalZones { "global-templates", "director-global" }; + std::vector setupGlobalZones; + + if (vm.count("global_zones")) + setupGlobalZones = vm["global_zones"].as >(); + + 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); + + std::fstream fp; + String tempApiPath = Utility::CreateTempFile(apipath + ".XXXXXX", 0644, fp); + + fp << "/**\n" + << " * The API listener is used for distributed monitoring setups.\n" + << " */\n" + << "object ApiListener \"api\" {\n"; + + if (vm.count("listen")) { + std::vector tokens = String(vm["listen"].as()).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.close(); + + Utility::RenameFile(tempApiPath, apipath); + + /* 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& 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(); + + 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 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(); + else if (vm.count("master_host")) /* TODO: Remove in 2.10.0. */ + parentHostInfo = vm["master_host"].as(); + + std::vector 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(); + + 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 --port <5665> --key local.key --cert local.crt --trustedcert trusted-parent.crt')."; + return 1; + } + + String trustedCert = vm["trustedcert"].as(); + + 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() << "'."; + + 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); + + std::fstream fp; + String tempApiPath = Utility::CreateTempFile(apipath + ".XXXXXX", 0644, fp); + + fp << "/**\n" + << " * The API listener is used for distributed monitoring setups.\n" + << " */\n" + << "object ApiListener \"api\" {\n"; + + if (vm.count("listen")) { + std::vector tokens = String(vm["listen"].as()).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.close(); + + Utility::RenameFile(tempApiPath, apipath); + + /* 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(); + + /* Allow to specify the parent zone name. */ + String parentZoneName = "master"; + + if (vm.count("parent_zone")) + parentZoneName = vm["parent_zone"].as(); + + std::vector globalZones { "global-templates", "director-global" }; + std::vector setupGlobalZones; + + if (vm.count("global_zones")) + setupGlobalZones = vm["global_zones"].as >(); + + 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 >(), 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"; + + String tempTicketPath = Utility::CreateTempFile(ticketPath + ".XXXXXX", 0600, fp); + + if (!Utility::SetFileOwnership(tempTicketPath, user, group)) { + Log(LogWarning, "cli") + << "Cannot set ownership for user '" << user + << "' group '" << group + << "' on file '" << tempTicketPath << "'. Verify it yourself!"; + } + + fp << ticket; + + fp.close(); + + Utility::RenameFile(tempTicketPath, ticketPath); + } + + /* 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 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& ap) const override; + +private: + static int SetupMaster(const boost::program_options::variables_map& vm, const std::vector& ap); + static int SetupNode(const boost::program_options::variables_map& vm, const std::vector& ap); +}; + +} + +#endif /* NODESETUPCOMMAND_H */ diff --git a/lib/cli/nodeutility.cpp b/lib/cli/nodeutility.cpp new file mode 100644 index 0000000..bd343ab --- /dev/null +++ b/lib/cli/nodeutility.cpp @@ -0,0 +1,390 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "cli/nodeutility.hpp" +#include "cli/clicommand.hpp" +#include "cli/variableutility.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 +#include +#include +#include + +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& endpoints, + const std::vector& 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 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& 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!"; + } + if (!Utility::SetFileOwnership(filename, user, group)) { + Log(LogWarning, "cli") + << "Cannot set ownership for user '" << user << "' group '" << group << "' on path '" << path << "'. Verify it yourself!"; + } + + std::fstream fp; + String tempFilename = Utility::CreateTempFile(filename + ".XXXXXX", 0644, fp); + + 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.close(); + + Utility::RenameFile(tempFilename, filename); + + 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()); + std::fstream ofp; + String tempFile = Utility::CreateTempFile(configurationFile + ".XXXXXX", 0644, ofp); + + 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.close(); + + Utility::RenameFile(tempFile, configurationFile); + + 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()); + std::fstream ofp; + String tempFile = Utility::CreateTempFile(constantsConfPath + ".XXXXXX", 0644, ofp); + + 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.close(); + + Utility::RenameFile(tempFile, constantsConfPath); +} 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 + +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& endpoints, + const std::vector& globalZones); + static int GenerateNodeMasterIcingaConfig(const String& endpointName, const String& zoneName, + const std::vector& 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..abf87d8 --- /dev/null +++ b/lib/cli/nodewizardcommand.cpp @@ -0,0 +1,824 @@ +/* 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/logger.hpp" +#include "base/console.hpp" +#include "base/application.hpp" +#include "base/tlsutility.hpp" +#include "base/scriptglobal.hpp" +#include "base/exception.hpp" +#include +#include +#include +#include +#include +#include +#include + +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& 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 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 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 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); + + std::fstream fp; + String tempApiConfPath = Utility::CreateTempFile(apiConfPath + ".XXXXXX", 0644, fp); + + 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.close(); + + Utility::RenameFile(tempApiConfPath, apiConfPath); + + /* 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 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"; + + String tempTicketPath = Utility::CreateTempFile(ticketPath + ".XXXXXX", 0600, fp); + + if (!Utility::SetFileOwnership(tempTicketPath, user, group)) { + Log(LogWarning, "cli") + << "Cannot set ownership for user '" << user + << "' group '" << group + << "' on file '" << tempTicketPath << "'. Verify it yourself!"; + } + + fp << ticket; + + fp.close(); + + Utility::RenameFile(tempTicketPath, ticketPath); + } + + /* 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 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); + + std::fstream fp; + String tempApiConfPath = Utility::CreateTempFile(apiConfPath + ".XXXXXX", 0644, fp); + + 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.close(); + + Utility::RenameFile(tempApiConfPath, apiConfPath); + + /* 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& 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..f931d8c --- /dev/null +++ b/lib/cli/objectlistcommand.cpp @@ -0,0 +1,122 @@ +/* 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 +#include +#include +#include +#include + +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(), "filter by name matches") + ("type,t", po::value(), "filter by type matches"); +} + +/** + * 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& 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' 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 type_count; + + String name_filter, type_filter; + + if (vm.count("name")) + name_filter = vm["name"].as(); + if (vm.count("type")) + type_filter = vm["type"].as(); + + 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."; + + return 0; +} + +void ObjectListCommand::PrintTypeCounts(std::ostream& fp, const std::map& type_count) +{ + typedef std::map::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 + +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& ap) const override; + +private: + static void PrintTypeCounts(std::ostream& fp, const std::map& 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 +#include + +using namespace icinga; + +bool ObjectListUtility::PrintObject(std::ostream& fp, bool& first, const String& message, std::map& 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()) { + 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()) { + 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& 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& 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& 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(), "Common Name") + ("key", po::value(), "Key file path (output)") + ("csr", po::value(), "CSR file path (optional, output)") + ("cert", po::value(), "Certificate file path (optional, output)"); +} + +std::vector 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& 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(); + + if (vm.count("cert")) + cert = vm["cert"].as(); + + return PkiUtility::NewCert(vm["cn"].as(), vm["key"].as(), 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 GetArgumentSuggestions(const String& argument, const String& word) const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& 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 + +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(), "Key file path (input)") + ("cert", po::value(), "Certificate file path (input + output)") + ("ca", po::value(), "CA file path (output)") + ("trustedcert", po::value(), "Trusted certificate file path (input)") + ("host", po::value(), "Icinga 2 host") + ("port", po::value(), "Icinga 2 port") + ("ticket", po::value(), "Icinga 2 PKI ticket"); +} + +std::vector 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& 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(); + + if (vm.count("ticket")) + ticket = vm["ticket"].as(); + + return PkiUtility::RequestCertificate(vm["host"].as(), port, vm["key"].as(), + vm["cert"].as(), vm["ca"].as(), GetX509Certificate(vm["trustedcert"].as()), + 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 GetArgumentSuggestions(const String& argument, const String& word) const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& 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 + +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(), "Trusted certificate file path (output)") + ("host", po::value(), "Parent Icinga instance to fetch the public TLS certificate from") + ("port", po::value()->default_value("5665"), "Icinga 2 port"); + + hiddenDesc.add_options() + ("key", po::value()) + ("cert", po::value()); +} + +std::vector 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& 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(); + String port = vm["port"].as(); + + Log(LogInformation, "cli") + << "Retrieving TLS certificate for '" << host << ":" << port << "'."; + + std::shared_ptr 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()); +} 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 GetArgumentSuggestions(const String& argument, const String& word) const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& 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(), "CSR file path (input)") + ("cert", po::value(), "Certificate file path (output)"); +} + +std::vector 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& 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(), vm["cert"].as()); +} 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 GetArgumentSuggestions(const String& argument, const String& word) const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& 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 + +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(), "Certificate common name") + ("salt", po::value(), "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& 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(); + + if (salt.IsEmpty()) { + Log(LogCritical, "cli", "Ticket salt (--salt) must be specified."); + return 1; + } + + return PkiUtility::GenTicket(vm["cn"].as(), 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& 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 + +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(), "Common Name (optional). Use with '--cert' to check the CN in the certificate.") + ("cert", po::value(), "Certificate file path (optional). Standalone: print certificate. With '--cacert': Verify against CA.") + ("cacert", po::value(), "CA certificate file path (optional). If passed standalone, verifies whether this is a CA certificate") + ("crl", po::value(), "CRL file path (optional). Check the certificate against this revocation list when verifying against CA."); +} + +std::vector 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& ap) const +{ + String cn, certFile, caCertFile, crlFile; + + if (vm.count("cn")) + cn = vm["cn"].as(); + + if (vm.count("cert")) + certFile = vm["cert"].as(); + + if (vm.count("cacert")) + caCertFile = vm["cacert"].as(); + + if (vm.count("crl")) + crlFile = vm["crl"].as(); + + /* Verify CN in certificate. */ + if (!cn.IsEmpty() && !certFile.IsEmpty()) { + std::shared_ptr 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 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 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(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 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 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 GetArgumentSuggestions(const String& argument, const String& word) const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& 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 +#include +#include +#include + +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& 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 + +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& 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 +#include +#include +#include + +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& 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 + +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& 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 + +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 + +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..84225c4 --- /dev/null +++ b/lib/compat/CMakeLists.txt @@ -0,0 +1,47 @@ +# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ + +mkclass_target(checkresultreader.ti checkresultreader-ti.cpp checkresultreader-ti.hpp) +mkclass_target(compatlogger.ti compatlogger-ti.cpp compatlogger-ti.hpp) +mkclass_target(externalcommandlistener.ti externalcommandlistener-ti.cpp externalcommandlistener-ti.hpp) +mkclass_target(statusdatawriter.ti statusdatawriter-ti.cpp statusdatawriter-ti.hpp) + +set(compat_SOURCES + checkresultreader.cpp checkresultreader.hpp checkresultreader-ti.hpp + compatlogger.cpp compatlogger.hpp compatlogger-ti.hpp + externalcommandlistener.cpp externalcommandlistener.hpp externalcommandlistener-ti.hpp + statusdatawriter.cpp statusdatawriter.hpp statusdatawriter-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_if_not_exists( + ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/statusdata.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/checkresultreader.cpp b/lib/compat/checkresultreader.cpp new file mode 100644 index 0000000..e4516a3 --- /dev/null +++ b/lib/compat/checkresultreader.cpp @@ -0,0 +1,166 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "icinga/compatutility.hpp" +#include "compat/checkresultreader.hpp" +#include "compat/checkresultreader-ti.cpp" +#include "icinga/service.hpp" +#include "icinga/pluginutility.hpp" +#include "icinga/icingaapplication.hpp" +#include "base/configtype.hpp" +#include "base/objectlock.hpp" +#include "base/logger.hpp" +#include "base/convert.hpp" +#include "base/application.hpp" +#include "base/utility.hpp" +#include "base/exception.hpp" +#include "base/context.hpp" +#include "base/statsfunction.hpp" +#include + +using namespace icinga; + +REGISTER_TYPE(CheckResultReader); + +REGISTER_STATSFUNCTION(CheckResultReader, &CheckResultReader::StatsFunc); + +void CheckResultReader::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&) +{ + DictionaryData nodes; + + for (const CheckResultReader::Ptr& checkresultreader : ConfigType::GetObjectsByType()) { + nodes.emplace_back(checkresultreader->GetName(), 1); //add more stats + } + + status->Set("checkresultreader", new Dictionary(std::move(nodes))); +} + +/** + * @threadsafety Always. + */ +void CheckResultReader::Start(bool runtimeCreated) +{ + ObjectImpl::Start(runtimeCreated); + + Log(LogInformation, "CheckResultReader") + << "'" << GetName() << "' started."; + + Log(LogWarning, "CheckResultReader") + << "This feature is DEPRECATED and may be removed in future releases. Check the roadmap at https://github.com/Icinga/icinga2/milestones"; + +#ifndef _WIN32 + m_ReadTimer = new Timer(); + m_ReadTimer->OnTimerExpired.connect([this](const Timer * const&) { ReadTimerHandler(); }); + m_ReadTimer->SetInterval(5); + m_ReadTimer->Start(); +#endif /* _WIN32 */ +} + +/** + * @threadsafety Always. + */ +void CheckResultReader::Stop(bool runtimeRemoved) +{ + Log(LogInformation, "CheckResultReader") + << "'" << GetName() << "' stopped."; + + ObjectImpl::Stop(runtimeRemoved); +} + +/** + * @threadsafety Always. + */ +void CheckResultReader::ReadTimerHandler() const +{ + CONTEXT("Processing check result files in '" + GetSpoolDir() + "'"); + + Utility::Glob(GetSpoolDir() + "/c??????.ok", [this](const String& path) { ProcessCheckResultFile(path); }, GlobFile); +} + +void CheckResultReader::ProcessCheckResultFile(const String& path) const +{ + CONTEXT("Processing check result file '" + path + "'"); + + String crfile = String(path.Begin(), path.End() - 3); /* Remove the ".ok" extension. */ + + std::ifstream fp; + fp.exceptions(std::ifstream::badbit); + fp.open(crfile.CStr()); + + std::map attrs; + + while (fp.good()) { + std::string line; + std::getline(fp, line); + + if (line.empty() || line[0] == '#') + continue; /* Ignore comments and empty lines. */ + + size_t pos = line.find_first_of('='); + + if (pos == std::string::npos) + continue; /* Ignore invalid lines. */ + + String key = line.substr(0, pos); + String value = line.substr(pos + 1); + + attrs[key] = value; + } + + /* Remove the checkresult files. */ + Utility::Remove(path); + + Utility::Remove(crfile); + + Checkable::Ptr checkable; + + Host::Ptr host = Host::GetByName(attrs["host_name"]); + + if (!host) { + Log(LogWarning, "CheckResultReader") + << "Ignoring checkresult file for host '" << attrs["host_name"] << "': Host does not exist."; + + return; + } + + if (attrs.find("service_description") != attrs.end()) { + Service::Ptr service = host->GetServiceByShortName(attrs["service_description"]); + + if (!service) { + Log(LogWarning, "CheckResultReader") + << "Ignoring checkresult file for host '" << attrs["host_name"] + << "', service '" << attrs["service_description"] << "': Service does not exist."; + + return; + } + + checkable = service; + } else + checkable = host; + + CheckResult::Ptr result = new CheckResult(); + String output = CompatUtility::UnEscapeString(attrs["output"]); + std::pair co = PluginUtility::ParseCheckOutput(output); + result->SetOutput(co.first); + result->SetPerformanceData(PluginUtility::SplitPerfdata(co.second)); + result->SetState(PluginUtility::ExitStatusToState(Convert::ToLong(attrs["return_code"]))); + + if (attrs.find("start_time") != attrs.end()) + result->SetExecutionStart(Convert::ToDouble(attrs["start_time"])); + else + result->SetExecutionStart(Utility::GetTime()); + + if (attrs.find("finish_time") != attrs.end()) + result->SetExecutionEnd(Convert::ToDouble(attrs["finish_time"])); + else + result->SetExecutionEnd(result->GetExecutionStart()); + + checkable->ProcessCheckResult(result); + + Log(LogDebug, "CheckResultReader") + << "Processed checkresult file for object '" << checkable->GetName() << "'"; + + /* Reschedule the next check. The side effect of this is that for as long + * as we receive check result files for a host/service we won't execute any + * active checks. */ + checkable->SetNextCheck(Utility::GetTime() + checkable->GetCheckInterval()); +} diff --git a/lib/compat/checkresultreader.hpp b/lib/compat/checkresultreader.hpp new file mode 100644 index 0000000..6cd28e3 --- /dev/null +++ b/lib/compat/checkresultreader.hpp @@ -0,0 +1,38 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef CHECKRESULTREADER_H +#define CHECKRESULTREADER_H + +#include "compat/checkresultreader-ti.hpp" +#include "base/timer.hpp" +#include + +namespace icinga +{ + +/** + * An Icinga checkresult reader. + * + * @ingroup compat + */ +class CheckResultReader final : public ObjectImpl +{ +public: + DECLARE_OBJECT(CheckResultReader); + DECLARE_OBJECTNAME(CheckResultReader); + + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + +protected: + void Start(bool runtimeCreated) override; + void Stop(bool runtimeRemoved) override; + +private: + Timer::Ptr m_ReadTimer; + void ReadTimerHandler() const; + void ProcessCheckResultFile(const String& path) const; +}; + +} + +#endif /* CHECKRESULTREADER_H */ diff --git a/lib/compat/checkresultreader.ti b/lib/compat/checkresultreader.ti new file mode 100644 index 0000000..0132818 --- /dev/null +++ b/lib/compat/checkresultreader.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 CheckResultReader : ConfigObject +{ + activation_priority 100; + + [config] String spool_dir { + default {{{ return Configuration::DataDir + "/spool/checkresults/"; }}} + }; +}; + +} diff --git a/lib/compat/compatlogger.cpp b/lib/compat/compatlogger.cpp new file mode 100644 index 0000000..5427298 --- /dev/null +++ b/lib/compat/compatlogger.cpp @@ -0,0 +1,612 @@ +/* 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 + +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()) { + 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::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& arguments) { + ExternalCommandHandler(command, arguments); + }); + + m_RotationTimer = new Timer(); + m_RotationTimer->OnTimerExpired.connect([this](const Timer * const&) { RotationTimerHandler(); }); + m_RotationTimer->Start(); + + ReopenFile(false); + ScheduleNextRotation(); +} + +/** + * @threadsafety Always. + */ +void CompatLogger::Stop(bool runtimeRemoved) +{ + Log(LogInformation, "CompatLogger") + << "'" << GetName() << "' stopped."; + + ObjectImpl::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(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& 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()) { + 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()) { + 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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 + +namespace icinga +{ + +/** + * An Icinga compat log writer. + * + * @ingroup compat + */ +class CompatLogger final : public ObjectImpl +{ +public: + DECLARE_OBJECT(CompatLogger); + DECLARE_OBJECTNAME(CompatLogger); + + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + + void ValidateRotationMethod(const Lazy& 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& 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()) { + 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::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::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 +#include + +namespace icinga +{ + +/** + * @ingroup compat + */ +class ExternalCommandListener final : public ObjectImpl +{ +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/compat/statusdatawriter.cpp b/lib/compat/statusdatawriter.cpp new file mode 100644 index 0000000..2c6a666 --- /dev/null +++ b/lib/compat/statusdatawriter.cpp @@ -0,0 +1,897 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "compat/statusdatawriter.hpp" +#include "compat/statusdatawriter-ti.cpp" +#include "icinga/icingaapplication.hpp" +#include "icinga/cib.hpp" +#include "icinga/hostgroup.hpp" +#include "icinga/servicegroup.hpp" +#include "icinga/checkcommand.hpp" +#include "icinga/eventcommand.hpp" +#include "icinga/timeperiod.hpp" +#include "icinga/notificationcommand.hpp" +#include "icinga/compatutility.hpp" +#include "icinga/pluginutility.hpp" +#include "icinga/dependency.hpp" +#include "base/configtype.hpp" +#include "base/objectlock.hpp" +#include "base/json.hpp" +#include "base/convert.hpp" +#include "base/logger.hpp" +#include "base/exception.hpp" +#include "base/application.hpp" +#include "base/context.hpp" +#include "base/statsfunction.hpp" +#include +#include +#include + +using namespace icinga; + +REGISTER_TYPE(StatusDataWriter); + +REGISTER_STATSFUNCTION(StatusDataWriter, &StatusDataWriter::StatsFunc); + +void StatusDataWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&) +{ + DictionaryData nodes; + + for (const StatusDataWriter::Ptr& statusdatawriter : ConfigType::GetObjectsByType()) { + nodes.emplace_back(statusdatawriter->GetName(), 1); //add more stats + } + + status->Set("statusdatawriter", new Dictionary(std::move(nodes))); +} + +/** + * Hint: The reason why we're using "\n" rather than std::endl is because + * std::endl also _flushes_ the output stream which severely degrades + * performance (see https://stackoverflow.com/questions/213907/c-stdendl-vs-n). + */ + +/** + * Starts the component. + */ +void StatusDataWriter::Start(bool runtimeCreated) +{ + ObjectImpl::Start(runtimeCreated); + + Log(LogInformation, "StatusDataWriter") + << "'" << GetName() << "' started."; + + Log(LogWarning, "StatusDataWriter") + << "This feature is DEPRECATED and may be removed in future releases. Check the roadmap at https://github.com/Icinga/icinga2/milestones"; + + m_ObjectsCacheOutdated = true; + + m_StatusTimer = new Timer(); + m_StatusTimer->SetInterval(GetUpdateInterval()); + m_StatusTimer->OnTimerExpired.connect([this](const Timer * const&){ StatusTimerHandler(); }); + m_StatusTimer->Start(); + m_StatusTimer->Reschedule(0); + + ConfigObject::OnVersionChanged.connect([this](const ConfigObject::Ptr&, const Value&) { ObjectHandler(); }); + ConfigObject::OnActiveChanged.connect([this](const ConfigObject::Ptr&, const Value&) { ObjectHandler(); }); +} + +/** + * Stops the component. + */ +void StatusDataWriter::Stop(bool runtimeRemoved) +{ + Log(LogInformation, "StatusDataWriter") + << "'" << GetName() << "' stopped."; + + ObjectImpl::Stop(runtimeRemoved); +} + +void StatusDataWriter::DumpComments(std::ostream& fp, const Checkable::Ptr& checkable) +{ + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); + + for (const Comment::Ptr& comment : checkable->GetComments()) { + if (comment->IsExpired()) + continue; + + if (service) + fp << "servicecomment {" << "\n" + << "\t" << "service_description=" << service->GetShortName() << "\n"; + else + fp << "hostcomment {" << "\n"; + + fp << "\t" "host_name=" << host->GetName() << "\n" + "\t" "comment_id=" << comment->GetLegacyId() << "\n" + "\t" "entry_time=" << comment->GetEntryTime() << "\n" + "\t" "entry_type=" << comment->GetEntryType() << "\n" + "\t" "persistent=" "1" "\n" + "\t" "author=" << comment->GetAuthor() << "\n" + "\t" "comment_data=" << comment->GetText() << "\n" + "\t" "expires=" << (comment->GetExpireTime() != 0 ? 1 : 0) << "\n" + "\t" "expire_time=" << comment->GetExpireTime() << "\n" + "\t" "}" "\n" + "\n"; + } +} + +void StatusDataWriter::DumpTimePeriod(std::ostream& fp, const TimePeriod::Ptr& tp) +{ + fp << "define timeperiod {" "\n" + "\t" "timeperiod_name" "\t" << tp->GetName() << "\n" + "\t" "alias" "\t" << tp->GetName() << "\n"; + + Dictionary::Ptr ranges = tp->GetRanges(); + + if (ranges) { + ObjectLock olock(ranges); + for (const Dictionary::Pair& kv : ranges) { + fp << "\t" << kv.first << "\t" << kv.second << "\n"; + } + } + + fp << "\t" "}" "\n" "\n"; +} + +void StatusDataWriter::DumpCommand(std::ostream& fp, const Command::Ptr& command) +{ + fp << "define command {" "\n" + "\t" "command_name\t"; + + fp << CompatUtility::GetCommandName(command) << "\n"; + + fp << "\t" "command_line" "\t" << CompatUtility::GetCommandLine(command); + + fp << "\n"; + + DumpCustomAttributes(fp, command); + + fp << "\n" "\t" "}" "\n" "\n"; +} + +void StatusDataWriter::DumpDowntimes(std::ostream& fp, const Checkable::Ptr& checkable) +{ + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); + + for (const Downtime::Ptr& downtime : checkable->GetDowntimes()) { + if (downtime->IsExpired()) + continue; + + if (service) + fp << "servicedowntime {" << "\n" + "\t" "service_description=" << service->GetShortName() << "\n"; + else + fp << "hostdowntime {" "\n"; + + Downtime::Ptr triggeredByObj = Downtime::GetByName(downtime->GetTriggeredBy()); + int triggeredByLegacy = 0; + if (triggeredByObj) + triggeredByLegacy = triggeredByObj->GetLegacyId(); + + fp << "\t" << "host_name=" << host->GetName() << "\n" + "\t" "downtime_id=" << downtime->GetLegacyId() << "\n" + "\t" "entry_time=" << downtime->GetEntryTime() << "\n" + "\t" "start_time=" << downtime->GetStartTime() << "\n" + "\t" "end_time=" << downtime->GetEndTime() << "\n" + "\t" "triggered_by=" << triggeredByLegacy << "\n" + "\t" "fixed=" << static_cast(downtime->GetFixed()) << "\n" + "\t" "duration=" << static_cast(downtime->GetDuration()) << "\n" + "\t" "is_in_effect=" << (downtime->IsInEffect() ? 1 : 0) << "\n" + "\t" "author=" << downtime->GetAuthor() << "\n" + "\t" "comment=" << downtime->GetComment() << "\n" + "\t" "trigger_time=" << downtime->GetTriggerTime() << "\n" + "\t" "}" "\n" + "\n"; + } +} + +void StatusDataWriter::DumpHostStatus(std::ostream& fp, const Host::Ptr& host) +{ + fp << "hoststatus {" "\n" "\t" "host_name=" << host->GetName() << "\n"; + + { + ObjectLock olock(host); + DumpCheckableStatusAttrs(fp, host); + } + + /* ugly but cgis parse only that */ + fp << "\t" "last_time_up=" << host->GetLastStateUp() << "\n" + "\t" "last_time_down=" << host->GetLastStateDown() << "\n" + "\t" "last_time_unreachable=" << host->GetLastStateUnreachable() << "\n"; + + fp << "\t" "}" "\n" "\n"; + + DumpDowntimes(fp, host); + DumpComments(fp, host); +} + +void StatusDataWriter::DumpHostObject(std::ostream& fp, const Host::Ptr& host) +{ + String notes = host->GetNotes(); + String notes_url = host->GetNotesUrl(); + String action_url = host->GetActionUrl(); + String icon_image = host->GetIconImage(); + String icon_image_alt = host->GetIconImageAlt(); + String display_name = host->GetDisplayName(); + String address = host->GetAddress(); + String address6 = host->GetAddress6(); + + fp << "define host {" "\n" + "\t" "host_name" "\t" << host->GetName() << "\n"; + if (!display_name.IsEmpty()) { + fp << "\t" "display_name" "\t" << host->GetDisplayName() << "\n" + "\t" "alias" "\t" << host->GetDisplayName() << "\n"; + } + if (!address.IsEmpty()) + fp << "\t" "address" "\t" << address << "\n"; + if (!address6.IsEmpty()) + fp << "\t" "address6" "\t" << address6 << "\n"; + if (!notes.IsEmpty()) + fp << "\t" "notes" "\t" << notes << "\n"; + if (!notes_url.IsEmpty()) + fp << "\t" "notes_url" "\t" << notes_url << "\n"; + if (!action_url.IsEmpty()) + fp << "\t" "action_url" "\t" << action_url << "\n"; + if (!icon_image.IsEmpty()) + fp << "\t" "icon_image" "\t" << icon_image << "\n"; + if (!icon_image_alt.IsEmpty()) + fp << "\t" "icon_image_alt" "\t" << icon_image_alt << "\n"; + + std::set parents = host->GetParents(); + + if (!parents.empty()) { + fp << "\t" "parents" "\t"; + DumpNameList(fp, parents); + fp << "\n"; + } + + ObjectLock olock(host); + + fp << "\t" "check_interval" "\t" << (host->GetCheckInterval() / 60.0) << "\n" + "\t" "retry_interval" "\t" << (host->GetRetryInterval() / 60.0) << "\n" + "\t" "max_check_attempts" "\t" << host->GetMaxCheckAttempts() << "\n" + "\t" "active_checks_enabled" "\t" << Convert::ToLong(host->GetEnableActiveChecks()) << "\n" + "\t" "passive_checks_enabled" "\t" << Convert::ToLong(host->GetEnablePassiveChecks()) << "\n" + "\t" "notifications_enabled" "\t" << Convert::ToLong(host->GetEnableNotifications()) << "\n" + "\t" "notification_options" "\t" << GetNotificationOptions(host) << "\n" + "\t" "notification_interval" "\t" << CompatUtility::GetCheckableNotificationNotificationInterval(host) << "\n" + "\t" "event_handler_enabled" "\t" << Convert::ToLong(host->GetEnableEventHandler()) << "\n"; + + CheckCommand::Ptr checkcommand = host->GetCheckCommand(); + if (checkcommand) + fp << "\t" "check_command" "\t" << CompatUtility::GetCommandName(checkcommand) << "!" << CompatUtility::GetCheckableCommandArgs(host) << "\n"; + + EventCommand::Ptr eventcommand = host->GetEventCommand(); + if (eventcommand && host->GetEnableEventHandler()) + fp << "\t" "event_handler" "\t" << CompatUtility::GetCommandName(eventcommand) << "\n"; + + TimePeriod::Ptr checkPeriod = host->GetCheckPeriod(); + if (checkPeriod) + fp << "\t" "check_period" "\t" << checkPeriod->GetName() << "\n"; + + fp << "\t" "contacts" "\t"; + DumpNameList(fp, CompatUtility::GetCheckableNotificationUsers(host)); + fp << "\n"; + + fp << "\t" "contact_groups" "\t"; + DumpNameList(fp, CompatUtility::GetCheckableNotificationUserGroups(host)); + fp << "\n"; + + fp << "\t" << "initial_state" "\t" "o" "\n" + "\t" "low_flap_threshold" "\t" << host->GetFlappingThresholdLow() << "\n" + "\t" "high_flap_threshold" "\t" << host->GetFlappingThresholdHigh() << "\n" + "\t" "process_perf_data" "\t" << Convert::ToLong(host->GetEnablePerfdata()) << "\n" + "\t" "check_freshness" "\t" "1" "\n"; + + fp << "\t" "host_groups" "\t"; + bool first = true; + + Array::Ptr groups = host->GetGroups(); + + if (groups) { + ObjectLock olock(groups); + + for (const String& name : groups) { + HostGroup::Ptr hg = HostGroup::GetByName(name); + + if (hg) { + if (!first) + fp << ","; + else + first = false; + + fp << hg->GetName(); + } + } + } + + fp << "\n"; + + DumpCustomAttributes(fp, host); + + fp << "\t" "}" "\n" "\n"; +} + +void StatusDataWriter::DumpCheckableStatusAttrs(std::ostream& fp, const Checkable::Ptr& checkable) +{ + CheckResult::Ptr cr = checkable->GetLastCheckResult(); + + EventCommand::Ptr eventcommand = checkable->GetEventCommand(); + CheckCommand::Ptr checkcommand = checkable->GetCheckCommand(); + + fp << "\t" << "check_command=" << CompatUtility::GetCommandName(checkcommand) << "!" << CompatUtility::GetCheckableCommandArgs(checkable) << "\n" + "\t" "event_handler=" << CompatUtility::GetCommandName(eventcommand) << "\n" + "\t" "check_interval=" << (checkable->GetCheckInterval() / 60.0) << "\n" + "\t" "retry_interval=" << (checkable->GetRetryInterval() / 60.0) << "\n" + "\t" "has_been_checked=" << Convert::ToLong(checkable->HasBeenChecked()) << "\n" + "\t" "should_be_scheduled=" << checkable->GetEnableActiveChecks() << "\n" + "\t" "event_handler_enabled=" << Convert::ToLong(checkable->GetEnableEventHandler()) << "\n"; + + TimePeriod::Ptr checkPeriod = checkable->GetCheckPeriod(); + if (checkPeriod) + fp << "\t" "check_period" "\t" << checkPeriod->GetName() << "\n"; + + if (cr) { + fp << "\t" << "check_execution_time=" << Convert::ToString(cr->CalculateExecutionTime()) << "\n" + "\t" "check_latency=" << Convert::ToString(cr->CalculateLatency()) << "\n"; + } + + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); + + if (service) { + fp << "\t" "current_state=" << service->GetState() << "\n" + "\t" "last_hard_state=" << service->GetLastHardState() << "\n" + "\t" "last_time_ok=" << static_cast(service->GetLastStateOK()) << "\n" + "\t" "last_time_warn=" << static_cast(service->GetLastStateWarning()) << "\n" + "\t" "last_time_critical=" << static_cast(service->GetLastStateCritical()) << "\n" + "\t" "last_time_unknown=" << static_cast(service->GetLastStateUnknown()) << "\n"; + } else { + int currentState = host->GetState(); + + if (currentState != HostUp && !host->IsReachable()) + currentState = 2; /* hardcoded compat state */ + + fp << "\t" "current_state=" << currentState << "\n" + "\t" "last_hard_state=" << host->GetLastHardState() << "\n" + "\t" "last_time_up=" << static_cast(host->GetLastStateUp()) << "\n" + "\t" "last_time_down=" << static_cast(host->GetLastStateDown()) << "\n"; + } + + fp << "\t" "state_type=" << checkable->GetStateType() << "\n" + "\t" "last_check=" << static_cast(host->GetLastCheck()) << "\n"; + + if (cr) { + fp << "\t" "plugin_output=" << CompatUtility::GetCheckResultOutput(cr) << "\n" + "\t" "long_plugin_output=" << CompatUtility::GetCheckResultLongOutput(cr) << "\n" + "\t" "performance_data=" << PluginUtility::FormatPerfdata(cr->GetPerformanceData()) << "\n"; + } + + fp << "\t" << "next_check=" << static_cast(checkable->GetNextCheck()) << "\n" + "\t" "current_attempt=" << checkable->GetCheckAttempt() << "\n" + "\t" "max_attempts=" << checkable->GetMaxCheckAttempts() << "\n" + "\t" "last_state_change=" << static_cast(checkable->GetLastStateChange()) << "\n" + "\t" "last_hard_state_change=" << static_cast(checkable->GetLastHardStateChange()) << "\n" + "\t" "last_update=" << static_cast(Utility::GetTime()) << "\n" + "\t" "notifications_enabled=" << Convert::ToLong(checkable->GetEnableNotifications()) << "\n" + "\t" "active_checks_enabled=" << Convert::ToLong(checkable->GetEnableActiveChecks()) << "\n" + "\t" "passive_checks_enabled=" << Convert::ToLong(checkable->GetEnablePassiveChecks()) << "\n" + "\t" "flap_detection_enabled=" << Convert::ToLong(checkable->GetEnableFlapping()) << "\n" + "\t" "is_flapping=" << Convert::ToLong(checkable->IsFlapping()) << "\n" + "\t" "percent_state_change=" << checkable->GetFlappingCurrent() << "\n" + "\t" "problem_has_been_acknowledged=" << (checkable->GetAcknowledgement() != AcknowledgementNone ? 1 : 0) << "\n" + "\t" "acknowledgement_type=" << checkable->GetAcknowledgement() << "\n" + "\t" "acknowledgement_end_time=" << checkable->GetAcknowledgementExpiry() << "\n" + "\t" "scheduled_downtime_depth=" << checkable->GetDowntimeDepth() << "\n" + "\t" "last_notification=" << CompatUtility::GetCheckableNotificationLastNotification(checkable) << "\n" + "\t" "next_notification=" << CompatUtility::GetCheckableNotificationNextNotification(checkable) << "\n" + "\t" "current_notification_number=" << CompatUtility::GetCheckableNotificationNotificationNumber(checkable) << "\n" + "\t" "is_reachable=" << Convert::ToLong(checkable->IsReachable()) << "\n"; +} + +void StatusDataWriter::DumpServiceStatus(std::ostream& fp, const Service::Ptr& service) +{ + Host::Ptr host = service->GetHost(); + + fp << "servicestatus {" "\n" + "\t" "host_name=" << host->GetName() << "\n" + "\t" "service_description=" << service->GetShortName() << "\n"; + + { + ObjectLock olock(service); + DumpCheckableStatusAttrs(fp, service); + } + + fp << "\t" "}" "\n" "\n"; + + DumpDowntimes(fp, service); + DumpComments(fp, service); +} + +void StatusDataWriter::DumpServiceObject(std::ostream& fp, const Service::Ptr& service) +{ + Host::Ptr host = service->GetHost(); + + { + ObjectLock olock(service); + + fp << "define service {" "\n" + "\t" "host_name" "\t" << host->GetName() << "\n" + "\t" "service_description" "\t" << service->GetShortName() << "\n" + "\t" "display_name" "\t" << service->GetDisplayName() << "\n" + "\t" "check_interval" "\t" << (service->GetCheckInterval() / 60.0) << "\n" + "\t" "retry_interval" "\t" << (service->GetRetryInterval() / 60.0) << "\n" + "\t" "max_check_attempts" "\t" << service->GetMaxCheckAttempts() << "\n" + "\t" "active_checks_enabled" "\t" << Convert::ToLong(service->GetEnableActiveChecks()) << "\n" + "\t" "passive_checks_enabled" "\t" << Convert::ToLong(service->GetEnablePassiveChecks()) << "\n" + "\t" "flap_detection_enabled" "\t" << Convert::ToLong(service->GetEnableFlapping()) << "\n" + "\t" "is_volatile" "\t" << Convert::ToLong(service->GetVolatile()) << "\n" + "\t" "notifications_enabled" "\t" << Convert::ToLong(service->GetEnableNotifications()) << "\n" + "\t" "notification_options" "\t" << GetNotificationOptions(service) << "\n" + "\t" "notification_interval" "\t" << CompatUtility::GetCheckableNotificationNotificationInterval(service) << "\n" + "\t" "notification_period" "\t" << "" << "\n" + "\t" "event_handler_enabled" "\t" << Convert::ToLong(service->GetEnableEventHandler()) << "\n"; + + CheckCommand::Ptr checkcommand = service->GetCheckCommand(); + if (checkcommand) + fp << "\t" "check_command" "\t" << CompatUtility::GetCommandName(checkcommand) << "!" << CompatUtility::GetCheckableCommandArgs(service)<< "\n"; + + EventCommand::Ptr eventcommand = service->GetEventCommand(); + if (eventcommand && service->GetEnableEventHandler()) + fp << "\t" "event_handler" "\t" << CompatUtility::GetCommandName(eventcommand) << "\n"; + + TimePeriod::Ptr checkPeriod = service->GetCheckPeriod(); + if (checkPeriod) + fp << "\t" "check_period" "\t" << checkPeriod->GetName() << "\n"; + + fp << "\t" "contacts" "\t"; + DumpNameList(fp, CompatUtility::GetCheckableNotificationUsers(service)); + fp << "\n"; + + fp << "\t" "contact_groups" "\t"; + DumpNameList(fp, CompatUtility::GetCheckableNotificationUserGroups(service)); + fp << "\n"; + + String notes = service->GetNotes(); + String notes_url = service->GetNotesUrl(); + String action_url = service->GetActionUrl(); + String icon_image = service->GetIconImage(); + String icon_image_alt = service->GetIconImageAlt(); + + fp << "\t" "initial_state" "\t" "o" "\n" + "\t" "low_flap_threshold" "\t" << service->GetFlappingThresholdLow() << "\n" + "\t" "high_flap_threshold" "\t" << service->GetFlappingThresholdHigh() << "\n" + "\t" "process_perf_data" "\t" << Convert::ToLong(service->GetEnablePerfdata()) << "\n" + "\t" "check_freshness" << "\t" "1" "\n"; + + if (!notes.IsEmpty()) + fp << "\t" "notes" "\t" << notes << "\n"; + if (!notes_url.IsEmpty()) + fp << "\t" "notes_url" "\t" << notes_url << "\n"; + if (!action_url.IsEmpty()) + fp << "\t" "action_url" "\t" << action_url << "\n"; + if (!icon_image.IsEmpty()) + fp << "\t" "icon_image" "\t" << icon_image << "\n"; + if (!icon_image_alt.IsEmpty()) + fp << "\t" "icon_image_alt" "\t" << icon_image_alt << "\n"; + } + + fp << "\t" "service_groups" "\t"; + bool first = true; + + Array::Ptr groups = service->GetGroups(); + + if (groups) { + ObjectLock olock(groups); + + for (const String& name : groups) { + ServiceGroup::Ptr sg = ServiceGroup::GetByName(name); + + if (sg) { + if (!first) + fp << ","; + else + first = false; + + fp << sg->GetName(); + } + } + } + + fp << "\n"; + + DumpCustomAttributes(fp, service); + + fp << "\t" "}" "\n" "\n"; +} + +void StatusDataWriter::DumpCustomAttributes(std::ostream& fp, const CustomVarObject::Ptr& object) +{ + Dictionary::Ptr vars = object->GetVars(); + + if (!vars) + return; + + bool is_json = false; + + ObjectLock olock(vars); + for (const Dictionary::Pair& kv : vars) { + if (kv.first.IsEmpty()) + continue; + + Value value; + + if (kv.second.IsObjectType() || kv.second.IsObjectType()) { + value = JsonEncode(kv.second); + is_json = true; + } else + value = CompatUtility::EscapeString(kv.second); + + fp << "\t" "_" << kv.first << "\t" << value << "\n"; + } + + if (is_json) + fp << "\t" "_is_json" "\t" "1" "\n"; +} + +void StatusDataWriter::UpdateObjectsCache() +{ + CONTEXT("Writing objects.cache file"); + + /* Use the compat path here from the .ti generated class. */ + String objectsPath = GetObjectsPath(); + + std::fstream objectfp; + String tempObjectsPath = Utility::CreateTempFile(objectsPath + ".XXXXXX", 0644, objectfp); + + objectfp << std::fixed; + + objectfp << "# Icinga objects cache file" "\n" + "# This file is auto-generated. Do not modify this file." "\n" + "\n"; + + for (const Host::Ptr& host : ConfigType::GetObjectsByType()) { + std::ostringstream tempobjectfp; + tempobjectfp << std::fixed; + DumpHostObject(tempobjectfp, host); + objectfp << tempobjectfp.str(); + + for (const Service::Ptr& service : host->GetServices()) { + std::ostringstream tempobjectfp; + tempobjectfp << std::fixed; + DumpServiceObject(tempobjectfp, service); + objectfp << tempobjectfp.str(); + } + } + + for (const HostGroup::Ptr& hg : ConfigType::GetObjectsByType()) { + std::ostringstream tempobjectfp; + tempobjectfp << std::fixed; + + String display_name = hg->GetDisplayName(); + String notes = hg->GetNotes(); + String notes_url = hg->GetNotesUrl(); + String action_url = hg->GetActionUrl(); + + tempobjectfp << "define hostgroup {" "\n" + "\t" "hostgroup_name" "\t" << hg->GetName() << "\n"; + + if (!display_name.IsEmpty()) + tempobjectfp << "\t" "alias" "\t" << display_name << "\n"; + if (!notes.IsEmpty()) + tempobjectfp << "\t" "notes" "\t" << notes << "\n"; + if (!notes_url.IsEmpty()) + tempobjectfp << "\t" "notes_url" "\t" << notes_url << "\n"; + if (!action_url.IsEmpty()) + tempobjectfp << "\t" "action_url" "\t" << action_url << "\n"; + + DumpCustomAttributes(tempobjectfp, hg); + + tempobjectfp << "\t" "members" "\t"; + DumpNameList(tempobjectfp, hg->GetMembers()); + tempobjectfp << "\n" "\t" "}" "\n"; + + objectfp << tempobjectfp.str(); + } + + for (const ServiceGroup::Ptr& sg : ConfigType::GetObjectsByType()) { + std::ostringstream tempobjectfp; + tempobjectfp << std::fixed; + + String display_name = sg->GetDisplayName(); + String notes = sg->GetNotes(); + String notes_url = sg->GetNotesUrl(); + String action_url = sg->GetActionUrl(); + + tempobjectfp << "define servicegroup {" "\n" + "\t" "servicegroup_name" "\t" << sg->GetName() << "\n"; + + if (!display_name.IsEmpty()) + tempobjectfp << "\t" "alias" "\t" << display_name << "\n"; + if (!notes.IsEmpty()) + tempobjectfp << "\t" "notes" "\t" << notes << "\n"; + if (!notes_url.IsEmpty()) + tempobjectfp << "\t" "notes_url" "\t" << notes_url << "\n"; + if (!action_url.IsEmpty()) + tempobjectfp << "\t" "action_url" "\t" << action_url << "\n"; + + DumpCustomAttributes(tempobjectfp, sg); + + tempobjectfp << "\t" "members" "\t"; + + std::vector sglist; + for (const Service::Ptr& service : sg->GetMembers()) { + Host::Ptr host = service->GetHost(); + + sglist.emplace_back(host->GetName()); + sglist.emplace_back(service->GetShortName()); + } + + DumpStringList(tempobjectfp, sglist); + + tempobjectfp << "\n" "}" "\n"; + + objectfp << tempobjectfp.str(); + } + + for (const User::Ptr& user : ConfigType::GetObjectsByType()) { + std::ostringstream tempobjectfp; + tempobjectfp << std::fixed; + + String email = user->GetEmail(); + String pager = user->GetPager(); + String alias = user->GetDisplayName(); + + tempobjectfp << "define contact {" "\n" + "\t" "contact_name" "\t" << user->GetName() << "\n"; + + if (!alias.IsEmpty()) + tempobjectfp << "\t" "alias" "\t" << alias << "\n"; + if (!email.IsEmpty()) + tempobjectfp << "\t" "email" "\t" << email << "\n"; + if (!pager.IsEmpty()) + tempobjectfp << "\t" "pager" "\t" << pager << "\n"; + + tempobjectfp << "\t" "service_notification_options" "\t" "w,u,c,r,f,s" "\n" + "\t" "host_notification_options""\t" "d,u,r,f,s" "\n" + "\t" "host_notifications_enabled" "\t" "1" "\n" + "\t" "service_notifications_enabled" "\t" "1" "\n" + "\t" "}" "\n" + "\n"; + + objectfp << tempobjectfp.str(); + } + + for (const UserGroup::Ptr& ug : ConfigType::GetObjectsByType()) { + std::ostringstream tempobjectfp; + tempobjectfp << std::fixed; + + tempobjectfp << "define contactgroup {" "\n" + "\t" "contactgroup_name" "\t" << ug->GetName() << "\n" + "\t" "alias" "\t" << ug->GetDisplayName() << "\n"; + + tempobjectfp << "\t" "members" "\t"; + DumpNameList(tempobjectfp, ug->GetMembers()); + tempobjectfp << "\n" + "\t" "}" "\n"; + + objectfp << tempobjectfp.str(); + } + + for (const Command::Ptr& command : ConfigType::GetObjectsByType()) { + DumpCommand(objectfp, command); + } + + for (const Command::Ptr& command : ConfigType::GetObjectsByType()) { + DumpCommand(objectfp, command); + } + + for (const Command::Ptr& command : ConfigType::GetObjectsByType()) { + DumpCommand(objectfp, command); + } + + for (const TimePeriod::Ptr& tp : ConfigType::GetObjectsByType()) { + DumpTimePeriod(objectfp, tp); + } + + for (const Dependency::Ptr& dep : ConfigType::GetObjectsByType()) { + Checkable::Ptr parent = dep->GetParent(); + + if (!parent) { + Log(LogDebug, "StatusDataWriter") + << "Missing parent for dependency '" << dep->GetName() << "'."; + continue; + } + + Host::Ptr parent_host; + Service::Ptr parent_service; + tie(parent_host, parent_service) = GetHostService(parent); + + Checkable::Ptr child = dep->GetChild(); + + if (!child) { + Log(LogDebug, "StatusDataWriter") + << "Missing child for dependency '" << dep->GetName() << "'."; + continue; + } + + Host::Ptr child_host; + Service::Ptr child_service; + tie(child_host, child_service) = GetHostService(child); + + int state_filter = dep->GetStateFilter(); + std::vector failure_criteria; + if (state_filter & StateFilterOK || state_filter & StateFilterUp) + failure_criteria.emplace_back("o"); + if (state_filter & StateFilterWarning) + failure_criteria.emplace_back("w"); + if (state_filter & StateFilterCritical) + failure_criteria.emplace_back("c"); + if (state_filter & StateFilterUnknown) + failure_criteria.emplace_back("u"); + if (state_filter & StateFilterDown) + failure_criteria.emplace_back("d"); + + String criteria = boost::algorithm::join(failure_criteria, ","); + + /* Icinga 1.x only allows host->host, service->service dependencies */ + if (!child_service && !parent_service) { + objectfp << "define hostdependency {" "\n" + "\t" "dependent_host_name" "\t" << child_host->GetName() << "\n" + "\t" "host_name" "\t" << parent_host->GetName() << "\n" + "\t" "execution_failure_criteria" "\t" << criteria << "\n" + "\t" "notification_failure_criteria" "\t" << criteria << "\n" + "\t" "}" "\n" + "\n"; + } else if (child_service && parent_service){ + + objectfp << "define servicedependency {" "\n" + "\t" "dependent_host_name" "\t" << child_service->GetHost()->GetName() << "\n" + "\t" "dependent_service_description" "\t" << child_service->GetShortName() << "\n" + "\t" "host_name" "\t" << parent_service->GetHost()->GetName() << "\n" + "\t" "service_description" "\t" << parent_service->GetShortName() << "\n" + "\t" "execution_failure_criteria" "\t" << criteria << "\n" + "\t" "notification_failure_criteria" "\t" << criteria << "\n" + "\t" "}" "\n" + "\n"; + } + } + + objectfp.close(); + + Utility::RenameFile(tempObjectsPath, objectsPath); +} + +/** + * Periodically writes the status.dat and objects.cache files. + */ +void StatusDataWriter::StatusTimerHandler() +{ + if (m_ObjectsCacheOutdated) { + UpdateObjectsCache(); + m_ObjectsCacheOutdated = false; + } + + double start = Utility::GetTime(); + + String statusPath = GetStatusPath(); + + std::fstream statusfp; + String tempStatusPath = Utility::CreateTempFile(statusPath + ".XXXXXX", 0644, statusfp); + + statusfp << std::fixed; + + statusfp << "# Icinga status file" "\n" + "# This file is auto-generated. Do not modify this file." "\n" + "\n"; + + statusfp << "info {" "\n" + "\t" "created=" << Utility::GetTime() << "\n" + "\t" "version=" << Application::GetAppVersion() << "\n" + "\t" "}" "\n" + "\n"; + + statusfp << "programstatus {" "\n" + "\t" "icinga_pid=" << Utility::GetPid() << "\n" + "\t" "daemon_mode=1" "\n" + "\t" "program_start=" << static_cast(Application::GetStartTime()) << "\n" + "\t" "active_host_checks_enabled=" << Convert::ToLong(IcingaApplication::GetInstance()->GetEnableHostChecks()) << "\n" + "\t" "passive_host_checks_enabled=1" "\n" + "\t" "active_service_checks_enabled=" << Convert::ToLong(IcingaApplication::GetInstance()->GetEnableServiceChecks()) << "\n" + "\t" "passive_service_checks_enabled=1" "\n" + "\t" "check_service_freshness=1" "\n" + "\t" "check_host_freshness=1" "\n" + "\t" "enable_notifications=" << Convert::ToLong(IcingaApplication::GetInstance()->GetEnableNotifications()) << "\n" + "\t" "enable_event_handlers=" << Convert::ToLong(IcingaApplication::GetInstance()->GetEnableEventHandlers()) << "\n" + "\t" "enable_flap_detection=" << Convert::ToLong(IcingaApplication::GetInstance()->GetEnableFlapping()) << "\n" + "\t" "enable_failure_prediction=0" "\n" + "\t" "process_performance_data=" << Convert::ToLong(IcingaApplication::GetInstance()->GetEnablePerfdata()) << "\n" + "\t" "active_scheduled_host_check_stats=" << CIB::GetActiveHostChecksStatistics(60) << "," << CIB::GetActiveHostChecksStatistics(5 * 60) << "," << CIB::GetActiveHostChecksStatistics(15 * 60) << "\n" + "\t" "passive_host_check_stats=" << CIB::GetPassiveHostChecksStatistics(60) << "," << CIB::GetPassiveHostChecksStatistics(5 * 60) << "," << CIB::GetPassiveHostChecksStatistics(15 * 60) << "\n" + "\t" "active_scheduled_service_check_stats=" << CIB::GetActiveServiceChecksStatistics(60) << "," << CIB::GetActiveServiceChecksStatistics(5 * 60) << "," << CIB::GetActiveServiceChecksStatistics(15 * 60) << "\n" + "\t" "passive_service_check_stats=" << CIB::GetPassiveServiceChecksStatistics(60) << "," << CIB::GetPassiveServiceChecksStatistics(5 * 60) << "," << CIB::GetPassiveServiceChecksStatistics(15 * 60) << "\n" + "\t" "next_downtime_id=" << Downtime::GetNextDowntimeID() << "\n" + "\t" "next_comment_id=" << Comment::GetNextCommentID() << "\n"; + + statusfp << "\t" "}" "\n" + "\n"; + + for (const Host::Ptr& host : ConfigType::GetObjectsByType()) { + std::ostringstream tempstatusfp; + tempstatusfp << std::fixed; + DumpHostStatus(tempstatusfp, host); + statusfp << tempstatusfp.str(); + + for (const Service::Ptr& service : host->GetServices()) { + std::ostringstream tempstatusfp; + tempstatusfp << std::fixed; + DumpServiceStatus(tempstatusfp, service); + statusfp << tempstatusfp.str(); + } + } + + statusfp.close(); + + Utility::RenameFile(tempStatusPath, statusPath); + + Log(LogNotice, "StatusDataWriter") + << "Writing status.dat file took " << Utility::FormatDuration(Utility::GetTime() - start); +} + +void StatusDataWriter::ObjectHandler() +{ + m_ObjectsCacheOutdated = true; +} + +String StatusDataWriter::GetNotificationOptions(const Checkable::Ptr& checkable) +{ + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); + + unsigned long notification_type_filter = 0; + unsigned long notification_state_filter = 0; + + for (const Notification::Ptr& notification : checkable->GetNotifications()) { + notification_type_filter |= notification->GetTypeFilter(); + notification_state_filter |= notification->GetStateFilter(); + } + + std::vector notification_options; + + /* notification state filters */ + if (service) { + if (notification_state_filter & ServiceWarning) { + notification_options.push_back("w"); + } + if (notification_state_filter & ServiceUnknown) { + notification_options.push_back("u"); + } + if (notification_state_filter & ServiceCritical) { + notification_options.push_back("c"); + } + } else { + if (notification_state_filter & HostDown) { + notification_options.push_back("d"); + } + } + + /* notification type filters */ + if (notification_type_filter & NotificationRecovery) { + notification_options.push_back("r"); + } + if ((notification_type_filter & NotificationFlappingStart) || + (notification_type_filter & NotificationFlappingEnd)) { + notification_options.push_back("f"); + } + if ((notification_type_filter & NotificationDowntimeStart) || + (notification_type_filter & NotificationDowntimeEnd) || + (notification_type_filter & NotificationDowntimeRemoved)) { + notification_options.push_back("s"); + } + + return boost::algorithm::join(notification_options, ","); +} diff --git a/lib/compat/statusdatawriter.hpp b/lib/compat/statusdatawriter.hpp new file mode 100644 index 0000000..31a5efe --- /dev/null +++ b/lib/compat/statusdatawriter.hpp @@ -0,0 +1,89 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef STATUSDATAWRITER_H +#define STATUSDATAWRITER_H + +#include "compat/statusdatawriter-ti.hpp" +#include "icinga/customvarobject.hpp" +#include "icinga/host.hpp" +#include "icinga/service.hpp" +#include "icinga/command.hpp" +#include "icinga/compatutility.hpp" +#include "base/timer.hpp" +#include "base/utility.hpp" +#include + +namespace icinga +{ + +/** + * @ingroup compat + */ +class StatusDataWriter final : public ObjectImpl +{ +public: + DECLARE_OBJECT(StatusDataWriter); + DECLARE_OBJECTNAME(StatusDataWriter); + + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + +protected: + void Start(bool runtimeCreated) override; + void Stop(bool runtimeRemoved) override; + +private: + Timer::Ptr m_StatusTimer; + bool m_ObjectsCacheOutdated; + + void DumpCommand(std::ostream& fp, const Command::Ptr& command); + void DumpTimePeriod(std::ostream& fp, const TimePeriod::Ptr& tp); + void DumpDowntimes(std::ostream& fp, const Checkable::Ptr& owner); + void DumpComments(std::ostream& fp, const Checkable::Ptr& owner); + void DumpHostStatus(std::ostream& fp, const Host::Ptr& host); + void DumpHostObject(std::ostream& fp, const Host::Ptr& host); + + void DumpCheckableStatusAttrs(std::ostream& fp, const Checkable::Ptr& checkable); + + template + void DumpNameList(std::ostream& fp, const T& list) + { + bool first = true; + for (const auto& obj : list) { + if (!first) + fp << ","; + else + first = false; + + fp << obj->GetName(); + } + } + + template + void DumpStringList(std::ostream& fp, const T& list) + { + bool first = true; + for (const auto& str : list) { + if (!first) + fp << ","; + else + first = false; + + fp << str; + } + } + + void DumpServiceStatus(std::ostream& fp, const Service::Ptr& service); + void DumpServiceObject(std::ostream& fp, const Service::Ptr& service); + + void DumpCustomAttributes(std::ostream& fp, const CustomVarObject::Ptr& object); + + void UpdateObjectsCache(); + void StatusTimerHandler(); + void ObjectHandler(); + + static String GetNotificationOptions(const Checkable::Ptr& checkable); +}; + +} + +#endif /* STATUSDATAWRITER_H */ diff --git a/lib/compat/statusdatawriter.ti b/lib/compat/statusdatawriter.ti new file mode 100644 index 0000000..cc7eb11 --- /dev/null +++ b/lib/compat/statusdatawriter.ti @@ -0,0 +1,26 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "base/configobject.hpp" +#include "base/application.hpp" + +library compat; + +namespace icinga +{ + +class StatusDataWriter : ConfigObject +{ + activation_priority 100; + + [config] String status_path { + default {{{ return Configuration::CacheDir + "/status.dat"; }}} + }; + [config] String objects_path { + default {{{ return Configuration::CacheDir + "/objects.cache"; }}} + }; + [config] double update_interval { + default {{{ return 15; }}} + }; +}; + +} 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 > ActivationContext::m_ActivationStack; + +std::stack& ActivationContext::GetActivationStack() +{ + std::stack *actx = m_ActivationStack.get(); + + if (!actx) { + actx = new std::stack(); + 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& 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 +#include + +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& GetActivationStack(); + + static boost::thread_specific_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..210c677 --- /dev/null +++ b/lib/config/applyrule-targeted.cpp @@ -0,0 +1,254 @@ +/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */ + +#include "base/string.hpp" +#include "config/applyrule.hpp" +#include "config/expression.hpp" +#include +#include + +using namespace icinga; + +/** + * @returns All ApplyRules targeting only specific parent objects including the given host. (See AddTargetedRule().) + */ +const std::set& 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 noRules; + return noRules; +} + +/** + * @returns All ApplyRules targeting only specific parent objects including the given service. (See AddTargetedRule().) + */ +const std::set& 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 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 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> 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& hosts) +{ + auto lor (dynamic_cast(assignFilter)); + + if (lor) { + return GetTargetHosts(lor->GetOperand1().get(), hosts) + && GetTargetHosts(lor->GetOperand2().get(), hosts); + } + + auto name (GetComparedName(assignFilter, "host")); + + 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>& services) +{ + auto lor (dynamic_cast(assignFilter)); + + if (lor) { + return GetTargetServices(lor->GetOperand1().get(), services) + && GetTargetServices(lor->GetOperand2().get(), services); + } + + auto service (GetTargetService(assignFilter)); + + 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 ApplyRule::GetTargetService(Expression* assignFilter) +{ + auto land (dynamic_cast(assignFilter)); + + if (!land) { + return {nullptr, nullptr}; + } + + auto op1 (land->GetOperand1().get()); + auto op2 (land->GetOperand2().get()); + auto host (GetComparedName(op1, "host")); + + if (!host) { + std::swap(op1, op2); + host = GetComparedName(op1, "host"); + } + + if (host) { + auto service (GetComparedName(op2, "service")); + + 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) +{ + auto eq (dynamic_cast(assignFilter)); + + if (!eq) { + return nullptr; + } + + auto op1 (eq->GetOperand1().get()); + auto op2 (eq->GetOperand2().get()); + + if (IsNameIndexer(op1, lcType)) { + return GetLiteralStringValue(op2); + } + + if (IsNameIndexer(op2, lcType)) { + return GetLiteralStringValue(op1); + } + + return nullptr; +} + +/** + * @returns Whether the given expression is like $lcType$.name. + */ +bool ApplyRule::IsNameIndexer(Expression* exp, const char * lcType) +{ + auto ixr (dynamic_cast(exp)); + + if (!ixr) { + return false; + } + + auto var (dynamic_cast(ixr->GetOperand1().get())); + + if (!var || var->GetVariable() != lcType) { + return false; + } + + auto val (GetLiteralStringValue(ixr->GetOperand2().get())); + + return val && *val == "name"; +} + +/** + * @returns If the given expression is a string literal, the string. nullptr on failure. + */ +const String * ApplyRule::GetLiteralStringValue(Expression* exp) +{ + auto lit (dynamic_cast(exp)); + + if (!lit) { + return nullptr; + } + + auto& val (lit->GetValue()); + + if (!val.IsString()) { + return nullptr; + } + + return &val.Get(); +} diff --git a/lib/config/applyrule.cpp b/lib/config/applyrule.cpp new file mode 100644 index 0000000..5cfbf9e --- /dev/null +++ b/lib/config/applyrule.cpp @@ -0,0 +1,199 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "config/applyrule.hpp" +#include "base/logger.hpp" +#include +#include + +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; +} + +String ApplyRule::GetFKVar() const +{ + return m_FKVar; +} + +String ApplyRule::GetFVVar() const +{ + return m_FVVar; +} + +Expression::Ptr ApplyRule::GetFTerm() const +{ + return m_FTerm; +} + +bool ApplyRule::GetIgnoreOnError() const +{ + return m_IgnoreOnError; +} + +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& 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& ApplyRule::GetTargetTypes(const String& sourceType) +{ + auto it = m_Types.find(sourceType); + + if (it == m_Types.end()) { + static const std::vector noTypes; + return noTypes; + } + + return it->second; +} + +void ApplyRule::AddMatch() +{ + m_HasMatches = true; +} + +bool ApplyRule::HasMatches() const +{ + return m_HasMatches; +} + +const std::vector& 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 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 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..6b6fca8 --- /dev/null +++ b/lib/config/applyrule.hpp @@ -0,0 +1,115 @@ +/* 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 + +namespace icinga +{ + +/** + * @ingroup config + */ +class ApplyRule : public SharedObject +{ +public: + DECLARE_PTR_TYPEDEFS(ApplyRule); + + struct PerHost + { + std::set ForHost; + std::unordered_map> ForServices; + }; + + struct PerSourceType + { + std::unordered_map> Regular; + std::unordered_map 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 RuleMap; + + typedef std::map > TypeMap; + + String GetName() const; + Expression::Ptr GetExpression() const; + Expression::Ptr GetFilter() const; + String GetPackage() const; + String GetFKVar() const; + String GetFVVar() const; + Expression::Ptr GetFTerm() const; + bool GetIgnoreOnError() 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& GetRules(const Type::Ptr& sourceType, const Type::Ptr& targetType); + static const std::set& GetTargetedHostRules(const Type::Ptr& sourceType, const String& host); + static const std::set& GetTargetedServiceRules(const Type::Ptr& sourceType, const String& host, const String& service); + + static void RegisterType(const String& sourceType, const std::vector& targetTypes); + static bool IsValidSourceType(const String& sourceType); + static bool IsValidTargetType(const String& sourceType, const String& targetType); + static const std::vector& 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; + bool m_HasMatches; + + static TypeMap m_Types; + static RuleMap m_Rules; + + static bool AddTargetedRule(const ApplyRule::Ptr& rule, const String& targetType, PerSourceType& rules); + static bool GetTargetHosts(Expression* assignFilter, std::vector& hosts); + static bool GetTargetServices(Expression* assignFilter, std::vector>& services); + static std::pair GetTargetService(Expression* assignFilter); + static const String * GetComparedName(Expression* assignFilter, const char * lcType); + static bool IsNameIndexer(Expression* exp, const char * lcType); + static const String * GetLiteralStringValue(Expression* exp); + + 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 + +using namespace icinga; + +#include "config/config_parser.hh" +#include + +#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); + } + +\" { + 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; + } + +\n { + BOOST_THROW_EXCEPTION(ScriptError("Unterminated string literal", DebugInfoRange(yyextra->m_LocationBegin, *yylloc))); + } + +\\[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(result); + } + +\\[0-9]+ { + /* generate error - bad escape sequence; something + * like '\48' or '\0777777' + */ + BOOST_THROW_EXCEPTION(ScriptError("Bad escape sequence found: " + String(yytext), *yylloc)); + } +\\n { yyextra->m_LexBuffer += '\n'; } +\\\\ { yyextra->m_LexBuffer += '\\'; } +\\\" { yyextra->m_LexBuffer += '"'; } +\\t { yyextra->m_LexBuffer += '\t'; } +\\r { yyextra->m_LexBuffer += '\r'; } +\\b { yyextra->m_LexBuffer += '\b'; } +\\f { yyextra->m_LexBuffer += '\f'; } +\\\n { yyextra->m_LexBuffer += yytext[1]; } +\\. { + BOOST_THROW_EXCEPTION(ScriptError("Bad escape sequence found: " + String(yytext), *yylloc)); + } + +[^\\\n\"]+ { + char *yptr = yytext; + + while (*yptr) + yyextra->m_LexBuffer += *yptr++; + } + +<> { + 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); + } + +<> { + BOOST_THROW_EXCEPTION(ScriptError("End-of-file while in string literal", DebugInfoRange(yyextra->m_LocationBegin, *yylloc))); + } + +\}\}\} { + 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; + } + +(.|\n) { yyextra->m_LexBuffer += yytext[0]; } + +{ +"/*" BEGIN(C_COMMENT); +} + +{ +"*/" BEGIN(INITIAL); +[^*] /* ignore comment */ +"*" /* ignore star */ +} + +<> { + BOOST_THROW_EXCEPTION(ScriptError("End-of-file while in comment", *yylloc)); + } + + +\/\/[^\n]* /* ignore C++-style comments */ +#[^\n]* /* ignore shell-style comments */ +[ \t] /* ignore whitespace */ + +{ +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; } } +<> { 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 +#include + +#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 +static void MakeRBinaryOp(Expression** result, Expression *left, Expression *right, const DebugInfo& diLeft, const DebugInfo& diRight) +{ + *result = new T(std::unique_ptr(left), std::unique_ptr(right), DebugInfoRange(diLeft, diRight)); +} + +%} + +%pure-parser + +%locations +%defines +%error-verbose +%glr-parser + +%parse-param { std::vector, 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 *slist; + std::vector, EItemInfo> > *llist; + std::vector > *elist; + std::vector, std::unique_ptr > > *ebranchlist; + std::pair, std::unique_ptr > *ebranch; + std::pair > *cvitem; + std::map > *cvlist; + icinga::ScopeSpecifier scope; +} + +%token T_NEWLINE "new-line" +%token T_STRING +%token T_STRING_ANGLE +%token T_NUMBER +%token T_BOOLEAN +%token T_NULL +%token T_IDENTIFIER + +%token T_SET "= (T_SET)" +%token T_SET_ADD "+= (T_SET_ADD)" +%token T_SET_SUBTRACT "-= (T_SET_SUBTRACT)" +%token T_SET_MULTIPLY "*= (T_SET_MULTIPLY)" +%token T_SET_DIVIDE "/= (T_SET_DIVIDE)" +%token T_SET_MODULO "%= (T_SET_MODULO)" +%token T_SET_XOR "^= (T_SET_XOR)" +%token T_SET_BINARY_AND "&= (T_SET_BINARY_AND)" +%token 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 identifier +%type rterm_items +%type rterm_items_inner +%type identifier_items +%type identifier_items_inner +%type combined_set_op +%type statements +%type lterm_items +%type lterm_items_inner +%type rterm +%type rterm_array +%type rterm_dict +%type rterm_scope_require_side_effect +%type rterm_scope +%type else_if_branches +%type else_if_branch +%type rterm_side_effect +%type rterm_no_side_effect +%type rterm_no_side_effect_no_dict +%type lterm +%type object +%type apply +%type optional_rterm +%type target_type_specifier +%type default_specifier +%type ignore_specifier +%type use_specifier +%type use_specifier_items +%type use_specifier_item +%type 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, 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, 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 ConfigCompiler::Compile() +{ + std::vector, 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 > 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 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, EItemInfo> >(); + } + | lterm_items_inner + | lterm_items_inner sep + ; + +lterm_items_inner: lterm %dprec 2 + { + $$ = new std::vector, EItemInfo> >(); + $$->emplace_back(std::unique_ptr($1), EItemInfo{true, @1}); + } + | rterm_no_side_effect + { + $$ = new std::vector, EItemInfo> >(); + $$->emplace_back(std::unique_ptr($1), EItemInfo{false, @1}); + } + | lterm_items_inner sep lterm %dprec 1 + { + if ($1) + $$ = $1; + else + $$ = new std::vector, EItemInfo> >(); + + if ($3) { + $$->emplace_back(std::unique_ptr($3), EItemInfo{true, @3}); + } + } + | lterm_items_inner sep rterm_no_side_effect %dprec 1 + { + if ($1) + $$ = $1; + else + $$ = new std::vector, EItemInfo> >(); + + if ($3) { + $$->emplace_back(std::unique_ptr($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 ignore{std::move(context->m_Ignore.top())}; + context->m_Ignore.pop(); + + std::unique_ptr assign{std::move(context->m_Assign.top())}; + context->m_Assign.pop(); + + std::unique_ptr filter; + + if (seen_assign) { + if (ignore) { + std::unique_ptr 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($3), std::unique_ptr($4), + std::move(filter), context->GetZone(), context->GetPackage(), std::move(*$5), $6, $7, + std::unique_ptr($9), DebugInfoRange(@2, @7)); + delete $5; + } + ; + +object_declaration: T_OBJECT + { + $$ = false; + } + | T_TEMPLATE + { + $$ = true; + } + ; + +identifier_items: /* empty */ + { + $$ = new std::vector(); + } + | identifier_items_inner + | identifier_items_inner ',' + ; + +identifier_items_inner: identifier + { + $$ = new std::vector(); + $$->emplace_back(std::move(*$1)); + delete $1; + } + | identifier_items_inner ',' identifier + { + if ($1) + $$ = $1; + else + $$ = new std::vector(); + + $$->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($2), @$); + } + | rterm combined_set_op rterm + { + $$ = new SetExpression(std::unique_ptr($1), $2, std::unique_ptr($3), @$); + } + | T_INCLUDE rterm + { + $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr($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($2), MakeLiteral("*.conf"), NULL, IncludeRecursive, false, context->GetZone(), context->GetPackage(), @$); + } + | T_INCLUDE_RECURSIVE rterm ',' rterm + { + $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr($2), std::unique_ptr($4), NULL, IncludeRecursive, false, context->GetZone(), context->GetPackage(), @$); + } + | T_INCLUDE_ZONES rterm ',' rterm + { + $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr($4), MakeLiteral("*.conf"), std::unique_ptr($2), IncludeZones, false, context->GetZone(), context->GetPackage(), @$); + } + | T_INCLUDE_ZONES rterm ',' rterm ',' rterm + { + $$ = new IncludeExpression(Utility::DirName(context->GetPath()), std::unique_ptr($4), std::unique_ptr($6), std::unique_ptr($2), IncludeZones, false, context->GetZone(), context->GetPackage(), @$); + } + | T_IMPORT rterm + { + $$ = new ImportExpression(std::unique_ptr($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(context->m_Assign.top()), std::unique_ptr($4), @$); + else + context->m_Assign.top() = $4; + + $$ = MakeLiteralRaw(); + } + | T_ASSIGN T_WHERE rterm %dprec 1 + { + ASSERT(!dynamic_cast($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(context->m_Assign.top()), std::unique_ptr($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(context->m_Ignore.top()), std::unique_ptr($4), @$); + else + context->m_Ignore.top() = $4; + + $$ = MakeLiteralRaw(); + } + | T_IGNORE T_WHERE rterm %dprec 1 + { + ASSERT(!dynamic_cast($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(context->m_Ignore.top()), std::unique_ptr($3), @$); + else + context->m_Ignore.top() = $3; + + $$ = MakeLiteralRaw(); + } + | T_RETURN optional_rterm + { + UseFlowControl(context, FlowControlReturn, @$); + $$ = new ReturnExpression(std::unique_ptr($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 expr{$2}; + BindToScope(expr, ScopeGlobal); + $$ = new SetExpression(std::move(expr), OpSetLiteral, std::unique_ptr(new NamespaceExpression(std::unique_ptr($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($9), std::unique_ptr($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($6), std::unique_ptr($9), @$); + delete $4; + } + | T_FUNCTION identifier '(' identifier_items ')' use_specifier + { + BeginFlowControlBlock(context, FlowControlReturn, false); + } + rterm_scope + { + EndFlowControlBlock(context); + + std::unique_ptr fexpr{new FunctionExpression(*$2, std::move(*$4), std::move(*$6), std::unique_ptr($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($4), @$); + delete $2; + } + | T_VAR rterm + { + std::unique_ptr expr{$2}; + BindToScope(expr, ScopeLocal); + $$ = new SetExpression(std::move(expr), OpSetLiteral, MakeLiteral(), @$); + } + | T_VAR rterm combined_set_op rterm + { + std::unique_ptr expr{$2}; + BindToScope(expr, ScopeLocal); + $$ = new SetExpression(std::move(expr), $3, std::unique_ptr($4), @$); + } + | T_WHILE '(' rterm ')' + { + BeginFlowControlBlock(context, FlowControlContinue | FlowControlBreak, true); + } + rterm_scope + { + EndFlowControlBlock(context); + + $$ = new WhileExpression(std::unique_ptr($3), std::unique_ptr($6), @$); + } + | T_THROW rterm + { + $$ = new ThrowExpression(std::unique_ptr($2), false, @$); + } + | T_TRY rterm_scope T_EXCEPT rterm_scope + { + $$ = new TryExceptExpression(std::unique_ptr($2), std::unique_ptr($4), @$); + } + | rterm_side_effect + ; + +rterm_items: /* empty */ + { + $$ = new std::vector >(); + } + | rterm_items_inner + | rterm_items_inner ',' optional_newlines + | rterm_items_inner newlines + ; + +rterm_items_inner: rterm + { + $$ = new std::vector >(); + $$->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 > 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 > 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 > 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 >(std::unique_ptr($4), std::unique_ptr($6)); + } + ; + +else_if_branches: /* empty */ + { + $$ = new std::vector, std::unique_ptr > >(); + } + | 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($1), std::move(*$3), @$); + delete $3; + } + | T_IF '(' rterm ')' rterm_scope else_if_branches + { + std::vector, std::unique_ptr > > ebranches; + $6->swap(ebranches); + delete $6; + + std::unique_ptr 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($3), std::unique_ptr($5), std::move(afalse), @$); + } + | T_IF '(' rterm ')' rterm_scope else_if_branches T_ELSE rterm_scope + { + std::vector, std::unique_ptr > > ebranches; + $6->swap(ebranches); + delete $6; + + $8->MakeInline(); + + std::unique_ptr 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($3), std::unique_ptr($5), std::move(afalse), @$); + } + | rterm '?' rterm ':' rterm + { + $$ = new ConditionalExpression(std::unique_ptr($1), std::unique_ptr($3), std::unique_ptr($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($1), MakeLiteral(std::move(*$3)), @$); + delete $3; + } + | rterm '[' rterm ']' + { + $$ = new IndexerExpression(std::unique_ptr($1), std::unique_ptr($3), @$); + } + | T_IDENTIFIER + { + $$ = new VariableExpression(std::move(*$1), context->GetImports(), @1); + delete $1; + } + | T_MULTIPLY rterm %prec DEREF_OP + { + $$ = new DerefExpression(std::unique_ptr($2), @$); + } + | T_BINARY_AND rterm %prec REF_OP + { + $$ = new RefExpression(std::unique_ptr($2), @$); + } + | '!' rterm + { + $$ = new LogicalNegateExpression(std::unique_ptr($2), @$); + } + | '~' rterm + { + $$ = new NegateExpression(std::unique_ptr($2), @$); + } + | T_PLUS rterm %prec UNARY_PLUS + { + $$ = $2; + } + | T_MINUS rterm %prec UNARY_MINUS + { + $$ = new SubtractExpression(MakeLiteral(0), std::unique_ptr($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 args; + args.emplace_back(std::move(*$1)); + delete $1; + + $$ = new FunctionExpression("", std::move(args), {}, std::unique_ptr($4), @$); + } + | identifier T_FOLLOWS rterm %dprec 1 + { + ASSERT(!dynamic_cast($3)); + + std::vector args; + args.emplace_back(std::move(*$1)); + delete $1; + + $$ = new FunctionExpression("", std::move(args), {}, std::unique_ptr($3), @$); + } + | '(' identifier_items ')' use_specifier T_FOLLOWS + { + BeginFlowControlBlock(context, FlowControlReturn, false); + } + rterm_scope %dprec 2 + { + EndFlowControlBlock(context); + + $$ = new FunctionExpression("", std::move(*$2), std::move(*$4), std::unique_ptr($7), @$); + delete $2; + delete $4; + } + | '(' identifier_items ')' use_specifier T_FOLLOWS rterm %dprec 1 + { + ASSERT(!dynamic_cast($6)); + + $$ = new FunctionExpression("", std::move(*$2), std::move(*$4), std::unique_ptr($6), @$); + delete $2; + delete $4; + } + | rterm_array + | '(' + { + context->m_OpenBraces++; + } + rterm ')' + { + context->m_OpenBraces--; + $$ = $3; + } + | rterm T_LOGICAL_OR rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_LOGICAL_AND rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_BINARY_OR rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_BINARY_AND rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_IN rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_NOT_IN rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_EQUAL rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_NOT_EQUAL rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_LESS_THAN rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_LESS_THAN_OR_EQUAL rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_GREATER_THAN rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_GREATER_THAN_OR_EQUAL rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_SHIFT_LEFT rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_SHIFT_RIGHT rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_PLUS rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_MINUS rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_MULTIPLY rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_DIVIDE_OP rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_MODULO rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | rterm T_XOR rterm { MakeRBinaryOp(&$$, $1, $3, @1, @3); } + | T_FUNCTION '(' identifier_items ')' use_specifier + { + BeginFlowControlBlock(context, FlowControlReturn, false); + } + rterm_scope + { + EndFlowControlBlock(context); + + $$ = new FunctionExpression("", std::move(*$3), std::move(*$5), std::unique_ptr($7), @$); + delete $3; + delete $5; + } + | T_NULLARY_LAMBDA_BEGIN + { + BeginFlowControlBlock(context, FlowControlReturn, false); + } + statements T_NULLARY_LAMBDA_END + { + EndFlowControlBlock(context); + + std::vector > 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 aexpr{new DictExpression(std::move(dlist), @$)}; + aexpr->MakeInline(); + + $$ = new FunctionExpression("", {}, {}, std::move(aexpr), @$); + } + ; + +rterm_no_side_effect: + rterm_no_side_effect_no_dict %dprec 1 + | rterm_dict %dprec 2 + { + std::unique_ptr 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 >(); + } + | T_USE '(' use_specifier_items ')' + { + $$ = $3; + } + ; + +use_specifier_items: use_specifier_item + { + $$ = new std::map >(); + $$->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 var (new VariableExpression(*$1, context->GetImports(), @1)); + $$ = new std::pair >(std::move(*$1), std::move(var)); + delete $1; + } + | identifier T_SET rterm + { + $$ = new std::pair >(std::move(*$1), std::unique_ptr($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::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 ignore{context->m_Ignore.top()}; + context->m_Ignore.pop(); + + std::unique_ptr assign; + + if (!seen_assign) + assign = MakeLiteral(true); + else + assign.reset(context->m_Assign.top()); + + context->m_Assign.pop(); + + std::unique_ptr filter; + + if (ignore) { + std::unique_ptrrex{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 fterm{context->m_FTerm.top()}; + context->m_FTerm.pop(); + + $$ = new ApplyExpression(std::move(type), std::move(target), std::unique_ptr($4), std::move(filter), context->GetPackage(), std::move(fkvar), std::move(fvvar), std::move(fterm), std::move(*$7), $8, std::unique_ptr($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..2c8fa5c --- /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 + +using namespace icinga; + +std::vector ConfigCompiler::m_IncludeSearchDirs; +std::mutex ConfigCompiler::m_ZoneDirsMutex; +std::map > 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(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 >& 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 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 > 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 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 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 > expressions; + Utility::GlobRecursive(ppath, pattern, [&expressions, zone, package](const String& file) { + CollectIncludes(expressions, file, zone, package); + }, GlobFile); + + std::unique_ptr 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 >& 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 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 > expressions; + Utility::Glob(ppath + "/*", [newRelativeBase, tag, pattern, package, &expressions](const String& path) { + HandleIncludeZone(newRelativeBase, tag, path, pattern, package, expressions); + }, GlobDirectory); + + return std::unique_ptr(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 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(new ThrowExpression(MakeLiteral(ex.what()), ex.IsIncompleteExpression(), ex.GetDebugInfo())); + } catch (const std::exception& ex) { + return std::unique_ptr(new ThrowExpression(MakeLiteral(DiagnosticInformation(ex)), false)); + } +} + +/** + * Compiles a file. + * + * @param path The path. + * @returns Configuration items. + */ +std::unique_ptr 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 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 ConfigCompiler::GetZoneDirs(const String& zone) +{ + std::unique_lock lock(m_ZoneDirsMutex); + auto it = m_ZoneDirs.find(zone); + if (it == m_ZoneDirs.end()) + return std::vector(); + 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 lock(m_ZoneDirsMutex); + m_ZoneDirs[zoneName].push_back(zf); +} + +bool ConfigCompiler::HasZoneConfigAuthority(const String& zoneName) +{ + std::vector zoneDirs = m_ZoneDirs[zoneName]; + + bool empty = zoneDirs.empty(); + + if (!empty) { + std::vector 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 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 +#include +#include + +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 Compile(); + + static std::unique_ptrCompileStream(const String& path, std::istream *stream, + const String& zone = String(), const String& package = String()); + static std::unique_ptrCompileFile(const String& path, const String& zone = String(), + const String& package = String()); + static std::unique_ptrCompileText(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 GetImports() const; + + static void CollectIncludes(std::vector >& expressions, + const String& file, const String& zone, const String& package); + + static std::unique_ptr HandleInclude(const String& relativeBase, const String& path, bool search, + const String& zone, const String& package, const DebugInfo& debuginfo = DebugInfo()); + static std::unique_ptr HandleIncludeRecursive(const String& relativeBase, const String& path, + const String& pattern, const String& zone, const String& package, const DebugInfo& debuginfo = DebugInfo()); + static std::unique_ptr 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 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 m_Promise; + + String m_Path; + std::istream *m_Input; + String m_Zone; + String m_Package; + std::vector m_Imports; + + void *m_Scanner; + + static std::vector m_IncludeSearchDirs; + static std::mutex m_ZoneDirsMutex; + static std::map > 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 >& expressions); + + static bool IsAbsolutePath(const String& path); + +public: + bool m_Eof; + int m_OpenBraces; + + String m_LexBuffer; + CompilerDebugInfo m_LocationBegin; + + std::stack m_IgnoreNewlines; + std::stack m_Apply; + std::stack m_ObjectAssign; + std::stack m_SeenAssign; + std::stack m_SeenIgnore; + std::stack m_Assign; + std::stack m_Ignore; + std::stack m_FKVar; + std::stack m_FVVar; + std::stack m_FTerm; + std::stack m_FlowControlInfo; +}; + +} + +#endif /* CONFIGCOMPILER_H */ diff --git a/lib/config/configcompilercontext.cpp b/lib/config/configcompilercontext.cpp new file mode 100644 index 0000000..666fe56 --- /dev/null +++ b/lib/config/configcompilercontext.cpp @@ -0,0 +1,68 @@ +/* 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::GetInstance(); +} + +void ConfigCompilerContext::OpenObjectsFile(const String& filename) +{ + m_ObjectsPath = filename; + + auto *fp = new std::fstream(); + try { + m_ObjectsTempFile = Utility::CreateTempFile(filename + ".XXXXXX", 0600, *fp); + } catch (const std::exception& ex) { + Log(LogCritical, "cli", "Could not create temporary objects file: " + DiagnosticInformation(ex, false)); + Application::Exit(1); + } + + if (!*fp) + BOOST_THROW_EXCEPTION(std::runtime_error("Could not open '" + m_ObjectsTempFile + "' file")); + + m_ObjectsFP = fp; +} + +void ConfigCompilerContext::WriteObject(const Dictionary::Ptr& object) +{ + if (!m_ObjectsFP) + return; + + String json = JsonEncode(object); + + { + std::unique_lock lock(m_Mutex); + NetString::WriteStringToStream(*m_ObjectsFP, json); + } +} + +void ConfigCompilerContext::CancelObjectsFile() +{ + delete m_ObjectsFP; + m_ObjectsFP = nullptr; + +#ifdef _WIN32 + _unlink(m_ObjectsTempFile.CStr()); +#else /* _WIN32 */ + unlink(m_ObjectsTempFile.CStr()); +#endif /* _WIN32 */ +} + +void ConfigCompilerContext::FinishObjectsFile() +{ + delete m_ObjectsFP; + m_ObjectsFP = nullptr; + + Utility::RenameFile(m_ObjectsTempFile, m_ObjectsPath); +} + diff --git a/lib/config/configcompilercontext.hpp b/lib/config/configcompilercontext.hpp new file mode 100644 index 0000000..f02e268 --- /dev/null +++ b/lib/config/configcompilercontext.hpp @@ -0,0 +1,37 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef CONFIGCOMPILERCONTEXT_H +#define CONFIGCOMPILERCONTEXT_H + +#include "config/i2-config.hpp" +#include "base/dictionary.hpp" +#include +#include + +namespace icinga +{ + +/* + * @ingroup config + */ +class ConfigCompilerContext +{ +public: + void OpenObjectsFile(const String& filename); + void WriteObject(const Dictionary::Ptr& object); + void CancelObjectsFile(); + void FinishObjectsFile(); + + static ConfigCompilerContext *GetInstance(); + +private: + String m_ObjectsPath; + String m_ObjectsTempFile; + std::fstream *m_ObjectsFP{nullptr}; + + 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..55038f8 --- /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 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); \ + } \ + }, 5) + +#endif /* CONFIGFRAGMENT_H */ diff --git a/lib/config/configitem.cpp b/lib/config/configitem.cpp new file mode 100644 index 0000000..d615094 --- /dev/null +++ b/lib/config/configitem.cpp @@ -0,0 +1,820 @@ +/* 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 +#include +#include +#include +#include +#include + +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 && 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(type->Instantiate(std::vector())); + + 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 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(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 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 lock(m_Mutex); + m_IgnoredItems.push_back(m_DebugInfo.Path); + } + + return nullptr; + } + + throw; + } + + Value serializedObject; + + try { + serializedObject = Serialize(dobj, FAConfig); + } catch (const CircularReferenceError& ex) { + BOOST_THROW_EXCEPTION(ValidationError(dobj, ex.GetPath(), "Circular references are not allowed")); + } + + 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, + }) } + }); + + dhint.reset(); + + ConfigCompilerContext::GetInstance()->WriteObject(persistentItem); + persistentItem.reset(); + + dobj->Register(); + + m_Object = dobj; + + return dobj; +} + +/** + * Registers the configuration item. + */ +void ConfigItem::Register() +{ + m_ActivationContext = ActivationContext::GetCurrentContext(); + + std::unique_lock 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(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 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 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& newItems) +{ + typedef std::pair ItemPair; + std::vector items; + + { + std::unique_lock lock(m_Mutex); + + for (const TypeMap::value_type& kv : m_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); + } + } + + 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; + + items.emplace_back(item, true); + } + + m_UnnamedItems.swap(newUnnamedItems); + } + + if (items.empty()) + 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. + std::shuffle(std::begin(items), std::end(items), std::default_random_engine {}); + +#ifdef I2_DEBUG + Log(LogDebug, "configitem") + << "Committing " << items.size() << " new items."; +#endif /* I2_DEBUG */ + + std::set types; + std::set 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 (const String& loadDep : type->GetLoadDependencies()) { + Type::Ptr pLoadDep = Type::GetByName(loadDep); + if (types.find(pLoadDep) != types.end() && completed_types.find(pLoadDep) == completed_types.end()) { + unresolved_dep = true; + break; + } + } + + if (unresolved_dep) + continue; + + std::atomic committed_items(0); + std::mutex newItemsMutex; + + upq.ParallelFor(items, [&type, &committed_items, &newItems, &newItemsMutex](const ItemPair& ip) { + const ConfigItem::Ptr& item = ip.first; + + if (item->m_Type != type) + return; + + if (!item->Commit(ip.second)) { + if (item->IsIgnoreOnError()) { + item->Unregister(); + } + + return; + } + + committed_items++; + + std::unique_lock 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 (const String& loadDep : type->GetLoadDependencies()) { + Type::Ptr pLoadDep = Type::GetByName(loadDep); + if (types.find(pLoadDep) != types.end() && completed_types.find(pLoadDep) == completed_types.end()) { + unresolved_dep = true; + break; + } + } + + if (unresolved_dep) + continue; + + std::atomic notified_items(0); + upq.ParallelFor(items, [&type, ¬ified_items](const ItemPair& ip) { + const ConfigItem::Ptr& item = ip.first; + + if (!item->m_Object || item->m_Type != type) + 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 lock(item->m_Mutex); + item->m_IgnoredItems.push_back(item->m_DebugInfo.Path); + } + } + }); + + completed_types.insert(type); + + upq.Join(); + +#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 (const String& loadDep : type->GetLoadDependencies()) { + upq.ParallelFor(items, [loadDep, &type, ¬ified_items](const ItemPair& ip) { + const ConfigItem::Ptr& item = ip.first; + + if (!item->m_Object || item->m_Type->GetName() != loadDep) + 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& 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 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& newItems, bool runtimeCreated, + bool mainConfigActivation, bool withModAttrs, const Value& cookie) +{ + static std::mutex mtx; + std::unique_lock lock(mtx); + + if (withModAttrs) { + /* restore modified attributes */ + if (Utility::PathExists(Configuration::ModAttrPath)) { + std::unique_ptr 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 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 newItems; + + if (!CommitItems(scope.GetContext(), upq, newItems, true)) + return false; + + if (!ActivateItems(newItems, false, false)) + return false; + + return true; +} + +std::vector ConfigItem::GetItems(const Type::Ptr& type) +{ + std::vector items; + + std::unique_lock 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::GetDefaultTemplates(const Type::Ptr& type) +{ + std::vector items; + + std::unique_lock 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 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 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& newItems, bool silent = false); + static bool ActivateItems(const std::vector& newItems, bool runtimeCreated = false, + bool mainConfigActivation = false, bool withModAttrs = false, const Value& cookie = Empty); + + static bool RunWithActivationContext(const Function::Ptr& function); + + static std::vector GetItems(const Type::Ptr& type); + static std::vector 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 ItemMap; + typedef std::map TypeMap; + static TypeMap m_Items; /**< All registered configuration items. */ + static TypeMap m_DefaultTemplates; + + typedef std::vector ItemList; + static ItemList m_UnnamedItems; + + typedef std::vector 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& 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 + +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(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 > exprs; + + Array::Ptr templateArray = new Array({ m_Name }); + + exprs.emplace_back(new SetExpression(MakeIndexer(ScopeThis, "templates"), OpSetAdd, + std::unique_ptr(new LiteralExpression(templateArray)), m_DebugInfo)); + +#ifdef I2_DEBUG + if (!m_Abstract) { + bool foundDefaultImport = false; + + for (const std::unique_ptr& expr : m_Expressions) { + if (dynamic_cast(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 > 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..6a709ac --- /dev/null +++ b/lib/config/expression.cpp @@ -0,0 +1,1078 @@ +/* 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 +#include + +using namespace icinga; + +boost::signals2::signal Expression::OnBreakpoint; +boost::thread_specific_ptr 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(); + ExpressionResult result = DoEvaluate(frame, dhint); + frame.DecreaseStackDepth(); + return result; + } catch (ScriptError& ex) { + frame.DecreaseStackDepth(); + + ScriptBreakpoint(frame, &ex, GetDebugInfo()); + throw; + } catch (const std::exception& ex) { + frame.DecreaseStackDepth(); + + BOOST_THROW_EXCEPTION(ScriptError("Error while evaluating expression: " + String(ex.what()), GetDebugInfo()) + << boost::errinfo_nested_exception(boost::current_exception())); + } + + frame.DecreaseStackDepth(); +} + +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 icinga::MakeIndexer(ScopeSpecifier scopeSpec, const String& index) +{ + std::unique_ptr scope{new GetScopeExpression(scopeSpec)}; + return std::unique_ptr(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 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() && frame.Self.Get()->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() && frame.Self.Get()->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(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()) + 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()) + 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()) { + std::vector 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()) + 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 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& setLhs, const Value& setLhsParent, CombinedSetOp setOp, const DebugInfo& debug) +{ + auto var (dynamic_cast(setLhs.get())); + + if (var && setLhsParent.IsObject()) { + auto ns (dynamic_pointer_cast(setLhsParent.Get())); + + 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(); + + auto attr = globals->GetAttribute(m_Name); + + if (dynamic_pointer_cast(attr)) { + std::ostringstream msgbuf; + msgbuf << "Value for constant '" << m_Name << "' was modified. This behaviour is deprecated.\n"; + ShowCodeLocation(msgbuf, GetDebugInfo(), false); + Log(LogWarning, msgbuf.str()); + } + + ExpressionResult operandres = m_Operand->Evaluate(frame); + CHECK_RESULT(operandres); + Value operand = operandres.GetValue(); + + globals->SetAttribute(m_Name, new ConstEmbeddedNamespaceValue(operand)); + + 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& expr, ScopeSpecifier scopeSpec) +{ + auto *dexpr = dynamic_cast(expr.get()); + + if (dexpr) { + for (auto& expr : dexpr->m_Expressions) + BindToScope(expr, scopeSpec); + + return; + } + + auto *aexpr = dynamic_cast(expr.get()); + + if (aexpr) { + BindToScope(aexpr->m_Operand1, scopeSpec); + + return; + } + + auto *iexpr = dynamic_cast(expr.get()); + + if (iexpr) { + BindToScope(iexpr->m_Operand1, scopeSpec); + return; + } + + auto *lexpr = dynamic_cast(expr.get()); + + if (lexpr && lexpr->GetValue().IsString()) { + std::unique_ptr scope{new GetScopeExpression(scopeSpec)}; + expr.reset(new IndexerExpression(std::move(scope), std::move(expr), lexpr->GetDebugInfo())); + } + + auto *vexpr = dynamic_cast(expr.get()); + + if (vexpr) { + std::unique_ptr 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(new ConstNamespaceBehavior()); + + 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 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..7be667a --- /dev/null +++ b/lib/config/expression.hpp @@ -0,0 +1,981 @@ +/* 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 + +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 DefinitionMap; + +/** + * @ingroup config + */ +enum ExpressionResultCode +{ + ResultOK, + ResultReturn, + ResultContinue, + ResultBreak +}; + +/** + * @ingroup config + */ +struct ExpressionResult +{ +public: + template + 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 OnBreakpoint; + + static void ScriptBreakpoint(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di); +}; + +std::unique_ptr 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 MakeLiteral(const Value& literal = Value()) +{ + return std::unique_ptr(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 operand, const DebugInfo& debugInfo = DebugInfo()) + : DebuggableExpression(debugInfo), m_Operand(std::move(operand)) + { } + +protected: + std::unique_ptr m_Operand; +}; + +class BinaryExpression : public DebuggableExpression +{ +public: + BinaryExpression(std::unique_ptr operand1, std::unique_ptr operand2, const DebugInfo& debugInfo = DebugInfo()) + : DebuggableExpression(debugInfo), m_Operand1(std::move(operand1)), m_Operand2(std::move(operand2)) + { } + + inline const std::unique_ptr& GetOperand1() const noexcept + { + return m_Operand1; + } + + inline const std::unique_ptr& GetOperand2() const noexcept + { + return m_Operand2; + } + +protected: + std::unique_ptr m_Operand1; + std::unique_ptr m_Operand2; +}; + +class VariableExpression final : public DebuggableExpression +{ +public: + VariableExpression(String variable, std::vector 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 m_Imports; + + friend void BindToScope(std::unique_ptr& expr, ScopeSpecifier scopeSpec); +}; + +class DerefExpression final : public UnaryExpression +{ +public: + DerefExpression(std::unique_ptr 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 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 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 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 operand1, std::unique_ptr 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 fname, std::vector >&& 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 m_FName; + std::vector > m_Args; +}; + +class ArrayExpression final : public DebuggableExpression +{ +public: + ArrayExpression(std::vector >&& expressions, const DebugInfo& debugInfo = DebugInfo()) + : DebuggableExpression(debugInfo), m_Expressions(std::move(expressions)) + { } + +protected: + ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override; + +private: + std::vector > m_Expressions; +}; + +class DictExpression final : public DebuggableExpression +{ +public: + DictExpression(std::vector >&& expressions = {}, const DebugInfo& debugInfo = DebugInfo()) + : DebuggableExpression(debugInfo), m_Expressions(std::move(expressions)) + { } + + void MakeInline(); + +protected: + ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override; + +private: + std::vector > m_Expressions; + bool m_Inline{false}; + + friend void BindToScope(std::unique_ptr& expr, ScopeSpecifier scopeSpec); +}; + +class SetConstExpression final : public UnaryExpression +{ +public: + SetConstExpression(const String& name, std::unique_ptr 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 operand1, CombinedSetOp op, std::unique_ptr 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& expr, ScopeSpecifier scopeSpec); +}; + +class ConditionalExpression final : public DebuggableExpression +{ +public: + ConditionalExpression(std::unique_ptr condition, std::unique_ptr true_branch, std::unique_ptr 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 m_Condition; + std::unique_ptr m_TrueBranch; + std::unique_ptr m_FalseBranch; +}; + +class WhileExpression final : public DebuggableExpression +{ +public: + WhileExpression(std::unique_ptr condition, std::unique_ptr 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 m_Condition; + std::unique_ptr m_LoopBody; +}; + + +class ReturnExpression final : public UnaryExpression +{ +public: + ReturnExpression(std::unique_ptr 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 operand1, std::unique_ptr 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& expr, ScopeSpecifier scopeSpec); +}; + +void BindToScope(std::unique_ptr& expr, ScopeSpecifier scopeSpec); + +class ThrowExpression final : public DebuggableExpression +{ +public: + ThrowExpression(std::unique_ptr 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 m_Message; + bool m_IncompleteExpr; +}; + +class ImportExpression final : public DebuggableExpression +{ +public: + ImportExpression(std::unique_ptr 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 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 args, + std::map >&& closedVars, std::unique_ptr 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 m_Args; + std::map > m_ClosedVars; + Expression::Ptr m_Expression; +}; + +class ApplyExpression final : public DebuggableExpression +{ +public: + ApplyExpression(String type, String target, std::unique_ptr name, + std::unique_ptr filter, String package, String fkvar, String fvvar, + std::unique_ptr fterm, std::map >&& closedVars, bool ignoreOnError, + std::unique_ptr 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 m_Name; + Expression::Ptr m_Filter; + String m_Package; + String m_FKVar; + String m_FVVar; + Expression::Ptr m_FTerm; + bool m_IgnoreOnError; + std::map > m_ClosedVars; + Expression::Ptr m_Expression; +}; + +class NamespaceExpression final : public DebuggableExpression +{ +public: + NamespaceExpression(std::unique_ptr 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 type, std::unique_ptr name, std::unique_ptr filter, + String zone, String package, std::map >&& closedVars, + bool defaultTmpl, bool ignoreOnError, std::unique_ptr 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 m_Type; + std::unique_ptr m_Name; + Expression::Ptr m_Filter; + String m_Zone; + String m_Package; + bool m_DefaultTmpl; + bool m_IgnoreOnError; + std::map > m_ClosedVars; + Expression::Ptr m_Expression; +}; + +class ForExpression final : public DebuggableExpression +{ +public: + ForExpression(String fkvar, String fvvar, std::unique_ptr value, std::unique_ptr 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 m_Value; + std::unique_ptr m_Expression; +}; + +class LibraryExpression final : public UnaryExpression +{ +public: + LibraryExpression(std::unique_ptr 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 path, std::unique_ptr pattern, std::unique_ptr 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 m_Path; + std::unique_ptr m_Pattern; + std::unique_ptr 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 tryBody, std::unique_ptr 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 m_TryBody; + std::unique_ptr 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 + +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 + +namespace icinga +{ + +/** + * @ingroup config + */ +class ObjectRule +{ +public: + typedef std::set 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 +#include + +namespace icinga +{ + +class VMOps +{ +public: + static inline bool FindVarImportRef(ScriptFrame& frame, const std::vector& 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& 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& 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& 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& argNames, + const std::map >& closedVars, const Expression::Ptr& expression) + { + auto evaluatedClosedVars = EvaluateClosedVars(frame, closedVars); + + auto wrapper = [argNames, evaluatedClosedVars, expression](const std::vector& 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::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 >& 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 >& closedVars, const Expression::Ptr& expression, const DebugInfo& debugInfo = DebugInfo()) + { + ConfigItemBuilder item{debugInfo}; + + String checkName = name; + + if (!abstract) { + auto *nc = dynamic_cast(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, const DebugInfo& debugInfo = DebugInfo()) + { + if (value.IsObjectType()) { + 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()) { + if (fvvar.IsEmpty()) + BOOST_THROW_EXCEPTION(ScriptError("Cannot use array iterator for dictionary.", debugInfo)); + + Dictionary::Ptr dict = value; + std::vector 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()) { + if (fvvar.IsEmpty()) + BOOST_THROW_EXCEPTION(ScriptError("Cannot use array iterator for namespace.", debugInfo)); + + Namespace::Ptr ns = value; + std::vector 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 >& 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(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..62160d8 --- /dev/null +++ b/lib/db_ido/dbconnection.cpp @@ -0,0 +1,582 @@ +/* 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::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& multiQueries) { ExecuteMultipleQueries(multiQueries); }; + DbObject::OnMultipleQueries.connect(onMultipleQueries); + + DbObject::QueryCallbacks queryCallbacks; + queryCallbacks.Query = onQuery; + queryCallbacks.MultipleQueries = onMultipleQueries; + + DbObject::OnMakeQueries.connect([queryCallbacks](const std::function& queryFunc) { + queryFunc(queryCallbacks); + }); +} + +void DbConnection::Stop(bool runtimeRemoved) +{ + Log(LogInformation, "DbConnection") + << "'" << GetName() << "' stopped."; + + ObjectImpl::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 = new Timer(); + m_CleanUpTimer->SetInterval(60); + m_CleanUpTimer->OnTimerExpired.connect([this](const Timer * const&) { CleanUpHandler(); }); + m_CleanUpTimer->Start(); + + m_LogStatsTimeout = 0; + + m_LogStatsTimer = new Timer(); + 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_CleanUpTimer.reset(); + + 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 = new Timer(); + 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 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()->GetObjectCount()); + InsertRuntimeVariable("total_scheduled_services", ConfigType::Get()->GetObjectCount()); + InsertRuntimeVariable("total_hosts", ConfigType::Get()->GetObjectCount()); + InsertRuntimeVariable("total_scheduled_hosts", ConfigType::Get()->GetObjectCount()); +} + +void DbConnection::CleanUpHandler() +{ + auto now = static_cast(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(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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::ValidateFailoverTimeout(lvalue, utils); + + if (lvalue() < 30) + BOOST_THROW_EXCEPTION(ValidationError(this, { "failover_timeout" }, "Failover timeout minimum is 30s.")); +} + +void DbConnection::ValidateCategories(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 lock(m_StatsMutex); + m_QueryStats.InsertValue(now, 1); +} + +int DbConnection::GetQueryCount(RingBuffer::SizeType span) +{ + std::unique_lock 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 +#include + +namespace icinga +{ + +/** + * A database connection. + * + * @ingroup db_ido + */ +class DbConnection : public ObjectImpl +{ +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& lvalue, const ValidationUtils& utils) final; + void ValidateCategories(const Lazy& 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&) = 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, String> m_ConfigHashes; + std::map m_ObjectIDs; + std::map, DbReference> m_InsertIDs; + std::set m_ActiveObjects; + std::set m_ConfigUpdates; + std::set 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 m_PendingQueries{0}; +}; + +struct database_error : virtual std::exception, virtual boost::exception { }; + +struct errinfo_database_query_; +typedef boost::error_info 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 +#include + +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&, 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 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& 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& 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 now_bag = ConvertTimestamp(Utility::GetTime()); + std::pair 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 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 comments = checkable->GetComments(); + + std::vector queries; + + for (const Comment::Ptr& comment : comments) { + AddCommentInternal(queries, comment, false); + } + + DbObject::OnMultipleQueries(queries); +} + +void DbEvents::AddComment(const Comment::Ptr& comment) +{ + std::vector queries; + AddCommentInternal(queries, comment, false); + DbObject::OnMultipleQueries(queries); +} + +void DbEvents::AddCommentHistory(const Comment::Ptr& comment) +{ + std::vector queries; + AddCommentInternal(queries, comment, true); + DbObject::OnMultipleQueries(queries); +} + +void DbEvents::AddCommentInternal(std::vector& queries, const Comment::Ptr& comment, bool historical) +{ + Checkable::Ptr checkable = comment->GetCheckable(); + + std::pair 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 queries; + RemoveCommentInternal(queries, comment); + DbObject::OnMultipleQueries(queries); +} + +void DbEvents::RemoveCommentInternal(std::vector& queries, const Comment::Ptr& comment) +{ + Checkable::Ptr checkable = comment->GetCheckable(); + + std::pair 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 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 downtimes = checkable->GetDowntimes(); + + std::vector queries; + + for (const Downtime::Ptr& downtime : downtimes) { + AddDowntimeInternal(queries, downtime, false); + } + + DbObject::OnMultipleQueries(queries); +} + +void DbEvents::AddDowntime(const Downtime::Ptr& downtime) +{ + std::vector queries; + AddDowntimeInternal(queries, downtime, false); + DbObject::OnMultipleQueries(queries); +} + +void DbEvents::AddDowntimeHistory(const Downtime::Ptr& downtime) +{ + std::vector queries; + AddDowntimeInternal(queries, downtime, true); + DbObject::OnMultipleQueries(queries); +} + +void DbEvents::AddDowntimeInternal(std::vector& 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 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 queries; + RemoveDowntimeInternal(queries, downtime); + DbObject::OnMultipleQueries(queries); +} + +void DbEvents::RemoveDowntimeInternal(std::vector& 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 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 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 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& 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 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(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 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 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 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 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 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 timeBagStart = ConvertTimestamp(start); + std::pair 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 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& 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 DbEvents::ConvertTimestamp(double time) +{ + unsigned long time_sec = static_cast(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 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& 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& arguments); + +private: + DbEvents(); + + static void AddCommentInternal(std::vector& queries, const Comment::Ptr& comment, bool historical); + static void RemoveCommentInternal(std::vector& queries, const Comment::Ptr& comment); + static void AddDowntimeInternal(std::vector& queries, const Downtime::Ptr& downtime, bool historical); + static void RemoveDowntimeInternal(std::vector& 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 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 DbObject::OnQuery; +boost::signals2::signal&)> DbObject::OnMultipleQueries; +boost::signals2::signal&)> DbObject::OnMakeQueries; + +INITIALIZE_ONCE(&DbObject::StaticInitialize); + +DbObject::DbObject(intrusive_ptr 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::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(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(obj); + + if (!custom_var_object) + return; + + std::vector 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() || kv.second.IsObjectType()) { + 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(obj); + + if (!custom_var_object) + return; + + Dictionary::Ptr vars = custom_var_object->GetVars(); + + if (vars) { + std::vector 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() || kv.second.IsObjectType()) { + 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 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(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(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 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 Query; + std::function&)> MultipleQueries; + }; + + static boost::signals2::signal OnQuery; + static boost::signals2::signal&)> OnMultipleQueries; + static boost::signals2::signal&)> 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 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 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..01196a5 --- /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 DbQuery::m_CategoryFilterMap; + +void DbQuery::StaticInitialize() +{ + ScriptGlobal::Set("Icinga.DbCatConfig", DbCatConfig, true); + ScriptGlobal::Set("Icinga.DbCatState", DbCatState, true); + ScriptGlobal::Set("Icinga.DbCatAcknowledgement", DbCatAcknowledgement, true); + ScriptGlobal::Set("Icinga.DbCatComment", DbCatComment, true); + ScriptGlobal::Set("Icinga.DbCatDowntime", DbCatDowntime, true); + ScriptGlobal::Set("Icinga.DbCatEventHandler", DbCatEventHandler, true); + ScriptGlobal::Set("Icinga.DbCatExternalCommand", DbCatExternalCommand, true); + ScriptGlobal::Set("Icinga.DbCatFlapping", DbCatFlapping, true); + ScriptGlobal::Set("Icinga.DbCatCheck", DbCatCheck, true); + ScriptGlobal::Set("Icinga.DbCatLog", DbCatLog, true); + ScriptGlobal::Set("Icinga.DbCatNotification", DbCatNotification, true); + ScriptGlobal::Set("Icinga.DbCatProgramStatus", DbCatProgramStatus, true); + ScriptGlobal::Set("Icinga.DbCatRetention", DbCatRetention, true); + ScriptGlobal::Set("Icinga.DbCatStateHistory", DbCatStateHistory, true); + + ScriptGlobal::Set("Icinga.DbCatEverything", DbCatEverything, true); + + 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& 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 Object; + DbValue::Ptr NotificationInsertID; + bool ConfigUpdate{false}; + bool StatusUpdate{false}; + WorkQueuePriority Priority{PriorityNormal}; + + static void StaticInitialize(); + + static const std::map& GetCategoryFilterMap(); + +private: + static std::map 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 + +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 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 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 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::GetAllTypes() +{ + std::set result; + + { + std::unique_lock lock(GetStaticMutex()); + for (const auto& kv : GetTypes()) { + result.insert(kv.second); + } + } + + return result; +} + +DbTypeRegistry *DbTypeRegistry::GetInstance() +{ + return Singleton::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 + +namespace icinga +{ + +class DbObject; + +/** + * A database object type. + * + * @ingroup ido + */ +class DbType final : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(DbType); + + typedef std::function (const intrusive_ptr&, const String&, const String&)> ObjectFactory; + typedef std::map TypeMap; + typedef std::map, intrusive_ptr > 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 GetOrCreateObjectByName(const String& name1, const String& name2); + + static std::set 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 +{ +public: + static DbTypeRegistry *GetInstance(); +}; + +/** + * Factory function for DbObject-based classes. + * + * @ingroup ido + */ +template +intrusive_ptr 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); \ + 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()) + return false; + + DbValue::Ptr dbv = value; + return dbv->GetType() == DbValueTimestamp; +} + +bool DbValue::IsObjectInsertID(const Value& value) +{ + if (!value.IsObjectType()) + return false; + + DbValue::Ptr dbv = value; + return dbv->GetType() == DbValueObjectInsertID; +} + +Value DbValue::ExtractValue(const Value& value) +{ + if (!value.IsObjectType()) + 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(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(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& 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(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(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(GetObject()); + + /* groups */ + Array::Ptr groups = host->GetGroups(); + + std::vector 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(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(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(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(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(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..6a7f0d3 --- /dev/null +++ b/lib/db_ido/idochecktask.cpp @@ -0,0 +1,198 @@ +/* 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 + +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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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(type.get()); + VERIFY(dtype); + + DbConnection::Ptr conn = static_pointer_cast(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 + +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(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(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(GetObject()); + + /* groups */ + Array::Ptr groups = service->GetGroups(); + + std::vector 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(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(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(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(GetObject()); + + return new Dictionary({ + { "alias", tp->GetDisplayName() } + }); +} + +Dictionary::Ptr TimePeriodDbObject::GetStatusFields() const +{ + return Empty; +} + +void TimePeriodDbObject::OnConfigUpdateHeavy() +{ + TimePeriod::Ptr tp = static_pointer_cast(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(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(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(GetObject()); + + /* groups */ + Array::Ptr groups = user->GetGroups(); + + std::vector 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(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(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(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(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& 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..42c8332 --- /dev/null +++ b/lib/db_ido_mysql/idomysqlconnection.cpp @@ -0,0 +1,1268 @@ +/* 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 + +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::OnConfigLoaded(); + + m_QueryQueue.SetName("IdoMysqlConnection, " + GetName()); + + Library shimLibrary{"mysql_shim"}; + + auto create_mysql_shim = shimLibrary.GetSymbolAddress("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()) { + 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 = new Timer(); + m_TxTimer->SetInterval(1); + m_TxTimer->OnTimerExpired.connect([this](const Timer * const&) { NewTransaction(); }); + m_TxTimer->Start(); + + m_ReconnectTimer = new Timer(); + 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.reset(); + +#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(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(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(m_InstanceID); + result = Query(q1buf.str()); + + std::vector 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(m_InstanceID)) + " AND session_token <> " + + Convert::ToString(GetSessionToken())); +} + +void IdoMysqlConnection::AsyncQuery(const String& query, const std::function& 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 queries; + m_AsyncQueries.swap(queries); + + std::vector::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::size_type count = 0; + size_t num_bytes = 0; + + Defer decreaseQueries ([this, &offset, &count]() { + offset += count; + DecreasePendingQueries(count); + m_UncommittedAsyncQueries += count; + }); + + for (std::vector::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::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(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(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(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(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(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(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()) { + 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(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(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& 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& 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(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(m_InstanceID)) + " AND " + time_column + + " < FROM_UNIXTIME(" + Convert::ToString(static_cast(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 + +namespace icinga +{ + +typedef std::shared_ptr IdoMysqlResult; + +typedef std::function IdoAsyncCallback; + +struct IdoAsyncQuery +{ + String Query; + IdoAsyncCallback Callback; +}; + +/** + * An IDO MySQL database connection. + * + * @ingroup ido + */ +class IdoMysqlConnection final : public ObjectImpl +{ +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& 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 m_Mysql; + + MYSQL m_Connection; + int m_AffectedRows; + unsigned int m_MaxPacketSize; + + std::vector 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& 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..9a20b95 --- /dev/null +++ b/lib/db_ido_pgsql/idopgsqlconnection.cpp @@ -0,0 +1,1028 @@ +/* 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 + +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::OnConfigLoaded(); + + m_QueryQueue.SetName("IdoPgsqlConnection, " + GetName()); + + Library shimLibrary{"pgsql_shim"}; + + auto create_pgsql_shim = shimLibrary.GetSymbolAddress("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()) { + 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 = new Timer(); + m_TxTimer->SetInterval(1); + m_TxTimer->OnTimerExpired.connect([this](const Timer * const&) { NewTransaction(); }); + m_TxTimer->Start(); + + m_ReconnectTimer = new Timer(); + 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.reset(); + + 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(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(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(m_InstanceID); + IncreasePendingQueries(1); + result = Query(q1buf.str()); + + std::vector 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(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(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(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(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(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(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()) { + 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(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(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& 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& 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(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(m_InstanceID)) + " AND " + time_column + + " < TO_TIMESTAMP(" + Convert::ToString(static_cast(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 IdoPgsqlResult; + +/** + * An IDO pgSQL database connection. + * + * @ingroup ido + */ +class IdoPgsqlConnection final : public ObjectImpl +{ +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& 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 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& 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..7079d84 --- /dev/null +++ b/lib/icinga/CMakeLists.txt @@ -0,0 +1,75 @@ +# 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 + 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..bda0e33 --- /dev/null +++ b/lib/icinga/apiactions.cpp @@ -0,0 +1,898 @@ +/* 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/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 + +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) +{ + Checkable::Ptr checkable = static_pointer_cast(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")); + + checkable->ProcessCheckResult(cr); + + return ApiActions::CreateResult(200, "Successfully processed check result for object '" + checkable->GetName() + "'."); +} + +Dictionary::Ptr ApiActions::RescheduleCheck(const ConfigObject::Ptr& object, + const Dictionary::Ptr& params) +{ + Checkable::Ptr checkable = static_pointer_cast(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(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(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(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."); + } + + 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(object); + + if (!checkable) + return ApiActions::CreateResult(404, + "Cannot remove acknowledgement for non-existent checkable object " + + object->GetName() + "."); + + String removedBy (HttpUtility::GetLastParameter(params, "author")); + + checkable->ClearAcknowledgement(removedBy); + checkable->RemoveCommentsByType(CommentAcknowledgement, 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(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"); + } + + 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) +{ + auto author (HttpUtility::GetLastParameter(params, "author")); + Checkable::Ptr checkable = dynamic_pointer_cast(object); + + if (checkable) { + std::set 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(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(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."); + } + } + + 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 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(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) +{ + auto author (HttpUtility::GetLastParameter(params, "author")); + Checkable::Ptr checkable = dynamic_pointer_cast(object); + + size_t childCount = 0; + + if (checkable) { + std::set 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(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 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(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()) { + 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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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 + "'."); + + /* 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); + 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()) { + /* Fetch immediate child zone members */ + if (zone->GetParent() == localZone && zone->CanAccessObject(endpointPtr->GetZone())) { + std::set 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 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 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(service->GetState()) : static_cast(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& users, NotificationType type, + const CheckResult::Ptr& cr, const String& author, const String& text, const MessageOrigin::Ptr& origin) +{ + std::vector 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 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(service->GetState()) : static_cast(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 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(service->GetState()) : static_cast(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 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(service->GetState()) : static_cast(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 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 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 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 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 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 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 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& 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..a302bd9 --- /dev/null +++ b/lib/icinga/checkable-check.cpp @@ -0,0 +1,705 @@ +/* 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 Checkable::OnNewCheckResult; +boost::signals2::signal Checkable::OnStateChange; +boost::signals2::signal, const MessageOrigin::Ptr&)> Checkable::OnReachabilityChanged; +boost::signals2::signal Checkable::OnNotificationsRequested; +boost::signals2::signal Checkable::OnNextCheckUpdated; + +Atomic 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(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; +} + +void Checkable::ProcessCheckResult(const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin) +{ + { + ObjectLock olock(this); + m_CheckRunning = false; + } + + if (!cr) + return; + + 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; + + } + + if (!IsActive()) + return; + + 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; + } + } + } + + /* 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 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) + RemoveCommentsByType(CommentAcknowledgement); + + 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); +} + +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 lock(m_StatsMutex); + m_PendingChecks++; +} + +void Checkable::DecreasePendingChecks() +{ + std::unique_lock lock(m_StatsMutex); + m_PendingChecks--; + m_PendingChecksCV.notify_one(); +} + +int Checkable::GetPendingChecks() +{ + std::unique_lock lock(m_StatsMutex); + return m_PendingChecks; +} + +void Checkable::AquirePendingCheckSlot(int maxPendingChecks) +{ + std::unique_lock 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..bb26a6d --- /dev/null +++ b/lib/icinga/checkable-comment.cpp @@ -0,0 +1,70 @@ +/* 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 + +using namespace icinga; + + +void Checkable::RemoveAllComments() +{ + for (const Comment::Ptr& comment : GetComments()) { + Comment::RemoveComment(comment->GetName()); + } +} + +void Checkable::RemoveCommentsByType(int type, const String& removedBy) +{ + for (const Comment::Ptr& comment : GetComments()) { + /* Do not remove persistent comments from an acknowledgement */ + if (comment->GetEntryType() == CommentAcknowledgement && comment->GetPersistent()) + continue; + + if (comment->GetEntryType() == type) { + { + ObjectLock oLock (comment); + comment->SetRemovedBy(removedBy); + } + + Comment::RemoveComment(comment->GetName()); + } + } +} + +std::set Checkable::GetComments() const +{ + std::unique_lock lock(m_CommentMutex); + return m_Comments; +} + +Comment::Ptr Checkable::GetLastComment() const +{ + std::unique_lock lock (m_CommentMutex); + Comment::Ptr lastComment; + + for (auto& comment : m_Comments) { + if (!lastComment || comment->GetEntryTime() > lastComment->GetEntryTime()) { + lastComment = comment; + } + } + + return std::move(lastComment); +} + +void Checkable::RegisterComment(const Comment::Ptr& comment) +{ + std::unique_lock lock(m_CommentMutex); + m_Comments.insert(comment); +} + +void Checkable::UnregisterComment(const Comment::Ptr& comment) +{ + std::unique_lock 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..5ce9288 --- /dev/null +++ b/lib/icinga/checkable-dependency.cpp @@ -0,0 +1,158 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "icinga/service.hpp" +#include "icinga/dependency.hpp" +#include "base/logger.hpp" + +using namespace icinga; + +void Checkable::AddDependency(const Dependency::Ptr& dep) +{ + std::unique_lock lock(m_DependencyMutex); + m_Dependencies.insert(dep); +} + +void Checkable::RemoveDependency(const Dependency::Ptr& dep) +{ + std::unique_lock lock(m_DependencyMutex); + m_Dependencies.erase(dep); +} + +std::vector Checkable::GetDependencies() const +{ + std::unique_lock lock(m_DependencyMutex); + return std::vector(m_Dependencies.begin(), m_Dependencies.end()); +} + +void Checkable::AddReverseDependency(const Dependency::Ptr& dep) +{ + std::unique_lock lock(m_DependencyMutex); + m_ReverseDependencies.insert(dep); +} + +void Checkable::RemoveReverseDependency(const Dependency::Ptr& dep) +{ + std::unique_lock lock(m_DependencyMutex); + m_ReverseDependencies.erase(dep); +} + +std::vector Checkable::GetReverseDependencies() const +{ + std::unique_lock lock(m_DependencyMutex); + return std::vector(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(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(); + + int countDeps = deps.size(); + int countFailed = 0; + + for (const Dependency::Ptr& dep : deps) { + if (!dep->IsAvailable(dt)) { + countFailed++; + + if (failedDependency) + *failedDependency = dep; + } + } + + /* If there are dependencies, and all of them failed, mark as unreachable. */ + if (countDeps > 0 && countFailed == countDeps) { + Log(LogDebug, "Checkable") + << "All dependencies have failed for checkable '" << GetName() << "': Marking as unreachable."; + + return false; + } + if (failedDependency) + *failedDependency = nullptr; + + return true; +} + +std::set Checkable::GetParents() const +{ + std::set 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::GetChildren() const +{ + std::set 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::GetAllChildren() const +{ + std::set children = GetChildren(); + + GetAllChildrenInternal(children, 0); + + return children; +} + +void Checkable::GetAllChildrenInternal(std::set& children, int level) const +{ + if (level > 32) + return; + + std::set localChildren; + + for (const Checkable::Ptr& checkable : children) { + std::set 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 Checkable::GetDowntimes() const +{ + std::unique_lock lock(m_DowntimeMutex); + return m_Downtimes; +} + +void Checkable::RegisterDowntime(const Downtime::Ptr& downtime) +{ + std::unique_lock lock(m_DowntimeMutex); + m_Downtimes.insert(downtime); +} + +void Checkable::UnregisterDowntime(const Downtime::Ptr& downtime) +{ + std::unique_lock 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..2983be8 --- /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 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 +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 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..779ed4b --- /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&, + const NotificationType&, const CheckResult::Ptr&, const String&, const String&, + const MessageOrigin::Ptr&)> Checkable::OnNotificationSentToAllUsers; +boost::signals2::signal 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 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 Checkable::GetNotifications() const +{ + std::unique_lock lock(m_NotificationMutex); + return m_Notifications; +} + +void Checkable::RegisterNotification(const Notification::Ptr& notification) +{ + std::unique_lock lock(m_NotificationMutex); + m_Notifications.insert(notification); +} + +void Checkable::UnregisterNotification(const Notification::Ptr& notification) +{ + std::unique_lock 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 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->FireSuppressedNotifications(); + } + + for (auto& service : ConfigType::GetObjectsByType()) { + 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..b212389 --- /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 + +using namespace icinga; + +REGISTER_TYPE_WITH_PROTOTYPE(Checkable, Checkable::GetPrototype()); +INITIALIZE_ONCE(&Checkable::StaticInitialize); + +const std::map Checkable::m_FlappingStateFilterMap ({ + {"OK", FlappingStateFilterOk}, + {"Warning", FlappingStateFilterWarning}, + {"Critical", FlappingStateFilterCritical}, + {"Unknown", FlappingStateFilterUnknown}, + {"Up", FlappingStateFilterOk}, + {"Down", FlappingStateFilterCritical}, +}); + +boost::signals2::signal Checkable::OnAcknowledgementSet; +boost::signals2::signal Checkable::OnAcknowledgementCleared; +boost::signals2::signal Checkable::OnFlappingChange; + +static Timer::Ptr l_CheckablesFireSuppressedNotifications; +static Timer::Ptr l_CleanDeadlinedExecutions; + +thread_local std::function 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::OnConfigLoaded(); + + SetFlappingIgnoreStatesFilter(FilterArrayToInt(GetFlappingIgnoreStates(), m_FlappingStateFilterMap, ~0)); +} + +void Checkable::OnAllConfigLoaded() +{ + ObjectImpl::OnAllConfigLoaded(); + + Endpoint::Ptr endpoint = GetCommandEndpoint(); + + if (endpoint) { + Zone::Ptr checkableZone = static_pointer_cast(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::Start(runtimeCreated); + + static boost::once_flag once = BOOST_ONCE_INIT; + + boost::call_once(once, []() { + l_CheckablesFireSuppressedNotifications = new Timer(); + l_CheckablesFireSuppressedNotifications->SetInterval(5); + l_CheckablesFireSuppressedNotifications->OnTimerExpired.connect(&Checkable::FireSuppressedNotificationsTimer); + l_CheckablesFireSuppressedNotifications->Start(); + + l_CleanDeadlinedExecutions = new Timer(); + l_CleanDeadlinedExecutions->SetInterval(300); + l_CleanDeadlinedExecutions->OnTimerExpired.connect(&Checkable::CleanDeadlinedExecutions); + l_CleanDeadlinedExecutions->Start(); + }); +} + +void Checkable::AddGroup(const String& name) +{ + std::unique_lock lock(m_CheckableMutex); + + Array::Ptr groups; + auto *host = dynamic_cast(this); + + if (host) + groups = host->GetGroups(); + else + groups = static_cast(this)->GetGroups(); + + if (groups && groups->Contains(name)) + return; + + if (!groups) + groups = new Array(); + + groups->Add(name); +} + +AcknowledgementType Checkable::GetAcknowledgement() +{ + auto avalue = static_cast(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(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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::ValidateCheckInterval(lvalue, utils); + + if (lvalue() <= 0) + BOOST_THROW_EXCEPTION(ValidationError(this, { "check_interval" }, "Interval must be greater than 0.")); +} + +void Checkable::ValidateRetryInterval(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::ValidateRetryInterval(lvalue, utils); + + if (lvalue() <= 0) + BOOST_THROW_EXCEPTION(ValidationError(this, { "retry_interval" }, "Interval must be greater than 0.")); +} + +void Checkable::ValidateMaxCheckAttempts(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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()) { + 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()) { + 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..bb8e567 --- /dev/null +++ b/lib/icinga/checkable.hpp @@ -0,0 +1,256 @@ +/* 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 +#include +#include + +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 +{ +public: + DECLARE_OBJECT(Checkable); + DECLARE_OBJECTNAME(Checkable); + + static void StaticInitialize(); + static thread_local std::function ExecuteCommandProcessFinishedHandler; + + Checkable(); + + std::set GetParents() const; + std::set GetChildren() const; + std::set GetAllChildren() const; + + void AddGroup(const String& name); + + bool IsReachable(DependencyType dt = DependencyState, intrusive_ptr *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 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(); + void ProcessCheckResult(const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin = nullptr); + + Endpoint::Ptr GetCommandEndpoint() const; + + static boost::signals2::signal OnNewCheckResult; + static boost::signals2::signal OnStateChange; + static boost::signals2::signal, const MessageOrigin::Ptr&)> OnReachabilityChanged; + static boost::signals2::signal OnNotificationsRequested; + static boost::signals2::signal OnNotificationSentToUser; + static boost::signals2::signal&, + const NotificationType&, const CheckResult::Ptr&, const String&, + const String&, const MessageOrigin::Ptr&)> OnNotificationSentToAllUsers; + static boost::signals2::signal OnAcknowledgementSet; + static boost::signals2::signal OnAcknowledgementCleared; + static boost::signals2::signal OnFlappingChange; + static boost::signals2::signal OnNextCheckUpdated; + static boost::signals2::signal OnEventCommandExecuted; + + static Atomic CurrentConcurrentChecks; + + /* Downtimes */ + int GetDowntimeDepth() const final; + + void RemoveAllDowntimes(); + void TriggerDowntimes(double triggerTime); + bool IsInDowntime() const; + bool IsAcknowledged() const; + + std::set GetDowntimes() const; + void RegisterDowntime(const Downtime::Ptr& downtime); + void UnregisterDowntime(const Downtime::Ptr& downtime); + + /* Comments */ + void RemoveAllComments(); + void RemoveCommentsByType(int type, const String& removedBy = String()); + + std::set 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 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 GetEventCommand() const; + + /* Flapping Detection */ + bool IsFlapping() const; + + /* Dependencies */ + void AddDependency(const intrusive_ptr& dep); + void RemoveDependency(const intrusive_ptr& dep); + std::vector > GetDependencies() const; + + void AddReverseDependency(const intrusive_ptr& dep); + void RemoveReverseDependency(const intrusive_ptr& dep); + std::vector > GetReverseDependencies() const; + + void ValidateCheckInterval(const Lazy& lvalue, const ValidationUtils& value) final; + void ValidateRetryInterval(const Lazy& lvalue, const ValidationUtils& value) final; + void ValidateMaxCheckAttempts(const Lazy& 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 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 m_Comments; + mutable std::mutex m_CommentMutex; + + /* Notifications */ + std::set m_Notifications; + mutable std::mutex m_NotificationMutex; + + /* Dependencies */ + mutable std::mutex m_DependencyMutex; + std::set > m_Dependencies; + std::set > m_ReverseDependencies; + + void GetAllChildrenInternal(std::set& children, int level = 0) const; + + /* Flapping */ + static const std::map 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..eb8f5a0 --- /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 +{ +public: + DECLARE_OBJECT(CheckCommand); + DECLARE_OBJECTNAME(CheckCommand); + + static thread_local CheckCommand::Ptr ExecuteOverride; + + virtual 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..cb445af --- /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, true); + ScriptGlobal::Set("Icinga.ServiceWarning", ServiceWarning, true); + ScriptGlobal::Set("Icinga.ServiceCritical", ServiceCritical, true); + ScriptGlobal::Set("Icinga.ServiceUnknown", ServiceUnknown, true); + + ScriptGlobal::Set("Icinga.HostUp", HostUp, true); + ScriptGlobal::Set("Icinga.HostDown", HostDown, true); +}) + +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 +{ +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..898b0e7 --- /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()) { + 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()) { + 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()) { + 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()) { + 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 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(kv.second->Get())->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 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..1f86d45 --- /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 +#include + +using namespace icinga; + +std::mutex ClusterEvents::m_Mutex; +std::deque> 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 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 = new Timer(); + m_LogTimer->SetInterval(10); + m_LogTimer->OnTimerExpired.connect([](const Timer * const&) { LogRemoteCheckQueueInformation(); }); + m_LogTimer->Start(); + }); + + std::unique_lock 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..e7fb9f3 --- /dev/null +++ b/lib/icinga/clusterevents.cpp @@ -0,0 +1,1436 @@ +/* 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 + +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(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); + 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()) { + 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::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(static_cast(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; + } + } + + if (params->Contains("endpoint")) { + Endpoint::Ptr execEndpoint = Endpoint::GetByName(params->Get("endpoint")); + + 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()) { + /* Fetch immediate child zone members */ + if (zone->GetParent() == localZone && zone->CanAccessObject(endpointZone)) { + std::set 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", params->Get("source")); + 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; + } + } + } + } + + 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(static_cast(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(static_cast(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& 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(static_cast(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 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 (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; + } + + 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; + } + + 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, Zone::GetLocalZone(), 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..4cdadac --- /dev/null +++ b/lib/icinga/clusterevents.hpp @@ -0,0 +1,96 @@ +/* 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 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& 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> 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::Validate(types, utils); + + Dictionary::Ptr arguments = GetArguments(); + + if (!(types & FAConfig)) + return; + + if (arguments) { + if (!GetCommandLine().IsObjectType()) + 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::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 +{ +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..aa3d5e5 --- /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 + +using namespace icinga; + +static int l_NextCommentID = 1; +static std::mutex l_CommentMutex; +static std::map l_LegacyCommentsCache; +static Timer::Ptr l_CommentsExpireTimer; + +boost::signals2::signal Comment::OnCommentAdded; +boost::signals2::signal Comment::OnCommentRemoved; +boost::signals2::signal Comment::OnRemovalInfoChanged; + +REGISTER_TYPE(Comment); + +String CommentNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const +{ + Comment::Ptr comment = dynamic_pointer_cast(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 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::Start(runtimeCreated); + + static boost::once_flag once = BOOST_ONCE_INIT; + + boost::call_once(once, [this]() { + l_CommentsExpireTimer = new Timer(); + l_CommentsExpireTimer->SetInterval(60); + l_CommentsExpireTimer->OnTimerExpired.connect([](const Timer * const&) { CommentsExpireTimerHandler(); }); + l_CommentsExpireTimer->Start(); + }); + + { + std::unique_lock 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::Stop(runtimeRemoved); +} + +Checkable::Ptr Comment::GetCheckable() const +{ + return static_pointer_cast(m_Checkable); +} + +bool Comment::IsExpired() const +{ + double expire_time = GetExpireTime(); + + return (expire_time != 0 && expire_time < Utility::GetTime()); +} + +int Comment::GetNextCommentID() +{ + std::unique_lock 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 lock(l_CommentMutex); + + auto it = l_LegacyCommentsCache.find(id); + + if (it == l_LegacyCommentsCache.end()) + return Empty; + + return it->second; +} + +void Comment::CommentsExpireTimerHandler() +{ + std::vector comments; + + for (const Comment::Ptr& comment : ConfigType::GetObjectsByType()) { + 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 +{ +public: + DECLARE_OBJECT(Comment); + DECLARE_OBJECTNAME(Comment); + + static boost::signals2::signal OnCommentAdded; + static boost::signals2::signal OnCommentRemoved; + static boost::signals2::signal OnRemovalInfoChanged; + + intrusive_ptr 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, 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::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..853c28f --- /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, protected, required, navigation(host)] name(Host) host_name { + navigate {{{ + return Host::GetByName(GetHostName()); + }}} + }; + [config, 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..40c01f3 --- /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 +#include + +using namespace icinga; + +/* Used in DB IDO, StatusDataWriter and Livestatus. */ +String CompatUtility::GetCommandLine(const Command::Ptr& command) +{ + Value commandLine = command->GetCommandLine(); + + String result; + if (commandLine.IsObjectType()) { + 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 = ""; + } + + 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, StatusDataWriter and Livestatus. */ +{ + if (!command) + return Empty; + + return GetCommandNamePrefix(command) + command->GetName(); +} + +/* Used in DB IDO, StatusDataWriter 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, StatusDataWriter 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(last_notification); +} + +/* Used in DB IDO, StatusDataWriter 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(next_notification); +} + +/* Used in DB IDO, StatusDataWriter 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, StatusDataWriter 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, StatusDataWriter and Livestatus. */ +std::set CompatUtility::GetCheckableNotificationUsers(const Checkable::Ptr& checkable) +{ + /* Service -> Notifications -> (Users + UserGroups -> Users) */ + std::set allUsers; + std::set 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 members = ug->GetMembers(); + std::copy(members.begin(), members.end(), std::inserter(allUsers, allUsers.begin())); + } + } + + return allUsers; +} + +/* Used in DB IDO, StatusDataWriter and Livestatus. */ +std::set CompatUtility::GetCheckableNotificationUserGroups(const Checkable::Ptr& checkable) +{ + std::set 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, StatusDataWriter, 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, StatusDataWriter and Livestatus, 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, StatusDataWriter and Livestatus. Used in StatusDataWriter. */ +String CompatUtility::EscapeString(const String& str) +{ + String result = str; + boost::algorithm::replace_all(result, "\n", "\\n"); + return result; +} + +/* Used in ExternalCommandListener and CheckResultReader. */ +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 GetCheckableNotificationUsers(const Checkable::Ptr& checkable); + static std::set 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& lvalue, const ValidationUtils& utils) +{ + MacroProcessor::ValidateCustomVars(this, lvalue()); +} + +int icinga::FilterArrayToInt(const Array::Ptr& typeFilters, const std::map& 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 +{ +public: + DECLARE_OBJECT(CustomVarObject); + + void ValidateVars(const Lazy& lvalue, const ValidationUtils& utils) final; +}; + +int FilterArrayToInt(const Array::Ptr& typeFilters, const std::map& 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..8f8840c --- /dev/null +++ b/lib/icinga/dependency-apply.cpp @@ -0,0 +1,163 @@ +/* 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; + + DebugInfo 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) +{ + DebugInfo di = rule.GetDebugInfo(); + + std::ostringstream msgbuf; + msgbuf << "Evaluating 'apply' rule (" << di << ")"; + CONTEXT(msgbuf.str()); + + 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()) { + 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()) { + 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..7dd90f5 --- /dev/null +++ b/lib/icinga/dependency.cpp @@ -0,0 +1,213 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "icinga/dependency.hpp" +#include "icinga/dependency-ti.cpp" +#include "icinga/service.hpp" +#include "base/logger.hpp" +#include "base/exception.hpp" + +using namespace icinga; + +REGISTER_TYPE(Dependency); + +String DependencyNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const +{ + Dependency::Ptr dependency = dynamic_pointer_cast(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 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::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); +} + +void Dependency::Stop(bool runtimeRemoved) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 parent) +{ + m_Parent = parent; +} + +void Dependency::SetChild(intrusive_ptr child) +{ + m_Child = child; +} + diff --git a/lib/icinga/dependency.hpp b/lib/icinga/dependency.hpp new file mode 100644 index 0000000..75424cb --- /dev/null +++ b/lib/icinga/dependency.hpp @@ -0,0 +1,59 @@ +/* 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 +{ +public: + DECLARE_OBJECT(Dependency); + DECLARE_OBJECTNAME(Dependency); + + intrusive_ptr GetParent() const; + intrusive_ptr GetChild() const; + + TimePeriod::Ptr GetPeriod() const; + + bool IsAvailable(DependencyType dt) const; + + void ValidateStates(const Lazy& lvalue, const ValidationUtils& utils) override; + + static void EvaluateApplyRules(const intrusive_ptr& host); + static void EvaluateApplyRules(const intrusive_ptr& service); + + /* Note: Only use them for unit test mocks. Prefer OnConfigLoaded(). */ + void SetParent(intrusive_ptr parent); + void SetChild(intrusive_ptr child); + +protected: + void OnConfigLoaded() override; + void OnAllConfigLoaded() override; + void Stop(bool runtimeRemoved) override; + +private: + Checkable::Ptr m_Parent; + Checkable::Ptr m_Child; + + 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..10e2aae --- /dev/null +++ b/lib/icinga/dependency.ti @@ -0,0 +1,99 @@ +/* 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, required, navigation(child_host)] name(Host) child_host_name { + navigate {{{ + return Host::GetByName(GetChildHostName()); + }}} + }; + + [config, 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, required, navigation(parent_host)] name(Host) parent_host_name { + navigate {{{ + return Host::GetByName(GetParentHostName()); + }}} + }; + + [config, 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, 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..f19698e --- /dev/null +++ b/lib/icinga/downtime.cpp @@ -0,0 +1,546 @@ +/* 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 +#include + +using namespace icinga; + +static int l_NextDowntimeID = 1; +static std::mutex l_DowntimeMutex; +static std::map l_LegacyDowntimesCache; +static Timer::Ptr l_DowntimesExpireTimer; +static Timer::Ptr l_DowntimesStartTimer; + +boost::signals2::signal Downtime::OnDowntimeAdded; +boost::signals2::signal Downtime::OnDowntimeRemoved; +boost::signals2::signal Downtime::OnDowntimeStarted; +boost::signals2::signal Downtime::OnDowntimeTriggered; +boost::signals2::signal Downtime::OnRemovalInfoChanged; + +REGISTER_TYPE(Downtime); + +INITIALIZE_ONCE(&Downtime::StaticInitialize); + +void Downtime::StaticInitialize() +{ + ScriptGlobal::Set("Icinga.DowntimeNoChildren", "DowntimeNoChildren", true); + ScriptGlobal::Set("Icinga.DowntimeTriggeredChildren", "DowntimeTriggeredChildren", true); + ScriptGlobal::Set("Icinga.DowntimeNonTriggeredChildren", "DowntimeNonTriggeredChildren", true); +} + +String DowntimeNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const +{ + Downtime::Ptr downtime = dynamic_pointer_cast(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 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::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::Start(runtimeCreated); + + static boost::once_flag once = BOOST_ONCE_INIT; + + boost::call_once(once, [this]() { + l_DowntimesStartTimer = new Timer(); + l_DowntimesStartTimer->SetInterval(5); + l_DowntimesStartTimer->OnTimerExpired.connect([](const Timer * const&){ DowntimesStartTimerHandler(); }); + l_DowntimesStartTimer->Start(); + + l_DowntimesExpireTimer = new Timer(); + l_DowntimesExpireTimer->SetInterval(60); + l_DowntimesExpireTimer->OnTimerExpired.connect([](const Timer * const&) { DowntimesExpireTimerHandler(); }); + l_DowntimesExpireTimer->Start(); + }); + + { + std::unique_lock 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(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::Stop(runtimeRemoved); +} + +Checkable::Ptr Downtime::GetCheckable() const +{ + return static_pointer_cast(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(configOwner); +} + +int Downtime::GetNextDowntimeID() +{ + std::unique_lock 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 = ""; + } + + 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 lock(m_ChildrenMutex); + m_Children.insert(downtime); +} + +void Downtime::UnregisterChild(const Downtime::Ptr& downtime) +{ + std::unique_lock lock(m_ChildrenMutex); + m_Children.erase(downtime); +} + +std::set Downtime::GetChildren() const +{ + std::unique_lock 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::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); + } + + 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 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()) { + 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::DowntimesExpireTimerHandler() +{ + std::vector downtimes; + + for (const Downtime::Ptr& downtime : ConfigType::GetObjectsByType()) { + downtimes.push_back(downtime); + } + + for (const Downtime::Ptr& downtime : downtimes) { + /* Only remove downtimes which are activated after daemon start. */ + if (downtime->IsActive() && (downtime->IsExpired() || !downtime->HasValidConfigOwner())) + RemoveDowntime(downtime->GetName(), false, false, true); + } +} + +void Downtime::ValidateStartTime(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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(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..5f0c493 --- /dev/null +++ b/lib/icinga/downtime.hpp @@ -0,0 +1,92 @@ +/* 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 +{ +public: + DECLARE_OBJECT(Downtime); + DECLARE_OBJECTNAME(Downtime); + + static boost::signals2::signal OnDowntimeAdded; + static boost::signals2::signal OnDowntimeRemoved; + static boost::signals2::signal OnDowntimeStarted; + static boost::signals2::signal OnDowntimeTriggered; + static boost::signals2::signal OnRemovalInfoChanged; + + intrusive_ptr 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, 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 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 ValidateStartTime(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateEndTime(const Lazy& lvalue, const ValidationUtils& utils) override; + +private: + ObjectImpl::Ptr m_Checkable; + + std::set m_Children; + mutable std::mutex m_ChildrenMutex; + + bool CanBeTriggered(); + + static void DowntimesStartTimerHandler(); + static void DowntimesExpireTimerHandler(); +}; + +} + +#endif /* DOWNTIME_H */ diff --git a/lib/icinga/downtime.ti b/lib/icinga/downtime.ti new file mode 100644 index 0000000..9fe1823 --- /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, required, navigation(host)] name(Host) host_name { + navigate {{{ + return Host::GetByName(GetHostName()); + }}} + }; + [config, 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/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..064cb5a --- /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 +{ +public: + DECLARE_OBJECT(EventCommand); + DECLARE_OBJECTNAME(EventCommand); + + static thread_local EventCommand::Ptr ExecuteOverride; + + virtual 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..f781a34 --- /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 +#include + +using namespace icinga; + +boost::signals2::signal&)> 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 argv = args.Split(";"); + + if (argv.empty()) + BOOST_THROW_EXCEPTION(std::invalid_argument("Missing arguments in command: " + line)); + + std::vector argvExtra(argv.begin() + 1, argv.end()); + Execute(ts, argv[0], argvExtra); +} + +void ExternalCommandProcessor::Execute(double time, const String& command, const std::vector& arguments) +{ + ExternalCommandInfo eci; + + static boost::once_flag once = BOOST_ONCE_INIT; + + boost::call_once(once, []() { + RegisterCommands(); + }); + + { + std::unique_lock 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 realArguments; + realArguments.resize(argnum); + + if (argnum > 0) { + std::copy(arguments.begin(), arguments.begin() + argnum - 1, realArguments.begin()); + + String last_argument; + for (std::vector::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 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 >& 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 argv = args.Split(";"); + + if (argv.empty()) + BOOST_THROW_EXCEPTION(std::invalid_argument("Missing arguments in command: " + line)); + + std::vector 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& 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 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& 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 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& 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& 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& 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& 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& 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& 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& 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& 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&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Shutting down Icinga via external command."); + Application::RequestShutdown(); +} + +void ExternalCommandProcessor::RestartProcess(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Restarting Icinga via external command."); + Application::RequestRestart(); +} + +void ExternalCommandProcessor::ScheduleForcedHostSvcChecks(double, const std::vector& 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& 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& 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& 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& 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& 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& 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->RemoveCommentsByType(CommentAcknowledgement); +} + +void ExternalCommandProcessor::AcknowledgeHostProblem(double, const std::vector& 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& 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& 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->RemoveCommentsByType(CommentAcknowledgement); +} + +void ExternalCommandProcessor::EnableHostgroupSvcChecks(double, const std::vector& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& arguments) +{ + std::deque< std::vector > file_queue; + file_queue.push_back(arguments); + + while (!file_queue.empty()) { + std::vector 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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 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& 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 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally enabling notifications."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_notifications", true); +} + +void ExternalCommandProcessor::DisableNotifications(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally disabling notifications."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_notifications", false); +} + +void ExternalCommandProcessor::EnableFlapDetection(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally enabling flap detection."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_flapping", true); +} + +void ExternalCommandProcessor::DisableFlapDetection(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally disabling flap detection."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_flapping", false); +} + +void ExternalCommandProcessor::EnableEventHandlers(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally enabling event handlers."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_event_handlers", true); +} + +void ExternalCommandProcessor::DisableEventHandlers(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally disabling event handlers."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_event_handlers", false); +} + +void ExternalCommandProcessor::EnablePerformanceData(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally enabling performance data processing."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_perfdata", true); +} + +void ExternalCommandProcessor::DisablePerformanceData(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally disabling performance data processing."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_perfdata", false); +} + +void ExternalCommandProcessor::StartExecutingSvcChecks(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally enabling service checks."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_service_checks", true); +} + +void ExternalCommandProcessor::StopExecutingSvcChecks(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally disabling service checks."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_service_checks", false); +} + +void ExternalCommandProcessor::StartExecutingHostChecks(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally enabling host checks."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_host_checks", true); +} + +void ExternalCommandProcessor::StopExecutingHostChecks(double, const std::vector&) +{ + Log(LogNotice, "ExternalCommandProcessor", "Globally disabling host checks."); + + IcingaApplication::GetInstance()->ModifyAttribute("enable_host_checks", false); +} + +void ExternalCommandProcessor::ChangeNormalSvcCheckInterval(double, const std::vector& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& 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& ExternalCommandProcessor::GetCommands() +{ + static std::map 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 +#include + +namespace icinga +{ + +typedef std::function& 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& arguments); + + static boost::signals2::signal&)> OnNewExternalCommand; + +private: + ExternalCommandProcessor(); + + static void ExecuteFromFile(const String& line, std::deque< std::vector >& file_queue); + + static void ProcessHostCheckResult(double time, const std::vector& arguments); + static void ProcessServiceCheckResult(double time, const std::vector& arguments); + static void ScheduleHostCheck(double time, const std::vector& arguments); + static void ScheduleForcedHostCheck(double time, const std::vector& arguments); + static void ScheduleSvcCheck(double time, const std::vector& arguments); + static void ScheduleForcedSvcCheck(double time, const std::vector& arguments); + static void EnableHostCheck(double time, const std::vector& arguments); + static void DisableHostCheck(double time, const std::vector& arguments); + static void EnableSvcCheck(double time, const std::vector& arguments); + static void DisableSvcCheck(double time, const std::vector& arguments); + static void ShutdownProcess(double time, const std::vector& arguments); + static void RestartProcess(double time, const std::vector& arguments); + static void ScheduleForcedHostSvcChecks(double time, const std::vector& arguments); + static void ScheduleHostSvcChecks(double time, const std::vector& arguments); + static void EnableHostSvcChecks(double time, const std::vector& arguments); + static void DisableHostSvcChecks(double time, const std::vector& arguments); + static void AcknowledgeSvcProblem(double time, const std::vector& arguments); + static void AcknowledgeSvcProblemExpire(double time, const std::vector& arguments); + static void RemoveSvcAcknowledgement(double time, const std::vector& arguments); + static void AcknowledgeHostProblem(double time, const std::vector& arguments); + static void AcknowledgeHostProblemExpire(double time, const std::vector& arguments); + static void RemoveHostAcknowledgement(double time, const std::vector& arguments); + static void EnableHostgroupSvcChecks(double time, const std::vector& arguments); + static void DisableHostgroupSvcChecks(double time, const std::vector& arguments); + static void EnableServicegroupSvcChecks(double time, const std::vector& arguments); + static void DisableServicegroupSvcChecks(double time, const std::vector& arguments); + static void EnablePassiveHostChecks(double time, const std::vector& arguments); + static void DisablePassiveHostChecks(double time, const std::vector& arguments); + static void EnablePassiveSvcChecks(double time, const std::vector& arguments); + static void DisablePassiveSvcChecks(double time, const std::vector& arguments); + static void EnableServicegroupPassiveSvcChecks(double time, const std::vector& arguments); + static void DisableServicegroupPassiveSvcChecks(double time, const std::vector& arguments); + static void EnableHostgroupPassiveSvcChecks(double time, const std::vector& arguments); + static void DisableHostgroupPassiveSvcChecks(double time, const std::vector& arguments); + static void ProcessFile(double time, const std::vector& arguments); + static void ScheduleSvcDowntime(double time, const std::vector& arguments); + static void DelSvcDowntime(double time, const std::vector& arguments); + static void ScheduleHostDowntime(double time, const std::vector& arguments); + static void ScheduleAndPropagateHostDowntime(double, const std::vector& arguments); + static void ScheduleAndPropagateTriggeredHostDowntime(double, const std::vector& arguments); + static void DelHostDowntime(double time, const std::vector& arguments); + static void DelDowntimeByHostName(double, const std::vector& arguments); + static void ScheduleHostSvcDowntime(double time, const std::vector& arguments); + static void ScheduleHostgroupHostDowntime(double time, const std::vector& arguments); + static void ScheduleHostgroupSvcDowntime(double time, const std::vector& arguments); + static void ScheduleServicegroupHostDowntime(double time, const std::vector& arguments); + static void ScheduleServicegroupSvcDowntime(double time, const std::vector& arguments); + static void AddHostComment(double time, const std::vector& arguments); + static void DelHostComment(double time, const std::vector& arguments); + static void AddSvcComment(double time, const std::vector& arguments); + static void DelSvcComment(double time, const std::vector& arguments); + static void DelAllHostComments(double time, const std::vector& arguments); + static void DelAllSvcComments(double time, const std::vector& arguments); + static void SendCustomHostNotification(double time, const std::vector& arguments); + static void SendCustomSvcNotification(double time, const std::vector& arguments); + static void DelayHostNotification(double time, const std::vector& arguments); + static void DelaySvcNotification(double time, const std::vector& arguments); + static void EnableHostNotifications(double time, const std::vector& arguments); + static void DisableHostNotifications(double time, const std::vector& arguments); + static void EnableSvcNotifications(double time, const std::vector& arguments); + static void DisableSvcNotifications(double time, const std::vector& arguments); + static void EnableHostSvcNotifications(double, const std::vector& arguments); + static void DisableHostSvcNotifications(double, const std::vector& arguments); + static void DisableHostgroupHostChecks(double, const std::vector& arguments); + static void DisableHostgroupPassiveHostChecks(double, const std::vector& arguments); + static void DisableServicegroupHostChecks(double, const std::vector& arguments); + static void DisableServicegroupPassiveHostChecks(double, const std::vector& arguments); + static void EnableHostgroupHostChecks(double, const std::vector& arguments); + static void EnableHostgroupPassiveHostChecks(double, const std::vector& arguments); + static void EnableServicegroupHostChecks(double, const std::vector& arguments); + static void EnableServicegroupPassiveHostChecks(double, const std::vector& arguments); + static void EnableSvcFlapping(double time, const std::vector& arguments); + static void DisableSvcFlapping(double time, const std::vector& arguments); + static void EnableHostFlapping(double time, const std::vector& arguments); + static void DisableHostFlapping(double time, const std::vector& arguments); + static void EnableNotifications(double time, const std::vector& arguments); + static void DisableNotifications(double time, const std::vector& arguments); + static void EnableFlapDetection(double time, const std::vector& arguments); + static void DisableFlapDetection(double time, const std::vector& arguments); + static void EnableEventHandlers(double time, const std::vector& arguments); + static void DisableEventHandlers(double time, const std::vector& arguments); + static void EnablePerformanceData(double time, const std::vector& arguments); + static void DisablePerformanceData(double time, const std::vector& arguments); + static void StartExecutingSvcChecks(double time, const std::vector& arguments); + static void StopExecutingSvcChecks(double time, const std::vector& arguments); + static void StartExecutingHostChecks(double time, const std::vector& arguments); + static void StopExecutingHostChecks(double time, const std::vector& arguments); + + static void ChangeNormalSvcCheckInterval(double time, const std::vector& arguments); + static void ChangeNormalHostCheckInterval(double time, const std::vector& arguments); + static void ChangeRetrySvcCheckInterval(double time, const std::vector& arguments); + static void ChangeRetryHostCheckInterval(double time, const std::vector& arguments); + static void EnableHostEventHandler(double time, const std::vector& arguments); + static void DisableHostEventHandler(double time, const std::vector& arguments); + static void EnableSvcEventHandler(double time, const std::vector& arguments); + static void DisableSvcEventHandler(double time, const std::vector& arguments); + static void ChangeHostEventHandler(double time, const std::vector& arguments); + static void ChangeSvcEventHandler(double time, const std::vector& arguments); + static void ChangeHostCheckCommand(double time, const std::vector& arguments); + static void ChangeSvcCheckCommand(double time, const std::vector& arguments); + static void ChangeMaxHostCheckAttempts(double time, const std::vector& arguments); + static void ChangeMaxSvcCheckAttempts(double time, const std::vector& arguments); + static void ChangeHostCheckTimeperiod(double time, const std::vector& arguments); + static void ChangeSvcCheckTimeperiod(double time, const std::vector& arguments); + static void ChangeCustomHostVar(double time, const std::vector& arguments); + static void ChangeCustomSvcVar(double time, const std::vector& arguments); + static void ChangeCustomUserVar(double time, const std::vector& arguments); + static void ChangeCustomCheckcommandVar(double time, const std::vector& arguments); + static void ChangeCustomEventcommandVar(double time, const std::vector& arguments); + static void ChangeCustomNotificationcommandVar(double time, const std::vector& arguments); + + static void EnableHostgroupHostNotifications(double time, const std::vector& arguments); + static void EnableHostgroupSvcNotifications(double time, const std::vector& arguments); + static void DisableHostgroupHostNotifications(double time, const std::vector& arguments); + static void DisableHostgroupSvcNotifications(double time, const std::vector& arguments); + static void EnableServicegroupHostNotifications(double time, const std::vector& arguments); + static void EnableServicegroupSvcNotifications(double time, const std::vector& arguments); + static void DisableServicegroupHostNotifications(double time, const std::vector& arguments); + static void DisableServicegroupSvcNotifications(double time, const std::vector& 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& 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::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::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 Host::GetServices() const +{ + std::unique_lock lock(m_ServicesMutex); + + std::vector services; + services.reserve(m_Services.size()); + typedef std::pair ServicePair; + for (const ServicePair& kv : m_Services) { + services.push_back(kv.second); + } + + return services; +} + +void Host::AddService(const Service::Ptr& service) +{ + std::unique_lock lock(m_ServicesMutex); + + m_Services[service->GetShortName()] = service; +} + +void Host::RemoveService(const Service::Ptr& service) +{ + std::unique_lock 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 lock(m_ServicesMutex); + + auto it = m_Services.find(name); + + if (it != m_Services.end()) + return it->second; + } + + return nullptr; + } else if (name.IsObjectType()) { + 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(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, public MacroResolver +{ +public: + DECLARE_OBJECT(Host); + DECLARE_OBJECTNAME(Host); + + intrusive_ptr GetServiceByShortName(const Value& name); + + std::vector > GetServices() const; + void AddService(const intrusive_ptr& service); + void RemoveService(const intrusive_ptr& 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 > 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..a99d8ee --- /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 HostGroup::GetMembers() const +{ + std::unique_lock lock(m_HostGroupMutex); + return m_Members; +} + +void HostGroup::AddMember(const Host::Ptr& host) +{ + host->AddGroup(GetName()); + + std::unique_lock lock(m_HostGroupMutex); + m_Members.insert(host); +} + +void HostGroup::RemoveMember(const Host::Ptr& host) +{ + std::unique_lock 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 +{ +public: + DECLARE_OBJECT(HostGroup); + DECLARE_OBJECTNAME(HostGroup); + + std::set 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 m_Members; + + static bool EvaluateObjectRule(const Host::Ptr& host, const intrusive_ptr& 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..1ff303d --- /dev/null +++ b/lib/icinga/icingaapplication.cpp @@ -0,0 +1,327 @@ +/* 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/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 + +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, 50); + +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); + + auto icingaNSBehavior = new ConstNamespaceBehavior(); + icingaNSBehavior->Freeze(); + Namespace::Ptr icingaNS = new Namespace(icingaNSBehavior); + globalNS->SetAttribute("Icinga", new ConstEmbeddedNamespaceValue(icingaNS)); +} + +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()) { + 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 = new Timer(); + 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(std::fstream& 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); + } + + std::fstream fp; + String tempFilename = Utility::CreateTempFile(path + ".tmp.XXXXXX", 0644, fp); + fp.exceptions(std::ofstream::failbit | std::ofstream::badbit); + + 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.close(); + + Utility::RenameFile(tempFilename, path); +} + +IcingaApplication::Ptr IcingaApplication::GetInstance() +{ + return static_pointer_cast(Application::GetInstance()); +} + +bool IcingaApplication::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const +{ + double now = Utility::GetTime(); + + if (macro == "timet") { + *result = static_cast(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; + } + + Dictionary::Ptr vars = GetVars(); + + if (vars && vars->Contains(macro)) { + *result = vars->Get(macro); + 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& 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, 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& 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(©); +} + +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 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 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 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 + +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..9d5f642 --- /dev/null +++ b/lib/icinga/macroprocessor.cpp @@ -0,0 +1,556 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "icinga/macroprocessor.hpp" +#include "icinga/macroresolver.hpp" +#include "icinga/customvarobject.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 + +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()) { + 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()) + resultArr.push_back(Utility::Join(value, ';')); + else + resultArr.push_back(value); + } + + result = new Array(std::move(resultArr)); + } else if (str.IsObjectType()) { + 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()) { + 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; +} + +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 tokens = macro.Split("."); + + String objName; + if (tokens.size() > 1) { + objName = tokens[0]; + tokens.erase(tokens.begin()); + } + + for (const ResolverSpec& resolver : resolvers) { + if (!objName.IsEmpty() && objName != resolver.first) + continue; + + if (objName.IsEmpty()) { + CustomVarObject::Ptr dobj = dynamic_pointer_cast(resolver.second); + + if (dobj) { + Dictionary::Ptr vars = dobj->GetVars(); + + if (vars && vars->Contains(macro)) { + *result = vars->Get(macro); + *recursive_macro = true; + return true; + } + } + } + + auto *mresolver = dynamic_cast(resolver.second.get()); + + if (mresolver && mresolver->ResolveMacro(boost::algorithm::join(tokens, "."), cr, result)) + return true; + + Value ref = resolver.second; + bool valid = true; + + for (const String& token : tokens) { + if (ref.IsObjectType()) { + 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(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(); + + for (const ResolverSpec& resolver : resolvers) { + resolvers_this->Set(resolver.first, resolver.second); + } + + auto internalResolveMacrosShim = [resolvers, cr, resolvedMacros, useResolvedMacros, recursionLevel](const std::vector& 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& 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()) { + 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::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()) + BOOST_THROW_EXCEPTION(std::invalid_argument("Mixing both strings and non-strings in macros is not allowed.")); + + if (resolved_macro.IsObjectType()) { + /* 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()) { + /* 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()) { + /* 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::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() || command.IsObjectType()) + resolvedCommand = MacroProcessor::ResolveMacros(command, resolvers, cr, nullptr, + EscapeMacroShellArg, resolvedMacros, useResolvedMacros, recursionLevel + 1); + else { + resolvedCommand = new Array({ command }); + } + + if (arguments) { + std::vector 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::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()) { + Log(LogWarning, "PluginUtility") + << "Tried to use dictionary in argument '" << arg.Key << "'."; + continue; + } else if (arg.AValue.IsObjectType()) { + bool first = true; + Array::Ptr arr = static_cast(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..d6c1610 --- /dev/null +++ b/lib/icinga/macroprocessor.hpp @@ -0,0 +1,61 @@ +/* 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 + +namespace icinga +{ + +/** + * Resolves macros. + * + * @ingroup icinga + */ +class MacroProcessor +{ +public: + typedef std::function EscapeCallback; + typedef std::pair ResolverSpec; + typedef std::vector 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..0983077 --- /dev/null +++ b/lib/icinga/notification-apply.cpp @@ -0,0 +1,163 @@ +/* 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; + + DebugInfo 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) +{ + DebugInfo di = rule.GetDebugInfo(); + + std::ostringstream msgbuf; + msgbuf << "Evaluating 'apply' rule (" << di << ")"; + CONTEXT(msgbuf.str()); + + 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()) { + 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()) { + 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..f3826ca --- /dev/null +++ b/lib/icinga/notification.cpp @@ -0,0 +1,777 @@ +/* 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 + +using namespace icinga; + +REGISTER_TYPE(Notification); +INITIALIZE_ONCE(&Notification::StaticInitialize); + +std::map Notification::m_StateFilterMap; +std::map Notification::m_TypeFilterMap; + +boost::signals2::signal Notification::OnNextNotificationChanged; + +String NotificationNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const +{ + Notification::Ptr notification = dynamic_pointer_cast(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 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", true); + ScriptGlobal::Set("Icinga.Warning", "Warning", true); + ScriptGlobal::Set("Icinga.Critical", "Critical", true); + ScriptGlobal::Set("Icinga.Unknown", "Unknown", true); + ScriptGlobal::Set("Icinga.Up", "Up", true); + ScriptGlobal::Set("Icinga.Down", "Down", true); + + ScriptGlobal::Set("Icinga.DowntimeStart", "DowntimeStart", true); + ScriptGlobal::Set("Icinga.DowntimeEnd", "DowntimeEnd", true); + ScriptGlobal::Set("Icinga.DowntimeRemoved", "DowntimeRemoved", true); + ScriptGlobal::Set("Icinga.Custom", "Custom", true); + ScriptGlobal::Set("Icinga.Acknowledgement", "Acknowledgement", true); + ScriptGlobal::Set("Icinga.Problem", "Problem", true); + ScriptGlobal::Set("Icinga.Recovery", "Recovery", true); + ScriptGlobal::Set("Icinga.FlappingStart", "FlappingStart", true); + ScriptGlobal::Set("Icinga.FlappingEnd", "FlappingEnd", true); + + 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::OnConfigLoaded(); + + SetTypeFilter(FilterArrayToInt(GetTypes(), GetTypeFilterMap(), ~0)); + SetStateFilter(FilterArrayToInt(GetStates(), GetStateFilterMap(), ~0)); +} + +void Notification::OnAllConfigLoaded() +{ + ObjectImpl::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::Start(runtimeCreated); +} + +void Notification::Stop(bool runtimeRemoved) +{ + ObjectImpl::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(m_Checkable); +} + +NotificationCommand::Ptr Notification::GetCommand() const +{ + return NotificationCommand::GetByName(GetCommandRaw()); +} + +std::set Notification::GetUsers() const +{ + std::set 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 Notification::GetUserGroups() const +{ + std::set 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 << "'."; + + 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 allUsers; + + std::set users = GetUsers(); + std::copy(users.begin(), users.end(), std::inserter(allUsers, allUsers.begin())); + + for (const UserGroup::Ptr& ug : GetUserGroups()) { + std::set members = ug->GetMembers(); + std::copy(members.begin(), members.end(), std::inserter(allUsers, allUsers.begin())); + } + + std::set 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; + } + } + + 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); + + /* 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& filterMap) +{ + std::vector sFilters; + + typedef std::pair 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& 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::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(), "Validation failed: No users/user_groups specified.")); +} + +void Notification::ValidateStates(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& Notification::GetStateFilterMap() +{ + return m_StateFilterMap; +} + +const std::map& Notification::GetTypeFilterMap() +{ + return m_TypeFilterMap; +} diff --git a/lib/icinga/notification.hpp b/lib/icinga/notification.hpp new file mode 100644 index 0000000..ec9164f --- /dev/null +++ b/lib/icinga/notification.hpp @@ -0,0 +1,133 @@ +/* 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" + +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 +{ +public: + DECLARE_OBJECT(Notification); + DECLARE_OBJECTNAME(Notification); + + static void StaticInitialize(); + + intrusive_ptr GetCheckable() const; + intrusive_ptr GetCommand() const; + TimePeriod::Ptr GetPeriod() const; + std::set GetUsers() const; + std::set 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& filterMap); + + static String NotificationServiceStateToString(ServiceState state); + static String NotificationHostStateToString(HostState state); + + static boost::signals2::signal OnNextNotificationChanged; + + void Validate(int types, const ValidationUtils& utils) override; + + void ValidateStates(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateTypes(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateTimes(const Lazy& lvalue, const ValidationUtils& utils) override; + + static void EvaluateApplyRules(const intrusive_ptr& host); + static void EvaluateApplyRules(const intrusive_ptr& service); + + static const std::map& GetStateFilterMap(); + static const std::map& GetTypeFilterMap(); + +protected: + void OnConfigLoaded() override; + void OnAllConfigLoaded() override; + void Start(bool runtimeCreated) override; + void Stop(bool runtimeRemoved) override; + +private: + ObjectImpl::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, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter); + static bool EvaluateApplyRule(const intrusive_ptr& checkable, const ApplyRule& rule, bool skipFilter = false); + + static std::map m_StateFilterMap; + static std::map 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..ab633c5 --- /dev/null +++ b/lib/icinga/notification.ti @@ -0,0 +1,107 @@ +/* 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, protected, required, navigation(host)] name(Host) host_name { + navigate {{{ + return Host::GetByName(GetHostName()); + }}} + }; + [config, 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()); + }}} + }; + + [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; }}} + }; + + [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 +{ +public: + DECLARE_OBJECT(NotificationCommand); + DECLARE_OBJECTNAME(NotificationCommand); + + static thread_local NotificationCommand::Ptr ExecuteOverride; + + virtual Dictionary::Ptr Execute(const intrusive_ptr& 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()) + 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()) + 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..2197d16 --- /dev/null +++ b/lib/icinga/pluginutility.cpp @@ -0,0 +1,217 @@ +/* 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 + +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& 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()) + 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 PluginUtility::ParseCheckOutput(const String& output) +{ + String text; + String perfdata; + + std::vector 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); + + 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()) { + result << static_cast(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 + +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& callback = std::function()); + + static ServiceState ExitStatusToState(int exitStatus); + static std::pair 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..9571706 --- /dev/null +++ b/lib/icinga/scheduleddowntime-apply.cpp @@ -0,0 +1,161 @@ +/* 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; + + DebugInfo 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) +{ + DebugInfo di = rule.GetDebugInfo(); + + std::ostringstream msgbuf; + msgbuf << "Evaluating 'apply' rule (" << di << ")"; + CONTEXT(msgbuf.str()); + + 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()) { + 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()) { + 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..972a428 --- /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 +#include + +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(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 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::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::Start(runtimeCreated); + + static boost::once_flag once = BOOST_ONCE_INIT; + + boost::call_once(once, [this]() { + l_Timer = new Timer(); + 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()) { + 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 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 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 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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 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 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 + +namespace icinga +{ + +class ApplyRule; +struct ScriptFrame; +class Host; +class Service; + +/** + * An Icinga scheduled downtime specification. + * + * @ingroup icinga + */ +class ScheduledDowntime final : public ObjectImpl +{ +public: + DECLARE_OBJECT(ScheduledDowntime); + DECLARE_OBJECTNAME(ScheduledDowntime); + + Checkable::Ptr GetCheckable() const; + + static void EvaluateApplyRules(const intrusive_ptr& host); + static void EvaluateApplyRules(const intrusive_ptr& service); + static bool AllConfigIsLoaded(); + + void ValidateRanges(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateChildOptions(const Lazy& lvalue, const ValidationUtils& utils) override; + String HashDowntimeOptions(); + +protected: + void OnAllConfigLoaded() override; + void Start(bool runtimeCreated) override; + +private: + static void TimerProc(); + + std::pair FindRunningSegment(double minEnd = 0); + std::pair FindNextSegment(); + void CreateNextDowntime(); + void RemoveObsoleteDowntimes(); + + static std::atomic 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..7dfb8bf --- /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, required, navigation(host)] name(Host) host_name { + navigate {{{ + return Host::GetByName(GetHostName()); + }}} + }; + [config, 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, 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..666847c --- /dev/null +++ b/lib/icinga/service-apply.cpp @@ -0,0 +1,135 @@ +/* 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; + + DebugInfo 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) +{ + DebugInfo di = rule.GetDebugInfo(); + + std::ostringstream msgbuf; + msgbuf << "Evaluating 'apply' rule (" << di << ")"; + CONTEXT(msgbuf.str()); + + 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()) { + 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()) { + 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 Service::OnHostProblemChanged; + +String ServiceNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const +{ + Service::Ptr service = dynamic_pointer_cast(context); + + if (!service) + return ""; + + return service->GetHostName() + "!" + shortName; +} + +Dictionary::Ptr ServiceNameComposer::ParseName(const String& name) const +{ + std::vector 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::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(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 icinga::GetHostService(const Checkable::Ptr& checkable) +{ + Service::Ptr service = dynamic_pointer_cast(checkable); + + if (service) + return std::make_pair(service->GetHost(), service); + else + return std::make_pair(static_pointer_cast(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 + +using std::tie; + +namespace icinga +{ + +/** + * An Icinga service. + * + * @ingroup icinga + */ +class Service final : public ObjectImpl, 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 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 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..bbd2412 --- /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, 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..8784b50 --- /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 ServiceGroup::GetMembers() const +{ + std::unique_lock lock(m_ServiceGroupMutex); + return m_Members; +} + +void ServiceGroup::AddMember(const Service::Ptr& service) +{ + service->AddGroup(GetName()); + + std::unique_lock lock(m_ServiceGroupMutex); + m_Members.insert(service); +} + +void ServiceGroup::RemoveMember(const Service::Ptr& service) +{ + std::unique_lock 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 +{ +public: + DECLARE_OBJECT(ServiceGroup); + DECLARE_OBJECTNAME(ServiceGroup); + + std::set 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 m_Members; + + static bool EvaluateObjectRule(const Service::Ptr& service, const intrusive_ptr& 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..98bc8c6 --- /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 + +using namespace icinga; + +REGISTER_TYPE(TimePeriod); + +static Timer::Ptr l_UpdateTimer; + +void TimePeriod::Start(bool runtimeCreated) +{ + ObjectImpl::Start(runtimeCreated); + + static boost::once_flag once = BOOST_ONCE_INIT; + + boost::call_once(once, [this]() { + l_UpdateTimer = new Timer(); + 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()) { + 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& 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 +{ +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& 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::OnConfigLoaded(); + + SetTypeFilter(FilterArrayToInt(GetTypes(), Notification::GetTypeFilterMap(), ~0)); + SetStateFilter(FilterArrayToInt(GetStates(), Notification::GetStateFilterMap(), ~0)); +} + +void User::OnAllConfigLoaded() +{ + ObjectImpl::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::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 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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 +{ +public: + DECLARE_OBJECT(User); + DECLARE_OBJECTNAME(User); + + void AddGroup(const String& name); + + /* Notifications */ + TimePeriod::Ptr GetPeriod() const; + + void ValidateStates(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateTypes(const Lazy& 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..49bd718 --- /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 UserGroup::GetMembers() const +{ + std::unique_lock lock(m_UserGroupMutex); + return m_Members; +} + +void UserGroup::AddMember(const User::Ptr& user) +{ + user->AddGroup(GetName()); + + std::unique_lock lock(m_UserGroupMutex); + m_Members.insert(user); +} + +void UserGroup::RemoveMember(const User::Ptr& user) +{ + std::unique_lock lock(m_UserGroupMutex); + m_Members.erase(user); +} + +std::set UserGroup::GetNotifications() const +{ + std::unique_lock lock(m_UserGroupMutex); + return m_Notifications; +} + +void UserGroup::AddNotification(const Notification::Ptr& notification) +{ + std::unique_lock lock(m_UserGroupMutex); + m_Notifications.insert(notification); +} + +void UserGroup::RemoveNotification(const Notification::Ptr& notification) +{ + std::unique_lock 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 +{ +public: + DECLARE_OBJECT(UserGroup); + DECLARE_OBJECTNAME(UserGroup); + + std::set GetMembers() const; + void AddMember(const User::Ptr& user); + void RemoveMember(const User::Ptr& user); + + std::set> GetNotifications() const; + void AddNotification(const intrusive_ptr& notification); + void RemoveNotification(const intrusive_ptr& 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 m_Members; + std::set> m_Notifications; + + static bool EvaluateObjectRule(const User::Ptr& user, const intrusive_ptr& 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..ac1469b --- /dev/null +++ b/lib/icingadb/icingadb-objects.cpp @@ -0,0 +1,2932 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace icinga; + +using Prio = RedisConnection::QueryPriority; + +std::unordered_set IcingaDB::m_IndexedTypes; + +INITIALIZE_ONCE(&IcingaDB::ConfigStaticInitialize); + +std::vector 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 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& 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 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 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(type.get()); + if (!ctype) + return; + + auto& rcon (m_Rcons.at(ctype)); + + std::vector keys = GetTypeOverwriteKeys(lcType); + DeleteKeys(rcon, keys, Prio::Config); + + WorkQueue upqObjectType(25000, Configuration::Concurrency, LogNotice); + upqObjectType.SetName("IcingaDB:ConfigDump:" + lcType); + + std::map 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>> ourContentRaw {{configCheckSum, {}}, {configObject, {}}}; + std::mutex ourContentMutex; + + upqObjectType.ParallelFor(objectChunks, [&](decltype(objectChunks)::const_reference chunk) { + std::map> hMSets; + // Two values are appended per object: Object ID (Hash encoded) and Object State (IcingaDB::SerializeState() -> JSON encoded) + std::vector 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 statesChksms = {"HMSET", m_PrefixConfigCheckSum + lcType + ":state"}; + std::vector > transaction = {{"MULTI"}}; + std::vector hostZAdds = {"ZADD", "icinga:nextupdate:host"}, serviceZAdds = {"ZADD", "icinga:nextupdate:service"}; + + auto skimObjects ([&]() { + std::lock_guard 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 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(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(object)); + + if (checkable && checkable->GetEnableActiveChecks()) { + auto zAdds (dynamic_pointer_cast(checkable) ? &serviceZAdds : &hostZAdds); + + zAdds->emplace_back(Convert::ToString(checkable->GetNextUpdate())); + zAdds->emplace_back(GetObjectIdentifier(checkable)); + + if (zAdds->size() >= 102u) { + std::vector 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> 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 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> transaction; + + transaction.emplace_back(std::vector{"MULTI"}); + transaction.emplace_back(std::move(setChecksum)); + transaction.emplace_back(std::move(setObject)); + transaction.emplace_back(std::vector{"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> transaction; + + transaction.emplace_back(std::vector{"MULTI"}); + transaction.emplace_back(std::move(delChecksum)); + transaction.emplace_back(std::move(delObject)); + transaction.emplace_back(std::vector{"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 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>> IcingaDB::ChunkObjects(std::vector> objects, size_t chunkSize) { + std::vector>> 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 std::move(chunks); +} + +void IcingaDB::DeleteKeys(const RedisConnection::Ptr& conn, const std::vector& keys, RedisConnection::QueryPriority priority) { + std::vector query = {"DEL"}; + for (auto& key : keys) { + query.emplace_back(key); + } + + conn->FireAndForgetQuery(std::move(query), priority); +} + +std::vector IcingaDB::GetTypeOverwriteKeys(const String& type) +{ + std::vector 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 std::move(keys); +} + +std::vector IcingaDB::GetTypeDumpSignalKeys(const Type::Ptr& type) +{ + String lcType = type->GetName().ToLower(); + std::vector 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 std::move(keys); +} + +template +static ConfigObject::Ptr GetObjectByName(const String& name) +{ + return ConfigObject::GetObject(name); +} + +void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const String typeName, std::map>& hMSets, + std::vector& runtimeUpdates, bool runtimeUpdate) +{ + String objectKey = GetObjectIdentifier(object); + String objectKeyName = typeName + "_id"; + + Type::Ptr type = object->GetReflectionType(); + + CustomVarObject::Ptr customVarObject = dynamic_pointer_cast(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(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; + } else { + groups = host->GetGroups(); + getGroup = &::GetObjectByName; + } + + 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(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; + + 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 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; + + 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 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(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(object); + + std::set 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(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()) { + values = kv.second; + values = values->ShallowClone(); + } else if (kv.second.IsObjectType()) { + 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()->ToString()); + break; + default: + values->Set(attr, JsonEncode(value)); + } + } + } + + { + 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()) { + values = kv.second; + values = values->ShallowClone(); + } else if (kv.second.IsObjectType()) { + 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 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> hMSets; + std::vector runtimeUpdates; + + CreateConfigUpdate(object, typeName, hMSets, runtimeUpdates, runtimeUpdate); + Checkable::Ptr checkable = dynamic_pointer_cast(object); + if (checkable) { + UpdateState(checkable, runtimeUpdate ? StateUpdate::Full : StateUpdate::Volatile); + } + + std::vector > 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 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& 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) +{ + attributes->Set("name_checksum", SHA1(object->GetName())); + attributes->Set("environment_id", m_EnvironmentId); + attributes->Set("name", object->GetName()); + + Zone::Ptr ObjectsZone; + Type::Ptr type = object->GetReflectionType(); + + if (type == Endpoint::TypeInstance) { + ObjectsZone = static_cast(object.get())->GetZone(); + } else { + ObjectsZone = static_pointer_cast(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(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(object); + + attributes->Set("checkcommand_name", checkable->GetCheckCommand()->GetName()); + attributes->Set("max_check_attempts", checkable->GetMaxCheckAttempts()); + attributes->Set("check_timeout", checkable->GetCheckTimeout()); + 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(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(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(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()) { + attributes->Set("times_begin", notification->GetTimes()->Get("begin")); + attributes->Set("times_end",notification->GetTimes()->Get("end")); + } + + attributes->Set("notification_interval", notification->GetInterval()); + attributes->Set("states", notification->GetStates()); + attributes->Set("types", notification->GetTypes()); + + return true; + } + + if (type == Comment::TypeInstance) { + Comment::Ptr comment = static_pointer_cast(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(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(downtime->GetEndTime() - downtime->GetStartTime())); + attributes->Set("flexible_duration", TimestampToMilliseconds(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() + downtime->GetDuration()))); + } + + auto duration = downtime->GetDuration(); + if (downtime->GetFixed()) { + duration = downtime->GetEndTime() - downtime->GetStartTime(); + } + attributes->Set("duration", TimestampToMilliseconds(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(object); + + attributes->Set("display_name", userGroup->GetDisplayName()); + + return true; + } + + if (type == HostGroup::TypeInstance) { + HostGroup::Ptr hostGroup = static_pointer_cast(object); + + attributes->Set("display_name", hostGroup->GetDisplayName()); + + return true; + } + + if (type == ServiceGroup::TypeInstance) { + ServiceGroup::Ptr serviceGroup = static_pointer_cast(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(object); + + attributes->Set("command", JsonEncode(command->GetCommandLine())); + attributes->Set("timeout", 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>& hMSets, + std::vector& 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) +{ + 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(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(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(checkable, service->GetGroups(), nullptr); + } else { + SendGroupsChanged(checkable, host->GetGroups(), nullptr); + } + + return; + } + + if (type == TimePeriod::TypeInstance) { + TimePeriod::Ptr timeperiod = static_pointer_cast(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(object); + SendGroupsChanged(user, user->GetGroups(), nullptr); + return; + } + + if (type == Notification::TypeInstance) { + Notification::Ptr notification = static_pointer_cast(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(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(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 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& 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 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 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(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())), + "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() + 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 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(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() + 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 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 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 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(checkable) ? "icinga:nextupdate:service" : "icinga:nextupdate:host", + Convert::ToString(checkable->GetNextUpdate()), + GetObjectIdentifier(checkable) + }, + Prio::CheckResult + ); + } else { + m_Rcon->FireAndForgetQuery( + { + "ZREM", + dynamic_pointer_cast(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 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 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 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 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 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 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 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 +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 deletedGroups = GetArrayDeletedValues(oldValues, newValues); + String typeName = GetLowerCaseTypeNameDB(object); + + for (const auto& groupName : deletedGroups) { + typename T::Ptr group = ConfigObject::GetObject(groupName); + String id = HashValue(new Array({m_EnvironmentId, group->GetName(), object->GetName()})); + DeleteRelationship(id, typeName + "group:member"); + + if (std::is_same::value) { + UserGroup::Ptr userGroup = dynamic_pointer_cast(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 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 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 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 +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(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()) { + rw->SendStateChange(object, cr, type); + } +} + +void IcingaDB::ReachabilityChangeHandler(const std::set& children) +{ + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + 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()) { + 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()) { + if (rw) + rw->SendConfigDelete(object); + } + } +} + +void IcingaDB::DowntimeStartedHandler(const Downtime::Ptr& downtime) +{ + for (auto& rw : ConfigType::GetObjectsByType()) { + rw->SendStartedDowntime(downtime); + } +} + +void IcingaDB::DowntimeRemovedHandler(const Downtime::Ptr& downtime) +{ + for (auto& rw : ConfigType::GetObjectsByType()) { + rw->SendRemovedDowntime(downtime); + } +} + +void IcingaDB::NotificationSentToAllUsersHandler( + const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set& users, + NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text +) +{ + auto rws (ConfigType::GetObjectsByType()); + 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()) { + rw->SendAddedComment(comment); + } +} + +void IcingaDB::CommentRemovedHandler(const Comment::Ptr& comment) +{ + for (auto& rw : ConfigType::GetObjectsByType()) { + rw->SendRemovedComment(comment); + } +} + +void IcingaDB::FlappingChangeHandler(const Checkable::Ptr& checkable, double changeTime) +{ + auto flappingLastChange (checkable->GetFlappingLastChange()); + + for (auto& rw : ConfigType::GetObjectsByType()) { + rw->SendFlappingChange(checkable, changeTime, flappingLastChange); + } +} + +void IcingaDB::NewCheckResultHandler(const Checkable::Ptr& checkable) +{ + for (auto& rw : ConfigType::GetObjectsByType()) { + rw->UpdateState(checkable, StateUpdate::Volatile); + rw->SendNextUpdate(checkable); + } +} + +void IcingaDB::NextCheckUpdatedHandler(const Checkable::Ptr& checkable) +{ + for (auto& rw : ConfigType::GetObjectsByType()) { + rw->UpdateState(checkable, StateUpdate::Volatile); + rw->SendNextUpdate(checkable); + } +} + +void IcingaDB::HostProblemChangedHandler(const Service::Ptr& service) { + for (auto& rw : ConfigType::GetObjectsByType()) { + /* 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()); + + 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()); + + if (!rws.empty()) { + auto rb (Shared::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()) { + 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()) { + 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()) { + 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()) { + 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()) { + 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()) { + rw->SendGroupsChanged(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()) { + rw->SendGroupsChanged(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()) { + rw->SendGroupsChanged(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()) { + 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()) { + 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()) { + 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> 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..375d0f7 --- /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->Get(); + + 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..bb24e03 --- /dev/null +++ b/lib/icingadb/icingadb-utility.cpp @@ -0,0 +1,313 @@ +/* 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 +#include +#include +#include + +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::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) +{ + return HashValue(new Array({m_EnvironmentId, object->GetName()})); +} + +/** + * 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 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 propertiesBlacklistEmpty; + +String IcingaDB::HashValue(const Value& value) +{ + return HashValue(value, propertiesBlacklistEmpty); +} + +String IcingaDB::HashValue(const Value& value, const std::set& 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((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(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 IcingaDB::GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew) { + std::vector deletedValues; + + if (!arrayOld) { + return deletedValues; + } + + if (!arrayNew) { + ObjectLock olock (arrayOld); + return std::vector(arrayOld->Begin(), arrayOld->End()); + } + + std::vector 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 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 IcingaDB::GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew) { + std::vector deletedKeys; + + if (!dictOld) { + return deletedKeys; + } + + std::vector oldKeys = dictOld->GetKeys(); + + if (!dictNew) { + return oldKeys; + } + + std::vector 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..f96656e --- /dev/null +++ b/lib/icingadb/icingadb.cpp @@ -0,0 +1,309 @@ +/* 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 +#include +#include +#include + +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::Validate(types, utils); + + if (!(types & FAConfig)) + return; + + if (GetEnableTls() && GetCertPath().IsEmpty() != GetKeyPath().IsEmpty()) { + BOOST_THROW_EXCEPTION(ValidationError(this, std::vector(), "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("Validation failed: ") + e.what())); + } +} + +/** + * Starts the component. + */ +void IcingaDB::Start(bool runtimeCreated) +{ + ObjectImpl::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(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 = new Timer(); + 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 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."; + } + + Log(LogInformation, "IcingaDB") + << "'" << GetName() << "' stopped."; + + ObjectImpl::Stop(runtimeRemoved); +} + +void IcingaDB::ValidateTlsProtocolmin(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 l (m_Mutex); + m_Ids.clear(); +} + +String IcingaDB::GetEnvironmentId() const { + return m_EnvironmentId; +} + +bool IcingaDB::DumpedGlobals::IsNew(const String& id) +{ + std::lock_guard 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 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 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 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 +#include +#include +#include +#include +#include +#include +#include +#include + +namespace icinga +{ + +/** + * @ingroup icingadb + */ +class IcingaDB : public ObjectImpl +{ +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 + 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& lvalue, const ValidationUtils& utils) override; + void ValidateConnectTimeout(const Lazy& lvalue, const ValidationUtils& utils) override; + +private: + class DumpedGlobals + { + public: + void Reset(); + bool IsNew(const String& id); + + private: + std::set 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>> ChunkObjects(std::vector> objects, size_t chunkSize); + void DeleteKeys(const RedisConnection::Ptr& conn, const std::vector& keys, RedisConnection::QueryPriority priority); + std::vector GetTypeOverwriteKeys(const String& type); + std::vector GetTypeDumpSignalKeys(const Type::Ptr& type); + void InsertObjectDependencies(const ConfigObject::Ptr& object, const String typeName, std::map>& hMSets, + std::vector& 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>& hMSets, + std::vector& 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& 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& 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 + 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 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 GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew); + static std::vector 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& 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& 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& 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 GetTypes(); + + static void InitEnvironmentId(); + static void PersistEnvironmentId(); + + Timer::Ptr m_StatsTimer; + WorkQueue m_WorkQueue{0, 1, LogNotice}; + + std::future m_HistoryThread; + Bulker 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 m_RconLocked; + std::unordered_map 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 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..c2f1a36 --- /dev/null +++ b/lib/icingadb/icingadbchecktask.cpp @@ -0,0 +1,514 @@ +/* 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 + +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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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 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 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 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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::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::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 promise; + auto future (promise.get_future()); + auto item (Shared>>::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 promise; + auto future (promise.get_future()); + auto item (Shared>>::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& 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>::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::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 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::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::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 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..f3e05f9 --- /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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace icinga +{ +/** + * An Async Redis connection. + * + * @ingroup icingadb + */ + class RedisConnection final : public Object + { + public: + DECLARE_PTR_TYPEDEFS(RedisConnection); + + typedef std::vector Query; + typedef std::vector Queries; + typedef Value Reply; + typedef std::vector 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& callback, QueryPriority priority); + void Sync(); + double GetOldestPendingQueryTs(); + + void SuppressQueryKind(QueryPriority kind); + void UnsuppressQueryKind(QueryPriority kind); + + void SetConnectedCallback(std::function 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::Ptr FireAndForgetQuery; + Shared::Ptr FireAndForgetQueries; + Shared>>::Ptr GetResultOfQuery; + Shared>>::Ptr GetResultsOfQueries; + std::function Callback; + + double CTime; + QueryAffects Affects; + }; + + typedef boost::asio::ip::tcp Tcp; + typedef boost::asio::local::stream_protocol Unix; + + typedef boost::asio::buffered_stream TcpConn; + typedef boost::asio::buffered_stream UnixConn; + + Shared::Ptr m_TLSContext; + + template + static Value ReadRESP(AsyncReadStream& stream, boost::asio::yield_context& yc); + + template + static std::vector ReadLine(AsyncReadStream& stream, boost::asio::yield_context& yc, size_t hint = 0); + + template + 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 + Reply ReadOne(StreamPtr& stream, boost::asio::yield_context& yc); + + template + 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 + void Handshake(StreamPtr& stream, boost::asio::yield_context& yc); + + template + 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::Ptr m_TcpConn; + Shared::Ptr m_UnixConn; + Shared::Ptr m_TlsConn; + Atomic m_Connecting, m_Connected, m_Started; + + struct { + // Items to be send to Redis + std::map> Writes; + // Requestors, each waiting for a single response + std::queue> ReplyPromises; + // Requestors, each waiting for multiple responses at once + std::queue> RepliesPromises; + // Metadata about all of the above + std::queue FutureResponseActions; + } m_Queues; + + // Kinds of queries not to actually send yet + std::set m_SuppressedQueryKinds; + + // Indicate that there's something to send/receive + AsioConditionVariable m_QueuedWrites, m_QueuedReads; + + std::function 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 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 m_What; +}; + +/** + * Read a Redis server response from stream + * + * @param stream Redis server connection + * + * @return The response + */ +template +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 +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 +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()) { + // 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()) { + 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()) { + // 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 +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 +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(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(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(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 +std::vector RedisConnection::ReadLine(AsyncReadStream& stream, boost::asio::yield_context& yc, size_t hint) +{ + namespace asio = boost::asio; + + std::vector 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 std::move(line); + } + + line.emplace_back(next); + } +} + +/** + * Write a Redis protocol value to stream + * + * @param stream Redis server connection + * @param query Redis protocol value + */ +template +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 +#include + +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::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(value) == Convert::ToDouble(m_Operand)); + else + return (static_cast(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(value) < Convert::ToDouble(m_Operand)); + else + return (static_cast(value) < m_Operand); + } else if (m_Operator == ">") { + if (value.GetType() == ValueNumber) + return (static_cast(value) > Convert::ToDouble(m_Operand)); + else + return (static_cast(value) > m_Operand); + } else if (m_Operator == "<=") { + if (value.GetType() == ValueNumber) + return (static_cast(value) <= Convert::ToDouble(m_Operand)); + else + return (static_cast(value) <= m_Operand); + } else if (m_Operator == ">=") { + if (value.GetType() == ValueNumber) + return (static_cast(value) >= Convert::ToDouble(m_Operand)); + else + return (static_cast(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(*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 ValueAccessor; + typedef std::function 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 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 + +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()) { + if (!addRowFn(object, LivestatusGroupByNone, Empty)) + return; + } + + for (const ConfigObject::Ptr& object : ConfigType::GetObjectsByType()) { + if (!addRowFn(object, LivestatusGroupByNone, Empty)) + return; + } + + for (const ConfigObject::Ptr& object : ConfigType::GetObjectsByType()) { + if (!addRowFn(object, LivestatusGroupByNone, Empty)) + return; + } +} + +Value CommandsTable::NameAccessor(const Value& row) +{ + Command::Ptr command = static_cast(row); + + return CompatUtility::GetCommandName(command); +} + +Value CommandsTable::LineAccessor(const Value& row) +{ + Command::Ptr command = static_cast(row); + + if (!command) + return Empty; + + return CompatUtility::GetCommandLine(command); +} + +Value CommandsTable::CustomVariableNamesAccessor(const Value& row) +{ + Command::Ptr command = static_cast(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(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(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()) { + if (!addRowFn(comment, LivestatusGroupByNone, Empty)) + return; + } +} + +Object::Ptr CommentsTable::HostAccessor(const Value& row, const Column::ObjectAccessor&) +{ + Comment::Ptr comment = static_cast(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(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(row); + + if (!comment) + return Empty; + + return comment->GetAuthor(); +} + +Value CommentsTable::CommentAccessor(const Value& row) +{ + Comment::Ptr comment = static_cast(row); + + if (!comment) + return Empty; + + return comment->GetText(); +} + +Value CommentsTable::IdAccessor(const Value& row) +{ + Comment::Ptr comment = static_cast(row); + + if (!comment) + return Empty; + + return comment->GetLegacyId(); +} + +Value CommentsTable::EntryTimeAccessor(const Value& row) +{ + Comment::Ptr comment = static_cast(row); + + if (!comment) + return Empty; + + return static_cast(comment->GetEntryTime()); +} + +Value CommentsTable::TypeAccessor(const Value& row) +{ + Comment::Ptr comment = static_cast(row); + Checkable::Ptr checkable = comment->GetCheckable(); + + if (!checkable) + return Empty; + + if (dynamic_pointer_cast(checkable)) + return 1; + else + return 2; +} + +Value CommentsTable::IsServiceAccessor(const Value& row) +{ + Comment::Ptr comment = static_cast(row); + Checkable::Ptr checkable = comment->GetCheckable(); + + if (!checkable) + return Empty; + + return (dynamic_pointer_cast(checkable) ? 0 : 1); +} + +Value CommentsTable::EntryTypeAccessor(const Value& row) +{ + Comment::Ptr comment = static_cast(row); + + if (!comment) + return Empty; + + return comment->GetEntryType(); +} + +Value CommentsTable::ExpiresAccessor(const Value& row) +{ + Comment::Ptr comment = static_cast(row); + + if (!comment) + return Empty; + + return comment->GetExpireTime() != 0; +} + +Value CommentsTable::ExpireTimeAccessor(const Value& row) +{ + Comment::Ptr comment = static_cast(row); + + if (!comment) + return Empty; + + return static_cast(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()) { + if (!addRowFn(ug, LivestatusGroupByNone, Empty)) + return; + } +} + +Value ContactGroupsTable::NameAccessor(const Value& row) +{ + UserGroup::Ptr user_group = static_cast(row); + + if (!user_group) + return Empty; + + return user_group->GetName(); +} + +Value ContactGroupsTable::AliasAccessor(const Value& row) +{ + UserGroup::Ptr user_group = static_cast(row); + + if (!user_group) + return Empty; + + return user_group->GetName(); +} + +Value ContactGroupsTable::MembersAccessor(const Value& row) +{ + UserGroup::Ptr user_group = static_cast(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()) { + if (!addRowFn(user, LivestatusGroupByNone, Empty)) + return; + } +} + +Value ContactsTable::NameAccessor(const Value& row) +{ + User::Ptr user = static_cast(row); + + if (!user) + return Empty; + + return user->GetName(); +} + +Value ContactsTable::AliasAccessor(const Value& row) +{ + User::Ptr user = static_cast(row); + + if (!user) + return Empty; + + return user->GetDisplayName(); +} + +Value ContactsTable::EmailAccessor(const Value& row) +{ + User::Ptr user = static_cast(row); + + if (!user) + return Empty; + + return user->GetEmail(); +} + +Value ContactsTable::PagerAccessor(const Value& row) +{ + User::Ptr user = static_cast(row); + + if (!user) + return Empty; + + return user->GetPager(); +} + +Value ContactsTable::HostNotificationPeriodAccessor(const Value& row) +{ + User::Ptr user = static_cast(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(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(row); + + if (!user) + return Empty; + + return (user->GetEnableNotifications() ? 1 : 0); +} + +Value ContactsTable::ServiceNotificationsEnabledAccessor(const Value& row) +{ + User::Ptr user = static_cast(row); + + if (!user) + return Empty; + + return (user->GetEnableNotifications() ? 1 : 0); +} + +Value ContactsTable::InHostNotificationPeriodAccessor(const Value& row) +{ + User::Ptr user = static_cast(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(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(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(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() || kv.second.IsObjectType()) + 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(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() || kv.second.IsObjectType()) + 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(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() || kv.second.IsObjectType()) + 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(*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()) { + if (!addRowFn(downtime, LivestatusGroupByNone, Empty)) + return; + } +} + +Object::Ptr DowntimesTable::HostAccessor(const Value& row, const Column::ObjectAccessor&) +{ + Downtime::Ptr downtime = static_cast(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(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(row); + + return downtime->GetAuthor(); +} + +Value DowntimesTable::CommentAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + + return downtime->GetComment(); +} + +Value DowntimesTable::IdAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + + return downtime->GetLegacyId(); +} + +Value DowntimesTable::EntryTimeAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + + return static_cast(downtime->GetEntryTime()); +} + +Value DowntimesTable::TypeAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + // 1 .. active, 0 .. pending + return (downtime->IsInEffect() ? 1 : 0); +} + +Value DowntimesTable::IsServiceAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + Checkable::Ptr checkable = downtime->GetCheckable(); + + return (dynamic_pointer_cast(checkable) ? 0 : 1); +} + +Value DowntimesTable::StartTimeAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + + return static_cast(downtime->GetStartTime()); +} + +Value DowntimesTable::EndTimeAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + + return static_cast(downtime->GetEndTime()); +} + +Value DowntimesTable::FixedAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + + return downtime->GetFixed(); +} + +Value DowntimesTable::DurationAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(row); + + return downtime->GetDuration(); +} + +Value DowntimesTable::TriggeredByAccessor(const Value& row) +{ + Downtime::Ptr downtime = static_cast(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 + +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()) { + if (!addRowFn(endpoint, LivestatusGroupByNone, Empty)) + return; + } +} + +Value EndpointsTable::NameAccessor(const Value& row) +{ + Endpoint::Ptr endpoint = static_cast(row); + + if (!endpoint) + return Empty; + + return endpoint->GetName(); +} + +Value EndpointsTable::IdentityAccessor(const Value& row) +{ + Endpoint::Ptr endpoint = static_cast(row); + + if (!endpoint) + return Empty; + + return endpoint->GetName(); +} + +Value EndpointsTable::NodeAccessor(const Value& row) +{ + Endpoint::Ptr endpoint = static_cast(row); + + if (!endpoint) + return Empty; + + return IcingaApplication::GetInstance()->GetNodeName(); +} + +Value EndpointsTable::IsConnectedAccessor(const Value& row) +{ + Endpoint::Ptr endpoint = static_cast(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(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()) { + if (!addRowFn(hg, LivestatusGroupByNone, Empty)) + return; + } +} + +Value HostGroupsTable::NameAccessor(const Value& row) +{ + HostGroup::Ptr hg = static_cast(row); + + if (!hg) + return Empty; + + return hg->GetName(); +} + +Value HostGroupsTable::AliasAccessor(const Value& row) +{ + HostGroup::Ptr hg = static_cast(row); + + if (!hg) + return Empty; + + return hg->GetDisplayName(); +} + +Value HostGroupsTable::NotesAccessor(const Value& row) +{ + HostGroup::Ptr hg = static_cast(row); + + if (!hg) + return Empty; + + return hg->GetNotes(); +} + +Value HostGroupsTable::NotesUrlAccessor(const Value& row) +{ + HostGroup::Ptr hg = static_cast(row); + + if (!hg) + return Empty; + + return hg->GetNotesUrl(); +} + +Value HostGroupsTable::ActionUrlAccessor(const Value& row) +{ + HostGroup::Ptr hg = static_cast(row); + + if (!hg) + return Empty; + + return hg->GetActionUrl(); +} + +Value HostGroupsTable::MembersAccessor(const Value& row) +{ + HostGroup::Ptr hg = static_cast(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(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(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(row); + + if (!hg) + return Empty; + + return hg->GetMembers().size(); +} + +Value HostGroupsTable::NumHostsPendingAccessor(const Value& row) +{ + HostGroup::Ptr hg = static_cast(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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..ad049ed --- /dev/null +++ b/lib/livestatus/hoststable.cpp @@ -0,0 +1,1522 @@ +/* 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/icingaapplication.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 + +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()) { + 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()) { + 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(row); + + if (!host) + return Empty; + + return host->GetName(); +} + +Value HostsTable::DisplayNameAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetDisplayName(); +} + +Value HostsTable::AddressAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetAddress(); +} + +Value HostsTable::Address6Accessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetAddress6(); +} + +Value HostsTable::CheckCommandAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(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(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(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(row); + + if (!host) + return Empty; + + return host->GetNotes(); +} + +Value HostsTable::NotesExpandedAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + MacroProcessor::ResolverList resolvers { + { "host", host }, + { "icinga", IcingaApplication::GetInstance() } + }; + + return MacroProcessor::ResolveMacros(host->GetNotes(), resolvers); +} + +Value HostsTable::NotesUrlAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetNotesUrl(); +} + +Value HostsTable::NotesUrlExpandedAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + MacroProcessor::ResolverList resolvers { + { "host", host }, + { "icinga", IcingaApplication::GetInstance() } + }; + + return MacroProcessor::ResolveMacros(host->GetNotesUrl(), resolvers); +} + +Value HostsTable::ActionUrlAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetActionUrl(); +} + +Value HostsTable::ActionUrlExpandedAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + MacroProcessor::ResolverList resolvers { + { "host", host }, + { "icinga", IcingaApplication::GetInstance() } + }; + + return MacroProcessor::ResolveMacros(host->GetActionUrl(), resolvers); +} + +Value HostsTable::PluginOutputAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(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(row); + + if (!host) + return Empty; + + return host->GetIconImage(); +} + +Value HostsTable::IconImageExpandedAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + MacroProcessor::ResolverList resolvers { + { "host", host }, + { "icinga", IcingaApplication::GetInstance() } + }; + + return MacroProcessor::ResolveMacros(host->GetIconImage(), resolvers); +} + +Value HostsTable::IconImageAltAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetIconImageAlt(); +} + +Value HostsTable::LongPluginOutputAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(row); + + if (!host) + return Empty; + + return host->GetMaxCheckAttempts(); +} + +Value HostsTable::FlapDetectionEnabledAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return Convert::ToLong(host->GetEnableFlapping()); +} + +Value HostsTable::AcceptPassiveChecksAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return Convert::ToLong(host->GetEnablePassiveChecks()); +} + +Value HostsTable::EventHandlerEnabledAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return Convert::ToLong(host->GetEnableEventHandler()); +} + +Value HostsTable::AcknowledgementTypeAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + ObjectLock olock(host); + return host->GetAcknowledgement(); +} + +Value HostsTable::CheckTypeAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(row); + + if (!host) + return Empty; + + return host->GetLastState(); +} + +Value HostsTable::LastHardStateAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetLastHardState(); +} + +Value HostsTable::CurrentAttemptAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetCheckAttempt(); +} + +Value HostsTable::LastNotificationAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return CompatUtility::GetCheckableNotificationLastNotification(host); +} + +Value HostsTable::NextNotificationAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return CompatUtility::GetCheckableNotificationNextNotification(host); +} + +Value HostsTable::NextCheckAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return static_cast(host->GetNextCheck()); +} + +Value HostsTable::LastHardStateChangeAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return static_cast(host->GetLastHardStateChange()); +} + +Value HostsTable::HasBeenCheckedAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return Convert::ToLong(host->HasBeenChecked()); +} + +Value HostsTable::CurrentNotificationNumberAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return CompatUtility::GetCheckableNotificationNotificationNumber(host); +} + +Value HostsTable::TotalServicesAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetTotalServices(); +} + +Value HostsTable::ChecksEnabledAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return Convert::ToLong(host->GetEnableActiveChecks()); +} + +Value HostsTable::NotificationsEnabledAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return Convert::ToLong(host->GetEnableNotifications()); +} + +Value HostsTable::ProcessPerformanceDataAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return Convert::ToLong(host->GetEnablePerfdata()); +} + +Value HostsTable::AcknowledgedAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + ObjectLock olock(host); + return host->IsAcknowledged(); +} + +Value HostsTable::StateAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->IsReachable() ? host->GetState() : 2; +} + +Value HostsTable::StateTypeAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetStateType(); +} + +Value HostsTable::NoMoreNotificationsAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(row); + + if (!host) + return Empty; + + return static_cast(host->GetLastCheck()); +} + +Value HostsTable::LastStateChangeAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return static_cast(host->GetLastStateChange()); +} + +Value HostsTable::LastTimeUpAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return static_cast(host->GetLastStateUp()); +} + +Value HostsTable::LastTimeDownAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return static_cast(host->GetLastStateDown()); +} + +Value HostsTable::LastTimeUnreachableAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return static_cast(host->GetLastStateUnreachable()); +} + +Value HostsTable::IsFlappingAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->IsFlapping(); +} + +Value HostsTable::ScheduledDowntimeDepthAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetDowntimeDepth(); +} + +Value HostsTable::ActiveChecksEnabledAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return Convert::ToLong(host->GetEnableActiveChecks()); +} + +Value HostsTable::CheckIntervalAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetCheckInterval() / LIVESTATUS_INTERVAL_LENGTH; +} + +Value HostsTable::RetryIntervalAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetRetryInterval() / LIVESTATUS_INTERVAL_LENGTH; +} + +Value HostsTable::NotificationIntervalAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return CompatUtility::GetCheckableNotificationNotificationInterval(host); +} + +Value HostsTable::LowFlapThresholdAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetFlappingThresholdLow(); +} + +Value HostsTable::HighFlapThresholdAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + return host->GetFlappingThresholdHigh(); +} + +Value HostsTable::LatencyAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(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(row); + + if (!host) + return Empty; + + return host->GetFlappingCurrent(); +} + +Value HostsTable::InNotificationPeriodAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(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(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(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(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(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(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(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(comment->GetEntryTime()) + })); + } + + return new Array(std::move(result)); +} + +Value HostsTable::CustomVariableNamesAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(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() || kv.second.IsObjectType()) + 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(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() || kv.second.IsObjectType()) + 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(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() || kv.second.IsObjectType()) + cv_is_json = true; + } + + return cv_is_json; +} + +Value HostsTable::ParentsAccessor(const Value& row) +{ + Host::Ptr host = static_cast(row); + + if (!host) + return Empty; + + ArrayData result; + + for (const Checkable::Ptr& parent : host->GetParents()) { + Host::Ptr parent_host = dynamic_pointer_cast(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(row); + + if (!host) + return Empty; + + ArrayData result; + + for (const Checkable::Ptr& child : host->GetChildren()) { + Host::Ptr child_host = dynamic_pointer_cast(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(row); + + if (!host) + return Empty; + + return host->GetTotalServices(); +} + +Value HostsTable::WorstServiceStateAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(row); + + if (!host) + return Empty; + + std::vector 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(row); + + if (!host) + return Empty; + + std::vector 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(row); + + if (!host) + return Empty; + + std::vector 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(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(row); + + if (!host) + return Empty; + + return host->IsReachable(); +} + +Value HostsTable::OriginalAttributesAccessor(const Value& row) +{ + Host::Ptr host = static_cast(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(*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(*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()) { + 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::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::Stop(runtimeRemoved); + + Log(LogInformation, "LivestatusListener") + << "'" << GetName() << "' stopped."; + + m_Listener->Close(); + + if (m_Thread.joinable()) + m_Thread.join(); +} + +int LivestatusListener::GetClientsConnected() +{ + std::unique_lock lock(l_ComponentMutex); + + return l_ClientsConnected; +} + +int LivestatusListener::GetConnections() +{ + std::unique_lock 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 lock(l_ComponentMutex); + l_ClientsConnected++; + l_Connections++; + } + + Stream::Ptr stream = new NetworkStream(client); + + StreamReadContext context; + + for (;;) { + String line; + + std::vector 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 lock(l_ComponentMutex); + l_ClientsConnected--; + } +} + + +void LivestatusListener::ValidateSocketType(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 + +using namespace icinga; + +namespace icinga +{ + +/** + * @ingroup livestatus + */ +class LivestatusListener final : public ObjectImpl +{ +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& 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 +#include +#include +#include + +using namespace icinga; + +void LivestatusLogUtility::CreateLogIndex(const String& path, std::map& 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& 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 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 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& index); + static void CreateLogIndexFileHandler(const String& path, std::map& index); + static void CreateLogCache(std::map 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 +#include + +using namespace icinga; + +static int l_ExternalCommands = 0; +static std::mutex l_QueryMutex; + +LivestatusQuery::LivestatusQuery(const std::vector& 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(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 filters, stats; + std::deque 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 separators = params.Split(" "); + + /* ugly ascii long to char conversion, but works */ + if (separators.size() > 0) + m_Separators[0] = String(1, static_cast(Convert::ToLong(separators[0]))); + if (separators.size() > 1) + m_Separators[1] = String(1, static_cast(Convert::ToLong(separators[1]))); + if (separators.size() > 2) + m_Separators[2] = String(1, static_cast(Convert::ToLong(separators[2]))); + if (separators.size() > 3) + m_Separators[3] = String(1, static_cast(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 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& 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(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& 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 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 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()) + 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()) + 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()) + 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 objects = table->FilterRows(m_Filter, m_Limit); + std::vector 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 ColumnPair; + + std::vector 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 > allStats; + + /* add aggregated stats */ + for (const LivestatusRowValue& object : objects) { + std::vector 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 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 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(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 + +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& 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 m_Columns; + std::vector m_Separators; + + Filter::Ptr m_Filter; + std::deque 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 +#include +#include +#include + +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(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(row)->Get("host_name"); + String service_description = static_cast(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(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(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(row)->Get("time"); +} + +Value LogTable::LinenoAccessor(const Value& row) +{ + return static_cast(row)->Get("lineno"); +} + +Value LogTable::ClassAccessor(const Value& row) +{ + return static_cast(row)->Get("class"); +} + +Value LogTable::MessageAccessor(const Value& row) +{ + return static_cast(row)->Get("message"); +} + +Value LogTable::TypeAccessor(const Value& row) +{ + return static_cast(row)->Get("type"); +} + +Value LogTable::OptionsAccessor(const Value& row) +{ + return static_cast(row)->Get("options"); +} + +Value LogTable::CommentAccessor(const Value& row) +{ + return static_cast(row)->Get("comment"); +} + +Value LogTable::PluginOutputAccessor(const Value& row) +{ + return static_cast(row)->Get("plugin_output"); +} + +Value LogTable::StateAccessor(const Value& row) +{ + return static_cast(row)->Get("state"); +} + +Value LogTable::StateTypeAccessor(const Value& row) +{ + return static_cast(row)->Get("state_type"); +} + +Value LogTable::AttemptAccessor(const Value& row) +{ + return static_cast(row)->Get("attempt"); +} + +Value LogTable::ServiceDescriptionAccessor(const Value& row) +{ + return static_cast(row)->Get("service_description"); +} + +Value LogTable::HostNameAccessor(const Value& row) +{ + return static_cast(row)->Get("host_name"); +} + +Value LogTable::ContactNameAccessor(const Value& row) +{ + return static_cast(row)->Get("contact_name"); +} + +Value LogTable::CommandNameAccessor(const Value& row) +{ + return static_cast(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 m_LogFileIndex; + std::map 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(*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(*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 + +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()) { + if (!addRowFn(sg, LivestatusGroupByNone, Empty)) + return; + } +} + +Value ServiceGroupsTable::NameAccessor(const Value& row) +{ + ServiceGroup::Ptr sg = static_cast(row); + + if (!sg) + return Empty; + + return sg->GetName(); +} + +Value ServiceGroupsTable::AliasAccessor(const Value& row) +{ + ServiceGroup::Ptr sg = static_cast(row); + + if (!sg) + return Empty; + + return sg->GetDisplayName(); +} + +Value ServiceGroupsTable::NotesAccessor(const Value& row) +{ + ServiceGroup::Ptr sg = static_cast(row); + + if (!sg) + return Empty; + + return sg->GetNotes(); +} + +Value ServiceGroupsTable::NotesUrlAccessor(const Value& row) +{ + ServiceGroup::Ptr sg = static_cast(row); + + if (!sg) + return Empty; + + return sg->GetNotesUrl(); +} + +Value ServiceGroupsTable::ActionUrlAccessor(const Value& row) +{ + ServiceGroup::Ptr sg = static_cast(row); + + if (!sg) + return Empty; + + return sg->GetActionUrl(); +} + +Value ServiceGroupsTable::MembersAccessor(const Value& row) +{ + ServiceGroup::Ptr sg = static_cast(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(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(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(row); + + if (!sg) + return Empty; + + return sg->GetMembers().size(); +} + +Value ServiceGroupsTable::NumServicesOkAccessor(const Value& row) +{ + ServiceGroup::Ptr sg = static_cast(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(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(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(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(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(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(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(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(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..bb5d4fb --- /dev/null +++ b/lib/livestatus/servicestable.cpp @@ -0,0 +1,1205 @@ +/* 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/icingaapplication.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 + +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()) { + 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()) { + 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()) { + 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); + + 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(row); + + if (!service) + return Empty; + + return service->GetShortName(); +} + +Value ServicesTable::DisplayNameAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetDisplayName(); +} + +Value ServicesTable::CheckCommandAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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(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(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(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(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(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(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(row); + + if (!service) + return Empty; + + return service->GetNotes(); +} + +Value ServicesTable::NotesExpandedAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + MacroProcessor::ResolverList resolvers { + { "service", service }, + { "host", service->GetHost() }, + { "icinga", IcingaApplication::GetInstance() } + }; + + return MacroProcessor::ResolveMacros(service->GetNotes(), resolvers); +} + +Value ServicesTable::NotesUrlAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetNotesUrl(); +} + +Value ServicesTable::NotesUrlExpandedAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + MacroProcessor::ResolverList resolvers { + { "service", service }, + { "host", service->GetHost() }, + { "icinga", IcingaApplication::GetInstance() } + }; + + return MacroProcessor::ResolveMacros(service->GetNotesUrl(), resolvers); +} + +Value ServicesTable::ActionUrlAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetActionUrl(); +} + +Value ServicesTable::ActionUrlExpandedAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + MacroProcessor::ResolverList resolvers { + { "service", service }, + { "host", service->GetHost() }, + { "icinga", IcingaApplication::GetInstance() } + }; + + return MacroProcessor::ResolveMacros(service->GetActionUrl(), resolvers); +} + +Value ServicesTable::IconImageAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetIconImage(); +} + +Value ServicesTable::IconImageExpandedAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + MacroProcessor::ResolverList resolvers { + { "service", service }, + { "host", service->GetHost() }, + { "icinga", IcingaApplication::GetInstance() } + }; + + return MacroProcessor::ResolveMacros(service->GetIconImage(), resolvers); +} + +Value ServicesTable::IconImageAltAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetIconImageAlt(); +} + +Value ServicesTable::MaxCheckAttemptsAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetMaxCheckAttempts(); +} + +Value ServicesTable::CurrentAttemptAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetCheckAttempt(); +} + +Value ServicesTable::StateAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetState(); +} + +Value ServicesTable::HasBeenCheckedAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return Convert::ToLong(service->HasBeenChecked()); +} + +Value ServicesTable::LastStateAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetLastState(); +} + +Value ServicesTable::LastHardStateAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetLastHardState(); +} + +Value ServicesTable::StateTypeAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetStateType(); +} + +Value ServicesTable::CheckTypeAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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(row); + + if (!service) + return Empty; + + ObjectLock olock(service); + return service->IsAcknowledged(); +} + +Value ServicesTable::AcknowledgementTypeAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + ObjectLock olock(service); + return service->GetAcknowledgement(); +} + +Value ServicesTable::NoMoreNotificationsAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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(row); + + if (!service) + return Empty; + + return static_cast(service->GetLastStateOK()); +} + +Value ServicesTable::LastTimeWarningAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return static_cast(service->GetLastStateWarning()); +} + +Value ServicesTable::LastTimeCriticalAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return static_cast(service->GetLastStateCritical()); +} + +Value ServicesTable::LastTimeUnknownAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return static_cast(service->GetLastStateUnknown()); +} + +Value ServicesTable::LastCheckAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return static_cast(service->GetLastCheck()); +} + +Value ServicesTable::NextCheckAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return static_cast(service->GetNextCheck()); +} + +Value ServicesTable::LastNotificationAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return CompatUtility::GetCheckableNotificationLastNotification(service); +} + +Value ServicesTable::NextNotificationAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return CompatUtility::GetCheckableNotificationNextNotification(service); +} + +Value ServicesTable::CurrentNotificationNumberAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return CompatUtility::GetCheckableNotificationNotificationNumber(service); +} + +Value ServicesTable::LastStateChangeAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return static_cast(service->GetLastStateChange()); +} + +Value ServicesTable::LastHardStateChangeAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return static_cast(service->GetLastHardStateChange()); +} + +Value ServicesTable::ScheduledDowntimeDepthAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetDowntimeDepth(); +} + +Value ServicesTable::IsFlappingAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->IsFlapping(); +} + +Value ServicesTable::ChecksEnabledAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return Convert::ToLong(service->GetEnableActiveChecks()); +} + +Value ServicesTable::AcceptPassiveChecksAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return Convert::ToLong(service->GetEnablePassiveChecks()); +} + +Value ServicesTable::EventHandlerEnabledAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return Convert::ToLong(service->GetEnableEventHandler()); +} + +Value ServicesTable::NotificationsEnabledAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return Convert::ToLong(service->GetEnableNotifications()); +} + +Value ServicesTable::ProcessPerformanceDataAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return Convert::ToLong(service->GetEnablePerfdata()); +} + +Value ServicesTable::ActiveChecksEnabledAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return Convert::ToLong(service->GetEnableActiveChecks()); +} + +Value ServicesTable::FlapDetectionEnabledAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return Convert::ToLong(service->GetEnableFlapping()); +} + +Value ServicesTable::StalenessAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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(row); + + if (!service) + return Empty; + + return service->GetCheckInterval() / LIVESTATUS_INTERVAL_LENGTH; +} + +Value ServicesTable::RetryIntervalAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetRetryInterval() / LIVESTATUS_INTERVAL_LENGTH; +} + +Value ServicesTable::NotificationIntervalAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return CompatUtility::GetCheckableNotificationNotificationInterval(service); +} + +Value ServicesTable::LowFlapThresholdAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetFlappingThresholdLow(); +} + +Value ServicesTable::HighFlapThresholdAccessor(const Value& row) +{ + Service::Ptr service = static_cast(row); + + if (!service) + return Empty; + + return service->GetFlappingThresholdHigh(); +} + +Value ServicesTable::LatencyAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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(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(row); + + if (!service) + return Empty; + + return service->GetFlappingCurrent(); +} + +Value ServicesTable::InCheckPeriodAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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(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(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(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(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(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(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(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(comment->GetEntryTime()) + })); + } + + return new Array(std::move(result)); +} + +Value ServicesTable::CustomVariableNamesAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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(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() || kv.second.IsObjectType()) + 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(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() || kv.second.IsObjectType()) + 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(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() || kv.second.IsObjectType()) + cv_is_json = true; + } + + return cv_is_json; +} + +Value ServicesTable::GroupsAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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(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(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(row); + + if (!service) + return Empty; + + return service->IsReachable(); +} + +Value ServicesTable::OriginalAttributesAccessor(const Value& row) +{ + Service::Ptr service = static_cast(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 +#include +#include +#include + +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(checkable); + Host::Ptr host; + + if (service) + host = service->GetHost(); + else + host = static_pointer_cast(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(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(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(row)->Get("host_name"); + String service_description = static_cast(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(row)->Get("time"); +} + +Value StateHistTable::LinenoAccessor(const Value& row) +{ + return static_cast(row)->Get("lineno"); +} + +Value StateHistTable::FromAccessor(const Value& row) +{ + return static_cast(row)->Get("from"); +} + +Value StateHistTable::UntilAccessor(const Value& row) +{ + return static_cast(row)->Get("until"); +} + +Value StateHistTable::DurationAccessor(const Value& row) +{ + Dictionary::Ptr state_hist_bag = static_cast(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(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(row)->Get("state"); +} + +Value StateHistTable::HostDownAccessor(const Value& row) +{ + return static_cast(row)->Get("host_down"); +} + +Value StateHistTable::InDowntimeAccessor(const Value& row) +{ + return static_cast(row)->Get("in_downtime"); +} + +Value StateHistTable::InHostDowntimeAccessor(const Value& row) +{ + return static_cast(row)->Get("in_host_downtime"); +} + +Value StateHistTable::IsFlappingAccessor(const Value& row) +{ + return static_cast(row)->Get("is_flapping"); +} + +Value StateHistTable::InNotificationPeriodAccessor(const Value& row) +{ + return static_cast(row)->Get("in_notification_period"); +} + +Value StateHistTable::NotificationPeriodAccessor(const Value& row) +{ + return static_cast(row)->Get("notification_period"); +} + +Value StateHistTable::HostNameAccessor(const Value& row) +{ + return static_cast(row)->Get("host_name"); +} + +Value StateHistTable::ServiceDescriptionAccessor(const Value& row) +{ + return static_cast(row)->Get("service_description"); +} + +Value StateHistTable::LogOutputAccessor(const Value& row) +{ + return static_cast(row)->Get("log_output"); +} + +Value StateHistTable::DurationOkAccessor(const Value& row) +{ + Dictionary::Ptr state_hist_bag = static_cast(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(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(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(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(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(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(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(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(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(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 m_LogFileIndex; + std::map 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(Utility::GetTime() - Application::GetStartTime()); + return CIB::GetActiveHostChecksStatistics(timespan); +} + +Value StatusTable::HostChecksRateAccessor(const Value&) +{ + auto timespan = static_cast(Utility::GetTime() - Application::GetStartTime()); + return (CIB::GetActiveHostChecksStatistics(timespan) / (Utility::GetTime() - Application::GetStartTime())); +} + +Value StatusTable::ServiceChecksAccessor(const Value&) +{ + auto timespan = static_cast(Utility::GetTime() - Application::GetStartTime()); + return CIB::GetActiveServiceChecksStatistics(timespan); +} + +Value StatusTable::ServiceChecksRateAccessor(const Value&) +{ + auto timespan = static_cast(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(Application::GetStartTime()); +} + +Value StatusTable::IntervalLengthAccessor(const Value&) +{ + return LIVESTATUS_INTERVAL_LENGTH; +} + +Value StatusTable::NumHostsAccessor(const Value&) +{ + return ConfigType::Get()->GetObjectCount(); +} + +Value StatusTable::NumServicesAccessor(const Value&) +{ + return ConfigType::Get()->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 + +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(*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(*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 + +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 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 Table::GetColumnNames() const +{ + std::vector names; + + for (const auto& kv : m_Columns) { + names.push_back(kv.first); + } + + return names; +} + +std::vector Table::FilterRows(const Filter::Ptr& filter, int limit) +{ + std::vector 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& rs, const Filter::Ptr& filter, int limit, const Value& row, LivestatusGroupByType groupByType, const Object::Ptr& groupByObject) +{ + if (limit != -1 && static_cast(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 + +namespace icinga +{ + +// Well, don't ask. +#define LIVESTATUS_INTERVAL_LENGTH 60.0 + +struct LivestatusRowValue { + Value Row; + LivestatusGroupByType GroupByType; + Value GroupByObject; +}; + +typedef std::function 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 FilterRows(const intrusive_ptr& filter, int limit = -1); + + void AddColumn(const String& name, const Column& column); + Column GetColumn(const String& name) const; + std::vector 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 m_Columns; + + bool FilteredAddRow(std::vector& rs, const intrusive_ptr& 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 + +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()) { + if (!addRowFn(tp, LivestatusGroupByNone, Empty)) + return; + } +} + +Value TimePeriodsTable::NameAccessor(const Value& row) +{ + return static_cast(row)->GetName(); +} + +Value TimePeriodsTable::AliasAccessor(const Value& row) +{ + return static_cast(row)->GetDisplayName(); +} + +Value TimePeriodsTable::InAccessor(const Value& row) +{ + return (static_cast(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()) { + if (!addRowFn(zone, LivestatusGroupByNone, Empty)) + return; + } +} + +Value ZonesTable::NameAccessor(const Value& row) +{ + Zone::Ptr zone = static_cast(row); + + if (!zone) + return Empty; + + return zone->GetName(); +} + +Value ZonesTable::ParentAccessor(const Value& row) +{ + Zone::Ptr zone = static_cast(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(row); + + if (!zone) + return Empty; + + std::set 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(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..9fde9fd --- /dev/null +++ b/lib/methods/CMakeLists.txt @@ -0,0 +1,33 @@ +# 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 + 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 + +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 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 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..c5b6bbb --- /dev/null +++ b/lib/methods/clusterzonechecktask.cpp @@ -0,0 +1,211 @@ +/* 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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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; + + for (const Endpoint::Ptr& endpoint : zone->GetEndpoints()) { + 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(); + } + + 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..561415c --- /dev/null +++ b/lib/methods/dummychecktask.cpp @@ -0,0 +1,77 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#ifndef _WIN32 +# include +#endif /* _WIN32 */ +#include "methods/dummychecktask.hpp" +#include "icinga/icingaapplication.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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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 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 +#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..4079549 --- /dev/null +++ b/lib/methods/icingachecktask.cpp @@ -0,0 +1,211 @@ +/* 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/icingaapplication.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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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 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 endpoints = ConfigType::GetObjectsByType(); + + 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/methods-itl.conf b/lib/methods/methods-itl.conf new file mode 100644 index 0000000..f9126f7 --- /dev/null +++ b/lib/methods/methods-itl.conf @@ -0,0 +1,85 @@ +/* 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 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", + "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 +#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..9bfa722 --- /dev/null +++ b/lib/methods/pluginchecktask.cpp @@ -0,0 +1,91 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "methods/pluginchecktask.hpp" +#include "icinga/pluginutility.hpp" +#include "icinga/checkcommand.hpp" +#include "icinga/macroprocessor.hpp" +#include "icinga/icingaapplication.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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + int timeout = commandObj->GetTimeout(); + + if (!checkable->GetCheckTimeout().IsEmpty()) + timeout = checkable->GetCheckTimeout(); + + std::function 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 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..8b2fc44 --- /dev/null +++ b/lib/methods/plugineventtask.cpp @@ -0,0 +1,63 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "methods/plugineventtask.hpp" +#include "icinga/eventcommand.hpp" +#include "icinga/macroprocessor.hpp" +#include "icinga/pluginutility.hpp" +#include "icinga/icingaapplication.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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + int timeout = commandObj->GetTimeout(); + std::function 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..17f34cd --- /dev/null +++ b/lib/methods/pluginnotificationtask.cpp @@ -0,0 +1,80 @@ +/* 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 "icinga/icingaapplication.hpp" +#include "base/function.hpp" +#include "base/logger.hpp" +#include "base/utility.hpp" +#include "base/process.hpp" +#include "base/convert.hpp" + +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(itype); + + Checkable::Ptr checkable = notification->GetCheckable(); + + Dictionary::Ptr notificationExtra = new Dictionary({ + { "type", Notification::NotificationTypeToStringCompat(type) }, //TODO: Change that to our types. + { "author", author }, + { "comment", comment } + }); + + 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) + resolvers.emplace_back("service", service); + resolvers.emplace_back("host", host); + resolvers.emplace_back("command", commandObj); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + int timeout = commandObj->GetTimeout(); + std::function 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 +#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(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..a018de4 --- /dev/null +++ b/lib/methods/sleepchecktask.cpp @@ -0,0 +1,69 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "methods/sleepchecktask.hpp" +#include "icinga/icingaapplication.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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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 +#include + +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..53fcffe --- /dev/null +++ b/lib/notification/notificationcomponent.cpp @@ -0,0 +1,269 @@ +/* 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()) { + 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::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 = new Timer(); + m_NotificationTimer->SetInterval(5); + m_NotificationTimer->OnTimerExpired.connect([this](const Timer * const&) { NotificationTimerHandler(); }); + m_NotificationTimer->Start(); +} + +void NotificationComponent::Stop(bool runtimeRemoved) +{ + Log(LogInformation, "NotificationComponent") + << "'" << GetName() << "' stopped."; + + ObjectImpl::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()) { + 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 +{ +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..3df9c91 --- /dev/null +++ b/lib/perfdata/elasticsearchwriter.cpp @@ -0,0 +1,686 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace icinga; + +REGISTER_TYPE(ElasticsearchWriter); + +REGISTER_STATSFUNCTION(ElasticsearchWriter, &ElasticsearchWriter::StatsFunc); + +void ElasticsearchWriter::OnConfigLoaded() +{ + ObjectImpl::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()) { + 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::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 = new Timer(); + 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& 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(); + + Flush(); + m_WorkQueue.Join(); + Flush(); + + Log(LogInformation, "ElasticsearchWriter") + << "'" << GetName() << "' paused."; + + ObjectImpl::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()) + 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& 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& 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 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(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 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 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())); + + /* ES 6 removes multiple _type mappings: https://www.elastic.co/guide/en/elasticsearch/reference/6.x/removal-of-types.html + * Best practice is to statically define 'doc', as ES 5.X does not allow types starting with '_'. + */ + path.emplace_back("doc"); + + /* 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 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 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::Ptr sslContext; + + try { + sslContext = MakeAsioSslContext(GetCertPath(), GetKeyPath(), GetCaPath()); + } catch (const std::exception&) { + Log(LogWarning, "ElasticsearchWriter") + << "Unable to create SSL context."; + throw; + } + + stream.first = Shared::Make(IoEngine::Get().GetIoContext(), *sslContext, GetHost()); + + } else { + stream.second = Shared::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 std::move(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((ts - static_cast(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 +{ +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 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& 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& 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..843f56a --- /dev/null +++ b/lib/perfdata/gelfwriter.cpp @@ -0,0 +1,538 @@ +/* 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 +#include +#include "base/io-engine.hpp" +#include +#include +#include +#include + +using namespace icinga; + +REGISTER_TYPE(GelfWriter); + +REGISTER_STATSFUNCTION(GelfWriter, &GelfWriter::StatsFunc); + +void GelfWriter::OnConfigLoaded() +{ + ObjectImpl::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()) { + 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::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 = new Timer(); + 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.reset(); + + try { + ReconnectInternal(); + } catch (const std::exception&) { + Log(LogInformation, "GelfWriter") + << "'" << GetName() << "' paused. Unable to connect, not flushing buffers. Data may be lost on reload."; + + ObjectImpl::Pause(); + return; + } + + m_WorkQueue.Join(); + DisconnectInternal(); + + Log(LogInformation, "GelfWriter") + << "'" << GetName() << "' paused."; + + ObjectImpl::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::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::Make(IoEngine::Get().GetIoContext(), *sslContext, GetHost()); + + } else { + m_Stream.second = Shared::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()) + 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(); + + ObjectLock olock(this); + + 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 + +namespace icinga +{ + +/** + * An Icinga Gelf writer for Graylog. + * + * @ingroup perfdata + */ +class GelfWriter final : public ObjectImpl +{ +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..fc1c7ed --- /dev/null +++ b/lib/perfdata/graphitewriter.cpp @@ -0,0 +1,515 @@ +/* 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 +#include +#include + +using namespace icinga; + +REGISTER_TYPE(GraphiteWriter); + +REGISTER_STATSFUNCTION(GraphiteWriter, &GraphiteWriter::StatsFunc); + +/* + * Enable HA capabilities once the config object is loaded. + */ +void GraphiteWriter::OnConfigLoaded() +{ + ObjectImpl::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()) { + 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::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 = new Timer(); + 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.reset(); + + try { + ReconnectInternal(); + } catch (const std::exception&) { + Log(LogInformation, "GraphiteWriter") + << "'" << GetName() << "' paused. Unable to connect, not flushing buffers. Data may be lost on reload."; + + ObjectImpl::Pause(); + return; + } + + m_WorkQueue.Join(); + DisconnectInternal(); + + Log(LogInformation, "GraphiteWriter") + << "'" << GetName() << "' paused."; + + ObjectImpl::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::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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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()) + 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(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 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::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 +#include + +namespace icinga +{ + +/** + * An Icinga graphite writer. + * + * @ingroup perfdata + */ +class GraphiteWriter final : public ObjectImpl +{ +public: + DECLARE_OBJECT(GraphiteWriter); + DECLARE_OBJECTNAME(GraphiteWriter); + + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + + void ValidateHostNameTemplate(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateServiceNameTemplate(const Lazy& lvalue, const ValidationUtils& utils) override; + +protected: + void OnConfigLoaded() override; + void Resume() override; + void Pause() override; + +private: + Shared::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..57fc94e --- /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 +#include +#include + +using namespace icinga; + +REGISTER_TYPE(Influxdb2Writer); + +REGISTER_STATSFUNCTION(Influxdb2Writer, &Influxdb2Writer::StatsFunc); + +void Influxdb2Writer::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata) +{ + InfluxdbCommonWriter::StatsFunc(status, perfdata); +} + +boost::beast::http::request Influxdb2Writer::AssembleRequest(String body) +{ + auto request (AssembleBaseRequest(std::move(body))); + + request.set(boost::beast::http::field::authorization, "Token " + GetAuthToken()); + + return std::move(request); +} + +Url::Ptr Influxdb2Writer::AssembleUrl() +{ + auto url (AssembleBaseUrl()); + + std::vector path ({"api", "v2", "write"}); + url->SetPath(path); + + url->AddQueryElement("org", GetOrganization()); + url->AddQueryElement("bucket", GetBucket()); + + return std::move(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 +#include + +namespace icinga +{ + +/** + * An Icinga InfluxDB v2 writer. + * + * @ingroup perfdata + */ +class Influxdb2Writer final : public ObjectImpl +{ +public: + DECLARE_OBJECT(Influxdb2Writer); + DECLARE_OBJECTNAME(Influxdb2Writer); + + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + +protected: + boost::beast::http::request 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..36d88e2 --- /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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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::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::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 = new Timer(); + 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_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::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::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::Make(IoEngine::Get().GetIoContext(), *sslContext, GetHost()); + + } else { + stream.second = Shared::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 std::move(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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + 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(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()) + 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()) { + std::ostringstream os; + os << static_cast(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(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(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 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 InfluxdbCommonWriter::AssembleBaseRequest(String body) +{ + namespace http = boost::beast::http; + + auto url (AssembleUrl()); + http::request 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 std::move(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 std::move(url); +} + +void InfluxdbCommonWriter::ValidateHostTemplate(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 +#include +#include +#include + +namespace icinga +{ + +/** + * Common base class for InfluxDB v1/v2 writers. + * + * @ingroup perfdata + */ +class InfluxdbCommonWriter : public ObjectImpl +{ +public: + DECLARE_OBJECT(InfluxdbCommonWriter); + + template + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + + void ValidateHostTemplate(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateServiceTemplate(const Lazy& lvalue, const ValidationUtils& utils) override; + +protected: + void OnConfigLoaded() override; + void Resume() override; + void Pause() override; + + boost::beast::http::request AssembleBaseRequest(String body); + Url::Ptr AssembleBaseUrl(); + virtual boost::beast::http::request 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 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 +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()) { + 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..30240f7 --- /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 +#include +#include + +using namespace icinga; + +REGISTER_TYPE(InfluxdbWriter); + +REGISTER_STATSFUNCTION(InfluxdbWriter, &InfluxdbWriter::StatsFunc); + +void InfluxdbWriter::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata) +{ + InfluxdbCommonWriter::StatsFunc(status, perfdata); +} + +boost::beast::http::request 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 std::move(request); +} + +Url::Ptr InfluxdbWriter::AssembleUrl() +{ + auto url (AssembleBaseUrl()); + + std::vector 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 std::move(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 +{ +public: + DECLARE_OBJECT(InfluxdbWriter); + DECLARE_OBJECTNAME(InfluxdbWriter); + + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + +protected: + boost::beast::http::request 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..2d5720e --- /dev/null +++ b/lib/perfdata/opentsdbwriter.cpp @@ -0,0 +1,526 @@ +/* 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 +#include + +using namespace icinga; + +REGISTER_TYPE(OpenTsdbWriter); + +REGISTER_STATSFUNCTION(OpenTsdbWriter, &OpenTsdbWriter::StatsFunc); + +/* + * Enable HA capabilities once the config object is loaded. + */ +void OpenTsdbWriter::OnConfigLoaded() +{ + ObjectImpl::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()) { + 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::Resume(); + + Log(LogInformation, "OpentsdbWriter") + << "'" << GetName() << "' resumed."; + + ReadConfigTemplate(m_ServiceConfigTemplate, m_HostConfigTemplate); + + m_ReconnectTimer = new Timer(); + 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.reset(); + + Log(LogInformation, "OpentsdbWriter") + << "'" << GetName() << "' paused."; + + m_Stream->close(); + + SetConnected(false); + + ObjectImpl::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::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(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(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 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); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + // 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& 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()) + 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 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& 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 + * "tags" must include at least one tag, we use "host=HOSTNAME" + */ + msgbuf << "put " << metric << " " << static_cast(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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 + +namespace icinga +{ + +/** + * An Icinga opentsdb writer. + * + * @ingroup perfdata + */ +class OpenTsdbWriter final : public ObjectImpl +{ +public: + DECLARE_OBJECT(OpenTsdbWriter); + DECLARE_OBJECTNAME(OpenTsdbWriter); + + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + + void ValidateHostTemplate(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateServiceTemplate(const Lazy& lvalue, const ValidationUtils& utils) override; + +protected: + void OnConfigLoaded() override; + void Resume() override; + void Pause() override; + +private: + Shared::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& tags, double value, double ts); + void SendPerfdata(const Checkable::Ptr& checkable, const String& metric, + const std::map& 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..bb6fda3 --- /dev/null +++ b/lib/perfdata/perfdatawriter.cpp @@ -0,0 +1,202 @@ +/* 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::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()) { + nodes.emplace_back(perfdatawriter->GetName(), 1); //add more stats + } + + status->Set("perfdatawriter", new Dictionary(std::move(nodes))); +} + +void PerfdataWriter::Resume() +{ + ObjectImpl::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 = new Timer(); + 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.reset(); + +#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::Pause(); +} + +Value PerfdataWriter::EscapeMacroMetric(const Value& value) +{ + if (value.IsObjectType()) + 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(checkable); + Host::Ptr host; + + if (service) + host = service->GetHost(); + else + host = static_pointer_cast(checkable); + + MacroProcessor::ResolverList resolvers; + if (service) + resolvers.emplace_back("service", service); + resolvers.emplace_back("host", host); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + if (service) { + String line = MacroProcessor::ResolveMacros(GetServiceFormatTemplate(), resolvers, cr, nullptr, &PerfdataWriter::EscapeMacroMetric); + + { + std::unique_lock 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 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 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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 + +namespace icinga +{ + +/** + * An Icinga perfdata writer. + * + * @ingroup icinga + */ +class PerfdataWriter final : public ObjectImpl +{ +public: + DECLARE_OBJECT(PerfdataWriter); + DECLARE_OBJECTNAME(PerfdataWriter); + + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + + void ValidateHostFormatTemplate(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateServiceFormatTemplate(const Lazy& 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 +#include + +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..2c5a032 --- /dev/null +++ b/lib/remote/CMakeLists.txt @@ -0,0 +1,66 @@ +# 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 + 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..80f06e6 --- /dev/null +++ b/lib/remote/actionshandler.cpp @@ -0,0 +1,139 @@ +/* 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 + +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& request, + const Url::Ptr& url, + boost::beast::http::response& 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& types = action->GetTypes(); + std::vector objs; + + String permission = "actions/" + actionName; + + if (!types.empty()) { + qd.Types = std::set(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; + } + } 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 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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& 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::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 +#include + +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 Callback; + + ApiAction(std::vector registerTypes, Callback function); + + Value Invoke(const ConfigObject::Ptr& target, const Dictionary::Ptr& params); + + const std::vector& 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 m_Types; + Callback m_Callback; +}; + +/** + * A registry for API actions. + * + * @ingroup remote + */ +class ApiActionRegistry : public Registry +{ +public: + static ApiActionRegistry *GetInstance(); +}; + +#define REGISTER_APIACTION(name, types, callback) \ + INITIALIZE_ONCE([]() { \ + String registerName = #name; \ + boost::algorithm::replace_all(registerName, "_", "-"); \ + std::vector 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::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 + +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 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 +{ +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 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 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(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..6271411 --- /dev/null +++ b/lib/remote/apilistener-configsync.cpp @@ -0,0 +1,474 @@ +/* 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 + +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(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 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(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") { + String file; + + try { + file = ConfigObjectUtility::GetObjectConfigPath(object->GetReflectionType(), object->GetName()); + } catch (const std::exception& ex) { + Log(LogNotice, "ApiListener") + << "Cannot sync object '" << object->GetName() << "': " << ex.what(); + return; + } + + std::ifstream fp(file.CStr(), std::ifstream::binary); + if (!fp) + return; + + String content((std::istreambuf_iterator(fp)), std::istreambuf_iterator()); + 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 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(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(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(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..c181c6d --- /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 +#include +#include + +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()) { + 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()) { + 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 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 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 'System.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& 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("System.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(fp)), std::istreambuf_iterator()); + + 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..472bd90 --- /dev/null +++ b/lib/remote/apilistener.cpp @@ -0,0 +1,1929 @@ +/* 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/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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace icinga; + +REGISTER_TYPE(ApiListener); + +boost::signals2::signal 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 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 ApiListener::RenewCert(const std::shared_ptr& cert) +{ + std::shared_ptr 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)); + + /* 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 lock (m_SSLContextMutex); + + m_SSLContext = std::move(ctx); + } + + for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType()) { + 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 = new Timer(); + + if (Utility::PathExists(GetIcingaCADir() + "/ca.key")) { + RenewOwnCert(); + m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) { RenewOwnCert(); }); + } else { + m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) { + JsonRpcConnection::SendCertificateRequest(nullptr, nullptr, String()); + }); + } + + m_RenewOwnCertTimer->SetInterval(RENEW_INTERVAL); + m_RenewOwnCertTimer->Start(); + + ObjectImpl::Start(runtimeCreated); + + { + std::unique_lock 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 = new Timer(); + m_Timer->OnTimerExpired.connect([this](const Timer * const&) { ApiTimerHandler(); }); + m_Timer->SetInterval(5); + m_Timer->Start(); + m_Timer->Reschedule(0); + + m_ReconnectTimer = new Timer(); + 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 = new Timer(); + m_AuthorityTimer->OnTimerExpired.connect([](const Timer * const&) { UpdateObjectAuthority(); }); + m_AuthorityTimer->SetInterval(10); + m_AuthorityTimer->Start(); + + m_CleanupCertificateRequestsTimer = new Timer(); + m_CleanupCertificateRequestsTimer->OnTimerExpired.connect([this](const Timer * const&) { CleanupCertificateRequestsTimerHandler(); }); + m_CleanupCertificateRequestsTimer->SetInterval(3600); + m_CleanupCertificateRequestsTimer->Start(); + m_CleanupCertificateRequestsTimer->Reschedule(0); + + m_ApiPackageIntegrityTimer = new Timer(); + 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; + } + + std::fstream certfp; + auto tempCertPath (Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp)); + + certfp.exceptions(std::ofstream::failbit | std::ofstream::badbit); + certfp << CertificateToString(cert); + certfp.close(); + + Utility::RenameFile(tempCertPath, certPath); + UpdateSSLContext(); +} + +void ApiListener::Stop(bool runtimeDeleted) +{ + ObjectImpl::Stop(runtimeDeleted); + + Log(LogInformation, "ApiListener") + << "'" << GetName() << "' stopped."; + + { + std::unique_lock 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 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::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(&optFalse), sizeof(optFalse)); + + const int optTrue = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&optTrue), sizeof(optTrue)); +#ifdef SO_REUSEPORT + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&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::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 lock (m_SSLContextMutex); + auto sslConn (Shared::Make(io, *m_SSLContext)); + + lock.unlock(); + sslConn->lowest_layer() = std::move(socket); + + auto strand (Shared::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::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 lock (m_SSLContextMutex); + auto sslConn (Shared::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::Ptr& strand, + const Shared::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(match[1].str()) + + 100u * boost::lexical_cast(match[2].str()) + + boost::lexical_cast(match[3].str()); +})()); + +static const auto l_MyCapabilities (ApiCapabilities::ExecuteArbitraryCommand); + +/** + * Processes a new client connection. + * + * @param client The new client. + */ +void ApiListener::NewClientHandlerInternal( + boost::asio::yield_context yc, const Shared::Ptr& strand, + const Shared::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 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 cert path: '" << GetDefaultCertPath() << "'."; + 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; + + 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; + } + } + + 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 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()) { + 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()) { + 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()) { + /* 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 names; + for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType()) + 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 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 skippedEndpoints; + + std::set 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()) { + 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(secobj); + else + target_zone = static_pointer_cast(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); + + auto *fp = new 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, 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(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& 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 lock(m_LogLock); + + CloseLogFile(); + + if (count == -1 || count > 50000) { + OpenLogFile(); + lock.unlock(); + } else { + last_sync = true; + } + + count = 0; + + std::vector files; + Utility::Glob(GetApiDir() + "log/*", [&files](const String& file) { LogGlobHandler(files, file); }, GlobFile); + std::sort(files.begin(), files.end()); + + std::vector> 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 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 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()) { + /* 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 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 lock(m_AnonymousClientsLock); + m_AnonymousClients.erase(aclient); +} + +std::set ApiListener::GetAnonymousClients() const +{ + std::unique_lock lock(m_AnonymousClientsLock); + return m_AnonymousClients; +} + +void ApiListener::AddHttpClient(const HttpServerConnection::Ptr& aclient) +{ + std::unique_lock lock(m_HttpClientsLock); + m_HttpClients.insert(aclient); +} + +void ApiListener::RemoveHttpClient(const HttpServerConnection::Ptr& aclient) +{ + std::unique_lock lock(m_HttpClientsLock); + m_HttpClients.erase(aclient); +} + +std::set ApiListener::GetHttpClients() const +{ + std::unique_lock 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 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 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 lock(m_ActivePackageStagesLock); + m_ActivePackageStages[package] = stage; +} + +String ApiListener::GetActivePackageStage(const String& package) +{ + std::unique_lock 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 lock(m_ActivePackageStagesLock); + + auto it = m_ActivePackageStages.find(package); + + if (it == m_ActivePackageStages.end()) + return; + + m_ActivePackageStages.erase(it); +} + +void ApiListener::ValidateTlsProtocolmin(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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..1be2952 --- /dev/null +++ b/lib/remote/apilistener.hpp @@ -0,0 +1,263 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 +}; + +/** +* @ingroup remote +*/ +class ApiListener final : public ObjectImpl +{ +public: + DECLARE_OBJECT(ApiListener); + DECLARE_OBJECTNAME(ApiListener); + + static boost::signals2::signal OnMasterChanged; + + ApiListener(); + + static String GetApiDir(); + static String GetApiZonesDir(); + static String GetApiZonesStageDir(); + static String GetCertsDir(); + static String GetCaDir(); + static String GetCertificateRequestsDir(); + + std::shared_ptr RenewCert(const std::shared_ptr& cert); + 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 GetStatus(); + + bool AddAnonymousClient(const JsonRpcConnection::Ptr& aclient); + void RemoveAnonymousClient(const JsonRpcConnection::Ptr& aclient); + std::set GetAnonymousClients() const; + + void AddHttpClient(const HttpServerConnection::Ptr& aclient); + void RemoveHttpClient(const HttpServerConnection::Ptr& aclient); + std::set 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& lvalue, const ValidationUtils& utils) override; + void ValidateTlsHandshakeTimeout(const Lazy& lvalue, const ValidationUtils& utils) override; + +private: + Shared::Ptr m_SSLContext; + boost::shared_mutex m_SSLContextMutex; + + mutable std::mutex m_AnonymousClientsLock; + mutable std::mutex m_HttpClientsLock; + std::set m_AnonymousClients; + std::set 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 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::Ptr& strand, + const Shared::Ptr& client, const String& hostname, ConnectionRole role + ); + void NewClientHandlerInternal( + boost::asio::yield_context yc, const Shared::Ptr& strand, + const Shared::Ptr& client, const String& hostname, ConnectionRole role + ); + void ListenerCoroutineProc(boost::asio::yield_context yc, const Shared::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& 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 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& 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 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()) { + 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 +{ +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 +#include + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/config/files", ConfigFilesHandler); + +bool ConfigFilesHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& 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& 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 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(fp)), std::istreambuf_iterator()); + 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& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + HttpServerConnection& server + ) override; +}; + +} + +#endif /* CONFIGFILESHANDLER_H */ diff --git a/lib/remote/configobjectutility.cpp b/lib/remote/configobjectutility.cpp new file mode 100644 index 0000000..1faeb9a --- /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 +#include +#include +#include +#include + +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::GetObjectConfigPath(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. + */ + if ((type->GetName() != "Comment" && type->GetName() != "Downtime") || Utility::PathExists(longPath)) { + return std::move(longPath); + } + + /* Maximum length 80 bytes object name + 3 bytes "..." + 40 bytes SHA1 (hex-encoded) */ + return prefix + Utility::TruncateUsingHash<80+3+40>(escapedName) + ".conf"; +} + +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 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(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(type.get())); + + if (configType && configType->GetObject(fullName)) { + errors->Add("Object '" + fullName + "' already exists."); + return false; + } + } + + String path; + + try { + path = GetObjectConfigPath(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 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 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(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 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(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; + } + + String path; + + try { + path = GetObjectConfigPath(object->GetReflectionType(), name); + } catch (const std::exception& ex) { + errors->Add("Config package broken: " + DiagnosticInformation(ex, false)); + return false; + } + + Utility::Remove(path); + + 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..f383a21 --- /dev/null +++ b/lib/remote/configobjectutility.hpp @@ -0,0 +1,46 @@ +/* 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 GetObjectConfigPath(const Type::Ptr& type, const String& fullName); + 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params +) +{ + namespace http = boost::beast::http; + + FilterUtility::CheckPermission(user, "config/query"); + + std::vector 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 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + HttpServerConnection& server + ) override; + +private: + void HandleGet( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + void HandlePost( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + void HandleDelete( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& 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 +#include +#include +#include +#include + +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 ConfigPackageUtility::GetPackages() +{ + String packageDir = GetPackageDir(); + + std::vector 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::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 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::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 ConfigPackageUtility::GetStages(const String& packageName) +{ + std::vector 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 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 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 > ConfigPackageUtility::GetFiles(const String& packageName, const String& stageName) +{ + std::vector > 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 >& 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 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 + +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 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 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::Ptr& resetPackageUpdates); + + static std::vector > 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 >& 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::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 ConfigStagesHandler::m_RunningPackageUpdates (false); + +bool ConfigStagesHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 > 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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::Make([]() { ConfigStagesHandler::m_RunningPackageUpdates.store(false); })); + + std::unique_lock 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 + +namespace icinga +{ + +class ConfigStagesHandler final : public HttpHandler +{ +public: + DECLARE_PTR_TYPEDEFS(ConfigStagesHandler); + + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + HttpServerConnection& server + ) override; + +private: + void HandleGet( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + void HandlePost( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + void HandleDelete( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + + static std::atomic m_RunningPackageUpdates; +}; + +} + +#endif /* CONFIGSTAGESHANDLER_H */ diff --git a/lib/remote/consolehandler.cpp b/lib/remote/consolehandler.cpp new file mode 100644 index 0000000..b790626 --- /dev/null +++ b/lib/remote/consolehandler.cpp @@ -0,0 +1,319 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#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 +#include + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/console", ConsoleHandler); + +static std::mutex l_QueryMutex; +static std::map l_ApiScriptFrames; +static Timer::Ptr l_FrameCleanupTimer; +static std::mutex l_ApiScriptMutex; + +static void ScriptFrameCleanupHandler() +{ + std::unique_lock lock(l_ApiScriptMutex); + + std::vector cleanup_keys; + + typedef std::pair 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 = new Timer(); + 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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"); + + 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& request, + boost::beast::http::response& 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 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& request, + boost::beast::http::response& 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& matches, const String& word, const String& suggestion) +{ + if (suggestion.Find(word) != 0) + return; + + matches.push_back(suggestion); +} + +static void AddSuggestions(std::vector& matches, const String& word, const String& pword, bool withFields, const Value& value) +{ + String prefix; + + if (!pword.IsEmpty()) + prefix = pword + "."; + + if (value.IsObjectType()) { + Dictionary::Ptr dict = value; + + ObjectLock olock(dict); + for (const Dictionary::Pair& kv : dict) { + AddSuggestion(matches, word, prefix + kv.first); + } + } + + if (value.IsObjectType()) { + 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(prototype); + + if (dict) { + ObjectLock olock(dict); + for (const Dictionary::Pair& kv : dict) { + AddSuggestion(matches, word, prefix + kv.first); + } + } + + type = type->GetBaseType(); + } + } +} + +std::vector ConsoleHandler::GetAutocompletionSuggestions(const String& word, ScriptFrame& frame) +{ + std::vector 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 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 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& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + HttpServerConnection& server + ) override; + + static std::vector GetAutocompletionSuggestions(const String& word, ScriptFrame& frame); + +private: + static bool ExecuteScriptHelper(boost::beast::http::request& request, + boost::beast::http::response& response, + const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed); + static bool AutocompleteScriptHelper(boost::beast::http::request& request, + boost::beast::http::response& 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..c01b236 --- /dev/null +++ b/lib/remote/createobjecthandler.cpp @@ -0,0 +1,147 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "remote/createobjecthandler.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 + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/objects", CreateObjectHandler); + +bool CreateObjectHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& 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"); + + /* 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(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& request, + const Url::Ptr& url, + boost::beast::http::response& 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..2edb0e4 --- /dev/null +++ b/lib/remote/deleteobjecthandler.cpp @@ -0,0 +1,115 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "remote/deleteobjecthandler.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 +#include + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/objects", DeleteObjectHandler); + +bool DeleteObjectHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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"); + + 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 Endpoint::OnConnected; +boost::signals2::signal Endpoint::OnDisconnected; + +void Endpoint::OnAllConfigLoaded() +{ + ObjectImpl::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 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 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 Endpoint::GetClients() const +{ + std::unique_lock lock(m_ClientsLock); + return m_Clients; +} + +Zone::Ptr Endpoint::GetZone() const +{ + return m_Zone; +} + +bool Endpoint::GetConnected() const +{ + std::unique_lock 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 + +namespace icinga +{ + +class JsonRpcConnection; +class Zone; + +/** + * An endpoint that can be used to send and receive messages. + * + * @ingroup remote + */ +class Endpoint final : public ObjectImpl +{ +public: + DECLARE_OBJECT(Endpoint); + DECLARE_OBJECTNAME(Endpoint); + + static boost::signals2::signal&)> OnConnected; + static boost::signals2::signal&)> OnDisconnected; + + void AddClient(const intrusive_ptr& client); + void RemoveClient(const intrusive_ptr& client); + std::set > GetClients() const; + + intrusive_ptr GetZone() const; + + bool GetConnected() const override; + + static Endpoint::Ptr GetLocalEndpoint(); + + void SetCachedZone(const intrusive_ptr& 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 > m_Clients; + intrusive_ptr 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 + +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..fd911f0 --- /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 +#include +#include +#include +#include +#include + +using namespace icinga; + +EventQueue::EventQueue(String name) + : m_Name(std::move(name)) +{ } + +bool EventQueue::CanProcessEvent(const String& type) const +{ + std::unique_lock 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 lock(m_Mutex); + + typedef std::pair > 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 lock(m_Mutex); + + auto result = m_Events.insert(std::make_pair(client, std::deque())); + ASSERT(result.second); + +#ifndef I2_DEBUG + (void)result; +#endif /* I2_DEBUG */ +} + +void EventQueue::RemoveClient(void *client) +{ + std::unique_lock lock(m_Mutex); + + m_Events.erase(client); +} + +void EventQueue::UnregisterIfUnused(const String& name, const EventQueue::Ptr& queue) +{ + std::unique_lock lock(queue->m_Mutex); + + if (queue->m_Events.empty()) + Unregister(name); +} + +void EventQueue::SetTypes(const std::set& types) +{ + std::unique_lock lock(m_Mutex); + m_Types = types; +} + +void EventQueue::SetFilter(std::unique_ptr filter) +{ + std::unique_lock lock(m_Mutex); + m_Filter.swap(filter); +} + +Dictionary::Ptr EventQueue::WaitForEvent(void *client, double timeout) +{ + std::unique_lock 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(timeout)) == std::cv_status::timeout) + return nullptr; + } +} + +std::vector EventQueue::GetQueuesForType(const String& type) +{ + EventQueueRegistry::ItemMap queues = EventQueueRegistry::GetInstance()->GetItems(); + + std::vector availQueues; + + typedef std::pair 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::GetInstance(); +} + +std::mutex EventsInbox::m_FiltersMutex; +std::map 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 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 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 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 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 std::move(event); +} + +EventsSubscriber::EventsSubscriber(std::set 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> 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& types, const EventsInbox::Ptr& inbox) +{ + const auto& filter (inbox->GetFilter()); + std::unique_lock 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& types, const EventsInbox::Ptr& inbox) +{ + const auto& filter (inbox->GetFilter()); + std::unique_lock 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 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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& types); + void SetFilter(std::unique_ptr filter); + + Dictionary::Ptr WaitForEvent(void *client, double timeout = 5); + + static std::vector 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 m_Types; + std::unique_ptr m_Filter; + + std::map > m_Events; +}; + +/** + * A registry for API event queues. + * + * @ingroup base + */ +class EventQueueRegistry : public Registry +{ +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 m_Filters; + + std::mutex m_Mutex; + decltype(m_Filters.begin()) m_Filter; + std::queue m_Queue; + boost::asio::deadline_timer m_Timer; +}; + +class EventsSubscriber +{ +public: + EventsSubscriber(std::set 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 m_Types; + EventsInbox::Ptr m_Inbox; +}; + +class EventsFilter +{ +public: + EventsFilter(std::map> inboxes); + + operator bool(); + + void Push(Dictionary::Ptr event); + +private: + std::map> m_Inboxes; +}; + +class EventsRouter +{ +public: + static EventsRouter& GetInstance(); + + void Subscribe(const std::set& types, const EventsInbox::Ptr& inbox); + void Unsubscribe(const std::set& 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>> 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 +#include +#include +#include +#include + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/events", EventsHandler); + +const std::map 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 (""); + +bool EventsHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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..9bcc60f --- /dev/null +++ b/lib/remote/filterutility.cpp @@ -0,0 +1,297 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "remote/filterutility.hpp" +#include "remote/httputility.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 + +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& addTarget) const +{ + Type::Ptr ptype = Type::GetByName(type); + auto *ctype = dynamic_cast(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()); + + 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& 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, 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::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 > args; + args.emplace_back(new GetScopeExpression(ScopeThis)); + std::unique_ptr indexer{new IndexerExpression(std::unique_ptr(MakeLiteral(filter)), std::unique_ptr(MakeLiteral("call")))}; + FunctionCallExpression *fexpr = new FunctionCallExpression(std::move(indexer), std::move(args)); + + if (!*permissionFilter) + *permissionFilter = fexpr; + else + *permissionFilter = new LogicalOrExpression(std::unique_ptr(*permissionFilter), std::unique_ptr(fexpr)); + } + } + } + + if (!foundPermission) { + Log(LogWarning, "FilterUtility") + << "Missing permission: " << requiredPermission; + } + + return foundPermission; +} + +void FilterUtility::CheckPermission(const ApiUser::Ptr& user, const String& permission, Expression **permissionFilter) +{ + if (!HasPermission(user, permission, permissionFilter)) { + BOOST_THROW_EXCEPTION(ScriptError("Missing permission: " + permission.ToLower())); + } +} + +std::vector FilterUtility::GetFilterTargets(const QueryDescription& qd, const Dictionary::Ptr& query, const ApiUser::Ptr& user, const String& variableName) +{ + std::vector result; + + TargetProvider::Ptr provider; + + if (qd.Provider) + provider = qd.Provider; + else + provider = new ConfigObjectTargetProvider(); + + 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, 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, 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 ufilter = ConfigCompiler::CompileText("", filter); + + Dictionary::Ptr filter_vars = query->Get("filter_vars"); + if (filter_vars) { + ObjectLock olock(filter_vars); + for (const Dictionary::Pair& 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, 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, 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..1cebffc --- /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 + +namespace icinga +{ + +class TargetProvider : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(TargetProvider); + + virtual void FindTargets(const String& type, const std::function& 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& 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 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, Expression **filter = nullptr); + static bool HasPermission(const ApiUser::Ptr& user, const String& permission, Expression **permissionFilter = nullptr); + static std::vector 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..e1bb4f4 --- /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 +#include + +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& request, + boost::beast::http::response& response, + boost::asio::yield_context& yc, + HttpServerConnection& server +) +{ + Dictionary::Ptr node = m_UrlTree; + std::vector handlers; + + Url::Ptr url = new Url(request.target().to_string()); + auto& path (url->GetPath()); + + for (std::vector::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 +#include +#include + +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& request, + const Url::Ptr& url, + boost::beast::http::response& 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& request, + boost::beast::http::response& 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..cb07557 --- /dev/null +++ b/lib/remote/httpserverconnection.cpp @@ -0,0 +1,609 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace icinga; + +auto const l_ServerHeader ("Icinga/" + Application::GetAppVersion()); + +HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared::Ptr& stream) + : HttpServerConnection(identity, authenticated, stream, IoEngine::Get().GetIoContext()) +{ +} + +HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared::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& parser, + boost::beast::http::response& 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("

Bad Request

") + errorMsg + "

"; + 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& request, + boost::asio::yield_context& yc +) +{ + namespace http = boost::beast::http; + + if (request[http::field::expect] == "100-continue") { + http::response 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& request, + boost::beast::http::response& 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()); + + if (!allowedOrigins.empty()) { + auto& origin (request[http::field::origin]); + + if (allowedOrigins.find(origin.to_string()) != 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& request, + boost::beast::http::response& 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() = "

Accept header is missing or not set to 'application/json'.

"; + 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& request, + ApiUser::Ptr& authenticatedUser, + boost::beast::http::response& 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() = "

Unauthorized. Please check your user credentials.

"; + 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& parser, + ApiUser::Ptr& authenticatedUser, + boost::beast::http::response& 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()) { + permission = static_cast(permissionInfo)->Get("permission"); + } else { + permission = permissionInfo; + } + + static std::vector> 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: + * + * + */ + + 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("

Bad Request

") + ec.message() + "

"; + 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& request, + ApiUser::Ptr& authenticatedUser, + boost::beast::http::response& 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(&ex)); + + if (sysErr && sysErr->code() == boost::asio::error::operation_aborted) { + throw; + } + + http::response 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; + + 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 parser; + http::response 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& 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(request[http::field::authorization].to_string()); + } + + Log logMsg (LogInformation, "HttpServerConnection"); + + logMsg << "Request: " << request.method_string() << ' ' << request.target() + << " (from " << m_PeerAddress + << "), user: " << (authenticatedUser ? authenticatedUser->GetName() : "") + << ", agent: " << request[http::field::user_agent]; //operator[] - Returns the value for a field, or "" if it does not exist. + + Defer addRespCode ([&response, &logMsg]() { + logMsg << ", status: " << response.result() << ")."; + }); + + 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::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 +#include +#include +#include +#include + +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::Ptr& stream); + + void Start(); + void Disconnect(); + void StartStreaming(); + + bool Disconnected(); + +private: + ApiUser::Ptr m_ApiUser; + Shared::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::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 +#include +#include +#include + +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> 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()) + 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& 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& 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 +#include + +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& response, const Dictionary::Ptr& params, const Value& val); + static void SendJsonError(boost::beast::http::response& 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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::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 = "Icinga 2

Hello from Icinga 2 (Version: " + Application::GetAppVersion() + ")!

"; + body += "

You are authenticated as " + user->GetName() + ". "; + + if (!permInfo.empty()) { + body += "Your user has the following permissions:

    "; + + for (const String& perm : permInfo) { + body += "
  • " + perm + "
  • "; + } + + body += "
"; + } else + body += "Your user does not have any permissions.

"; + + body += R"(

More information about API requests is available in the documentation.

)"; + 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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..5828800 --- /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 +#include +#include +#include + +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::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::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::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::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 std::move(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::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 std::move(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()) { + 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 +#include + +namespace icinga +{ + +/** + * A JSON-RPC connection. + * + * @ingroup remote + */ +class JsonRpc +{ +public: + static size_t SendMessage(const Shared::Ptr& stream, const Dictionary::Ptr& message); + static size_t SendMessage(const Shared::Ptr& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc); + static size_t SendRawMessage(const Shared::Ptr& stream, const String& json, boost::asio::yield_context yc); + + static String ReadMessage(const Shared::Ptr& stream, ssize_t maxMessageLength = -1); + static String ReadMessage(const Shared::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 +#include +#include + +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..d661dcf --- /dev/null +++ b/lib/remote/jsonrpcconnection-pki.cpp @@ -0,0 +1,402 @@ +/* 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/configtype.hpp" +#include "base/objectlock.hpp" +#include "base/utility.hpp" +#include "base/logger.hpp" +#include "base/exception.hpp" +#include "base/convert.hpp" +#include +#include +#include +#include +#include + +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 cert; + + Dictionary::Ptr result = new Dictionary(); + + /* Use the presented client certificate if not provided. */ + if (certText.IsEmpty()) { + auto stream (origin->FromClient->GetStream()); + cert = stream->next_layer().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 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 << " not signed by our CA"; + if (const unsigned long *openssl_code = boost::get_error_info(ex)) { + logmsg << ": " << X509_verify_cert_error_string(long(*openssl_code)) << " (code " << *openssl_code << ")"; + } else { + logmsg << "."; + } + } + } + + if (signedByCA) { + if (IsCertUptodate(cert)) { + + Log(LogInformation, "JsonRpcConnection") + << "The certificate for CN '" << cn << "' is valid and uptodate. Skipping automated renewal."; + result->Set("status_code", 1); + result->Set("error", "The certificate for CN '" + cn + "' is 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 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") } + }); + + 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(fp)), std::istreambuf_iterator()); + fp.close(); + + params->Set("ticket", ticket); + } else { + Dictionary::Ptr request = Utility::LoadJsonFile(path); + + if (request->Contains("cert_response")) + return; + + params->Set("cert_request", request->Get("cert_request")); + params->Set("ticket", request->Get("ticket")); + } + + /* 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 oldCert = GetX509Certificate(listener->GetDefaultCertPath()); + std::shared_ptr 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 oldKey = std::shared_ptr(X509_get_pubkey(oldCert.get()), EVP_PKEY_free); + std::shared_ptr newKey = std::shared_ptr(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 << "'."; + + std::fstream cafp; + String tempCaPath = Utility::CreateTempFile(caPath + ".XXXXXX", 0644, cafp); + cafp << ca; + cafp.close(); + + Utility::RenameFile(tempCaPath, caPath); + + /* Update signed certificate. */ + String certPath = listener->GetDefaultCertPath(); + + Log(LogInformation, "JsonRpcConnection") + << "Updating client certificate for CN '" << cn << "' in '" << certPath << "'."; + + std::fstream certfp; + String tempCertPath = Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp); + certfp << cert; + certfp.close(); + + Utility::RenameFile(tempCertPath, certPath); + + /* 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 +#include +#include +#include +#include +#include +#include + +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::Ptr& stream, ConnectionRole role) + : JsonRpcConnection(identity, authenticated, stream, role, IoEngine::Get().GetIoContext()) +{ +} + +JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated, + const Shared::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::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 +#include +#include +#include +#include + +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::Ptr& stream, ConnectionRole role); + + void Start(); + + double GetTimestamp() const; + String GetIdentity() const; + bool IsAuthenticated() const; + Endpoint::Ptr GetEndpoint() const; + Shared::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& origin, const Dictionary::Ptr& params); + + static double GetWorkQueueRate(); + + static void SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const intrusive_ptr& origin, const String& path); + +private: + String m_Identity; + bool m_Authenticated; + Endpoint::Ptr m_Endpoint; + Shared::Ptr m_Stream; + ConnectionRole m_Role; + double m_Timestamp; + double m_Seen; + double m_NextHeartbeat; + boost::asio::io_context::strand m_IoStrand; + std::vector 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::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..cc008b9 --- /dev/null +++ b/lib/remote/modifyobjecthandler.cpp @@ -0,0 +1,120 @@ +/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ + +#include "remote/modifyobjecthandler.hpp" +#include "remote/httputility.hpp" +#include "remote/filterutility.hpp" +#include "remote/apiaction.hpp" +#include "base/exception.hpp" +#include +#include + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/objects", ModifyObjectHandler); + +bool ModifyObjectHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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) { + 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; + + bool verbose = false; + + if (params) + verbose = HttpUtility::GetLastParameter(params, "verbose"); + + 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 (attrs) { + ObjectLock olock(attrs); + for (const Dictionary::Pair& kv : attrs) { + key = kv.first; + obj->ModifyAttribute(kv.first, kv.second); + } + } + + result1->Set("code", 200); + result1->Set("status", "Attributes updated."); + } 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)); + } + + 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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..f059c03 --- /dev/null +++ b/lib/remote/objectqueryhandler.cpp @@ -0,0 +1,332 @@ +/* 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 +#include +#include + +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 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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 joinAttrs; + std::set 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> typePermissions; + std::unordered_map 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(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(); + Expression::Ptr permissionFilter; + + auto it = typePermissions.find(reflectionType.get()); + bool granted; + + if (it == typePermissions.end()) { + String permission = "objects/query/" + reflectionType->GetName(); + + Expression *filter = nullptr; + granted = FilterUtility::HasPermission(user, permission, &filter); + permissionFilter = filter; + + typePermissions.insert({reflectionType.get(), std::make_pair(granted, permissionFilter)}); + } else { + std::tie(granted, permissionFilter) = it->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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 +#include +#include +#include + +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 pubkey = std::shared_ptr(X509_REQ_get_pubkey(req), EVP_PKEY_free); + std::shared_ptr cert = CreateCertIcingaCA(pubkey.get(), X509_REQ_get_subject_name(req)); + + X509_REQ_free(req); + + WriteCert(cert, certfile); + + return 0; +} + +std::shared_ptr PkiUtility::FetchCert(const String& host, const String& port) +{ + Shared::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(); + } + + auto stream (Shared::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(); + } + + 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(); + } + + Defer shutdown ([&sslConn]() { sslConn.shutdown(); }); + + return sslConn.GetPeerCertificate(); +} + +int PkiUtility::WriteCert(const std::shared_ptr& 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& trustedCert, const String& ticket) +{ + Shared::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::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& 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(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 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(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 +#include + +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 FetchCert(const String& host, const String& port); + static int WriteCert(const std::shared_ptr& 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& trustedcert, + const String& ticket = String()); + static String GetCertificateInformation(const std::shared_ptr& 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& 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 +#include + +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& 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 + +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& 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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(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(field.Attributes & FAConfig) }, + { "state", static_cast(field.Attributes & FAState) }, + { "required", static_cast(field.Attributes & FARequired) }, + { "navigation", static_cast(field.Attributes & FANavigation) }, + { "no_user_modify", static_cast(field.Attributes & FANoUserModify) }, + { "no_user_view", static_cast(field.Attributes & FANoUserView) }, + { "deprecated", static_cast(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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 + +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& Url::GetPath() const +{ + return m_Path; +} + +const std::vector>& 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& path) +{ + m_Path = path; +} + +void Url::SetQuery(const std::vector>& 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 > 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 sep("/"); + boost::tokenizer > 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 sep("&"); + boost::tokenizer > 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 +#include +#include + +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& GetPath() const; + const std::vector>& 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& path); + void SetQuery(const std::vector>& 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 m_Path; + std::vector> 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..aef896e --- /dev/null +++ b/lib/remote/variablequeryhandler.cpp @@ -0,0 +1,118 @@ +/* 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 + +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& addTarget) const override + { + { + Namespace::Ptr globals = ScriptGlobal::GetGlobals(); + ObjectLock olock(globals); + for (const Namespace::Pair& kv : globals) { + addTarget(GetTargetForVar(kv.first, kv.second->Get())); + } + } + } + + 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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 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) { + 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& request, + const Url::Ptr& url, + boost::beast::http::response& 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::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 Zone::GetEndpoints() const +{ + std::set 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::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(object); + else + object_zone = static_pointer_cast(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& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::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 +{ +public: + DECLARE_OBJECT(Zone); + DECLARE_OBJECTNAME(Zone); + + void OnAllConfigLoaded() override; + + Zone::Ptr GetParent() const; + std::set GetEndpoints() const; + std::vector 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& lvalue, const ValidationUtils& utils) override; + +private: + Zone::Ptr m_Parent; + std::vector m_AllParents; +}; + +} + +#endif /* ZONE_H */ diff --git a/lib/remote/zone.ti b/lib/remote/zone.ti new file mode 100644 index 0000000..53fd4e6 --- /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, 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; + }; +}; + +} -- cgit v1.2.3