summaryrefslogtreecommitdiffstats
path: root/lib/icinga
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/icinga/CMakeLists.txt76
-rw-r--r--lib/icinga/apiactions.cpp962
-rw-r--r--lib/icinga/apiactions.hpp42
-rw-r--r--lib/icinga/apievents.cpp438
-rw-r--r--lib/icinga/apievents.hpp51
-rw-r--r--lib/icinga/checkable-check.cpp709
-rw-r--r--lib/icinga/checkable-comment.cpp75
-rw-r--r--lib/icinga/checkable-dependency.cpp176
-rw-r--r--lib/icinga/checkable-downtime.cpp64
-rw-r--r--lib/icinga/checkable-event.cpp81
-rw-r--r--lib/icinga/checkable-flapping.cpp114
-rw-r--r--lib/icinga/checkable-notification.cpp334
-rw-r--r--lib/icinga/checkable-script.cpp28
-rw-r--r--lib/icinga/checkable.cpp322
-rw-r--r--lib/icinga/checkable.hpp264
-rw-r--r--lib/icinga/checkable.ti192
-rw-r--r--lib/icinga/checkcommand.cpp22
-rw-r--r--lib/icinga/checkcommand.hpp32
-rw-r--r--lib/icinga/checkcommand.ti14
-rw-r--r--lib/icinga/checkresult.cpp34
-rw-r--r--lib/icinga/checkresult.hpp28
-rw-r--r--lib/icinga/checkresult.ti72
-rw-r--r--lib/icinga/cib.cpp346
-rw-r--r--lib/icinga/cib.hpp91
-rw-r--r--lib/icinga/clusterevents-check.cpp379
-rw-r--r--lib/icinga/clusterevents.cpp1623
-rw-r--r--lib/icinga/clusterevents.hpp102
-rw-r--r--lib/icinga/command.cpp68
-rw-r--r--lib/icinga/command.hpp30
-rw-r--r--lib/icinga/command.ti54
-rw-r--r--lib/icinga/comment.cpp258
-rw-r--r--lib/icinga/comment.hpp59
-rw-r--r--lib/icinga/comment.ti80
-rw-r--r--lib/icinga/compatutility.cpp302
-rw-r--r--lib/icinga/compatutility.hpp56
-rw-r--r--lib/icinga/customvarobject.cpp49
-rw-r--r--lib/icinga/customvarobject.hpp31
-rw-r--r--lib/icinga/customvarobject.ti15
-rw-r--r--lib/icinga/dependency-apply.cpp161
-rw-r--r--lib/icinga/dependency.cpp325
-rw-r--r--lib/icinga/dependency.hpp62
-rw-r--r--lib/icinga/dependency.ti101
-rw-r--r--lib/icinga/downtime.cpp584
-rw-r--r--lib/icinga/downtime.hpp99
-rw-r--r--lib/icinga/downtime.ti82
-rw-r--r--lib/icinga/envresolver.cpp20
-rw-r--r--lib/icinga/envresolver.hpp30
-rw-r--r--lib/icinga/eventcommand.cpp20
-rw-r--r--lib/icinga/eventcommand.hpp32
-rw-r--r--lib/icinga/eventcommand.ti15
-rw-r--r--lib/icinga/externalcommandprocessor.cpp2281
-rw-r--r--lib/icinga/externalcommandprocessor.hpp169
-rw-r--r--lib/icinga/host.cpp330
-rw-r--r--lib/icinga/host.hpp71
-rw-r--r--lib/icinga/host.ti48
-rw-r--r--lib/icinga/hostgroup.cpp108
-rw-r--r--lib/icinga/hostgroup.hpp43
-rw-r--r--lib/icinga/hostgroup.ti28
-rw-r--r--lib/icinga/i2-icinga.hpp15
-rw-r--r--lib/icinga/icinga-itl.conf15
-rw-r--r--lib/icinga/icingaapplication.cpp321
-rw-r--r--lib/icinga/icingaapplication.hpp52
-rw-r--r--lib/icinga/icingaapplication.ti41
-rw-r--r--lib/icinga/legacytimeperiod.cpp644
-rw-r--r--lib/icinga/legacytimeperiod.hpp45
-rw-r--r--lib/icinga/macroprocessor.cpp585
-rw-r--r--lib/icinga/macroprocessor.hpp75
-rw-r--r--lib/icinga/macroresolver.hpp31
-rw-r--r--lib/icinga/notification-apply.cpp161
-rw-r--r--lib/icinga/notification.cpp812
-rw-r--r--lib/icinga/notification.hpp135
-rw-r--r--lib/icinga/notification.ti111
-rw-r--r--lib/icinga/notificationcommand.cpp27
-rw-r--r--lib/icinga/notificationcommand.hpp36
-rw-r--r--lib/icinga/notificationcommand.ti14
-rw-r--r--lib/icinga/objectutils.cpp55
-rw-r--r--lib/icinga/objectutils.hpp29
-rw-r--r--lib/icinga/pluginutility.cpp218
-rw-r--r--lib/icinga/pluginutility.hpp42
-rw-r--r--lib/icinga/scheduleddowntime-apply.cpp159
-rw-r--r--lib/icinga/scheduleddowntime.cpp393
-rw-r--r--lib/icinga/scheduleddowntime.hpp60
-rw-r--r--lib/icinga/scheduleddowntime.ti76
-rw-r--r--lib/icinga/service-apply.cpp133
-rw-r--r--lib/icinga/service.cpp287
-rw-r--r--lib/icinga/service.hpp65
-rw-r--r--lib/icinga/service.ti71
-rw-r--r--lib/icinga/servicegroup.cpp111
-rw-r--r--lib/icinga/servicegroup.hpp43
-rw-r--r--lib/icinga/servicegroup.ti28
-rw-r--r--lib/icinga/timeperiod.cpp399
-rw-r--r--lib/icinga/timeperiod.hpp50
-rw-r--r--lib/icinga/timeperiod.ti47
-rw-r--r--lib/icinga/user.cpp103
-rw-r--r--lib/icinga/user.hpp44
-rw-r--r--lib/icinga/user.ti47
-rw-r--r--lib/icinga/usergroup.cpp128
-rw-r--r--lib/icinga/usergroup.hpp49
-rw-r--r--lib/icinga/usergroup.ti25
-rw-r--r--lib/icingadb/CMakeLists.txt32
-rw-r--r--lib/icingadb/icingadb-itl.conf24
-rw-r--r--lib/icingadb/icingadb-objects.cpp2966
-rw-r--r--lib/icingadb/icingadb-stats.cpp54
-rw-r--r--lib/icingadb/icingadb-utility.cpp319
-rw-r--r--lib/icingadb/icingadb.cpp311
-rw-r--r--lib/icingadb/icingadb.hpp241
-rw-r--r--lib/icingadb/icingadb.ti63
-rw-r--r--lib/icingadb/icingadbchecktask.cpp513
-rw-r--r--lib/icingadb/icingadbchecktask.hpp29
-rw-r--r--lib/icingadb/redisconnection.cpp773
-rw-r--r--lib/icingadb/redisconnection.hpp678
111 files changed, 24067 insertions, 0 deletions
diff --git a/lib/icinga/CMakeLists.txt b/lib/icinga/CMakeLists.txt
new file mode 100644
index 0000000..62077bc
--- /dev/null
+++ b/lib/icinga/CMakeLists.txt
@@ -0,0 +1,76 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(checkable.ti checkable-ti.cpp checkable-ti.hpp)
+mkclass_target(checkcommand.ti checkcommand-ti.cpp checkcommand-ti.hpp)
+mkclass_target(checkresult.ti checkresult-ti.cpp checkresult-ti.hpp)
+mkclass_target(command.ti command-ti.cpp command-ti.hpp)
+mkclass_target(comment.ti comment-ti.cpp comment-ti.hpp)
+mkclass_target(dependency.ti dependency-ti.cpp dependency-ti.hpp)
+mkclass_target(downtime.ti downtime-ti.cpp downtime-ti.hpp)
+mkclass_target(eventcommand.ti eventcommand-ti.cpp eventcommand-ti.hpp)
+mkclass_target(hostgroup.ti hostgroup-ti.cpp hostgroup-ti.hpp)
+mkclass_target(host.ti host-ti.cpp host-ti.hpp)
+mkclass_target(icingaapplication.ti icingaapplication-ti.cpp icingaapplication-ti.hpp)
+mkclass_target(customvarobject.ti customvarobject-ti.cpp customvarobject-ti.hpp)
+mkclass_target(notificationcommand.ti notificationcommand-ti.cpp notificationcommand-ti.hpp)
+mkclass_target(notification.ti notification-ti.cpp notification-ti.hpp)
+mkclass_target(scheduleddowntime.ti scheduleddowntime-ti.cpp scheduleddowntime-ti.hpp)
+mkclass_target(servicegroup.ti servicegroup-ti.cpp servicegroup-ti.hpp)
+mkclass_target(service.ti service-ti.cpp service-ti.hpp)
+mkclass_target(timeperiod.ti timeperiod-ti.cpp timeperiod-ti.hpp)
+mkclass_target(usergroup.ti usergroup-ti.cpp usergroup-ti.hpp)
+mkclass_target(user.ti user-ti.cpp user-ti.hpp)
+
+mkembedconfig_target(icinga-itl.conf icinga-itl.cpp)
+
+set(icinga_SOURCES
+ i2-icinga.hpp icinga-itl.cpp
+ apiactions.cpp apiactions.hpp
+ apievents.cpp apievents.hpp
+ checkable.cpp checkable.hpp checkable-ti.hpp
+ checkable-check.cpp checkable-comment.cpp checkable-dependency.cpp
+ checkable-downtime.cpp checkable-event.cpp checkable-flapping.cpp
+ checkable-notification.cpp checkable-script.cpp
+ checkcommand.cpp checkcommand.hpp checkcommand-ti.hpp
+ checkresult.cpp checkresult.hpp checkresult-ti.hpp
+ cib.cpp cib.hpp
+ clusterevents.cpp clusterevents.hpp clusterevents-check.cpp
+ command.cpp command.hpp command-ti.hpp
+ comment.cpp comment.hpp comment-ti.hpp
+ compatutility.cpp compatutility.hpp
+ customvarobject.cpp customvarobject.hpp customvarobject-ti.hpp
+ dependency.cpp dependency.hpp dependency-ti.hpp dependency-apply.cpp
+ downtime.cpp downtime.hpp downtime-ti.hpp
+ envresolver.cpp envresolver.hpp
+ eventcommand.cpp eventcommand.hpp eventcommand-ti.hpp
+ externalcommandprocessor.cpp externalcommandprocessor.hpp
+ host.cpp host.hpp host-ti.hpp
+ hostgroup.cpp hostgroup.hpp hostgroup-ti.hpp
+ icingaapplication.cpp icingaapplication.hpp icingaapplication-ti.hpp
+ legacytimeperiod.cpp legacytimeperiod.hpp
+ macroprocessor.cpp macroprocessor.hpp
+ macroresolver.hpp
+ notification.cpp notification.hpp notification-ti.hpp notification-apply.cpp
+ notificationcommand.cpp notificationcommand.hpp notificationcommand-ti.hpp
+ objectutils.cpp objectutils.hpp
+ pluginutility.cpp pluginutility.hpp
+ scheduleddowntime.cpp scheduleddowntime.hpp scheduleddowntime-ti.hpp scheduleddowntime-apply.cpp
+ service.cpp service.hpp service-ti.hpp service-apply.cpp
+ servicegroup.cpp servicegroup.hpp servicegroup-ti.hpp
+ timeperiod.cpp timeperiod.hpp timeperiod-ti.hpp
+ user.cpp user.hpp user-ti.hpp
+ usergroup.cpp usergroup.hpp usergroup-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(icinga icinga icinga_SOURCES)
+endif()
+
+add_library(icinga OBJECT ${icinga_SOURCES})
+
+add_dependencies(icinga base config remote)
+
+set_target_properties (
+ icinga PROPERTIES
+ FOLDER Lib
+)
diff --git a/lib/icinga/apiactions.cpp b/lib/icinga/apiactions.cpp
new file mode 100644
index 0000000..885834e
--- /dev/null
+++ b/lib/icinga/apiactions.cpp
@@ -0,0 +1,962 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/apiactions.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/service.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/clusterevents.hpp"
+#include "remote/apiaction.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/pkiutility.hpp"
+#include "remote/httputility.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/defer.hpp"
+#include "remote/actionshandler.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+REGISTER_APIACTION(process_check_result, "Service;Host", &ApiActions::ProcessCheckResult);
+REGISTER_APIACTION(reschedule_check, "Service;Host", &ApiActions::RescheduleCheck);
+REGISTER_APIACTION(send_custom_notification, "Service;Host", &ApiActions::SendCustomNotification);
+REGISTER_APIACTION(delay_notification, "Service;Host", &ApiActions::DelayNotification);
+REGISTER_APIACTION(acknowledge_problem, "Service;Host", &ApiActions::AcknowledgeProblem);
+REGISTER_APIACTION(remove_acknowledgement, "Service;Host", &ApiActions::RemoveAcknowledgement);
+REGISTER_APIACTION(add_comment, "Service;Host", &ApiActions::AddComment);
+REGISTER_APIACTION(remove_comment, "Service;Host;Comment", &ApiActions::RemoveComment);
+REGISTER_APIACTION(schedule_downtime, "Service;Host", &ApiActions::ScheduleDowntime);
+REGISTER_APIACTION(remove_downtime, "Service;Host;Downtime", &ApiActions::RemoveDowntime);
+REGISTER_APIACTION(shutdown_process, "", &ApiActions::ShutdownProcess);
+REGISTER_APIACTION(restart_process, "", &ApiActions::RestartProcess);
+REGISTER_APIACTION(generate_ticket, "", &ApiActions::GenerateTicket);
+REGISTER_APIACTION(execute_command, "Service;Host", &ApiActions::ExecuteCommand);
+
+Dictionary::Ptr ApiActions::CreateResult(int code, const String& status,
+ const Dictionary::Ptr& additional)
+{
+ Dictionary::Ptr result = new Dictionary({
+ { "code", code },
+ { "status", status }
+ });
+
+ if (additional)
+ additional->CopyTo(result);
+
+ return result;
+}
+
+Dictionary::Ptr ApiActions::ProcessCheckResult(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ using Result = Checkable::ProcessingResult;
+
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404,
+ "Cannot process passive check result for non-existent object.");
+
+ if (!checkable->GetEnablePassiveChecks())
+ return ApiActions::CreateResult(403, "Passive checks are disabled for object '" + checkable->GetName() + "'.");
+
+ if (!checkable->IsReachable(DependencyCheckExecution))
+ return ApiActions::CreateResult(200, "Ignoring passive check result for unreachable object '" + checkable->GetName() + "'.");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ if (!params->Contains("exit_status"))
+ return ApiActions::CreateResult(400, "Parameter 'exit_status' is required.");
+
+ int exitStatus = HttpUtility::GetLastParameter(params, "exit_status");
+
+ ServiceState state;
+
+ if (!service) {
+ if (exitStatus == 0)
+ state = ServiceOK;
+ else if (exitStatus == 1)
+ state = ServiceCritical;
+ else
+ return ApiActions::CreateResult(400, "Invalid 'exit_status' for Host "
+ + checkable->GetName() + ".");
+ } else {
+ state = PluginUtility::ExitStatusToState(exitStatus);
+ }
+
+ if (!params->Contains("plugin_output"))
+ return ApiActions::CreateResult(400, "Parameter 'plugin_output' is required");
+
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetOutput(HttpUtility::GetLastParameter(params, "plugin_output"));
+ cr->SetState(state);
+
+ if (params->Contains("execution_start"))
+ cr->SetExecutionStart(HttpUtility::GetLastParameter(params, "execution_start"));
+
+ if (params->Contains("execution_end"))
+ cr->SetExecutionEnd(HttpUtility::GetLastParameter(params, "execution_end"));
+
+ cr->SetCheckSource(HttpUtility::GetLastParameter(params, "check_source"));
+ cr->SetSchedulingSource(HttpUtility::GetLastParameter(params, "scheduling_source"));
+
+ Value perfData = params->Get("performance_data");
+
+ /* Allow to pass a performance data string from Icinga Web 2 next to the new Array notation. */
+ if (perfData.IsString())
+ cr->SetPerformanceData(PluginUtility::SplitPerfdata(perfData));
+ else
+ cr->SetPerformanceData(perfData);
+
+ cr->SetCommand(params->Get("check_command"));
+
+ /* Mark this check result as passive. */
+ cr->SetActive(false);
+
+ /* Result TTL allows to overrule the next expected freshness check. */
+ if (params->Contains("ttl"))
+ cr->SetTtl(HttpUtility::GetLastParameter(params, "ttl"));
+
+ Result result = checkable->ProcessCheckResult(cr);
+ switch (result) {
+ case Result::Ok:
+ return ApiActions::CreateResult(200, "Successfully processed check result for object '" + checkable->GetName() + "'.");
+ case Result::NoCheckResult:
+ return ApiActions::CreateResult(400, "Could not process check result for object '" + checkable->GetName() + "' because no check result was passed.");
+ case Result::CheckableInactive:
+ return ApiActions::CreateResult(503, "Could not process check result for object '" + checkable->GetName() + "' because the object is inactive.");
+ case Result::NewerCheckResultPresent:
+ return ApiActions::CreateResult(409, "Newer check result already present. Check result for '" + checkable->GetName() + "' was discarded.");
+ }
+
+ return ApiActions::CreateResult(500, "Unexpected result (" + std::to_string(static_cast<int>(result)) + ") for object '" + checkable->GetName() + "'. Please submit a bug report at https://github.com/Icinga/icinga2");
+}
+
+Dictionary::Ptr ApiActions::RescheduleCheck(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot reschedule check for non-existent object.");
+
+ if (Convert::ToBool(HttpUtility::GetLastParameter(params, "force")))
+ checkable->SetForceNextCheck(true);
+
+ double nextCheck;
+ if (params->Contains("next_check"))
+ nextCheck = HttpUtility::GetLastParameter(params, "next_check");
+ else
+ nextCheck = Utility::GetTime();
+
+ checkable->SetNextCheck(nextCheck);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(checkable);
+
+ return ApiActions::CreateResult(200, "Successfully rescheduled check for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::SendCustomNotification(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot send notification for non-existent object.");
+
+ if (!params->Contains("author"))
+ return ApiActions::CreateResult(400, "Parameter 'author' is required.");
+
+ if (!params->Contains("comment"))
+ return ApiActions::CreateResult(400, "Parameter 'comment' is required.");
+
+ if (Convert::ToBool(HttpUtility::GetLastParameter(params, "force")))
+ checkable->SetForceNextNotification(true);
+
+ Checkable::OnNotificationsRequested(checkable, NotificationCustom, checkable->GetLastCheckResult(),
+ HttpUtility::GetLastParameter(params, "author"), HttpUtility::GetLastParameter(params, "comment"), nullptr);
+
+ return ApiActions::CreateResult(200, "Successfully sent custom notification for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::DelayNotification(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot delay notifications for non-existent object");
+
+ if (!params->Contains("timestamp"))
+ return ApiActions::CreateResult(400, "A timestamp is required to delay notifications");
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ notification->SetNextNotification(HttpUtility::GetLastParameter(params, "timestamp"));
+ }
+
+ return ApiActions::CreateResult(200, "Successfully delayed notifications for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::AcknowledgeProblem(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot acknowledge problem for non-existent object.");
+
+ if (!params->Contains("author") || !params->Contains("comment"))
+ return ApiActions::CreateResult(400, "Acknowledgements require author and comment.");
+
+ AcknowledgementType sticky = AcknowledgementNormal;
+ bool notify = false;
+ bool persistent = false;
+ double timestamp = 0.0;
+
+ if (params->Contains("sticky") && HttpUtility::GetLastParameter(params, "sticky"))
+ sticky = AcknowledgementSticky;
+ if (params->Contains("notify"))
+ notify = HttpUtility::GetLastParameter(params, "notify");
+ if (params->Contains("persistent"))
+ persistent = HttpUtility::GetLastParameter(params, "persistent");
+ if (params->Contains("expiry")) {
+ timestamp = HttpUtility::GetLastParameter(params, "expiry");
+
+ if (timestamp <= Utility::GetTime())
+ return ApiActions::CreateResult(409, "Acknowledgement 'expiry' timestamp must be in the future for object " + checkable->GetName());
+ } else
+ timestamp = 0;
+
+ ObjectLock oLock (checkable);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ if (!service) {
+ if (host->GetState() == HostUp)
+ return ApiActions::CreateResult(409, "Host " + checkable->GetName() + " is UP.");
+ } else {
+ if (service->GetState() == ServiceOK)
+ return ApiActions::CreateResult(409, "Service " + checkable->GetName() + " is OK.");
+ }
+
+ if (checkable->IsAcknowledged()) {
+ return ApiActions::CreateResult(409, (service ? "Service " : "Host ") + checkable->GetName() + " is already acknowledged.");
+ }
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ Comment::AddComment(checkable, CommentAcknowledgement, HttpUtility::GetLastParameter(params, "author"),
+ HttpUtility::GetLastParameter(params, "comment"), persistent, timestamp, sticky == AcknowledgementSticky);
+ checkable->AcknowledgeProblem(HttpUtility::GetLastParameter(params, "author"),
+ HttpUtility::GetLastParameter(params, "comment"), sticky, notify, persistent, Utility::GetTime(), timestamp);
+
+ return ApiActions::CreateResult(200, "Successfully acknowledged problem for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::RemoveAcknowledgement(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404,
+ "Cannot remove acknowledgement for non-existent checkable object "
+ + object->GetName() + ".");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ String removedBy (HttpUtility::GetLastParameter(params, "author"));
+
+ checkable->ClearAcknowledgement(removedBy);
+ checkable->RemoveAckComments(removedBy);
+
+ return ApiActions::CreateResult(200, "Successfully removed acknowledgement for object '" + checkable->GetName() + "'.");
+}
+
+Dictionary::Ptr ApiActions::AddComment(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Cannot add comment for non-existent object");
+
+ if (!params->Contains("author") || !params->Contains("comment"))
+ return ApiActions::CreateResult(400, "Comments require author and comment.");
+
+ double timestamp = 0.0;
+
+ if (params->Contains("expiry")) {
+ timestamp = HttpUtility::GetLastParameter(params, "expiry");
+ }
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ String commentName = Comment::AddComment(checkable, CommentUser,
+ HttpUtility::GetLastParameter(params, "author"),
+ HttpUtility::GetLastParameter(params, "comment"), false, timestamp);
+
+ Comment::Ptr comment = Comment::GetByName(commentName);
+
+ Dictionary::Ptr additional = new Dictionary({
+ { "name", commentName },
+ { "legacy_id", comment->GetLegacyId() }
+ });
+
+ return ApiActions::CreateResult(200, "Successfully added comment '"
+ + commentName + "' for object '" + checkable->GetName()
+ + "'.", additional);
+}
+
+Dictionary::Ptr ApiActions::RemoveComment(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ auto author (HttpUtility::GetLastParameter(params, "author"));
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+
+ if (checkable) {
+ std::set<Comment::Ptr> comments = checkable->GetComments();
+
+ for (const Comment::Ptr& comment : comments) {
+ Comment::RemoveComment(comment->GetName(), true, author);
+ }
+
+ return ApiActions::CreateResult(200, "Successfully removed all comments for object '" + checkable->GetName() + "'.");
+ }
+
+ Comment::Ptr comment = static_pointer_cast<Comment>(object);
+
+ if (!comment)
+ return ApiActions::CreateResult(404, "Cannot remove non-existent comment object.");
+
+ String commentName = comment->GetName();
+
+ Comment::RemoveComment(commentName, true, author);
+
+ return ApiActions::CreateResult(200, "Successfully removed comment '" + commentName + "'.");
+}
+
+Dictionary::Ptr ApiActions::ScheduleDowntime(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Can't schedule downtime for non-existent object.");
+
+ if (!params->Contains("start_time") || !params->Contains("end_time") ||
+ !params->Contains("author") || !params->Contains("comment")) {
+
+ return ApiActions::CreateResult(400, "Options 'start_time', 'end_time', 'author' and 'comment' are required");
+ }
+
+ bool fixed = true;
+ if (params->Contains("fixed"))
+ fixed = HttpUtility::GetLastParameter(params, "fixed");
+
+ if (!fixed && !params->Contains("duration"))
+ return ApiActions::CreateResult(400, "Option 'duration' is required for flexible downtime");
+
+ double duration = 0.0;
+ if (params->Contains("duration"))
+ duration = HttpUtility::GetLastParameter(params, "duration");
+
+ String triggerName;
+ if (params->Contains("trigger_name"))
+ triggerName = HttpUtility::GetLastParameter(params, "trigger_name");
+
+ String author = HttpUtility::GetLastParameter(params, "author");
+ String comment = HttpUtility::GetLastParameter(params, "comment");
+ double startTime = HttpUtility::GetLastParameter(params, "start_time");
+ double endTime = HttpUtility::GetLastParameter(params, "end_time");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ DowntimeChildOptions childOptions = DowntimeNoChildren;
+ if (params->Contains("child_options")) {
+ try {
+ childOptions = Downtime::ChildOptionsFromValue(HttpUtility::GetLastParameter(params, "child_options"));
+ } catch (const std::exception&) {
+ return ApiActions::CreateResult(400, "Option 'child_options' provided an invalid value.");
+ }
+ }
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ Downtime::Ptr downtime = Downtime::AddDowntime(checkable, author, comment, startTime, endTime,
+ fixed, triggerName, duration);
+ String downtimeName = downtime->GetName();
+
+ Dictionary::Ptr additional = new Dictionary({
+ { "name", downtimeName },
+ { "legacy_id", downtime->GetLegacyId() }
+ });
+
+ /* Schedule downtime for all services for the host type. */
+ bool allServices = false;
+
+ if (params->Contains("all_services"))
+ allServices = HttpUtility::GetLastParameter(params, "all_services");
+
+ if (allServices && !service) {
+ ArrayData serviceDowntimes;
+
+ for (const Service::Ptr& hostService : host->GetServices()) {
+ Log(LogNotice, "ApiActions")
+ << "Creating downtime for service " << hostService->GetName() << " on host " << host->GetName();
+
+ Downtime::Ptr serviceDowntime = Downtime::AddDowntime(hostService, author, comment, startTime, endTime,
+ fixed, triggerName, duration, String(), String(), downtimeName);
+ String serviceDowntimeName = serviceDowntime->GetName();
+
+ serviceDowntimes.push_back(new Dictionary({
+ { "name", serviceDowntimeName },
+ { "legacy_id", serviceDowntime->GetLegacyId() }
+ }));
+ }
+
+ additional->Set("service_downtimes", new Array(std::move(serviceDowntimes)));
+ }
+
+ /* Schedule downtime for all child objects. */
+ if (childOptions != DowntimeNoChildren) {
+ /* 'DowntimeTriggeredChildren' schedules child downtimes triggered by the parent downtime.
+ * 'DowntimeNonTriggeredChildren' schedules non-triggered downtimes for all children.
+ */
+ if (childOptions == DowntimeTriggeredChildren)
+ triggerName = downtimeName;
+
+ Log(LogNotice, "ApiActions")
+ << "Processing child options " << childOptions << " for downtime " << downtimeName;
+
+ ArrayData childDowntimes;
+
+ std::set<Checkable::Ptr> allChildren = checkable->GetAllChildren();
+ for (const Checkable::Ptr& child : allChildren) {
+ Host::Ptr childHost;
+ Service::Ptr childService;
+ tie(childHost, childService) = GetHostService(child);
+
+ if (allServices && childService &&
+ allChildren.find(static_pointer_cast<Checkable>(childHost)) != allChildren.end()) {
+ /* When scheduling downtimes for all service and all children, the current child is a service, and its
+ * host is also a child, skip it here. The downtime for this service will be scheduled below together
+ * with the downtimes of all services for that host. Scheduling it below ensures that the relation
+ * from the child service downtime to the child host downtime is set properly. */
+ continue;
+ }
+
+ Log(LogNotice, "ApiActions")
+ << "Scheduling downtime for child object " << child->GetName();
+
+ Downtime::Ptr childDowntime = Downtime::AddDowntime(child, author, comment, startTime, endTime,
+ fixed, triggerName, duration);
+ String childDowntimeName = childDowntime->GetName();
+
+ Log(LogNotice, "ApiActions")
+ << "Add child downtime '" << childDowntimeName << "'.";
+
+ Dictionary::Ptr childAdditional = new Dictionary({
+ { "name", childDowntimeName },
+ { "legacy_id", childDowntime->GetLegacyId() }
+ });
+
+ /* For a host, also schedule all service downtimes if requested. */
+ if (allServices && !childService) {
+ ArrayData childServiceDowntimes;
+
+ for (const Service::Ptr& childService : childHost->GetServices()) {
+ Log(LogNotice, "ApiActions")
+ << "Creating downtime for service " << childService->GetName() << " on child host " << childHost->GetName();
+
+ Downtime::Ptr serviceDowntime = Downtime::AddDowntime(childService, author, comment, startTime, endTime,
+ fixed, triggerName, duration, String(), String(), childDowntimeName);
+ String serviceDowntimeName = serviceDowntime->GetName();
+
+ childServiceDowntimes.push_back(new Dictionary({
+ { "name", serviceDowntimeName },
+ { "legacy_id", serviceDowntime->GetLegacyId() }
+ }));
+ }
+
+ childAdditional->Set("service_downtimes", new Array(std::move(childServiceDowntimes)));
+ }
+
+ childDowntimes.push_back(childAdditional);
+ }
+
+ additional->Set("child_downtimes", new Array(std::move(childDowntimes)));
+ }
+
+ return ApiActions::CreateResult(200, "Successfully scheduled downtime '" +
+ downtimeName + "' for object '" + checkable->GetName() + "'.", additional);
+}
+
+Dictionary::Ptr ApiActions::RemoveDowntime(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ return ApiActions::CreateResult(503, "Icinga is reloading.");
+ }
+
+ auto author (HttpUtility::GetLastParameter(params, "author"));
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+
+ size_t childCount = 0;
+
+ if (checkable) {
+ std::set<Downtime::Ptr> downtimes = checkable->GetDowntimes();
+
+ for (const Downtime::Ptr& downtime : downtimes) {
+ childCount += downtime->GetChildren().size();
+
+ try {
+ Downtime::RemoveDowntime(downtime->GetName(), true, true, false, author);
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ApiActions") << error.what();
+
+ return ApiActions::CreateResult(400, error.what());
+ }
+ }
+
+ return ApiActions::CreateResult(200, "Successfully removed all downtimes for object '" +
+ checkable->GetName() + "' and " + std::to_string(childCount) + " child downtimes.");
+ }
+
+ Downtime::Ptr downtime = static_pointer_cast<Downtime>(object);
+
+ if (!downtime)
+ return ApiActions::CreateResult(404, "Cannot remove non-existent downtime object.");
+
+ childCount += downtime->GetChildren().size();
+
+ try {
+ String downtimeName = downtime->GetName();
+ Downtime::RemoveDowntime(downtimeName, true, true, false, author);
+
+ return ApiActions::CreateResult(200, "Successfully removed downtime '" + downtimeName +
+ "' and " + std::to_string(childCount) + " child downtimes.");
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ApiActions") << error.what();
+
+ return ApiActions::CreateResult(400, error.what());
+ }
+}
+
+Dictionary::Ptr ApiActions::ShutdownProcess(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Application::RequestShutdown();
+
+ return ApiActions::CreateResult(200, "Shutting down Icinga 2.");
+}
+
+Dictionary::Ptr ApiActions::RestartProcess(const ConfigObject::Ptr& object,
+ const Dictionary::Ptr& params)
+{
+ Application::RequestRestart();
+
+ return ApiActions::CreateResult(200, "Restarting Icinga 2.");
+}
+
+Dictionary::Ptr ApiActions::GenerateTicket(const ConfigObject::Ptr&,
+ const Dictionary::Ptr& params)
+{
+ if (!params->Contains("cn"))
+ return ApiActions::CreateResult(400, "Option 'cn' is required");
+
+ String cn = HttpUtility::GetLastParameter(params, "cn");
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+ String salt = listener->GetTicketSalt();
+
+ if (salt.IsEmpty())
+ return ApiActions::CreateResult(500, "Ticket salt is not configured in ApiListener object");
+
+ String ticket = PBKDF2_SHA1(cn, salt, 50000);
+
+ Dictionary::Ptr additional = new Dictionary({
+ { "ticket", ticket }
+ });
+
+ return ApiActions::CreateResult(200, "Generated PKI ticket '" + ticket + "' for common name '"
+ + cn + "'.", additional);
+}
+
+Value ApiActions::GetSingleObjectByNameUsingPermissions(const String& type, const String& objectName, const ApiUser::Ptr& user)
+{
+ Dictionary::Ptr queryParams = new Dictionary();
+ queryParams->Set("type", type);
+ queryParams->Set(type.ToLower(), objectName);
+
+ QueryDescription qd;
+ qd.Types.insert(type);
+ qd.Permission = "objects/query/" + type;
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, queryParams, user);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ApiActions") << DiagnosticInformation(ex);
+ return nullptr;
+ }
+
+ if (objs.empty())
+ return nullptr;
+
+ return objs.at(0);
+};
+
+Dictionary::Ptr ApiActions::ExecuteCommand(const ConfigObject::Ptr& object, const Dictionary::Ptr& params)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("No ApiListener instance configured."));
+
+ /* Get command_type */
+ String command_type = "EventCommand";
+
+ if (params->Contains("command_type"))
+ command_type = HttpUtility::GetLastParameter(params, "command_type");
+
+ /* Validate command_type */
+ if (command_type != "EventCommand" && command_type != "CheckCommand" && command_type != "NotificationCommand")
+ return ApiActions::CreateResult(400, "Invalid command_type '" + command_type + "'.");
+
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+
+ if (!checkable)
+ return ApiActions::CreateResult(404, "Can't start a command execution for a non-existent object.");
+
+ /* Get TTL param */
+ if (!params->Contains("ttl"))
+ return ApiActions::CreateResult(400, "Parameter ttl is required.");
+
+ double ttl = HttpUtility::GetLastParameter(params, "ttl");
+
+ if (ttl <= 0)
+ return ApiActions::CreateResult(400, "Parameter ttl must be greater than 0.");
+
+ ObjectLock oLock (checkable);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ String endpoint = "$command_endpoint$";
+
+ if (params->Contains("endpoint"))
+ endpoint = HttpUtility::GetLastParameter(params, "endpoint");
+
+ MacroProcessor::ResolverList resolvers;
+ Value macros;
+
+ if (params->Contains("macros")) {
+ macros = HttpUtility::GetLastParameter(params, "macros");
+ if (macros.IsObjectType<Dictionary>()) {
+ resolvers.emplace_back("override", macros);
+ } else {
+ return ApiActions::CreateResult(400, "Parameter macros must be a dictionary.");
+ }
+ }
+
+ if (service)
+ resolvers.emplace_back("service", service);
+
+ resolvers.emplace_back("host", host);
+
+ String resolved_endpoint = MacroProcessor::ResolveMacros(
+ endpoint, resolvers, checkable->GetLastCheckResult(),
+ nullptr, MacroProcessor::EscapeCallback(), nullptr, false
+ );
+
+ if (!ActionsHandler::AuthenticatedApiUser)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Can't find API user."));
+
+ /* Get endpoint */
+ Endpoint::Ptr endpointPtr = GetSingleObjectByNameUsingPermissions(Endpoint::GetTypeName(), resolved_endpoint, ActionsHandler::AuthenticatedApiUser);
+
+ if (!endpointPtr)
+ return ApiActions::CreateResult(404, "Can't find a valid endpoint for '" + resolved_endpoint + "'.");
+
+ /* Return an error when
+ * the endpoint is different from the command endpoint of the checkable
+ * and the endpoint zone can't access the checkable.
+ * The endpoints are checked to allow for the case where command_endpoint is specified in the checkable
+ * but checkable is not actually present in the agent.
+ */
+ Zone::Ptr endpointZone = endpointPtr->GetZone();
+ Endpoint::Ptr commandEndpoint = checkable->GetCommandEndpoint();
+ if (endpointPtr != commandEndpoint && !endpointZone->CanAccessObject(checkable)) {
+ return ApiActions::CreateResult(
+ 409,
+ "Zone '" + endpointZone->GetName() + "' cannot access checkable '" + checkable->GetName() + "'."
+ );
+ }
+
+ /* Get command */
+ String command;
+
+ if (!params->Contains("command")) {
+ if (command_type == "CheckCommand" ) {
+ command = "$check_command$";
+ } else if (command_type == "EventCommand") {
+ command = "$event_command$";
+ } else if (command_type == "NotificationCommand") {
+ command = "$notification_command$";
+ }
+ } else {
+ command = HttpUtility::GetLastParameter(params, "command");
+ }
+
+ /* Resolve command macro */
+ String resolved_command = MacroProcessor::ResolveMacros(
+ command, resolvers, checkable->GetLastCheckResult(), nullptr,
+ MacroProcessor::EscapeCallback(), nullptr, false
+ );
+
+ CheckResult::Ptr cr = checkable->GetLastCheckResult();
+
+ if (!cr)
+ cr = new CheckResult();
+
+ /* Check if resolved_command exists and it is of type command_type */
+ Dictionary::Ptr execMacros = new Dictionary();
+
+ MacroResolver::OverrideMacros = macros;
+ Defer o ([]() {
+ MacroResolver::OverrideMacros = nullptr;
+ });
+
+ /* Create execution parameters */
+ Dictionary::Ptr execParams = new Dictionary();
+
+ if (command_type == "CheckCommand") {
+ CheckCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(CheckCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser);
+
+ if (!cmd)
+ return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'.");
+ else {
+ CheckCommand::ExecuteOverride = cmd;
+ Defer resetCheckCommandOverride([]() {
+ CheckCommand::ExecuteOverride = nullptr;
+ });
+ cmd->Execute(checkable, cr, execMacros, false);
+ }
+ } else if (command_type == "EventCommand") {
+ EventCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(EventCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser);
+
+ if (!cmd)
+ return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'.");
+ else {
+ EventCommand::ExecuteOverride = cmd;
+ Defer resetEventCommandOverride ([]() {
+ EventCommand::ExecuteOverride = nullptr;
+ });
+ cmd->Execute(checkable, execMacros, false);
+ }
+ } else if (command_type == "NotificationCommand") {
+ NotificationCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(NotificationCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser);
+
+ if (!cmd)
+ return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'.");
+ else {
+ /* Get user */
+ String user_string = "";
+
+ if (params->Contains("user"))
+ user_string = HttpUtility::GetLastParameter(params, "user");
+
+ /* Resolve user macro */
+ String resolved_user = MacroProcessor::ResolveMacros(
+ user_string, resolvers, checkable->GetLastCheckResult(), nullptr,
+ MacroProcessor::EscapeCallback(), nullptr, false
+ );
+
+ User::Ptr user = GetSingleObjectByNameUsingPermissions(User::GetTypeName(), resolved_user, ActionsHandler::AuthenticatedApiUser);
+
+ if (!user)
+ return ApiActions::CreateResult(404, "Can't find a valid user for '" + resolved_user + "'.");
+
+ execParams->Set("user", user->GetName());
+
+ /* Get notification */
+ String notification_string = "";
+
+ if (params->Contains("notification"))
+ notification_string = HttpUtility::GetLastParameter(params, "notification");
+
+ /* Resolve notification macro */
+ String resolved_notification = MacroProcessor::ResolveMacros(
+ notification_string, resolvers, checkable->GetLastCheckResult(), nullptr,
+ MacroProcessor::EscapeCallback(), nullptr, false
+ );
+
+ Notification::Ptr notification = GetSingleObjectByNameUsingPermissions(Notification::GetTypeName(), resolved_notification, ActionsHandler::AuthenticatedApiUser);
+
+ if (!notification)
+ return ApiActions::CreateResult(404, "Can't find a valid notification for '" + resolved_notification + "'.");
+
+ execParams->Set("notification", notification->GetName());
+
+ NotificationCommand::ExecuteOverride = cmd;
+ Defer resetNotificationCommandOverride ([]() {
+ NotificationCommand::ExecuteOverride = nullptr;
+ });
+
+ cmd->Execute(notification, user, cr, NotificationType::NotificationCustom,
+ ActionsHandler::AuthenticatedApiUser->GetName(), "", execMacros, false);
+ }
+ }
+
+ /* This generates a UUID */
+ String uuid = Utility::NewUniqueID();
+
+ /* Create the deadline */
+ double deadline = Utility::GetTime() + ttl;
+
+ /* Update executions */
+ Dictionary::Ptr pending_execution = new Dictionary();
+ pending_execution->Set("pending", true);
+ pending_execution->Set("deadline", deadline);
+ pending_execution->Set("endpoint", resolved_endpoint);
+ Dictionary::Ptr executions = checkable->GetExecutions();
+
+ if (!executions)
+ executions = new Dictionary();
+
+ executions->Set(uuid, pending_execution);
+ checkable->SetExecutions(executions);
+
+ /* Broadcast the update */
+ Dictionary::Ptr executionsToBroadcast = new Dictionary();
+ executionsToBroadcast->Set(uuid, pending_execution);
+ Dictionary::Ptr updateParams = new Dictionary();
+ updateParams->Set("host", host->GetName());
+
+ if (service)
+ updateParams->Set("service", service->GetShortName());
+
+ updateParams->Set("executions", executionsToBroadcast);
+
+ Dictionary::Ptr updateMessage = new Dictionary();
+ updateMessage->Set("jsonrpc", "2.0");
+ updateMessage->Set("method", "event::UpdateExecutions");
+ updateMessage->Set("params", updateParams);
+
+ MessageOrigin::Ptr origin = new MessageOrigin();
+ listener->RelayMessage(origin, checkable, updateMessage, true);
+
+ /* Populate execution parameters */
+ if (command_type == "CheckCommand")
+ execParams->Set("command_type", "check_command");
+ else if (command_type == "EventCommand")
+ execParams->Set("command_type", "event_command");
+ else if (command_type == "NotificationCommand")
+ execParams->Set("command_type", "notification_command");
+
+ execParams->Set("command", resolved_command);
+ execParams->Set("host", host->GetName());
+
+ if (service)
+ execParams->Set("service", service->GetShortName());
+
+ /*
+ * If the host/service object specifies the 'check_timeout' attribute,
+ * forward this to the remote endpoint to limit the command execution time.
+ */
+ if (!checkable->GetCheckTimeout().IsEmpty())
+ execParams->Set("check_timeout", checkable->GetCheckTimeout());
+
+ execParams->Set("source", uuid);
+ execParams->Set("deadline", deadline);
+ execParams->Set("macros", execMacros);
+ execParams->Set("endpoint", resolved_endpoint);
+
+ /* Execute command */
+ bool local = endpointPtr == Endpoint::GetLocalEndpoint();
+
+ if (local) {
+ ClusterEvents::ExecuteCommandAPIHandler(origin, execParams);
+ } else {
+ /* Check if the child endpoints have Icinga version >= 2.13 */
+ Zone::Ptr localZone = Zone::GetLocalZone();
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ /* Fetch immediate child zone members */
+ if (zone->GetParent() == localZone && zone->CanAccessObject(endpointPtr->GetZone())) {
+ std::set<Endpoint::Ptr> endpoints = zone->GetEndpoints();
+
+ for (const Endpoint::Ptr& childEndpoint : endpoints) {
+ if (!(childEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand)) {
+ /* Update execution */
+ double now = Utility::GetTime();
+ pending_execution->Set("exit", 126);
+ pending_execution->Set("output", "Endpoint '" + childEndpoint->GetName() + "' doesn't support executing arbitrary commands.");
+ pending_execution->Set("start", now);
+ pending_execution->Set("end", now);
+ pending_execution->Remove("pending");
+
+ listener->RelayMessage(origin, checkable, updateMessage, true);
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("checkable", checkable->GetName());
+ result->Set("execution", uuid);
+ return ApiActions::CreateResult(202, "Accepted", result);
+ }
+ }
+ }
+ }
+
+ Dictionary::Ptr execMessage = new Dictionary();
+ execMessage->Set("jsonrpc", "2.0");
+ execMessage->Set("method", "event::ExecuteCommand");
+ execMessage->Set("params", execParams);
+
+ listener->RelayMessage(origin, endpointPtr->GetZone(), execMessage, true);
+ }
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("checkable", checkable->GetName());
+ result->Set("execution", uuid);
+ return ApiActions::CreateResult(202, "Accepted", result);
+}
diff --git a/lib/icinga/apiactions.hpp b/lib/icinga/apiactions.hpp
new file mode 100644
index 0000000..b6ba835
--- /dev/null
+++ b/lib/icinga/apiactions.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIACTIONS_H
+#define APIACTIONS_H
+
+#include "icinga/i2-icinga.hpp"
+#include "base/configobject.hpp"
+#include "base/dictionary.hpp"
+#include "remote/apiuser.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+class ApiActions
+{
+public:
+ static Dictionary::Ptr ProcessCheckResult(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RescheduleCheck(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr SendCustomNotification(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr DelayNotification(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr AcknowledgeProblem(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RemoveAcknowledgement(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr AddComment(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RemoveComment(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr ScheduleDowntime(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RemoveDowntime(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr ShutdownProcess(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr RestartProcess(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr GenerateTicket(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+ static Dictionary::Ptr ExecuteCommand(const ConfigObject::Ptr& object, const Dictionary::Ptr& params);
+
+private:
+ static Dictionary::Ptr CreateResult(int code, const String& status, const Dictionary::Ptr& additional = nullptr);
+ static Value GetSingleObjectByNameUsingPermissions(const String& type, const String& value, const ApiUser::Ptr& user);
+};
+
+}
+
+#endif /* APIACTIONS_H */
diff --git a/lib/icinga/apievents.cpp b/lib/icinga/apievents.cpp
new file mode 100644
index 0000000..53008fd
--- /dev/null
+++ b/lib/icinga/apievents.cpp
@@ -0,0 +1,438 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/apievents.hpp"
+#include "icinga/service.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "base/initialize.hpp"
+#include "base/serializer.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE(&ApiEvents::StaticInitialize);
+
+void ApiEvents::StaticInitialize()
+{
+ Checkable::OnNewCheckResult.connect(&ApiEvents::CheckResultHandler);
+ Checkable::OnStateChange.connect(&ApiEvents::StateChangeHandler);
+ Checkable::OnNotificationSentToAllUsers.connect(&ApiEvents::NotificationSentToAllUsersHandler);
+
+ Checkable::OnFlappingChanged.connect(&ApiEvents::FlappingChangedHandler);
+
+ Checkable::OnAcknowledgementSet.connect(&ApiEvents::AcknowledgementSetHandler);
+ Checkable::OnAcknowledgementCleared.connect(&ApiEvents::AcknowledgementClearedHandler);
+
+ Comment::OnCommentAdded.connect(&ApiEvents::CommentAddedHandler);
+ Comment::OnCommentRemoved.connect(&ApiEvents::CommentRemovedHandler);
+
+ Downtime::OnDowntimeAdded.connect(&ApiEvents::DowntimeAddedHandler);
+ Downtime::OnDowntimeRemoved.connect(&ApiEvents::DowntimeRemovedHandler);
+ Downtime::OnDowntimeStarted.connect(&ApiEvents::DowntimeStartedHandler);
+ Downtime::OnDowntimeTriggered.connect(&ApiEvents::DowntimeTriggeredHandler);
+
+ ConfigObject::OnActiveChanged.connect(&ApiEvents::OnActiveChangedHandler);
+ ConfigObject::OnVersionChanged.connect(&ApiEvents::OnVersionChangedHandler);
+}
+
+void ApiEvents::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("CheckResult");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::CheckResult));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'CheckResult'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "CheckResult");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("check_result", Serialize(cr));
+
+ result->Set("downtime_depth", checkable->GetDowntimeDepth());
+ result->Set("acknowledgement", checkable->IsAcknowledged());
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("StateChange");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::StateChange));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'StateChange'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "StateChange");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("state", service ? static_cast<int>(service->GetState()) : static_cast<int>(host->GetState()));
+ result->Set("state_type", checkable->GetStateType());
+ result->Set("check_result", Serialize(cr));
+
+ result->Set("downtime_depth", checkable->GetDowntimeDepth());
+ result->Set("acknowledgement", checkable->IsAcknowledged());
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::NotificationSentToAllUsersHandler(const Notification::Ptr& notification,
+ const Checkable::Ptr& checkable, const std::set<User::Ptr>& users, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("Notification");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::Notification));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'Notification'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "Notification");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ NotificationCommand::Ptr command = notification->GetCommand();
+
+ if (command)
+ result->Set("command", command->GetName());
+
+ ArrayData userNames;
+
+ for (const User::Ptr& user : users) {
+ userNames.push_back(user->GetName());
+ }
+
+ result->Set("users", new Array(std::move(userNames)));
+ result->Set("notification_type", Notification::NotificationTypeToStringCompat(type)); //TODO: Change this to our own types.
+ result->Set("author", author);
+ result->Set("text", text);
+ result->Set("check_result", Serialize(cr));
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::FlappingChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("Flapping");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::Flapping));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'Flapping'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "Flapping");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("state", service ? static_cast<int>(service->GetState()) : static_cast<int>(host->GetState()));
+ result->Set("state_type", checkable->GetStateType());
+ result->Set("is_flapping", checkable->IsFlapping());
+ result->Set("flapping_current", checkable->GetFlappingCurrent());
+ result->Set("threshold_low", checkable->GetFlappingThresholdLow());
+ result->Set("threshold_high", checkable->GetFlappingThresholdHigh());
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::AcknowledgementSetHandler(const Checkable::Ptr& checkable,
+ const String& author, const String& comment, AcknowledgementType type,
+ bool notify, bool persistent, double, double expiry, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("AcknowledgementSet");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::AcknowledgementSet));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'AcknowledgementSet'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "AcknowledgementSet");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("state", service ? static_cast<int>(service->GetState()) : static_cast<int>(host->GetState()));
+ result->Set("state_type", checkable->GetStateType());
+
+ result->Set("author", author);
+ result->Set("comment", comment);
+ result->Set("acknowledgement_type", type);
+ result->Set("notify", notify);
+ result->Set("persistent", persistent);
+ result->Set("expiry", expiry);
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double, const MessageOrigin::Ptr& origin)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("AcknowledgementCleared");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::AcknowledgementCleared));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'AcknowledgementCleared'.");
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("type", "AcknowledgementCleared");
+ result->Set("timestamp", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ result->Set("host", host->GetName());
+ if (service)
+ result->Set("service", service->GetShortName());
+
+ result->Set("state", service ? static_cast<int>(service->GetState()) : static_cast<int>(host->GetState()));
+ result->Set("state_type", checkable->GetStateType());
+ result->Set("acknowledgement_type", AcknowledgementNone);
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::CommentAddedHandler(const Comment::Ptr& comment)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("CommentAdded");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::CommentAdded));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'CommentAdded'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "CommentAdded" },
+ { "timestamp", Utility::GetTime() },
+ { "comment", Serialize(comment, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::CommentRemovedHandler(const Comment::Ptr& comment)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("CommentRemoved");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::CommentRemoved));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'CommentRemoved'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "CommentRemoved" },
+ { "timestamp", Utility::GetTime() },
+ { "comment", Serialize(comment, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::DowntimeAddedHandler(const Downtime::Ptr& downtime)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("DowntimeAdded");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::DowntimeAdded));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'DowntimeAdded'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "DowntimeAdded" },
+ { "timestamp", Utility::GetTime() },
+ { "downtime", Serialize(downtime, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::DowntimeRemovedHandler(const Downtime::Ptr& downtime)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("DowntimeRemoved");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::DowntimeRemoved));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'DowntimeRemoved'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "DowntimeRemoved" },
+ { "timestamp", Utility::GetTime() },
+ { "downtime", Serialize(downtime, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::DowntimeStartedHandler(const Downtime::Ptr& downtime)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("DowntimeStarted");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::DowntimeStarted));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'DowntimeStarted'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "DowntimeStarted" },
+ { "timestamp", Utility::GetTime() },
+ { "downtime", Serialize(downtime, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::DowntimeTriggeredHandler(const Downtime::Ptr& downtime)
+{
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType("DowntimeTriggered");
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(EventType::DowntimeTriggered));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents", "Processing event type 'DowntimeTriggered'.");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", "DowntimeTriggered" },
+ { "timestamp", Utility::GetTime() },
+ { "downtime", Serialize(downtime, FAConfig | FAState) }
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
+
+void ApiEvents::OnActiveChangedHandler(const ConfigObject::Ptr& object, const Value&)
+{
+ if (object->IsActive()) {
+ ApiEvents::SendObjectChangeEvent(object, EventType::ObjectCreated, "ObjectCreated");
+ } else if (!object->IsActive() && !object->GetExtension("ConfigObjectDeleted").IsEmpty()) {
+ ApiEvents::SendObjectChangeEvent(object, EventType::ObjectDeleted, "ObjectDeleted");
+ }
+}
+
+void ApiEvents::OnVersionChangedHandler(const ConfigObject::Ptr& object, const Value&)
+{
+ ApiEvents::SendObjectChangeEvent(object, EventType::ObjectModified, "ObjectModified");
+}
+
+void ApiEvents::SendObjectChangeEvent(const ConfigObject::Ptr& object, const EventType& eventType, const String& eventQueue) {
+ std::vector<EventQueue::Ptr> queues = EventQueue::GetQueuesForType(eventQueue);
+ auto inboxes (EventsRouter::GetInstance().GetInboxes(eventType));
+
+ if (queues.empty() && !inboxes)
+ return;
+
+ Log(LogDebug, "ApiEvents") << "Processing event type '" + eventQueue + "'.";
+
+ Dictionary::Ptr result = new Dictionary ({
+ {"type", eventQueue},
+ {"timestamp", Utility::GetTime()},
+ {"object_type", object->GetReflectionType()->GetName()},
+ {"object_name", object->GetName()},
+ });
+
+ for (const EventQueue::Ptr& queue : queues) {
+ queue->ProcessEvent(result);
+ }
+
+ inboxes.Push(std::move(result));
+}
diff --git a/lib/icinga/apievents.hpp b/lib/icinga/apievents.hpp
new file mode 100644
index 0000000..07d5c60
--- /dev/null
+++ b/lib/icinga/apievents.hpp
@@ -0,0 +1,51 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIEVENTS_H
+#define APIEVENTS_H
+
+#include "remote/eventqueue.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+class ApiEvents
+{
+public:
+ static void StaticInitialize();
+
+ static void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin);
+ static void StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr& origin);
+
+
+ static void NotificationSentToAllUsersHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable,
+ const std::set<User::Ptr>& users, NotificationType type, const CheckResult::Ptr& cr, const String& author,
+ const String& text, const MessageOrigin::Ptr& origin);
+
+ static void FlappingChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+
+ static void AcknowledgementSetHandler(const Checkable::Ptr& checkable,
+ const String& author, const String& comment, AcknowledgementType type,
+ bool notify, bool persistent, double, double expiry, const MessageOrigin::Ptr& origin);
+ static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double, const MessageOrigin::Ptr& origin);
+
+ static void CommentAddedHandler(const Comment::Ptr& comment);
+ static void CommentRemovedHandler(const Comment::Ptr& comment);
+
+ static void DowntimeAddedHandler(const Downtime::Ptr& downtime);
+ static void DowntimeRemovedHandler(const Downtime::Ptr& downtime);
+ static void DowntimeStartedHandler(const Downtime::Ptr& downtime);
+ static void DowntimeTriggeredHandler(const Downtime::Ptr& downtime);
+
+ static void OnActiveChangedHandler(const ConfigObject::Ptr& object, const Value&);
+ static void OnVersionChangedHandler(const ConfigObject::Ptr& object, const Value&);
+ static void SendObjectChangeEvent(const ConfigObject::Ptr& object, const EventType& eventType, const String& eventQueue);
+};
+
+}
+
+#endif /* APIEVENTS_H */
diff --git a/lib/icinga/checkable-check.cpp b/lib/icinga/checkable-check.cpp
new file mode 100644
index 0000000..efa9477
--- /dev/null
+++ b/lib/icinga/checkable-check.cpp
@@ -0,0 +1,709 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/service.hpp"
+#include "icinga/host.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/cib.hpp"
+#include "icinga/clusterevents.hpp"
+#include "remote/messageorigin.hpp"
+#include "remote/apilistener.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/context.hpp"
+
+using namespace icinga;
+
+boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, const MessageOrigin::Ptr&)> Checkable::OnNewCheckResult;
+boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, StateType, const MessageOrigin::Ptr&)> Checkable::OnStateChange;
+boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, std::set<Checkable::Ptr>, const MessageOrigin::Ptr&)> Checkable::OnReachabilityChanged;
+boost::signals2::signal<void (const Checkable::Ptr&, NotificationType, const CheckResult::Ptr&, const String&, const String&, const MessageOrigin::Ptr&)> Checkable::OnNotificationsRequested;
+boost::signals2::signal<void (const Checkable::Ptr&)> Checkable::OnNextCheckUpdated;
+
+Atomic<uint_fast64_t> Checkable::CurrentConcurrentChecks (0);
+
+std::mutex Checkable::m_StatsMutex;
+int Checkable::m_PendingChecks = 0;
+std::condition_variable Checkable::m_PendingChecksCV;
+
+CheckCommand::Ptr Checkable::GetCheckCommand() const
+{
+ return dynamic_pointer_cast<CheckCommand>(NavigateCheckCommandRaw());
+}
+
+TimePeriod::Ptr Checkable::GetCheckPeriod() const
+{
+ return TimePeriod::GetByName(GetCheckPeriodRaw());
+}
+
+void Checkable::SetSchedulingOffset(long offset)
+{
+ m_SchedulingOffset = offset;
+}
+
+long Checkable::GetSchedulingOffset()
+{
+ return m_SchedulingOffset;
+}
+
+void Checkable::UpdateNextCheck(const MessageOrigin::Ptr& origin)
+{
+ double interval;
+
+ if (GetStateType() == StateTypeSoft && GetLastCheckResult() != nullptr)
+ interval = GetRetryInterval();
+ else
+ interval = GetCheckInterval();
+
+ double now = Utility::GetTime();
+ double adj = 0;
+
+ if (interval > 1)
+ adj = fmod(now * 100 + GetSchedulingOffset(), interval * 100) / 100.0;
+
+ if (adj != 0.0)
+ adj = std::min(0.5 + fmod(GetSchedulingOffset(), interval * 5) / 100.0, adj);
+
+ double nextCheck = now - adj + interval;
+ double lastCheck = GetLastCheck();
+
+ Log(LogDebug, "Checkable")
+ << "Update checkable '" << GetName() << "' with check interval '" << GetCheckInterval()
+ << "' from last check time at " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", (lastCheck < 0 ? 0 : lastCheck))
+ << " (" << GetLastCheck() << ") to next check time at " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", nextCheck) << " (" << nextCheck << ").";
+
+ SetNextCheck(nextCheck, false, origin);
+}
+
+bool Checkable::HasBeenChecked() const
+{
+ return GetLastCheckResult() != nullptr;
+}
+
+double Checkable::GetLastCheck() const
+{
+ CheckResult::Ptr cr = GetLastCheckResult();
+ double schedule_end = -1;
+
+ if (cr)
+ schedule_end = cr->GetScheduleEnd();
+
+ return schedule_end;
+}
+
+Checkable::ProcessingResult Checkable::ProcessCheckResult(const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin)
+{
+ using Result = Checkable::ProcessingResult;
+
+ {
+ ObjectLock olock(this);
+ m_CheckRunning = false;
+ }
+
+ if (!cr)
+ return Result::NoCheckResult;
+
+ double now = Utility::GetTime();
+
+ if (cr->GetScheduleStart() == 0)
+ cr->SetScheduleStart(now);
+
+ if (cr->GetScheduleEnd() == 0)
+ cr->SetScheduleEnd(now);
+
+ if (cr->GetExecutionStart() == 0)
+ cr->SetExecutionStart(now);
+
+ if (cr->GetExecutionEnd() == 0)
+ cr->SetExecutionEnd(now);
+
+ if (!origin || origin->IsLocal())
+ cr->SetSchedulingSource(IcingaApplication::GetInstance()->GetNodeName());
+
+ Endpoint::Ptr command_endpoint = GetCommandEndpoint();
+
+ if (cr->GetCheckSource().IsEmpty()) {
+ if ((!origin || origin->IsLocal()))
+ cr->SetCheckSource(IcingaApplication::GetInstance()->GetNodeName());
+
+ /* override check source if command_endpoint was defined */
+ if (command_endpoint && !GetExtension("agent_check"))
+ cr->SetCheckSource(command_endpoint->GetName());
+ }
+
+ /* agent checks go through the api */
+ if (command_endpoint && GetExtension("agent_check")) {
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener) {
+ /* send message back to its origin */
+ Dictionary::Ptr message = ClusterEvents::MakeCheckResultMessage(this, cr);
+ listener->SyncSendMessage(command_endpoint, message);
+ }
+
+ return Result::Ok;
+
+ }
+
+ if (!IsActive())
+ return Result::CheckableInactive;
+
+ bool reachable = IsReachable();
+ bool notification_reachable = IsReachable(DependencyNotification);
+
+ ObjectLock olock(this);
+
+ CheckResult::Ptr old_cr = GetLastCheckResult();
+ ServiceState old_state = GetStateRaw();
+ StateType old_stateType = GetStateType();
+ long old_attempt = GetCheckAttempt();
+ bool recovery = false;
+
+ /* When we have an check result already (not after fresh start),
+ * prevent to accept old check results and allow overrides for
+ * CRs happened in the future.
+ */
+ if (old_cr) {
+ double currentCRTimestamp = old_cr->GetExecutionStart();
+ double newCRTimestamp = cr->GetExecutionStart();
+
+ /* Our current timestamp may be from the future (wrong server time adjusted again). Allow overrides here. */
+ if (currentCRTimestamp > now) {
+ /* our current CR is from the future, let the new CR override it. */
+ Log(LogDebug, "Checkable")
+ << std::fixed << std::setprecision(6) << "Processing check result for checkable '" << GetName() << "' from "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newCRTimestamp) << " (" << newCRTimestamp
+ << "). Overriding since ours is from the future at "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", currentCRTimestamp) << " (" << currentCRTimestamp << ").";
+ } else {
+ /* Current timestamp is from the past, but the new timestamp is even more in the past. Skip it. */
+ if (newCRTimestamp < currentCRTimestamp) {
+ Log(LogDebug, "Checkable")
+ << std::fixed << std::setprecision(6) << "Skipping check result for checkable '" << GetName() << "' from "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newCRTimestamp) << " (" << newCRTimestamp
+ << "). It is in the past compared to ours at "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", currentCRTimestamp) << " (" << currentCRTimestamp << ").";
+ return Result::NewerCheckResultPresent;
+ }
+ }
+ }
+
+ /* The ExecuteCheck function already sets the old state, but we need to do it again
+ * in case this was a passive check result. */
+ SetLastStateRaw(old_state);
+ SetLastStateType(old_stateType);
+ SetLastReachable(reachable);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(this);
+
+ CheckableType checkableType = CheckableHost;
+ if (service)
+ checkableType = CheckableService;
+
+ long attempt = 1;
+
+ std::set<Checkable::Ptr> children = GetChildren();
+
+ if (IsStateOK(cr->GetState())) {
+ SetStateType(StateTypeHard); // NOT-OK -> HARD OK
+
+ if (!IsStateOK(old_state))
+ recovery = true;
+
+ ResetNotificationNumbers();
+ SaveLastState(ServiceOK, cr->GetExecutionEnd());
+ } else {
+ /* OK -> NOT-OK change, first SOFT state. Reset attempt counter. */
+ if (IsStateOK(old_state)) {
+ SetStateType(StateTypeSoft);
+ attempt = 1;
+ }
+
+ /* SOFT state change, increase attempt counter. */
+ if (old_stateType == StateTypeSoft && !IsStateOK(old_state)) {
+ SetStateType(StateTypeSoft);
+ attempt = old_attempt + 1;
+ }
+
+ /* HARD state change (e.g. previously 2/3 and this next attempt). Reset attempt counter. */
+ if (attempt >= GetMaxCheckAttempts()) {
+ SetStateType(StateTypeHard);
+ attempt = 1;
+ }
+
+ if (!IsStateOK(cr->GetState())) {
+ SaveLastState(cr->GetState(), cr->GetExecutionEnd());
+ }
+ }
+
+ if (!reachable)
+ SetLastStateUnreachable(cr->GetExecutionEnd());
+
+ SetCheckAttempt(attempt);
+
+ ServiceState new_state = cr->GetState();
+ SetStateRaw(new_state);
+
+ bool stateChange;
+
+ /* Exception on state change calculation for hosts. */
+ if (checkableType == CheckableService)
+ stateChange = (old_state != new_state);
+ else
+ stateChange = (Host::CalculateState(old_state) != Host::CalculateState(new_state));
+
+ /* Store the current last state change for the next iteration. */
+ SetPreviousStateChange(GetLastStateChange());
+
+ if (stateChange) {
+ SetLastStateChange(cr->GetExecutionEnd());
+
+ /* remove acknowledgements */
+ if (GetAcknowledgement() == AcknowledgementNormal ||
+ (GetAcknowledgement() == AcknowledgementSticky && IsStateOK(new_state))) {
+ ClearAcknowledgement("");
+ }
+ }
+
+ bool remove_acknowledgement_comments = false;
+
+ if (GetAcknowledgement() == AcknowledgementNone)
+ remove_acknowledgement_comments = true;
+
+ bool hardChange = (GetStateType() == StateTypeHard && old_stateType == StateTypeSoft);
+
+ if (stateChange && old_stateType == StateTypeHard && GetStateType() == StateTypeHard)
+ hardChange = true;
+
+ bool is_volatile = GetVolatile();
+
+ if (hardChange || is_volatile) {
+ SetLastHardStateRaw(new_state);
+ SetLastHardStateChange(cr->GetExecutionEnd());
+ SetLastHardStatesRaw(GetLastHardStatesRaw() / 100u + new_state * 100u);
+ }
+
+ if (stateChange) {
+ SetLastSoftStatesRaw(GetLastSoftStatesRaw() / 100u + new_state * 100u);
+ }
+
+ cr->SetPreviousHardState(ServiceState(GetLastHardStatesRaw() % 100u));
+
+ if (!IsStateOK(new_state))
+ TriggerDowntimes(cr->GetExecutionEnd());
+
+ /* statistics for external tools */
+ Checkable::UpdateStatistics(cr, checkableType);
+
+ bool in_downtime = IsInDowntime();
+
+ bool send_notification = false;
+ bool suppress_notification = !notification_reachable || in_downtime || IsAcknowledged();
+
+ /* Send notifications whether when a hard state change occurred. */
+ if (hardChange && !(old_stateType == StateTypeSoft && IsStateOK(new_state)))
+ send_notification = true;
+ /* Or if the checkable is volatile and in a HARD state. */
+ else if (is_volatile && GetStateType() == StateTypeHard)
+ send_notification = true;
+
+ if (IsStateOK(old_state) && old_stateType == StateTypeSoft)
+ send_notification = false; /* Don't send notifications for SOFT-OK -> HARD-OK. */
+
+ if (is_volatile && IsStateOK(old_state) && IsStateOK(new_state))
+ send_notification = false; /* Don't send notifications for volatile OK -> OK changes. */
+
+ olock.Unlock();
+
+ if (remove_acknowledgement_comments)
+ RemoveAckComments(String(), cr->GetExecutionEnd());
+
+ Dictionary::Ptr vars_after = new Dictionary({
+ { "state", new_state },
+ { "state_type", GetStateType() },
+ { "attempt", GetCheckAttempt() },
+ { "reachable", reachable }
+ });
+
+ if (old_cr)
+ cr->SetVarsBefore(old_cr->GetVarsAfter());
+
+ cr->SetVarsAfter(vars_after);
+
+ olock.Lock();
+
+ if (service) {
+ SetLastCheckResult(cr);
+ } else {
+ bool wasProblem = GetProblem();
+
+ SetLastCheckResult(cr);
+
+ if (GetProblem() != wasProblem) {
+ auto services = host->GetServices();
+ olock.Unlock();
+ for (auto& service : services) {
+ Service::OnHostProblemChanged(service, cr, origin);
+ }
+ olock.Lock();
+ }
+ }
+
+ bool was_flapping = IsFlapping();
+
+ UpdateFlappingStatus(cr->GetState());
+
+ bool is_flapping = IsFlapping();
+
+ if (cr->GetActive()) {
+ UpdateNextCheck(origin);
+ } else {
+ /* Reschedule the next check for external passive check results. The side effect of
+ * this is that for as long as we receive results for a service we
+ * won't execute any active checks. */
+ double offset;
+ double ttl = cr->GetTtl();
+
+ if (ttl > 0)
+ offset = ttl;
+ else
+ offset = GetCheckInterval();
+
+ SetNextCheck(Utility::GetTime() + offset, false, origin);
+ }
+
+ olock.Unlock();
+
+#ifdef I2_DEBUG /* I2_DEBUG */
+ Log(LogDebug, "Checkable")
+ << "Flapping: Checkable " << GetName()
+ << " was: " << was_flapping
+ << " is: " << is_flapping
+ << " threshold low: " << GetFlappingThresholdLow()
+ << " threshold high: " << GetFlappingThresholdHigh()
+ << "% current: " << GetFlappingCurrent() << "%.";
+#endif /* I2_DEBUG */
+
+ if (recovery) {
+ for (auto& child : children) {
+ if (child->GetProblem() && child->GetEnableActiveChecks()) {
+ auto nextCheck (now + Utility::Random() % 60);
+
+ ObjectLock oLock (child);
+
+ if (nextCheck < child->GetNextCheck()) {
+ child->SetNextCheck(nextCheck);
+ }
+ }
+ }
+ }
+
+ if (stateChange) {
+ /* reschedule direct parents */
+ for (const Checkable::Ptr& parent : GetParents()) {
+ if (parent.get() == this)
+ continue;
+
+ if (!parent->GetEnableActiveChecks())
+ continue;
+
+ if (parent->GetNextCheck() >= now + parent->GetRetryInterval()) {
+ ObjectLock olock(parent);
+ parent->SetNextCheck(now);
+ }
+ }
+ }
+
+ OnNewCheckResult(this, cr, origin);
+
+ /* signal status updates to for example db_ido */
+ OnStateChanged(this);
+
+ String old_state_str = (service ? Service::StateToString(old_state) : Host::StateToString(Host::CalculateState(old_state)));
+ String new_state_str = (service ? Service::StateToString(new_state) : Host::StateToString(Host::CalculateState(new_state)));
+
+ /* Whether a hard state change or a volatile state change except OK -> OK happened. */
+ if (hardChange || (is_volatile && !(IsStateOK(old_state) && IsStateOK(new_state)))) {
+ OnStateChange(this, cr, StateTypeHard, origin);
+ Log(LogNotice, "Checkable")
+ << "State Change: Checkable '" << GetName() << "' hard state change from " << old_state_str << " to " << new_state_str << " detected." << (is_volatile ? " Checkable is volatile." : "");
+ }
+ /* Whether a state change happened or the state type is SOFT (must be logged too). */
+ else if (stateChange || GetStateType() == StateTypeSoft) {
+ OnStateChange(this, cr, StateTypeSoft, origin);
+ Log(LogNotice, "Checkable")
+ << "State Change: Checkable '" << GetName() << "' soft state change from " << old_state_str << " to " << new_state_str << " detected.";
+ }
+
+ if (GetStateType() == StateTypeSoft || hardChange || recovery ||
+ (is_volatile && !(IsStateOK(old_state) && IsStateOK(new_state))))
+ ExecuteEventHandler();
+
+ int suppressed_types = 0;
+
+ /* Flapping start/end notifications */
+ if (!was_flapping && is_flapping) {
+ /* FlappingStart notifications happen on state changes, not in downtimes */
+ if (!IsPaused()) {
+ if (in_downtime) {
+ suppressed_types |= NotificationFlappingStart;
+ } else {
+ OnNotificationsRequested(this, NotificationFlappingStart, cr, "", "", nullptr);
+ }
+ }
+
+ Log(LogNotice, "Checkable")
+ << "Flapping Start: Checkable '" << GetName() << "' started flapping (Current flapping value "
+ << GetFlappingCurrent() << "% > high threshold " << GetFlappingThresholdHigh() << "%).";
+
+ NotifyFlapping(origin);
+ } else if (was_flapping && !is_flapping) {
+ /* FlappingEnd notifications are independent from state changes, must not happen in downtine */
+ if (!IsPaused()) {
+ if (in_downtime) {
+ suppressed_types |= NotificationFlappingEnd;
+ } else {
+ OnNotificationsRequested(this, NotificationFlappingEnd, cr, "", "", nullptr);
+ }
+ }
+
+ Log(LogNotice, "Checkable")
+ << "Flapping Stop: Checkable '" << GetName() << "' stopped flapping (Current flapping value "
+ << GetFlappingCurrent() << "% < low threshold " << GetFlappingThresholdLow() << "%).";
+
+ NotifyFlapping(origin);
+ }
+
+ if (send_notification && !is_flapping) {
+ if (!IsPaused()) {
+ /* If there are still some pending suppressed state notification, keep the suppression until these are
+ * handled by Checkable::FireSuppressedNotifications().
+ */
+ bool pending = GetSuppressedNotifications() & (NotificationRecovery|NotificationProblem);
+
+ if (suppress_notification || pending) {
+ suppressed_types |= (recovery ? NotificationRecovery : NotificationProblem);
+ } else {
+ OnNotificationsRequested(this, recovery ? NotificationRecovery : NotificationProblem, cr, "", "", nullptr);
+ }
+ }
+ }
+
+ if (suppressed_types) {
+ /* If some notifications were suppressed, but just because of e.g. a downtime,
+ * stash them into a notification types bitmask for maybe re-sending later.
+ */
+
+ ObjectLock olock (this);
+ int suppressed_types_before (GetSuppressedNotifications());
+ int suppressed_types_after (suppressed_types_before | suppressed_types);
+
+ const int conflict = NotificationFlappingStart | NotificationFlappingEnd;
+ if ((suppressed_types_after & conflict) == conflict) {
+ /* Flapping start and end cancel out each other. */
+ suppressed_types_after &= ~conflict;
+ }
+
+ const int stateNotifications = NotificationRecovery | NotificationProblem;
+ if (!(suppressed_types_before & stateNotifications) && (suppressed_types & stateNotifications)) {
+ /* A state-related notification is suppressed for the first time, store the previous state. When
+ * notifications are no longer suppressed, this can be compared with the current state to determine
+ * if a notification must be sent. This is done differently compared to flapping notifications just above
+ * as for state notifications, problem and recovery don't always cancel each other. For example,
+ * WARNING -> OK -> CRITICAL generates both types once, but there should still be a notification.
+ */
+ SetStateBeforeSuppression(old_stateType == StateTypeHard ? old_state : ServiceOK);
+ }
+
+ if (suppressed_types_after != suppressed_types_before) {
+ SetSuppressedNotifications(suppressed_types_after);
+ }
+ }
+
+ /* update reachability for child objects */
+ if ((stateChange || hardChange) && !children.empty())
+ OnReachabilityChanged(this, cr, children, origin);
+
+ return Result::Ok;
+}
+
+void Checkable::ExecuteRemoteCheck(const Dictionary::Ptr& resolvedMacros)
+{
+ CONTEXT("Executing remote check for object '" << GetName() << "'");
+
+ double scheduled_start = GetNextCheck();
+ double before_check = Utility::GetTime();
+
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetScheduleStart(scheduled_start);
+ cr->SetExecutionStart(before_check);
+
+ GetCheckCommand()->Execute(this, cr, resolvedMacros, true);
+}
+
+void Checkable::ExecuteCheck()
+{
+ CONTEXT("Executing check for object '" << GetName() << "'");
+
+ /* keep track of scheduling info in case the check type doesn't provide its own information */
+ double scheduled_start = GetNextCheck();
+ double before_check = Utility::GetTime();
+
+ SetLastCheckStarted(Utility::GetTime());
+
+ /* This calls SetNextCheck() which updates the CheckerComponent's idle/pending
+ * queues and ensures that checks are not fired multiple times. ProcessCheckResult()
+ * is called too late. See #6421.
+ */
+ UpdateNextCheck();
+
+ bool reachable = IsReachable();
+
+ {
+ ObjectLock olock(this);
+
+ /* don't run another check if there is one pending */
+ if (m_CheckRunning)
+ return;
+
+ m_CheckRunning = true;
+
+ SetLastStateRaw(GetStateRaw());
+ SetLastStateType(GetLastStateType());
+ SetLastReachable(reachable);
+ }
+
+ CheckResult::Ptr cr = new CheckResult();
+
+ cr->SetScheduleStart(scheduled_start);
+ cr->SetExecutionStart(before_check);
+
+ Endpoint::Ptr endpoint = GetCommandEndpoint();
+ bool local = !endpoint || endpoint == Endpoint::GetLocalEndpoint();
+
+ if (local) {
+ GetCheckCommand()->Execute(this, cr, nullptr, false);
+ } else {
+ Dictionary::Ptr macros = new Dictionary();
+ GetCheckCommand()->Execute(this, cr, macros, false);
+
+ if (endpoint->GetConnected()) {
+ /* perform check on remote endpoint */
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ExecuteCommand");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(this);
+
+ Dictionary::Ptr params = new Dictionary();
+ message->Set("params", params);
+ params->Set("command_type", "check_command");
+ params->Set("command", GetCheckCommand()->GetName());
+ params->Set("host", host->GetName());
+
+ if (service)
+ params->Set("service", service->GetShortName());
+
+ /*
+ * If the host/service object specifies the 'check_timeout' attribute,
+ * forward this to the remote endpoint to limit the command execution time.
+ */
+ if (!GetCheckTimeout().IsEmpty())
+ params->Set("check_timeout", GetCheckTimeout());
+
+ params->Set("macros", macros);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener)
+ listener->SyncSendMessage(endpoint, message);
+
+ /* Re-schedule the check so we don't run it again until after we've received
+ * a check result from the remote instance. The check will be re-scheduled
+ * using the proper check interval once we've received a check result.
+ */
+ SetNextCheck(Utility::GetTime() + GetCheckCommand()->GetTimeout() + 30);
+
+ /*
+ * Let the user know that there was a problem with the check if
+ * 1) The endpoint is not syncing (replay log, etc.)
+ * 2) Outside of the cold startup window (5min)
+ */
+ } else if (!endpoint->GetSyncing() && Application::GetInstance()->GetStartTime() < Utility::GetTime() - 300) {
+ /* fail to perform check on unconnected endpoint */
+ cr->SetState(ServiceUnknown);
+
+ String output = "Remote Icinga instance '" + endpoint->GetName() + "' is not connected to ";
+
+ Endpoint::Ptr localEndpoint = Endpoint::GetLocalEndpoint();
+
+ if (localEndpoint)
+ output += "'" + localEndpoint->GetName() + "'";
+ else
+ output += "this instance";
+
+ cr->SetOutput(output);
+
+ ProcessCheckResult(cr);
+ }
+
+ {
+ ObjectLock olock(this);
+ m_CheckRunning = false;
+ }
+ }
+}
+
+void Checkable::UpdateStatistics(const CheckResult::Ptr& cr, CheckableType type)
+{
+ time_t ts = cr->GetScheduleEnd();
+
+ if (type == CheckableHost) {
+ if (cr->GetActive())
+ CIB::UpdateActiveHostChecksStatistics(ts, 1);
+ else
+ CIB::UpdatePassiveHostChecksStatistics(ts, 1);
+ } else if (type == CheckableService) {
+ if (cr->GetActive())
+ CIB::UpdateActiveServiceChecksStatistics(ts, 1);
+ else
+ CIB::UpdatePassiveServiceChecksStatistics(ts, 1);
+ } else {
+ Log(LogWarning, "Checkable", "Unknown checkable type for statistic update.");
+ }
+}
+
+void Checkable::IncreasePendingChecks()
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ m_PendingChecks++;
+}
+
+void Checkable::DecreasePendingChecks()
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ m_PendingChecks--;
+ m_PendingChecksCV.notify_one();
+}
+
+int Checkable::GetPendingChecks()
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ return m_PendingChecks;
+}
+
+void Checkable::AquirePendingCheckSlot(int maxPendingChecks)
+{
+ std::unique_lock<std::mutex> lock(m_StatsMutex);
+ while (m_PendingChecks >= maxPendingChecks)
+ m_PendingChecksCV.wait(lock);
+
+ m_PendingChecks++;
+}
diff --git a/lib/icinga/checkable-comment.cpp b/lib/icinga/checkable-comment.cpp
new file mode 100644
index 0000000..71cfac6
--- /dev/null
+++ b/lib/icinga/checkable-comment.cpp
@@ -0,0 +1,75 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "remote/configobjectutility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/timer.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include <utility>
+
+using namespace icinga;
+
+
+void Checkable::RemoveAllComments()
+{
+ for (const Comment::Ptr& comment : GetComments()) {
+ Comment::RemoveComment(comment->GetName());
+ }
+}
+
+void Checkable::RemoveAckComments(const String& removedBy, double createdBefore)
+{
+ for (const Comment::Ptr& comment : GetComments()) {
+ if (comment->GetEntryType() == CommentAcknowledgement) {
+ /* Do not remove persistent comments from an acknowledgement */
+ if (comment->GetPersistent()) {
+ continue;
+ }
+
+ if (comment->GetEntryTime() > createdBefore) {
+ continue;
+ }
+
+ {
+ ObjectLock oLock (comment);
+ comment->SetRemovedBy(removedBy);
+ }
+
+ Comment::RemoveComment(comment->GetName());
+ }
+ }
+}
+
+std::set<Comment::Ptr> Checkable::GetComments() const
+{
+ std::unique_lock<std::mutex> lock(m_CommentMutex);
+ return m_Comments;
+}
+
+Comment::Ptr Checkable::GetLastComment() const
+{
+ std::unique_lock<std::mutex> lock (m_CommentMutex);
+ Comment::Ptr lastComment;
+
+ for (auto& comment : m_Comments) {
+ if (!lastComment || comment->GetEntryTime() > lastComment->GetEntryTime()) {
+ lastComment = comment;
+ }
+ }
+
+ return lastComment;
+}
+
+void Checkable::RegisterComment(const Comment::Ptr& comment)
+{
+ std::unique_lock<std::mutex> lock(m_CommentMutex);
+ m_Comments.insert(comment);
+}
+
+void Checkable::UnregisterComment(const Comment::Ptr& comment)
+{
+ std::unique_lock<std::mutex> lock(m_CommentMutex);
+ m_Comments.erase(comment);
+}
diff --git a/lib/icinga/checkable-dependency.cpp b/lib/icinga/checkable-dependency.cpp
new file mode 100644
index 0000000..58d6b57
--- /dev/null
+++ b/lib/icinga/checkable-dependency.cpp
@@ -0,0 +1,176 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "icinga/dependency.hpp"
+#include "base/logger.hpp"
+#include <unordered_map>
+
+using namespace icinga;
+
+void Checkable::AddDependency(const Dependency::Ptr& dep)
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ m_Dependencies.insert(dep);
+}
+
+void Checkable::RemoveDependency(const Dependency::Ptr& dep)
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ m_Dependencies.erase(dep);
+}
+
+std::vector<Dependency::Ptr> Checkable::GetDependencies() const
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ return std::vector<Dependency::Ptr>(m_Dependencies.begin(), m_Dependencies.end());
+}
+
+void Checkable::AddReverseDependency(const Dependency::Ptr& dep)
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ m_ReverseDependencies.insert(dep);
+}
+
+void Checkable::RemoveReverseDependency(const Dependency::Ptr& dep)
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ m_ReverseDependencies.erase(dep);
+}
+
+std::vector<Dependency::Ptr> Checkable::GetReverseDependencies() const
+{
+ std::unique_lock<std::mutex> lock(m_DependencyMutex);
+ return std::vector<Dependency::Ptr>(m_ReverseDependencies.begin(), m_ReverseDependencies.end());
+}
+
+bool Checkable::IsReachable(DependencyType dt, Dependency::Ptr *failedDependency, int rstack) const
+{
+ /* Anything greater than 256 causes recursion bus errors. */
+ int limit = 256;
+
+ if (rstack > limit) {
+ Log(LogWarning, "Checkable")
+ << "Too many nested dependencies (>" << limit << ") for checkable '" << GetName() << "': Dependency failed.";
+
+ return false;
+ }
+
+ for (const Checkable::Ptr& checkable : GetParents()) {
+ if (!checkable->IsReachable(dt, failedDependency, rstack + 1))
+ return false;
+ }
+
+ /* implicit dependency on host if this is a service */
+ const auto *service = dynamic_cast<const Service *>(this);
+ if (service && (dt == DependencyState || dt == DependencyNotification)) {
+ Host::Ptr host = service->GetHost();
+
+ if (host && host->GetState() != HostUp && host->GetStateType() == StateTypeHard) {
+ if (failedDependency)
+ *failedDependency = nullptr;
+
+ return false;
+ }
+ }
+
+ auto deps = GetDependencies();
+
+ std::unordered_map<std::string, Dependency::Ptr> violated; // key: redundancy group, value: nullptr if satisfied, violating dependency otherwise
+
+ for (const Dependency::Ptr& dep : deps) {
+ std::string redundancy_group = dep->GetRedundancyGroup();
+
+ if (!dep->IsAvailable(dt)) {
+ if (redundancy_group.empty()) {
+ Log(LogDebug, "Checkable")
+ << "Non-redundant dependency '" << dep->GetName() << "' failed for checkable '" << GetName() << "': Marking as unreachable.";
+
+ if (failedDependency)
+ *failedDependency = dep;
+
+ return false;
+ }
+
+ // tentatively mark this dependency group as failed unless it is already marked;
+ // so it either passed before (don't overwrite) or already failed (so don't care)
+ // note that std::unordered_map::insert() will not overwrite an existing entry
+ violated.insert(std::make_pair(redundancy_group, dep));
+ } else if (!redundancy_group.empty()) {
+ violated[redundancy_group] = nullptr;
+ }
+ }
+
+ auto violator = std::find_if(violated.begin(), violated.end(), [](auto& v) { return v.second != nullptr; });
+ if (violator != violated.end()) {
+ Log(LogDebug, "Checkable")
+ << "All dependencies in redundancy group '" << violator->first << "' have failed for checkable '" << GetName() << "': Marking as unreachable.";
+
+ if (failedDependency)
+ *failedDependency = violator->second;
+
+ return false;
+ }
+
+ if (failedDependency)
+ *failedDependency = nullptr;
+
+ return true;
+}
+
+std::set<Checkable::Ptr> Checkable::GetParents() const
+{
+ std::set<Checkable::Ptr> parents;
+
+ for (const Dependency::Ptr& dep : GetDependencies()) {
+ Checkable::Ptr parent = dep->GetParent();
+
+ if (parent && parent.get() != this)
+ parents.insert(parent);
+ }
+
+ return parents;
+}
+
+std::set<Checkable::Ptr> Checkable::GetChildren() const
+{
+ std::set<Checkable::Ptr> parents;
+
+ for (const Dependency::Ptr& dep : GetReverseDependencies()) {
+ Checkable::Ptr service = dep->GetChild();
+
+ if (service && service.get() != this)
+ parents.insert(service);
+ }
+
+ return parents;
+}
+
+std::set<Checkable::Ptr> Checkable::GetAllChildren() const
+{
+ std::set<Checkable::Ptr> children = GetChildren();
+
+ GetAllChildrenInternal(children, 0);
+
+ return children;
+}
+
+void Checkable::GetAllChildrenInternal(std::set<Checkable::Ptr>& children, int level) const
+{
+ if (level > 32)
+ return;
+
+ std::set<Checkable::Ptr> localChildren;
+
+ for (const Checkable::Ptr& checkable : children) {
+ std::set<Checkable::Ptr> cChildren = checkable->GetChildren();
+
+ if (!cChildren.empty()) {
+ GetAllChildrenInternal(cChildren, level + 1);
+ localChildren.insert(cChildren.begin(), cChildren.end());
+ }
+
+ localChildren.insert(checkable);
+ }
+
+ children.insert(localChildren.begin(), localChildren.end());
+}
diff --git a/lib/icinga/checkable-downtime.cpp b/lib/icinga/checkable-downtime.cpp
new file mode 100644
index 0000000..d96003d
--- /dev/null
+++ b/lib/icinga/checkable-downtime.cpp
@@ -0,0 +1,64 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+void Checkable::RemoveAllDowntimes()
+{
+ for (const Downtime::Ptr& downtime : GetDowntimes()) {
+ Downtime::RemoveDowntime(downtime->GetName(), true, true, true);
+ }
+}
+
+void Checkable::TriggerDowntimes(double triggerTime)
+{
+ for (const Downtime::Ptr& downtime : GetDowntimes()) {
+ downtime->TriggerDowntime(triggerTime);
+ }
+}
+
+bool Checkable::IsInDowntime() const
+{
+ for (const Downtime::Ptr& downtime : GetDowntimes()) {
+ if (downtime->IsInEffect())
+ return true;
+ }
+
+ return false;
+}
+
+int Checkable::GetDowntimeDepth() const
+{
+ int downtime_depth = 0;
+
+ for (const Downtime::Ptr& downtime : GetDowntimes()) {
+ if (downtime->IsInEffect())
+ downtime_depth++;
+ }
+
+ return downtime_depth;
+}
+
+std::set<Downtime::Ptr> Checkable::GetDowntimes() const
+{
+ std::unique_lock<std::mutex> lock(m_DowntimeMutex);
+ return m_Downtimes;
+}
+
+void Checkable::RegisterDowntime(const Downtime::Ptr& downtime)
+{
+ std::unique_lock<std::mutex> lock(m_DowntimeMutex);
+ m_Downtimes.insert(downtime);
+}
+
+void Checkable::UnregisterDowntime(const Downtime::Ptr& downtime)
+{
+ std::unique_lock<std::mutex> lock(m_DowntimeMutex);
+ m_Downtimes.erase(downtime);
+}
diff --git a/lib/icinga/checkable-event.cpp b/lib/icinga/checkable-event.cpp
new file mode 100644
index 0000000..fb315d9
--- /dev/null
+++ b/lib/icinga/checkable-event.cpp
@@ -0,0 +1,81 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/service.hpp"
+#include "remote/apilistener.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+
+using namespace icinga;
+
+boost::signals2::signal<void (const Checkable::Ptr&)> Checkable::OnEventCommandExecuted;
+
+EventCommand::Ptr Checkable::GetEventCommand() const
+{
+ return EventCommand::GetByName(GetEventCommandRaw());
+}
+
+void Checkable::ExecuteEventHandler(const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ CONTEXT("Executing event handler for object '" << GetName() << "'");
+
+ if (!IcingaApplication::GetInstance()->GetEnableEventHandlers() || !GetEnableEventHandler())
+ return;
+
+ /* HA enabled zones. */
+ if (IsActive() && IsPaused()) {
+ Log(LogNotice, "Checkable")
+ << "Skipping event handler for HA-paused checkable '" << GetName() << "'";
+ return;
+ }
+
+ EventCommand::Ptr ec = GetEventCommand();
+
+ if (!ec)
+ return;
+
+ Log(LogNotice, "Checkable")
+ << "Executing event handler '" << ec->GetName() << "' for checkable '" << GetName() << "'";
+
+ Dictionary::Ptr macros;
+ Endpoint::Ptr endpoint = GetCommandEndpoint();
+
+ if (endpoint && !useResolvedMacros)
+ macros = new Dictionary();
+ else
+ macros = resolvedMacros;
+
+ ec->Execute(this, macros, useResolvedMacros);
+
+ if (endpoint && !GetExtension("agent_check")) {
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ExecuteCommand");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(this);
+
+ Dictionary::Ptr params = new Dictionary();
+ message->Set("params", params);
+ params->Set("command_type", "event_command");
+ params->Set("command", GetEventCommand()->GetName());
+ params->Set("host", host->GetName());
+
+ if (service)
+ params->Set("service", service->GetShortName());
+
+ params->Set("macros", macros);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener)
+ listener->SyncSendMessage(endpoint, message);
+
+ return;
+ }
+
+ OnEventCommandExecuted(this);
+}
diff --git a/lib/icinga/checkable-flapping.cpp b/lib/icinga/checkable-flapping.cpp
new file mode 100644
index 0000000..e905e05
--- /dev/null
+++ b/lib/icinga/checkable-flapping.cpp
@@ -0,0 +1,114 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+template<typename T>
+struct Bitset
+{
+public:
+ Bitset(T value)
+ : m_Data(value)
+ { }
+
+ void Modify(int index, bool bit)
+ {
+ if (bit)
+ m_Data |= 1 << index;
+ else
+ m_Data &= ~(1 << index);
+ }
+
+ bool Get(int index) const
+ {
+ return m_Data & (1 << index);
+ }
+
+ T GetValue() const
+ {
+ return m_Data;
+ }
+
+private:
+ T m_Data{0};
+};
+
+void Checkable::UpdateFlappingStatus(ServiceState newState)
+{
+ Bitset<unsigned long> stateChangeBuf = GetFlappingBuffer();
+ int oldestIndex = GetFlappingIndex();
+
+ ServiceState lastState = GetFlappingLastState();
+ bool stateChange = false;
+
+ int stateFilter = GetFlappingIgnoreStatesFilter();
+
+ /* Only count as state change if no state filter is set or the new state isn't filtered out */
+ if (stateFilter == -1 || !(ServiceStateToFlappingFilter(newState) & stateFilter)) {
+ stateChange = newState != lastState;
+ SetFlappingLastState(newState);
+ }
+
+ stateChangeBuf.Modify(oldestIndex, stateChange);
+ oldestIndex = (oldestIndex + 1) % 20;
+
+ double stateChanges = 0;
+
+ /* Iterate over our state array and compute a weighted total */
+ for (int i = 0; i < 20; i++) {
+ if (stateChangeBuf.Get((oldestIndex + i) % 20))
+ stateChanges += 0.8 + (0.02 * i);
+ }
+
+ double flappingValue = 100.0 * stateChanges / 20.0;
+
+ bool flapping;
+
+ if (GetFlapping())
+ flapping = flappingValue > GetFlappingThresholdLow();
+ else
+ flapping = flappingValue > GetFlappingThresholdHigh();
+
+ SetFlappingBuffer(stateChangeBuf.GetValue());
+ SetFlappingIndex(oldestIndex);
+ SetFlappingCurrent(flappingValue);
+
+ if (flapping != GetFlapping()) {
+ SetFlapping(flapping, true);
+
+ double ee = GetLastCheckResult()->GetExecutionEnd();
+
+ if (GetEnableFlapping() && IcingaApplication::GetInstance()->GetEnableFlapping()) {
+ OnFlappingChange(this, ee);
+ }
+
+ SetFlappingLastChange(ee);
+ }
+}
+
+bool Checkable::IsFlapping() const
+{
+ if (!GetEnableFlapping() || !IcingaApplication::GetInstance()->GetEnableFlapping())
+ return false;
+ else
+ return GetFlapping();
+}
+
+int Checkable::ServiceStateToFlappingFilter(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ return StateFilterOK;
+ case ServiceWarning:
+ return StateFilterWarning;
+ case ServiceCritical:
+ return StateFilterCritical;
+ case ServiceUnknown:
+ return StateFilterUnknown;
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
diff --git a/lib/icinga/checkable-notification.cpp b/lib/icinga/checkable-notification.cpp
new file mode 100644
index 0000000..79b5986
--- /dev/null
+++ b/lib/icinga/checkable-notification.cpp
@@ -0,0 +1,334 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/service.hpp"
+#include "base/dictionary.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/context.hpp"
+#include "base/convert.hpp"
+#include "base/lazy-init.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+boost::signals2::signal<void (const Notification::Ptr&, const Checkable::Ptr&, const std::set<User::Ptr>&,
+ const NotificationType&, const CheckResult::Ptr&, const String&, const String&,
+ const MessageOrigin::Ptr&)> Checkable::OnNotificationSentToAllUsers;
+boost::signals2::signal<void (const Notification::Ptr&, const Checkable::Ptr&, const User::Ptr&,
+ const NotificationType&, const CheckResult::Ptr&, const String&, const String&, const String&,
+ const MessageOrigin::Ptr&)> Checkable::OnNotificationSentToUser;
+
+void Checkable::ResetNotificationNumbers()
+{
+ for (const Notification::Ptr& notification : GetNotifications()) {
+ ObjectLock olock(notification);
+ notification->ResetNotificationNumber();
+ }
+}
+
+void Checkable::SendNotifications(NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text)
+{
+ String checkableName = GetName();
+
+ CONTEXT("Sending notifications for object '" << checkableName << "'");
+
+ bool force = GetForceNextNotification();
+
+ SetForceNextNotification(false);
+
+ if (!IcingaApplication::GetInstance()->GetEnableNotifications() || !GetEnableNotifications()) {
+ if (!force) {
+ Log(LogInformation, "Checkable")
+ << "Notifications are disabled for checkable '" << checkableName << "'.";
+ return;
+ }
+ }
+
+ std::set<Notification::Ptr> notifications = GetNotifications();
+
+ String notificationTypeName = Notification::NotificationTypeToString(type);
+
+ // Bail early if there are no notifications.
+ if (notifications.empty()) {
+ Log(LogNotice, "Checkable")
+ << "Skipping checkable '" << checkableName << "' which doesn't have any notification objects configured.";
+ return;
+ }
+
+ Log(LogInformation, "Checkable")
+ << "Checkable '" << checkableName << "' has " << notifications.size()
+ << " notification(s). Checking filters for type '" << notificationTypeName << "', sends will be logged.";
+
+ for (const Notification::Ptr& notification : notifications) {
+ // Re-send stashed notifications from cold startup.
+ if (ApiListener::UpdatedObjectAuthority()) {
+ try {
+ if (!notification->IsPaused()) {
+ auto stashedNotifications (notification->GetStashedNotifications());
+
+ if (stashedNotifications->GetLength()) {
+ Log(LogNotice, "Notification")
+ << "Notification '" << notification->GetName() << "': there are some stashed notifications. Stashing notification to preserve order.";
+
+ stashedNotifications->Add(new Dictionary({
+ {"notification_type", type},
+ {"cr", cr},
+ {"force", force},
+ {"reminder", false},
+ {"author", author},
+ {"text", text}
+ }));
+ } else {
+ notification->BeginExecuteNotification(type, cr, force, false, author, text);
+ }
+ } else {
+ Log(LogNotice, "Notification")
+ << "Notification '" << notification->GetName() << "': HA cluster active, this endpoint does not have the authority (paused=true). Skipping.";
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "Checkable")
+ << "Exception occurred during notification '" << notification->GetName() << "' for checkable '"
+ << GetName() << "': " << DiagnosticInformation(ex, false);
+ }
+ } else {
+ // Cold startup phase. Stash notification for later.
+ Log(LogNotice, "Notification")
+ << "Notification '" << notification->GetName() << "': object authority hasn't been updated, yet. Stashing notification.";
+
+ notification->GetStashedNotifications()->Add(new Dictionary({
+ {"notification_type", type},
+ {"cr", cr},
+ {"force", force},
+ {"reminder", false},
+ {"author", author},
+ {"text", text}
+ }));
+ }
+ }
+}
+
+std::set<Notification::Ptr> Checkable::GetNotifications() const
+{
+ std::unique_lock<std::mutex> lock(m_NotificationMutex);
+ return m_Notifications;
+}
+
+void Checkable::RegisterNotification(const Notification::Ptr& notification)
+{
+ std::unique_lock<std::mutex> lock(m_NotificationMutex);
+ m_Notifications.insert(notification);
+}
+
+void Checkable::UnregisterNotification(const Notification::Ptr& notification)
+{
+ std::unique_lock<std::mutex> lock(m_NotificationMutex);
+ m_Notifications.erase(notification);
+}
+
+void Checkable::FireSuppressedNotifications()
+{
+ if (!IsActive())
+ return;
+
+ if (IsPaused())
+ return;
+
+ if (!GetEnableNotifications())
+ return;
+
+ int suppressed_types (GetSuppressedNotifications());
+ if (!suppressed_types)
+ return;
+
+ int subtract = 0;
+
+ {
+ LazyInit<bool> wasLastParentRecoveryRecent ([this]() {
+ auto cr (GetLastCheckResult());
+
+ if (!cr) {
+ return true;
+ }
+
+ auto threshold (cr->GetExecutionStart());
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(this);
+
+ if (service) {
+ ObjectLock oLock (host);
+
+ if (!host->GetProblem() && host->GetLastStateChange() >= threshold) {
+ return true;
+ }
+ }
+
+ for (auto& dep : GetDependencies()) {
+ auto parent (dep->GetParent());
+ ObjectLock oLock (parent);
+
+ if (!parent->GetProblem() && parent->GetLastStateChange() >= threshold) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ if (suppressed_types & (NotificationProblem|NotificationRecovery)) {
+ CheckResult::Ptr cr = GetLastCheckResult();
+ NotificationType type = cr && IsStateOK(cr->GetState()) ? NotificationRecovery : NotificationProblem;
+ bool state_suppressed = NotificationReasonSuppressed(NotificationProblem) || NotificationReasonSuppressed(NotificationRecovery);
+
+ /* Only process (i.e. send or dismiss) suppressed state notifications if the following conditions are met:
+ *
+ * 1. State notifications are not suppressed at the moment. State notifications must only be removed from
+ * the suppressed notifications bitset after the reason for the suppression is gone as these bits are
+ * used as a marker for when to set the state_before_suppression attribute.
+ * 2. The checkable is in a hard state. Soft states represent a state where we are not certain yet about
+ * the actual state and wait with sending notifications. If we want to immediately send a notification,
+ * we might send a recovery notification for something that just started failing or a problem
+ * notification which might be for an intermittent problem that would have never received a
+ * notification if there was no suppression as it still was in a soft state. Both cases aren't ideal so
+ * better wait until we are certain.
+ * 3. The checkable isn't likely checked soon. For example, if a downtime ended, give the checkable a
+ * chance to recover afterwards before sending a notification.
+ * 4. No parent recovered recently. Similar to the previous condition, give the checkable a chance to
+ * recover after one of its dependencies recovered before sending a notification.
+ *
+ * If any of these conditions is not met, processing the suppressed notification is further delayed.
+ */
+ if (!state_suppressed && GetStateType() == StateTypeHard && !IsLikelyToBeCheckedSoon() && !wasLastParentRecoveryRecent.Get()) {
+ if (NotificationReasonApplies(type)) {
+ Checkable::OnNotificationsRequested(this, type, cr, "", "", nullptr);
+ }
+ subtract |= NotificationRecovery|NotificationProblem;
+ }
+ }
+
+ for (auto type : {NotificationFlappingStart, NotificationFlappingEnd}) {
+ if (suppressed_types & type) {
+ bool still_applies = NotificationReasonApplies(type);
+
+ if (still_applies) {
+ if (!NotificationReasonSuppressed(type) && !IsLikelyToBeCheckedSoon() && !wasLastParentRecoveryRecent.Get()) {
+ Checkable::OnNotificationsRequested(this, type, GetLastCheckResult(), "", "", nullptr);
+
+ subtract |= type;
+ }
+ } else {
+ subtract |= type;
+ }
+ }
+ }
+ }
+
+ if (subtract) {
+ ObjectLock olock (this);
+
+ int suppressed_types_before (GetSuppressedNotifications());
+ int suppressed_types_after (suppressed_types_before & ~subtract);
+
+ if (suppressed_types_after != suppressed_types_before) {
+ SetSuppressedNotifications(suppressed_types_after);
+ }
+ }
+}
+
+/**
+ * Re-sends all notifications previously suppressed by e.g. downtimes if the notification reason still applies.
+ */
+void Checkable::FireSuppressedNotificationsTimer(const Timer * const&)
+{
+ for (auto& host : ConfigType::GetObjectsByType<Host>()) {
+ host->FireSuppressedNotifications();
+ }
+
+ for (auto& service : ConfigType::GetObjectsByType<Service>()) {
+ service->FireSuppressedNotifications();
+ }
+}
+
+/**
+ * Returns whether sending a notification of type type right now would represent *this' current state correctly.
+ *
+ * @param type The type of notification to send (or not to send).
+ *
+ * @return Whether to send the notification.
+ */
+bool Checkable::NotificationReasonApplies(NotificationType type)
+{
+ switch (type) {
+ case NotificationProblem:
+ {
+ auto cr (GetLastCheckResult());
+ return cr && !IsStateOK(cr->GetState()) && cr->GetState() != GetStateBeforeSuppression();
+ }
+ case NotificationRecovery:
+ {
+ auto cr (GetLastCheckResult());
+ return cr && IsStateOK(cr->GetState()) && cr->GetState() != GetStateBeforeSuppression();
+ }
+ case NotificationFlappingStart:
+ return IsFlapping();
+ case NotificationFlappingEnd:
+ return !IsFlapping();
+ default:
+ VERIFY(!"Checkable#NotificationReasonStillApplies(): given type not implemented");
+ return false;
+ }
+}
+
+/**
+ * Checks if notifications of a given type should be suppressed for this Checkable at the moment.
+ *
+ * @param type The notification type for which to query the suppression status.
+ *
+ * @return true if no notification of this type should be sent.
+ */
+bool Checkable::NotificationReasonSuppressed(NotificationType type)
+{
+ switch (type) {
+ case NotificationProblem:
+ case NotificationRecovery:
+ return !IsReachable(DependencyNotification) || IsInDowntime() || IsAcknowledged();
+ case NotificationFlappingStart:
+ case NotificationFlappingEnd:
+ return IsInDowntime();
+ default:
+ return false;
+ }
+}
+
+/**
+ * E.g. we're going to re-send a stashed problem notification as *this is still not ok.
+ * But if the next check result recovers *this soon, we would send a recovery notification soon after the problem one.
+ * This is not desired, especially for lots of checkables at once.
+ * Because of that if there's likely to be a check result soon,
+ * we delay the re-sending of the stashed notification until the next check.
+ * That check either doesn't change anything and we finally re-send the stashed problem notification
+ * or recovers *this and we drop the stashed notification.
+ *
+ * @return Whether *this is likely to be checked soon
+ */
+bool Checkable::IsLikelyToBeCheckedSoon()
+{
+ if (!GetEnableActiveChecks()) {
+ return false;
+ }
+
+ // One minute unless the check interval is too short so the next check will always run during the next minute.
+ auto threshold (GetCheckInterval() - 10);
+
+ if (threshold > 60) {
+ threshold = 60;
+ } else if (threshold < 0) {
+ threshold = 0;
+ }
+
+ return GetNextCheck() <= Utility::GetTime() + threshold;
+}
diff --git a/lib/icinga/checkable-script.cpp b/lib/icinga/checkable-script.cpp
new file mode 100644
index 0000000..4a0d1d8
--- /dev/null
+++ b/lib/icinga/checkable-script.cpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "base/configobject.hpp"
+#include "base/dictionary.hpp"
+#include "base/function.hpp"
+#include "base/functionwrapper.hpp"
+#include "base/scriptframe.hpp"
+
+using namespace icinga;
+
+static void CheckableProcessCheckResult(const CheckResult::Ptr& cr)
+{
+ ScriptFrame *vframe = ScriptFrame::GetCurrentFrame();
+ Checkable::Ptr self = vframe->Self;
+ REQUIRE_NOT_NULL(self);
+ self->ProcessCheckResult(cr);
+}
+
+Object::Ptr Checkable::GetPrototype()
+{
+ static Dictionary::Ptr prototype = new Dictionary({
+ { "process_check_result", new Function("Checkable#process_check_result", CheckableProcessCheckResult, { "cr" }, false) }
+ });
+
+ return prototype;
+}
+
diff --git a/lib/icinga/checkable.cpp b/lib/icinga/checkable.cpp
new file mode 100644
index 0000000..ddf84cd
--- /dev/null
+++ b/lib/icinga/checkable.cpp
@@ -0,0 +1,322 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/checkable-ti.cpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/timer.hpp"
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE_WITH_PROTOTYPE(Checkable, Checkable::GetPrototype());
+INITIALIZE_ONCE(&Checkable::StaticInitialize);
+
+const std::map<String, int> Checkable::m_FlappingStateFilterMap ({
+ {"OK", FlappingStateFilterOk},
+ {"Warning", FlappingStateFilterWarning},
+ {"Critical", FlappingStateFilterCritical},
+ {"Unknown", FlappingStateFilterUnknown},
+ {"Up", FlappingStateFilterOk},
+ {"Down", FlappingStateFilterCritical},
+});
+
+boost::signals2::signal<void (const Checkable::Ptr&, const String&, const String&, AcknowledgementType, bool, bool, double, double, const MessageOrigin::Ptr&)> Checkable::OnAcknowledgementSet;
+boost::signals2::signal<void (const Checkable::Ptr&, const String&, double, const MessageOrigin::Ptr&)> Checkable::OnAcknowledgementCleared;
+boost::signals2::signal<void (const Checkable::Ptr&, double)> Checkable::OnFlappingChange;
+
+static Timer::Ptr l_CheckablesFireSuppressedNotifications;
+static Timer::Ptr l_CleanDeadlinedExecutions;
+
+thread_local std::function<void(const Value& commandLine, const ProcessResult&)> Checkable::ExecuteCommandProcessFinishedHandler;
+
+void Checkable::StaticInitialize()
+{
+ /* fixed downtime start */
+ Downtime::OnDowntimeStarted.connect([](const Downtime::Ptr& downtime) { Checkable::NotifyFixedDowntimeStart(downtime); });
+ /* flexible downtime start */
+ Downtime::OnDowntimeTriggered.connect([](const Downtime::Ptr& downtime) { Checkable::NotifyFlexibleDowntimeStart(downtime); });
+ /* fixed/flexible downtime end */
+ Downtime::OnDowntimeRemoved.connect([](const Downtime::Ptr& downtime) { Checkable::NotifyDowntimeEnd(downtime); });
+}
+
+Checkable::Checkable()
+{
+ SetSchedulingOffset(Utility::Random());
+}
+
+void Checkable::OnConfigLoaded()
+{
+ ObjectImpl<Checkable>::OnConfigLoaded();
+
+ SetFlappingIgnoreStatesFilter(FilterArrayToInt(GetFlappingIgnoreStates(), m_FlappingStateFilterMap, ~0));
+}
+
+void Checkable::OnAllConfigLoaded()
+{
+ ObjectImpl<Checkable>::OnAllConfigLoaded();
+
+ Endpoint::Ptr endpoint = GetCommandEndpoint();
+
+ if (endpoint) {
+ Zone::Ptr checkableZone = static_pointer_cast<Zone>(GetZone());
+
+ if (checkableZone) {
+ Zone::Ptr cmdZone = endpoint->GetZone();
+
+ if (cmdZone != checkableZone && cmdZone->GetParent() != checkableZone) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "command_endpoint" },
+ "Command endpoint must be in zone '" + checkableZone->GetName() + "' or in a direct child zone thereof."));
+ }
+ } else {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "command_endpoint" },
+ "Checkable with command endpoint requires a zone. Please check the troubleshooting documentation."));
+ }
+ }
+}
+
+void Checkable::Start(bool runtimeCreated)
+{
+ double now = Utility::GetTime();
+
+ {
+ auto cr (GetLastCheckResult());
+
+ if (GetLastCheckStarted() > (cr ? cr->GetExecutionEnd() : 0.0)) {
+ SetNextCheck(GetLastCheckStarted());
+ }
+ }
+
+ if (GetNextCheck() < now + 60) {
+ double delta = std::min(GetCheckInterval(), 60.0);
+ delta *= (double)std::rand() / RAND_MAX;
+ SetNextCheck(now + delta);
+ }
+
+ ObjectImpl<Checkable>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, []() {
+ l_CheckablesFireSuppressedNotifications = Timer::Create();
+ l_CheckablesFireSuppressedNotifications->SetInterval(5);
+ l_CheckablesFireSuppressedNotifications->OnTimerExpired.connect(&Checkable::FireSuppressedNotificationsTimer);
+ l_CheckablesFireSuppressedNotifications->Start();
+
+ l_CleanDeadlinedExecutions = Timer::Create();
+ l_CleanDeadlinedExecutions->SetInterval(300);
+ l_CleanDeadlinedExecutions->OnTimerExpired.connect(&Checkable::CleanDeadlinedExecutions);
+ l_CleanDeadlinedExecutions->Start();
+ });
+}
+
+void Checkable::AddGroup(const String& name)
+{
+ std::unique_lock<std::mutex> lock(m_CheckableMutex);
+
+ Array::Ptr groups;
+ auto *host = dynamic_cast<Host *>(this);
+
+ if (host)
+ groups = host->GetGroups();
+ else
+ groups = static_cast<Service *>(this)->GetGroups();
+
+ if (groups && groups->Contains(name))
+ return;
+
+ if (!groups)
+ groups = new Array();
+
+ groups->Add(name);
+}
+
+AcknowledgementType Checkable::GetAcknowledgement()
+{
+ auto avalue = static_cast<AcknowledgementType>(GetAcknowledgementRaw());
+
+ if (avalue != AcknowledgementNone) {
+ double expiry = GetAcknowledgementExpiry();
+
+ if (expiry != 0 && expiry < Utility::GetTime()) {
+ avalue = AcknowledgementNone;
+ ClearAcknowledgement("");
+ }
+ }
+
+ return avalue;
+}
+
+bool Checkable::IsAcknowledged() const
+{
+ return const_cast<Checkable *>(this)->GetAcknowledgement() != AcknowledgementNone;
+}
+
+void Checkable::AcknowledgeProblem(const String& author, const String& comment, AcknowledgementType type, bool notify, bool persistent, double changeTime, double expiry, const MessageOrigin::Ptr& origin)
+{
+ SetAcknowledgementRaw(type);
+ SetAcknowledgementExpiry(expiry);
+
+ if (notify && !IsPaused())
+ OnNotificationsRequested(this, NotificationAcknowledgement, GetLastCheckResult(), author, comment, nullptr);
+
+ Log(LogInformation, "Checkable")
+ << "Acknowledgement set for checkable '" << GetName() << "'.";
+
+ OnAcknowledgementSet(this, author, comment, type, notify, persistent, changeTime, expiry, origin);
+
+ SetAcknowledgementLastChange(changeTime);
+}
+
+void Checkable::ClearAcknowledgement(const String& removedBy, double changeTime, const MessageOrigin::Ptr& origin)
+{
+ ObjectLock oLock (this);
+
+ bool wasAcked = GetAcknowledgementRaw() != AcknowledgementNone;
+
+ SetAcknowledgementRaw(AcknowledgementNone);
+ SetAcknowledgementExpiry(0);
+
+ Log(LogInformation, "Checkable")
+ << "Acknowledgement cleared for checkable '" << GetName() << "'.";
+
+ if (wasAcked) {
+ OnAcknowledgementCleared(this, removedBy, changeTime, origin);
+
+ SetAcknowledgementLastChange(changeTime);
+ }
+}
+
+Endpoint::Ptr Checkable::GetCommandEndpoint() const
+{
+ return Endpoint::GetByName(GetCommandEndpointRaw());
+}
+
+int Checkable::GetSeverity() const
+{
+ /* overridden in Host/Service class. */
+ return 0;
+}
+
+bool Checkable::GetProblem() const
+{
+ auto cr (GetLastCheckResult());
+
+ return cr && !IsStateOK(cr->GetState());
+}
+
+bool Checkable::GetHandled() const
+{
+ return GetProblem() && (IsInDowntime() || IsAcknowledged());
+}
+
+Timestamp Checkable::GetNextUpdate() const
+{
+ auto cr (GetLastCheckResult());
+ double interval, latency;
+
+ // TODO: Document this behavior.
+ if (cr) {
+ interval = GetEnableActiveChecks() && GetProblem() && GetStateType() == StateTypeSoft ? GetRetryInterval() : GetCheckInterval();
+ latency = cr->GetExecutionEnd() - cr->GetScheduleStart();
+ } else {
+ interval = GetCheckInterval();
+ latency = 0.0;
+ }
+
+ return (GetEnableActiveChecks() ? GetNextCheck() : (cr ? cr->GetExecutionEnd() : Application::GetStartTime()) + interval) + interval + 2 * latency;
+}
+
+void Checkable::NotifyFixedDowntimeStart(const Downtime::Ptr& downtime)
+{
+ if (!downtime->GetFixed())
+ return;
+
+ NotifyDowntimeInternal(downtime);
+}
+
+void Checkable::NotifyFlexibleDowntimeStart(const Downtime::Ptr& downtime)
+{
+ if (downtime->GetFixed())
+ return;
+
+ NotifyDowntimeInternal(downtime);
+}
+
+void Checkable::NotifyDowntimeInternal(const Downtime::Ptr& downtime)
+{
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ if (!checkable->IsPaused())
+ OnNotificationsRequested(checkable, NotificationDowntimeStart, checkable->GetLastCheckResult(), downtime->GetAuthor(), downtime->GetComment(), nullptr);
+}
+
+void Checkable::NotifyDowntimeEnd(const Downtime::Ptr& downtime)
+{
+ /* don't send notifications for downtimes which never triggered */
+ if (!downtime->IsTriggered())
+ return;
+
+ Checkable::Ptr checkable = downtime->GetCheckable();
+
+ if (!checkable->IsPaused())
+ OnNotificationsRequested(checkable, NotificationDowntimeEnd, checkable->GetLastCheckResult(), downtime->GetAuthor(), downtime->GetComment(), nullptr);
+}
+
+void Checkable::ValidateCheckInterval(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Checkable>::ValidateCheckInterval(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "check_interval" }, "Interval must be greater than 0."));
+}
+
+void Checkable::ValidateRetryInterval(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Checkable>::ValidateRetryInterval(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "retry_interval" }, "Interval must be greater than 0."));
+}
+
+void Checkable::ValidateMaxCheckAttempts(const Lazy<int>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Checkable>::ValidateMaxCheckAttempts(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "max_check_attempts" }, "Value must be greater than 0."));
+}
+
+void Checkable::CleanDeadlinedExecutions(const Timer * const&)
+{
+ double now = Utility::GetTime();
+ Dictionary::Ptr executions;
+ Dictionary::Ptr execution;
+
+ for (auto& host : ConfigType::GetObjectsByType<Host>()) {
+ executions = host->GetExecutions();
+ if (executions) {
+ for (const String& key : executions->GetKeys()) {
+ execution = executions->Get(key);
+ if (execution->Contains("deadline") && now > execution->Get("deadline")) {
+ executions->Remove(key);
+ }
+ }
+ }
+ }
+
+ for (auto& service : ConfigType::GetObjectsByType<Service>()) {
+ executions = service->GetExecutions();
+ if (executions) {
+ for (const String& key : executions->GetKeys()) {
+ execution = executions->Get(key);
+ if (execution->Contains("deadline") && now > execution->Get("deadline")) {
+ executions->Remove(key);
+ }
+ }
+ }
+ }
+}
diff --git a/lib/icinga/checkable.hpp b/lib/icinga/checkable.hpp
new file mode 100644
index 0000000..3d48b14
--- /dev/null
+++ b/lib/icinga/checkable.hpp
@@ -0,0 +1,264 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CHECKABLE_H
+#define CHECKABLE_H
+
+#include "base/atomic.hpp"
+#include "base/timer.hpp"
+#include "base/process.hpp"
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkable-ti.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/comment.hpp"
+#include "icinga/downtime.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/messageorigin.hpp"
+#include <condition_variable>
+#include <cstdint>
+#include <functional>
+#include <limits>
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+enum DependencyType
+{
+ DependencyState,
+ DependencyCheckExecution,
+ DependencyNotification
+};
+
+/**
+ * Checkable Types
+ *
+ * @ingroup icinga
+ */
+enum CheckableType
+{
+ CheckableHost,
+ CheckableService
+};
+
+/**
+ * @ingroup icinga
+ */
+enum FlappingStateFilter
+{
+ FlappingStateFilterOk = 1,
+ FlappingStateFilterWarning = 2,
+ FlappingStateFilterCritical = 4,
+ FlappingStateFilterUnknown = 8,
+};
+
+class CheckCommand;
+class EventCommand;
+class Dependency;
+
+/**
+ * An Icinga service.
+ *
+ * @ingroup icinga
+ */
+class Checkable : public ObjectImpl<Checkable>
+{
+public:
+ DECLARE_OBJECT(Checkable);
+ DECLARE_OBJECTNAME(Checkable);
+
+ static void StaticInitialize();
+ static thread_local std::function<void(const Value& commandLine, const ProcessResult&)> ExecuteCommandProcessFinishedHandler;
+
+ Checkable();
+
+ std::set<Checkable::Ptr> GetParents() const;
+ std::set<Checkable::Ptr> GetChildren() const;
+ std::set<Checkable::Ptr> GetAllChildren() const;
+
+ void AddGroup(const String& name);
+
+ bool IsReachable(DependencyType dt = DependencyState, intrusive_ptr<Dependency> *failedDependency = nullptr, int rstack = 0) const;
+
+ AcknowledgementType GetAcknowledgement();
+
+ void AcknowledgeProblem(const String& author, const String& comment, AcknowledgementType type, bool notify = true, bool persistent = false, double changeTime = Utility::GetTime(), double expiry = 0, const MessageOrigin::Ptr& origin = nullptr);
+ void ClearAcknowledgement(const String& removedBy, double changeTime = Utility::GetTime(), const MessageOrigin::Ptr& origin = nullptr);
+
+ int GetSeverity() const override;
+ bool GetProblem() const override;
+ bool GetHandled() const override;
+ Timestamp GetNextUpdate() const override;
+
+ /* Checks */
+ intrusive_ptr<CheckCommand> GetCheckCommand() const;
+ TimePeriod::Ptr GetCheckPeriod() const;
+
+ long GetSchedulingOffset();
+ void SetSchedulingOffset(long offset);
+
+ void UpdateNextCheck(const MessageOrigin::Ptr& origin = nullptr);
+
+ bool HasBeenChecked() const;
+ virtual bool IsStateOK(ServiceState state) const = 0;
+
+ double GetLastCheck() const final;
+
+ virtual void SaveLastState(ServiceState state, double timestamp) = 0;
+
+ static void UpdateStatistics(const CheckResult::Ptr& cr, CheckableType type);
+
+ void ExecuteRemoteCheck(const Dictionary::Ptr& resolvedMacros = nullptr);
+ void ExecuteCheck();
+ enum class ProcessingResult
+ {
+ Ok,
+ NoCheckResult,
+ CheckableInactive,
+ NewerCheckResultPresent,
+ };
+ ProcessingResult ProcessCheckResult(const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin = nullptr);
+
+ Endpoint::Ptr GetCommandEndpoint() const;
+
+ static boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, const MessageOrigin::Ptr&)> OnNewCheckResult;
+ static boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, StateType, const MessageOrigin::Ptr&)> OnStateChange;
+ static boost::signals2::signal<void (const Checkable::Ptr&, const CheckResult::Ptr&, std::set<Checkable::Ptr>, const MessageOrigin::Ptr&)> OnReachabilityChanged;
+ static boost::signals2::signal<void (const Checkable::Ptr&, NotificationType, const CheckResult::Ptr&,
+ const String&, const String&, const MessageOrigin::Ptr&)> OnNotificationsRequested;
+ static boost::signals2::signal<void (const Notification::Ptr&, const Checkable::Ptr&, const User::Ptr&,
+ const NotificationType&, const CheckResult::Ptr&, const String&, const String&, const String&,
+ const MessageOrigin::Ptr&)> OnNotificationSentToUser;
+ static boost::signals2::signal<void (const Notification::Ptr&, const Checkable::Ptr&, const std::set<User::Ptr>&,
+ const NotificationType&, const CheckResult::Ptr&, const String&,
+ const String&, const MessageOrigin::Ptr&)> OnNotificationSentToAllUsers;
+ static boost::signals2::signal<void (const Checkable::Ptr&, const String&, const String&, AcknowledgementType,
+ bool, bool, double, double, const MessageOrigin::Ptr&)> OnAcknowledgementSet;
+ static boost::signals2::signal<void (const Checkable::Ptr&, const String&, double, const MessageOrigin::Ptr&)> OnAcknowledgementCleared;
+ static boost::signals2::signal<void (const Checkable::Ptr&, double)> OnFlappingChange;
+ static boost::signals2::signal<void (const Checkable::Ptr&)> OnNextCheckUpdated;
+ static boost::signals2::signal<void (const Checkable::Ptr&)> OnEventCommandExecuted;
+
+ static Atomic<uint_fast64_t> CurrentConcurrentChecks;
+
+ /* Downtimes */
+ int GetDowntimeDepth() const final;
+
+ void RemoveAllDowntimes();
+ void TriggerDowntimes(double triggerTime);
+ bool IsInDowntime() const;
+ bool IsAcknowledged() const;
+
+ std::set<Downtime::Ptr> GetDowntimes() const;
+ void RegisterDowntime(const Downtime::Ptr& downtime);
+ void UnregisterDowntime(const Downtime::Ptr& downtime);
+
+ /* Comments */
+ void RemoveAllComments();
+ void RemoveAckComments(const String& removedBy = String(), double createdBefore = std::numeric_limits<double>::max());
+
+ std::set<Comment::Ptr> GetComments() const;
+ Comment::Ptr GetLastComment() const;
+ void RegisterComment(const Comment::Ptr& comment);
+ void UnregisterComment(const Comment::Ptr& comment);
+
+ /* Notifications */
+ void SendNotifications(NotificationType type, const CheckResult::Ptr& cr, const String& author = "", const String& text = "");
+
+ std::set<Notification::Ptr> GetNotifications() const;
+ void RegisterNotification(const Notification::Ptr& notification);
+ void UnregisterNotification(const Notification::Ptr& notification);
+
+ void ResetNotificationNumbers();
+
+ /* Event Handler */
+ void ExecuteEventHandler(const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false);
+
+ intrusive_ptr<EventCommand> GetEventCommand() const;
+
+ /* Flapping Detection */
+ bool IsFlapping() const;
+
+ /* Dependencies */
+ void AddDependency(const intrusive_ptr<Dependency>& dep);
+ void RemoveDependency(const intrusive_ptr<Dependency>& dep);
+ std::vector<intrusive_ptr<Dependency> > GetDependencies() const;
+
+ void AddReverseDependency(const intrusive_ptr<Dependency>& dep);
+ void RemoveReverseDependency(const intrusive_ptr<Dependency>& dep);
+ std::vector<intrusive_ptr<Dependency> > GetReverseDependencies() const;
+
+ void ValidateCheckInterval(const Lazy<double>& lvalue, const ValidationUtils& value) final;
+ void ValidateRetryInterval(const Lazy<double>& lvalue, const ValidationUtils& value) final;
+ void ValidateMaxCheckAttempts(const Lazy<int>& lvalue, const ValidationUtils& value) final;
+
+ bool NotificationReasonApplies(NotificationType type);
+ bool NotificationReasonSuppressed(NotificationType type);
+ bool IsLikelyToBeCheckedSoon();
+
+ void FireSuppressedNotifications();
+
+ static void IncreasePendingChecks();
+ static void DecreasePendingChecks();
+ static int GetPendingChecks();
+ static void AquirePendingCheckSlot(int maxPendingChecks);
+
+ static Object::Ptr GetPrototype();
+
+protected:
+ void Start(bool runtimeCreated) override;
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+
+private:
+ mutable std::mutex m_CheckableMutex;
+ bool m_CheckRunning{false};
+ long m_SchedulingOffset;
+
+ static std::mutex m_StatsMutex;
+ static int m_PendingChecks;
+ static std::condition_variable m_PendingChecksCV;
+
+ /* Downtimes */
+ std::set<Downtime::Ptr> m_Downtimes;
+ mutable std::mutex m_DowntimeMutex;
+
+ static void NotifyFixedDowntimeStart(const Downtime::Ptr& downtime);
+ static void NotifyFlexibleDowntimeStart(const Downtime::Ptr& downtime);
+ static void NotifyDowntimeInternal(const Downtime::Ptr& downtime);
+
+ static void NotifyDowntimeEnd(const Downtime::Ptr& downtime);
+
+ static void FireSuppressedNotificationsTimer(const Timer * const&);
+ static void CleanDeadlinedExecutions(const Timer * const&);
+
+ /* Comments */
+ std::set<Comment::Ptr> m_Comments;
+ mutable std::mutex m_CommentMutex;
+
+ /* Notifications */
+ std::set<Notification::Ptr> m_Notifications;
+ mutable std::mutex m_NotificationMutex;
+
+ /* Dependencies */
+ mutable std::mutex m_DependencyMutex;
+ std::set<intrusive_ptr<Dependency> > m_Dependencies;
+ std::set<intrusive_ptr<Dependency> > m_ReverseDependencies;
+
+ void GetAllChildrenInternal(std::set<Checkable::Ptr>& children, int level = 0) const;
+
+ /* Flapping */
+ static const std::map<String, int> m_FlappingStateFilterMap;
+
+ void UpdateFlappingStatus(ServiceState newState);
+ static int ServiceStateToFlappingFilter(ServiceState state);
+};
+
+}
+
+#endif /* CHECKABLE_H */
+
+#include "icinga/dependency.hpp"
diff --git a/lib/icinga/checkable.ti b/lib/icinga/checkable.ti
new file mode 100644
index 0000000..6f7a5da
--- /dev/null
+++ b/lib/icinga/checkable.ti
@@ -0,0 +1,192 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/icingaapplication.hpp"
+#include "icinga/customvarobject.hpp"
+#include "base/array.hpp"
+#impl_include "icinga/checkcommand.hpp"
+#impl_include "icinga/eventcommand.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+/**
+ * The acknowledgement type of a service.
+ *
+ * @ingroup icinga
+ */
+enum AcknowledgementType
+{
+ AcknowledgementNone = 0,
+ AcknowledgementNormal = 1,
+ AcknowledgementSticky = 2
+};
+}}}
+
+abstract class Checkable : CustomVarObject
+{
+ [config, required, navigation] name(CheckCommand) check_command (CheckCommandRaw) {
+ navigate {{{
+ return CheckCommand::GetByName(GetCheckCommandRaw());
+ }}}
+ };
+ [config] int max_check_attempts {
+ default {{{ return 3; }}}
+ };
+ [config, navigation] name(TimePeriod) check_period (CheckPeriodRaw) {
+ navigate {{{
+ return TimePeriod::GetByName(GetCheckPeriodRaw());
+ }}}
+ };
+ [config] Value check_timeout;
+ [config] double check_interval {
+ default {{{ return 5 * 60; }}}
+ };
+ [config] double retry_interval {
+ default {{{ return 60; }}}
+ };
+ [config, navigation] name(EventCommand) event_command (EventCommandRaw) {
+ navigate {{{
+ return EventCommand::GetByName(GetEventCommandRaw());
+ }}}
+ };
+ [config] bool volatile;
+
+ [config] bool enable_active_checks {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_passive_checks {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_event_handler {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_notifications {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_flapping {
+ default {{{ return false; }}}
+ };
+ [config] bool enable_perfdata {
+ default {{{ return true; }}}
+ };
+
+ [config] array(String) flapping_ignore_states;
+ [no_user_view, no_user_modify] int flapping_ignore_states_filter_real (FlappingIgnoreStatesFilter);
+
+ [config, deprecated] double flapping_threshold;
+
+ [config] double flapping_threshold_low {
+ default {{{ return 25; }}}
+ };
+
+ [config] double flapping_threshold_high{
+ default {{{ return 30; }}}
+ };
+
+ [config] String notes;
+ [config] String notes_url;
+ [config] String action_url;
+ [config] String icon_image;
+ [config] String icon_image_alt;
+
+ [state] Timestamp next_check;
+ [state, no_user_view, no_user_modify] Timestamp last_check_started;
+
+ [state] int check_attempt {
+ default {{{ return 1; }}}
+ };
+ [state, enum, no_user_view, no_user_modify] ServiceState state_raw {
+ default {{{ return ServiceUnknown; }}}
+ };
+ [state, enum] StateType state_type {
+ default {{{ return StateTypeSoft; }}}
+ };
+ [state, enum, no_user_view, no_user_modify] ServiceState last_state_raw {
+ default {{{ return ServiceUnknown; }}}
+ };
+ [state, enum, no_user_view, no_user_modify] ServiceState last_hard_state_raw {
+ default {{{ return ServiceUnknown; }}}
+ };
+ [state, no_user_view, no_user_modify] "unsigned short" last_hard_states_raw {
+ default {{{ return /* current */ 99 * 100 + /* previous */ 99; }}}
+ };
+ [state, no_user_view, no_user_modify] "unsigned short" last_soft_states_raw {
+ default {{{ return /* current */ 99 * 100 + /* previous */ 99; }}}
+ };
+ [state, enum] StateType last_state_type {
+ default {{{ return StateTypeSoft; }}}
+ };
+ [state] bool last_reachable {
+ default {{{ return true; }}}
+ };
+ [state] CheckResult::Ptr last_check_result;
+ [state] Timestamp last_state_change {
+ default {{{ return Application::GetStartTime(); }}}
+ };
+ [state] Timestamp last_hard_state_change {
+ default {{{ return Application::GetStartTime(); }}}
+ };
+ [state] Timestamp last_state_unreachable;
+
+ [state] Timestamp previous_state_change {
+ default {{{ return Application::GetStartTime(); }}}
+ };
+ [no_storage] int severity {
+ get;
+ };
+ [no_storage] bool problem {
+ get;
+ };
+ [no_storage] bool handled {
+ get;
+ };
+ [no_storage] Timestamp next_update {
+ get;
+ };
+
+ [state] bool force_next_check;
+ [state] int acknowledgement (AcknowledgementRaw) {
+ default {{{ return AcknowledgementNone; }}}
+ };
+ [state] Timestamp acknowledgement_expiry;
+ [state] Timestamp acknowledgement_last_change;
+ [state] bool force_next_notification;
+ [no_storage] Timestamp last_check {
+ get;
+ };
+ [no_storage] int downtime_depth {
+ get;
+ };
+
+ [state] double flapping_current {
+ default {{{ return 0; }}}
+ };
+ [state] Timestamp flapping_last_change;
+
+ [state, enum, no_user_view, no_user_modify] ServiceState flapping_last_state {
+ default {{{ return ServiceUnknown; }}}
+ };
+ [state, no_user_view, no_user_modify] int flapping_buffer;
+ [state, no_user_view, no_user_modify] int flapping_index;
+ [state, protected] bool flapping;
+ [state, no_user_view, no_user_modify] int suppressed_notifications {
+ default {{{ return 0; }}}
+ };
+ [state, enum, no_user_view, no_user_modify] ServiceState state_before_suppression {
+ default {{{ return ServiceOK; }}}
+ };
+
+ [config, navigation] name(Endpoint) command_endpoint (CommandEndpointRaw) {
+ navigate {{{
+ return Endpoint::GetByName(GetCommandEndpointRaw());
+ }}}
+ };
+
+ [state, no_user_modify] Dictionary::Ptr executions;
+ [state, no_user_view, no_user_modify] Dictionary::Ptr pending_executions;
+};
+
+}
diff --git a/lib/icinga/checkcommand.cpp b/lib/icinga/checkcommand.cpp
new file mode 100644
index 0000000..fb8032a
--- /dev/null
+++ b/lib/icinga/checkcommand.cpp
@@ -0,0 +1,22 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkcommand.hpp"
+#include "icinga/checkcommand-ti.cpp"
+#include "base/configtype.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(CheckCommand);
+
+thread_local CheckCommand::Ptr CheckCommand::ExecuteOverride;
+
+void CheckCommand::Execute(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ GetExecute()->Invoke({
+ checkable,
+ cr,
+ resolvedMacros,
+ useResolvedMacros
+ });
+}
diff --git a/lib/icinga/checkcommand.hpp b/lib/icinga/checkcommand.hpp
new file mode 100644
index 0000000..c654cf9
--- /dev/null
+++ b/lib/icinga/checkcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CHECKCOMMAND_H
+#define CHECKCOMMAND_H
+
+#include "icinga/checkcommand-ti.hpp"
+#include "icinga/checkable.hpp"
+
+namespace icinga
+{
+
+/**
+ * A command.
+ *
+ * @ingroup icinga
+ */
+class CheckCommand final : public ObjectImpl<CheckCommand>
+{
+public:
+ DECLARE_OBJECT(CheckCommand);
+ DECLARE_OBJECTNAME(CheckCommand);
+
+ static thread_local CheckCommand::Ptr ExecuteOverride;
+
+ void Execute(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false);
+};
+
+}
+
+#endif /* CHECKCOMMAND_H */
diff --git a/lib/icinga/checkcommand.ti b/lib/icinga/checkcommand.ti
new file mode 100644
index 0000000..c211f0f
--- /dev/null
+++ b/lib/icinga/checkcommand.ti
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/command.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class CheckCommand : Command
+{
+};
+
+}
diff --git a/lib/icinga/checkresult.cpp b/lib/icinga/checkresult.cpp
new file mode 100644
index 0000000..07f7219
--- /dev/null
+++ b/lib/icinga/checkresult.cpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkresult.hpp"
+#include "icinga/checkresult-ti.cpp"
+#include "base/scriptglobal.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(CheckResult);
+
+INITIALIZE_ONCE([]() {
+ ScriptGlobal::Set("Icinga.ServiceOK", ServiceOK);
+ ScriptGlobal::Set("Icinga.ServiceWarning", ServiceWarning);
+ ScriptGlobal::Set("Icinga.ServiceCritical", ServiceCritical);
+ ScriptGlobal::Set("Icinga.ServiceUnknown", ServiceUnknown);
+
+ ScriptGlobal::Set("Icinga.HostUp", HostUp);
+ ScriptGlobal::Set("Icinga.HostDown", HostDown);
+})
+
+double CheckResult::CalculateExecutionTime() const
+{
+ return GetExecutionEnd() - GetExecutionStart();
+}
+
+double CheckResult::CalculateLatency() const
+{
+ double latency = (GetScheduleEnd() - GetScheduleStart()) - CalculateExecutionTime();
+
+ if (latency < 0)
+ latency = 0;
+
+ return latency;
+}
diff --git a/lib/icinga/checkresult.hpp b/lib/icinga/checkresult.hpp
new file mode 100644
index 0000000..ac54d6b
--- /dev/null
+++ b/lib/icinga/checkresult.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CHECKRESULT_H
+#define CHECKRESULT_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkresult-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * A check result.
+ *
+ * @ingroup icinga
+ */
+class CheckResult final : public ObjectImpl<CheckResult>
+{
+public:
+ DECLARE_OBJECT(CheckResult);
+
+ double CalculateExecutionTime() const;
+ double CalculateLatency() const;
+};
+
+}
+
+#endif /* CHECKRESULT_H */
diff --git a/lib/icinga/checkresult.ti b/lib/icinga/checkresult.ti
new file mode 100644
index 0000000..09312dc
--- /dev/null
+++ b/lib/icinga/checkresult.ti
@@ -0,0 +1,72 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+/**
+ * The state of a host.
+ *
+ * @ingroup icinga
+ */
+enum HostState
+{
+ HostUp = 0,
+ HostDown = 1
+};
+
+/**
+ * The state of a service.
+ *
+ * @ingroup icinga
+ */
+enum ServiceState
+{
+ ServiceOK = 0,
+ ServiceWarning = 1,
+ ServiceCritical = 2,
+ ServiceUnknown = 3
+};
+
+/**
+ * The state type of a host or service.
+ *
+ * @ingroup icinga
+ */
+enum StateType
+{
+ StateTypeSoft = 0,
+ StateTypeHard = 1
+};
+}}}
+
+class CheckResult
+{
+ [state] Timestamp schedule_start;
+ [state] Timestamp schedule_end;
+ [state] Timestamp execution_start;
+ [state] Timestamp execution_end;
+
+ [state] Value command;
+ [state] int exit_status;
+
+ [state, enum] ServiceState "state";
+ [state, enum] ServiceState previous_hard_state;
+ [state] String output;
+ [state] Array::Ptr performance_data;
+
+ [state] bool active {
+ default {{{ return true; }}}
+ };
+
+ [state] String check_source;
+ [state] String scheduling_source;
+ [state] double ttl;
+
+ [state] Dictionary::Ptr vars_before;
+ [state] Dictionary::Ptr vars_after;
+};
+
+}
diff --git a/lib/icinga/cib.cpp b/lib/icinga/cib.cpp
new file mode 100644
index 0000000..ce71a59
--- /dev/null
+++ b/lib/icinga/cib.cpp
@@ -0,0 +1,346 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/cib.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/clusterevents.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/configtype.hpp"
+#include "base/statsfunction.hpp"
+
+using namespace icinga;
+
+RingBuffer CIB::m_ActiveHostChecksStatistics(15 * 60);
+RingBuffer CIB::m_ActiveServiceChecksStatistics(15 * 60);
+RingBuffer CIB::m_PassiveHostChecksStatistics(15 * 60);
+RingBuffer CIB::m_PassiveServiceChecksStatistics(15 * 60);
+
+void CIB::UpdateActiveHostChecksStatistics(long tv, int num)
+{
+ m_ActiveHostChecksStatistics.InsertValue(tv, num);
+}
+
+void CIB::UpdateActiveServiceChecksStatistics(long tv, int num)
+{
+ m_ActiveServiceChecksStatistics.InsertValue(tv, num);
+}
+
+int CIB::GetActiveHostChecksStatistics(long timespan)
+{
+ return m_ActiveHostChecksStatistics.UpdateAndGetValues(Utility::GetTime(), timespan);
+}
+
+int CIB::GetActiveServiceChecksStatistics(long timespan)
+{
+ return m_ActiveServiceChecksStatistics.UpdateAndGetValues(Utility::GetTime(), timespan);
+}
+
+void CIB::UpdatePassiveHostChecksStatistics(long tv, int num)
+{
+ m_PassiveServiceChecksStatistics.InsertValue(tv, num);
+}
+
+void CIB::UpdatePassiveServiceChecksStatistics(long tv, int num)
+{
+ m_PassiveServiceChecksStatistics.InsertValue(tv, num);
+}
+
+int CIB::GetPassiveHostChecksStatistics(long timespan)
+{
+ return m_PassiveHostChecksStatistics.UpdateAndGetValues(Utility::GetTime(), timespan);
+}
+
+int CIB::GetPassiveServiceChecksStatistics(long timespan)
+{
+ return m_PassiveServiceChecksStatistics.UpdateAndGetValues(Utility::GetTime(), timespan);
+}
+
+CheckableCheckStatistics CIB::CalculateHostCheckStats()
+{
+ double min_latency = -1, max_latency = 0, sum_latency = 0;
+ int count_latency = 0;
+ double min_execution_time = -1, max_execution_time = 0, sum_execution_time = 0;
+ int count_execution_time = 0;
+ bool checkresult = false;
+
+ for (const Host::Ptr& host : ConfigType::GetObjectsByType<Host>()) {
+ ObjectLock olock(host);
+
+ CheckResult::Ptr cr = host->GetLastCheckResult();
+
+ if (!cr)
+ continue;
+
+ /* set to true, we have a checkresult */
+ checkresult = true;
+
+ /* latency */
+ double latency = cr->CalculateLatency();
+
+ if (min_latency == -1 || latency < min_latency)
+ min_latency = latency;
+
+ if (latency > max_latency)
+ max_latency = latency;
+
+ sum_latency += latency;
+ count_latency++;
+
+ /* execution_time */
+ double execution_time = cr->CalculateExecutionTime();
+
+ if (min_execution_time == -1 || execution_time < min_execution_time)
+ min_execution_time = execution_time;
+
+ if (execution_time > max_execution_time)
+ max_execution_time = execution_time;
+
+ sum_execution_time += execution_time;
+ count_execution_time++;
+ }
+
+ if (!checkresult) {
+ min_latency = 0;
+ min_execution_time = 0;
+ }
+
+ CheckableCheckStatistics ccs;
+
+ ccs.min_latency = min_latency;
+ ccs.max_latency = max_latency;
+ ccs.avg_latency = sum_latency / count_latency;
+ ccs.min_execution_time = min_execution_time;
+ ccs.max_execution_time = max_execution_time;
+ ccs.avg_execution_time = sum_execution_time / count_execution_time;
+
+ return ccs;
+}
+
+CheckableCheckStatistics CIB::CalculateServiceCheckStats()
+{
+ double min_latency = -1, max_latency = 0, sum_latency = 0;
+ int count_latency = 0;
+ double min_execution_time = -1, max_execution_time = 0, sum_execution_time = 0;
+ int count_execution_time = 0;
+ bool checkresult = false;
+
+ for (const Service::Ptr& service : ConfigType::GetObjectsByType<Service>()) {
+ ObjectLock olock(service);
+
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (!cr)
+ continue;
+
+ /* set to true, we have a checkresult */
+ checkresult = true;
+
+ /* latency */
+ double latency = cr->CalculateLatency();
+
+ if (min_latency == -1 || latency < min_latency)
+ min_latency = latency;
+
+ if (latency > max_latency)
+ max_latency = latency;
+
+ sum_latency += latency;
+ count_latency++;
+
+ /* execution_time */
+ double execution_time = cr->CalculateExecutionTime();
+
+ if (min_execution_time == -1 || execution_time < min_execution_time)
+ min_execution_time = execution_time;
+
+ if (execution_time > max_execution_time)
+ max_execution_time = execution_time;
+
+ sum_execution_time += execution_time;
+ count_execution_time++;
+ }
+
+ if (!checkresult) {
+ min_latency = 0;
+ min_execution_time = 0;
+ }
+
+ CheckableCheckStatistics ccs;
+
+ ccs.min_latency = min_latency;
+ ccs.max_latency = max_latency;
+ ccs.avg_latency = sum_latency / count_latency;
+ ccs.min_execution_time = min_execution_time;
+ ccs.max_execution_time = max_execution_time;
+ ccs.avg_execution_time = sum_execution_time / count_execution_time;
+
+ return ccs;
+}
+
+ServiceStatistics CIB::CalculateServiceStats()
+{
+ ServiceStatistics ss = {};
+
+ for (const Service::Ptr& service : ConfigType::GetObjectsByType<Service>()) {
+ ObjectLock olock(service);
+
+ if (service->GetState() == ServiceOK)
+ ss.services_ok++;
+ if (service->GetState() == ServiceWarning)
+ ss.services_warning++;
+ if (service->GetState() == ServiceCritical)
+ ss.services_critical++;
+ if (service->GetState() == ServiceUnknown)
+ ss.services_unknown++;
+
+ CheckResult::Ptr cr = service->GetLastCheckResult();
+
+ if (!cr)
+ ss.services_pending++;
+
+ if (!service->IsReachable())
+ ss.services_unreachable++;
+
+ if (service->IsFlapping())
+ ss.services_flapping++;
+ if (service->IsInDowntime())
+ ss.services_in_downtime++;
+ if (service->IsAcknowledged())
+ ss.services_acknowledged++;
+
+ if (service->GetHandled())
+ ss.services_handled++;
+ if (service->GetProblem())
+ ss.services_problem++;
+ }
+
+ return ss;
+}
+
+HostStatistics CIB::CalculateHostStats()
+{
+ HostStatistics hs = {};
+
+ for (const Host::Ptr& host : ConfigType::GetObjectsByType<Host>()) {
+ ObjectLock olock(host);
+
+ if (host->IsReachable()) {
+ if (host->GetState() == HostUp)
+ hs.hosts_up++;
+ if (host->GetState() == HostDown)
+ hs.hosts_down++;
+ } else
+ hs.hosts_unreachable++;
+
+ if (!host->GetLastCheckResult())
+ hs.hosts_pending++;
+
+ if (host->IsFlapping())
+ hs.hosts_flapping++;
+ if (host->IsInDowntime())
+ hs.hosts_in_downtime++;
+ if (host->IsAcknowledged())
+ hs.hosts_acknowledged++;
+
+ if (host->GetHandled())
+ hs.hosts_handled++;
+ if (host->GetProblem())
+ hs.hosts_problem++;
+ }
+
+ return hs;
+}
+
+/*
+ * 'perfdata' must be a flat dictionary with double values
+ * 'status' dictionary can contain multiple levels of dictionaries
+ */
+std::pair<Dictionary::Ptr, Array::Ptr> CIB::GetFeatureStats()
+{
+ Dictionary::Ptr status = new Dictionary();
+ Array::Ptr perfdata = new Array();
+
+ Namespace::Ptr statsFunctions = ScriptGlobal::Get("StatsFunctions", &Empty);
+
+ if (statsFunctions) {
+ ObjectLock olock(statsFunctions);
+
+ for (const Namespace::Pair& kv : statsFunctions)
+ static_cast<Function::Ptr>(kv.second.Val)->Invoke({ status, perfdata });
+ }
+
+ return std::make_pair(status, perfdata);
+}
+
+REGISTER_STATSFUNCTION(CIB, &CIB::StatsFunc);
+
+void CIB::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata) {
+ double interval = Utility::GetTime() - Application::GetStartTime();
+
+ if (interval > 60)
+ interval = 60;
+
+ status->Set("active_host_checks", GetActiveHostChecksStatistics(interval) / interval);
+ status->Set("passive_host_checks", GetPassiveHostChecksStatistics(interval) / interval);
+ status->Set("active_host_checks_1min", GetActiveHostChecksStatistics(60));
+ status->Set("passive_host_checks_1min", GetPassiveHostChecksStatistics(60));
+ status->Set("active_host_checks_5min", GetActiveHostChecksStatistics(60 * 5));
+ status->Set("passive_host_checks_5min", GetPassiveHostChecksStatistics(60 * 5));
+ status->Set("active_host_checks_15min", GetActiveHostChecksStatistics(60 * 15));
+ status->Set("passive_host_checks_15min", GetPassiveHostChecksStatistics(60 * 15));
+
+ status->Set("active_service_checks", GetActiveServiceChecksStatistics(interval) / interval);
+ status->Set("passive_service_checks", GetPassiveServiceChecksStatistics(interval) / interval);
+ status->Set("active_service_checks_1min", GetActiveServiceChecksStatistics(60));
+ status->Set("passive_service_checks_1min", GetPassiveServiceChecksStatistics(60));
+ status->Set("active_service_checks_5min", GetActiveServiceChecksStatistics(60 * 5));
+ status->Set("passive_service_checks_5min", GetPassiveServiceChecksStatistics(60 * 5));
+ status->Set("active_service_checks_15min", GetActiveServiceChecksStatistics(60 * 15));
+ status->Set("passive_service_checks_15min", GetPassiveServiceChecksStatistics(60 * 15));
+
+ // Checker related stats
+ status->Set("remote_check_queue", ClusterEvents::GetCheckRequestQueueSize());
+ status->Set("current_pending_callbacks", Application::GetTP().GetPending());
+ status->Set("current_concurrent_checks", Checkable::CurrentConcurrentChecks.load());
+
+ CheckableCheckStatistics scs = CalculateServiceCheckStats();
+
+ status->Set("min_latency", scs.min_latency);
+ status->Set("max_latency", scs.max_latency);
+ status->Set("avg_latency", scs.avg_latency);
+ status->Set("min_execution_time", scs.min_execution_time);
+ status->Set("max_execution_time", scs.max_execution_time);
+ status->Set("avg_execution_time", scs.avg_execution_time);
+
+ ServiceStatistics ss = CalculateServiceStats();
+
+ status->Set("num_services_ok", ss.services_ok);
+ status->Set("num_services_warning", ss.services_warning);
+ status->Set("num_services_critical", ss.services_critical);
+ status->Set("num_services_unknown", ss.services_unknown);
+ status->Set("num_services_pending", ss.services_pending);
+ status->Set("num_services_unreachable", ss.services_unreachable);
+ status->Set("num_services_flapping", ss.services_flapping);
+ status->Set("num_services_in_downtime", ss.services_in_downtime);
+ status->Set("num_services_acknowledged", ss.services_acknowledged);
+ status->Set("num_services_handled", ss.services_handled);
+ status->Set("num_services_problem", ss.services_problem);
+
+ double uptime = Application::GetUptime();
+ status->Set("uptime", uptime);
+
+ HostStatistics hs = CalculateHostStats();
+
+ status->Set("num_hosts_up", hs.hosts_up);
+ status->Set("num_hosts_down", hs.hosts_down);
+ status->Set("num_hosts_pending", hs.hosts_pending);
+ status->Set("num_hosts_unreachable", hs.hosts_unreachable);
+ status->Set("num_hosts_flapping", hs.hosts_flapping);
+ status->Set("num_hosts_in_downtime", hs.hosts_in_downtime);
+ status->Set("num_hosts_acknowledged", hs.hosts_acknowledged);
+ status->Set("num_hosts_handled", hs.hosts_handled);
+ status->Set("num_hosts_problem", hs.hosts_problem);
+}
diff --git a/lib/icinga/cib.hpp b/lib/icinga/cib.hpp
new file mode 100644
index 0000000..00461e3
--- /dev/null
+++ b/lib/icinga/cib.hpp
@@ -0,0 +1,91 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CIB_H
+#define CIB_H
+
+#include "icinga/i2-icinga.hpp"
+#include "base/ringbuffer.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+
+namespace icinga
+{
+
+struct CheckableCheckStatistics {
+ double min_latency;
+ double max_latency;
+ double avg_latency;
+ double min_execution_time;
+ double max_execution_time;
+ double avg_execution_time;
+};
+
+struct ServiceStatistics {
+ double services_ok;
+ double services_warning;
+ double services_critical;
+ double services_unknown;
+ double services_pending;
+ double services_unreachable;
+ double services_flapping;
+ double services_in_downtime;
+ double services_acknowledged;
+ double services_handled;
+ double services_problem;
+};
+
+struct HostStatistics {
+ double hosts_up;
+ double hosts_down;
+ double hosts_unreachable;
+ double hosts_pending;
+ double hosts_flapping;
+ double hosts_in_downtime;
+ double hosts_acknowledged;
+ double hosts_handled;
+ double hosts_problem;
+};
+
+/**
+ * Common Information Base class. Holds some statistics (and will likely be
+ * removed/refactored).
+ *
+ * @ingroup icinga
+ */
+class CIB
+{
+public:
+ static void UpdateActiveHostChecksStatistics(long tv, int num);
+ static int GetActiveHostChecksStatistics(long timespan);
+
+ static void UpdateActiveServiceChecksStatistics(long tv, int num);
+ static int GetActiveServiceChecksStatistics(long timespan);
+
+ static void UpdatePassiveHostChecksStatistics(long tv, int num);
+ static int GetPassiveHostChecksStatistics(long timespan);
+
+ static void UpdatePassiveServiceChecksStatistics(long tv, int num);
+ static int GetPassiveServiceChecksStatistics(long timespan);
+
+ static CheckableCheckStatistics CalculateHostCheckStats();
+ static CheckableCheckStatistics CalculateServiceCheckStats();
+ static HostStatistics CalculateHostStats();
+ static ServiceStatistics CalculateServiceStats();
+
+ static std::pair<Dictionary::Ptr, Array::Ptr> GetFeatureStats();
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+private:
+ CIB();
+
+ static std::mutex m_Mutex;
+ static RingBuffer m_ActiveHostChecksStatistics;
+ static RingBuffer m_PassiveHostChecksStatistics;
+ static RingBuffer m_ActiveServiceChecksStatistics;
+ static RingBuffer m_PassiveServiceChecksStatistics;
+};
+
+}
+
+#endif /* CIB_H */
diff --git a/lib/icinga/clusterevents-check.cpp b/lib/icinga/clusterevents-check.cpp
new file mode 100644
index 0000000..40325b4
--- /dev/null
+++ b/lib/icinga/clusterevents-check.cpp
@@ -0,0 +1,379 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/clusterevents.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "remote/apilistener.hpp"
+#include "base/configuration.hpp"
+#include "base/defer.hpp"
+#include "base/serializer.hpp"
+#include "base/exception.hpp"
+#include <boost/thread/once.hpp>
+#include <thread>
+
+using namespace icinga;
+
+std::mutex ClusterEvents::m_Mutex;
+std::deque<std::function<void ()>> ClusterEvents::m_CheckRequestQueue;
+bool ClusterEvents::m_CheckSchedulerRunning;
+int ClusterEvents::m_ChecksExecutedDuringInterval;
+int ClusterEvents::m_ChecksDroppedDuringInterval;
+Timer::Ptr ClusterEvents::m_LogTimer;
+
+void ClusterEvents::RemoteCheckThreadProc()
+{
+ Utility::SetThreadName("Remote Check Scheduler");
+
+ int maxConcurrentChecks = IcingaApplication::GetInstance()->GetMaxConcurrentChecks();
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ for(;;) {
+ if (m_CheckRequestQueue.empty())
+ break;
+
+ lock.unlock();
+ Checkable::AquirePendingCheckSlot(maxConcurrentChecks);
+ lock.lock();
+
+ auto callback = m_CheckRequestQueue.front();
+ m_CheckRequestQueue.pop_front();
+ m_ChecksExecutedDuringInterval++;
+ lock.unlock();
+
+ callback();
+ Checkable::DecreasePendingChecks();
+
+ lock.lock();
+ }
+
+ m_CheckSchedulerRunning = false;
+}
+
+void ClusterEvents::EnqueueCheck(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, []() {
+ m_LogTimer = Timer::Create();
+ m_LogTimer->SetInterval(10);
+ m_LogTimer->OnTimerExpired.connect([](const Timer * const&) { LogRemoteCheckQueueInformation(); });
+ m_LogTimer->Start();
+ });
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ if (m_CheckRequestQueue.size() >= 25000) {
+ m_ChecksDroppedDuringInterval++;
+ return;
+ }
+
+ m_CheckRequestQueue.emplace_back([origin, params]() { ExecuteCheckFromQueue(origin, params); });
+
+ if (!m_CheckSchedulerRunning) {
+ std::thread t(ClusterEvents::RemoteCheckThreadProc);
+ t.detach();
+ m_CheckSchedulerRunning = true;
+ }
+}
+
+static void SendEventExecutedCommand(const Dictionary::Ptr& params, long exitStatus, const String& output,
+ double start, double end, const ApiListener::Ptr& listener, const MessageOrigin::Ptr& origin,
+ const Endpoint::Ptr& sourceEndpoint)
+{
+ Dictionary::Ptr executedParams = new Dictionary();
+ executedParams->Set("execution", params->Get("source"));
+ executedParams->Set("host", params->Get("host"));
+
+ if (params->Contains("service"))
+ executedParams->Set("service", params->Get("service"));
+
+ executedParams->Set("exit", exitStatus);
+ executedParams->Set("output", output);
+ executedParams->Set("start", start);
+ executedParams->Set("end", end);
+
+ if (origin->IsLocal()) {
+ ClusterEvents::ExecutedCommandAPIHandler(origin, executedParams);
+ } else {
+ Dictionary::Ptr executedMessage = new Dictionary();
+ executedMessage->Set("jsonrpc", "2.0");
+ executedMessage->Set("method", "event::ExecutedCommand");
+ executedMessage->Set("params", executedParams);
+
+ listener->SyncSendMessage(sourceEndpoint, executedMessage);
+ }
+}
+
+void ClusterEvents::ExecuteCheckFromQueue(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params) {
+
+ Endpoint::Ptr sourceEndpoint;
+
+ if (origin->FromClient) {
+ sourceEndpoint = origin->FromClient->GetEndpoint();
+ } else if (origin->IsLocal()){
+ sourceEndpoint = Endpoint::GetLocalEndpoint();
+ }
+
+ if (!sourceEndpoint || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone))) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'execute command' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return;
+ }
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener) {
+ Log(LogCritical, "ApiListener") << "No instance available.";
+ return;
+ }
+
+ Defer resetExecuteCommandProcessFinishedHandler ([]() {
+ Checkable::ExecuteCommandProcessFinishedHandler = nullptr;
+ });
+
+ if (params->Contains("source")) {
+ String uuid = params->Get("source");
+
+ String checkableName = params->Get("host");
+
+ if (params->Contains("service"))
+ checkableName += "!" + params->Get("service");
+
+ /* Check deadline */
+ double deadline = params->Get("deadline");
+
+ if (Utility::GetTime() > deadline) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'ExecuteCheckFromQueue' event for checkable '" << checkableName
+ << "' from '" << origin->FromClient->GetIdentity() << "': Deadline has expired.";
+ return;
+ }
+
+ Checkable::ExecuteCommandProcessFinishedHandler = [checkableName, listener, sourceEndpoint, origin, params] (const Value& commandLine, const ProcessResult& pr) {
+ if (params->Get("command_type") == "check_command") {
+ Checkable::CurrentConcurrentChecks.fetch_sub(1);
+ Checkable::DecreasePendingChecks();
+ }
+
+ if (pr.ExitStatus > 3) {
+ Process::Arguments parguments = Process::PrepareCommand(commandLine);
+ Log(LogWarning, "ApiListener")
+ << "Command for object '" << checkableName << "' (PID: " << pr.PID
+ << ", arguments: " << Process::PrettyPrintArguments(parguments) << ") terminated with exit code "
+ << pr.ExitStatus << ", output: " << pr.Output;
+ }
+
+ SendEventExecutedCommand(params, pr.ExitStatus, pr.Output, pr.ExecutionStart, pr.ExecutionEnd, listener,
+ origin, sourceEndpoint);
+ };
+ }
+
+ if (!listener->GetAcceptCommands() && !origin->IsLocal()) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring command. '" << listener->GetName() << "' does not accept commands.";
+
+ String output = "Endpoint '" + Endpoint::GetLocalEndpoint()->GetName() + "' does not accept commands.";
+
+ if (params->Contains("source")) {
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, 126, output, now, now, listener, origin, sourceEndpoint);
+ } else {
+ Host::Ptr host = new Host();
+ Dictionary::Ptr attrs = new Dictionary();
+
+ attrs->Set("__name", params->Get("host"));
+ attrs->Set("type", "Host");
+ attrs->Set("enable_active_checks", false);
+
+ Deserialize(host, attrs, false, FAConfig);
+
+ if (params->Contains("service"))
+ host->SetExtension("agent_service_name", params->Get("service"));
+
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetState(ServiceUnknown);
+ cr->SetOutput(output);
+
+ Dictionary::Ptr message = MakeCheckResultMessage(host, cr);
+ listener->SyncSendMessage(sourceEndpoint, message);
+ }
+
+ return;
+ }
+
+ /* use a virtual host object for executing the command */
+ Host::Ptr host = new Host();
+ Dictionary::Ptr attrs = new Dictionary();
+
+ attrs->Set("__name", params->Get("host"));
+ attrs->Set("type", "Host");
+
+ /*
+ * Override the check timeout if the parent caller provided the value. Compatible with older versions not
+ * passing this inside the cluster message.
+ * This happens with host/service command_endpoint agents and the 'check_timeout' attribute being specified.
+ */
+ if (params->Contains("check_timeout"))
+ attrs->Set("check_timeout", params->Get("check_timeout"));
+
+ Deserialize(host, attrs, false, FAConfig);
+
+ if (params->Contains("service"))
+ host->SetExtension("agent_service_name", params->Get("service"));
+
+ String command = params->Get("command");
+ String command_type = params->Get("command_type");
+
+ if (command_type == "check_command") {
+ if (!CheckCommand::GetByName(command)) {
+ ServiceState state = ServiceUnknown;
+ String output = "Check command '" + command + "' does not exist.";
+ double now = Utility::GetTime();
+
+ if (params->Contains("source")) {
+ SendEventExecutedCommand(params, state, output, now, now, listener, origin, sourceEndpoint);
+ } else {
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetState(state);
+ cr->SetOutput(output);
+ Dictionary::Ptr message = MakeCheckResultMessage(host, cr);
+ listener->SyncSendMessage(sourceEndpoint, message);
+ }
+
+ return;
+ }
+ } else if (command_type == "event_command") {
+ if (!EventCommand::GetByName(command)) {
+ String output = "Event command '" + command + "' does not exist.";
+ Log(LogWarning, "ClusterEvents") << output;
+
+ if (params->Contains("source")) {
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, ServiceUnknown, output, now, now, listener, origin, sourceEndpoint);
+ }
+
+ return;
+ }
+ } else if (command_type == "notification_command") {
+ if (!NotificationCommand::GetByName(command)) {
+ String output = "Notification command '" + command + "' does not exist.";
+ Log(LogWarning, "ClusterEvents") << output;
+
+ if (params->Contains("source")) {
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, ServiceUnknown, output, now, now, listener, origin, sourceEndpoint);
+ }
+
+ return;
+ }
+ }
+
+ attrs->Set(command_type, params->Get("command"));
+ attrs->Set("command_endpoint", sourceEndpoint->GetName());
+
+ Deserialize(host, attrs, false, FAConfig);
+
+ host->SetExtension("agent_check", true);
+
+ Dictionary::Ptr macros = params->Get("macros");
+
+ if (command_type == "check_command") {
+ try {
+ host->ExecuteRemoteCheck(macros);
+ } catch (const std::exception& ex) {
+ String output = "Exception occurred while checking '" + host->GetName() + "': " + DiagnosticInformation(ex);
+ ServiceState state = ServiceUnknown;
+ double now = Utility::GetTime();
+
+ if (params->Contains("source")) {
+ SendEventExecutedCommand(params, state, output, now, now, listener, origin, sourceEndpoint);
+ } else {
+ CheckResult::Ptr cr = new CheckResult();
+ cr->SetState(state);
+ cr->SetOutput(output);
+ cr->SetScheduleStart(now);
+ cr->SetScheduleEnd(now);
+ cr->SetExecutionStart(now);
+ cr->SetExecutionEnd(now);
+
+ Dictionary::Ptr message = MakeCheckResultMessage(host, cr);
+ listener->SyncSendMessage(sourceEndpoint, message);
+ }
+
+ Log(LogCritical, "checker", output);
+ }
+ } else if (command_type == "event_command") {
+ try {
+ host->ExecuteEventHandler(macros, true);
+ } catch (const std::exception& ex) {
+ if (params->Contains("source")) {
+ String output = "Exception occurred while executing event command '" + command + "' for '" +
+ host->GetName() + "': " + DiagnosticInformation(ex);
+
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, ServiceUnknown, output, now, now, listener, origin, sourceEndpoint);
+ } else {
+ throw;
+ }
+ }
+ } else if (command_type == "notification_command" && params->Contains("source")) {
+ /* Get user */
+ User::Ptr user = new User();
+ Dictionary::Ptr attrs = new Dictionary();
+ attrs->Set("__name", params->Get("user"));
+ attrs->Set("type", User::GetTypeName());
+
+ Deserialize(user, attrs, false, FAConfig);
+
+ /* Get notification */
+ Notification::Ptr notification = new Notification();
+ attrs->Clear();
+ attrs->Set("__name", params->Get("notification"));
+ attrs->Set("type", Notification::GetTypeName());
+ attrs->Set("command", command);
+
+ Deserialize(notification, attrs, false, FAConfig);
+
+ try {
+ CheckResult::Ptr cr = new CheckResult();
+ String author = macros->Get("notification_author");
+ NotificationCommand::Ptr notificationCommand = NotificationCommand::GetByName(command);
+
+ notificationCommand->Execute(notification, user, cr, NotificationType::NotificationCustom,
+ author, "");
+ } catch (const std::exception& ex) {
+ String output = "Exception occurred during notification '" + notification->GetName()
+ + "' for checkable '" + notification->GetCheckable()->GetName()
+ + "' and user '" + user->GetName() + "' using command '" + command + "': "
+ + DiagnosticInformation(ex, false);
+ double now = Utility::GetTime();
+ SendEventExecutedCommand(params, ServiceUnknown, output, now, now, listener, origin, sourceEndpoint);
+ }
+ }
+}
+
+int ClusterEvents::GetCheckRequestQueueSize()
+{
+ return m_CheckRequestQueue.size();
+}
+
+void ClusterEvents::LogRemoteCheckQueueInformation() {
+ if (m_ChecksDroppedDuringInterval > 0) {
+ Log(LogCritical, "ClusterEvents")
+ << "Remote check queue ran out of slots. "
+ << m_ChecksDroppedDuringInterval << " checks dropped.";
+ m_ChecksDroppedDuringInterval = 0;
+ }
+
+ if (m_ChecksExecutedDuringInterval == 0)
+ return;
+
+ Log(LogInformation, "RemoteCheckQueue")
+ << "items: " << m_CheckRequestQueue.size()
+ << ", rate: " << m_ChecksExecutedDuringInterval / 10 << "/s "
+ << "(" << m_ChecksExecutedDuringInterval * 6 << "/min "
+ << m_ChecksExecutedDuringInterval * 6 * 5 << "/5min "
+ << m_ChecksExecutedDuringInterval * 6 * 15 << "/15min" << ");";
+
+ m_ChecksExecutedDuringInterval = 0;
+}
diff --git a/lib/icinga/clusterevents.cpp b/lib/icinga/clusterevents.cpp
new file mode 100644
index 0000000..fe5167b
--- /dev/null
+++ b/lib/icinga/clusterevents.cpp
@@ -0,0 +1,1623 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/clusterevents.hpp"
+#include "icinga/service.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/messageorigin.hpp"
+#include "remote/zone.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/eventqueue.hpp"
+#include "base/application.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/exception.hpp"
+#include "base/initialize.hpp"
+#include "base/serializer.hpp"
+#include "base/json.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+INITIALIZE_ONCE(&ClusterEvents::StaticInitialize);
+
+REGISTER_APIFUNCTION(CheckResult, event, &ClusterEvents::CheckResultAPIHandler);
+REGISTER_APIFUNCTION(SetNextCheck, event, &ClusterEvents::NextCheckChangedAPIHandler);
+REGISTER_APIFUNCTION(SetLastCheckStarted, event, &ClusterEvents::LastCheckStartedChangedAPIHandler);
+REGISTER_APIFUNCTION(SetStateBeforeSuppression, event, &ClusterEvents::StateBeforeSuppressionChangedAPIHandler);
+REGISTER_APIFUNCTION(SetSuppressedNotifications, event, &ClusterEvents::SuppressedNotificationsChangedAPIHandler);
+REGISTER_APIFUNCTION(SetSuppressedNotificationTypes, event, &ClusterEvents::SuppressedNotificationTypesChangedAPIHandler);
+REGISTER_APIFUNCTION(SetNextNotification, event, &ClusterEvents::NextNotificationChangedAPIHandler);
+REGISTER_APIFUNCTION(UpdateLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler);
+REGISTER_APIFUNCTION(ClearLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserClearedAPIHandler);
+REGISTER_APIFUNCTION(SetForceNextCheck, event, &ClusterEvents::ForceNextCheckChangedAPIHandler);
+REGISTER_APIFUNCTION(SetForceNextNotification, event, &ClusterEvents::ForceNextNotificationChangedAPIHandler);
+REGISTER_APIFUNCTION(SetAcknowledgement, event, &ClusterEvents::AcknowledgementSetAPIHandler);
+REGISTER_APIFUNCTION(ClearAcknowledgement, event, &ClusterEvents::AcknowledgementClearedAPIHandler);
+REGISTER_APIFUNCTION(ExecuteCommand, event, &ClusterEvents::ExecuteCommandAPIHandler);
+REGISTER_APIFUNCTION(SendNotifications, event, &ClusterEvents::SendNotificationsAPIHandler);
+REGISTER_APIFUNCTION(NotificationSentUser, event, &ClusterEvents::NotificationSentUserAPIHandler);
+REGISTER_APIFUNCTION(NotificationSentToAllUsers, event, &ClusterEvents::NotificationSentToAllUsersAPIHandler);
+REGISTER_APIFUNCTION(ExecutedCommand, event, &ClusterEvents::ExecutedCommandAPIHandler);
+REGISTER_APIFUNCTION(UpdateExecutions, event, &ClusterEvents::UpdateExecutionsAPIHandler);
+REGISTER_APIFUNCTION(SetRemovalInfo, event, &ClusterEvents::SetRemovalInfoAPIHandler);
+
+void ClusterEvents::StaticInitialize()
+{
+ Checkable::OnNewCheckResult.connect(&ClusterEvents::CheckResultHandler);
+ Checkable::OnNextCheckChanged.connect(&ClusterEvents::NextCheckChangedHandler);
+ Checkable::OnLastCheckStartedChanged.connect(&ClusterEvents::LastCheckStartedChangedHandler);
+ Checkable::OnStateBeforeSuppressionChanged.connect(&ClusterEvents::StateBeforeSuppressionChangedHandler);
+ Checkable::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationsChangedHandler);
+ Notification::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationTypesChangedHandler);
+ Notification::OnNextNotificationChanged.connect(&ClusterEvents::NextNotificationChangedHandler);
+ Notification::OnLastNotifiedStatePerUserUpdated.connect(&ClusterEvents::LastNotifiedStatePerUserUpdatedHandler);
+ Notification::OnLastNotifiedStatePerUserCleared.connect(&ClusterEvents::LastNotifiedStatePerUserClearedHandler);
+ Checkable::OnForceNextCheckChanged.connect(&ClusterEvents::ForceNextCheckChangedHandler);
+ Checkable::OnForceNextNotificationChanged.connect(&ClusterEvents::ForceNextNotificationChangedHandler);
+ Checkable::OnNotificationsRequested.connect(&ClusterEvents::SendNotificationsHandler);
+ Checkable::OnNotificationSentToUser.connect(&ClusterEvents::NotificationSentUserHandler);
+ Checkable::OnNotificationSentToAllUsers.connect(&ClusterEvents::NotificationSentToAllUsersHandler);
+
+ Checkable::OnAcknowledgementSet.connect(&ClusterEvents::AcknowledgementSetHandler);
+ Checkable::OnAcknowledgementCleared.connect(&ClusterEvents::AcknowledgementClearedHandler);
+
+ Comment::OnRemovalInfoChanged.connect(&ClusterEvents::SetRemovalInfoHandler);
+ Downtime::OnRemovalInfoChanged.connect(&ClusterEvents::SetRemovalInfoHandler);
+}
+
+Dictionary::Ptr ClusterEvents::MakeCheckResultMessage(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
+{
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::CheckResult");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ else {
+ Value agent_service_name = checkable->GetExtension("agent_service_name");
+
+ if (!agent_service_name.IsEmpty())
+ params->Set("service", agent_service_name);
+ }
+ params->Set("cr", Serialize(cr));
+
+ message->Set("params", params);
+
+ return message;
+}
+
+void ClusterEvents::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr message = MakeCheckResultMessage(checkable, cr);
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::CheckResultAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'check result' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ CheckResult::Ptr cr;
+ Array::Ptr vperf;
+
+ if (params->Contains("cr")) {
+ cr = new CheckResult();
+ Dictionary::Ptr vcr = params->Get("cr");
+
+ if (vcr && vcr->Contains("performance_data")) {
+ vperf = vcr->Get("performance_data");
+
+ if (vperf)
+ vcr->Remove("performance_data");
+
+ Deserialize(cr, vcr, true);
+ }
+ }
+
+ if (!cr)
+ return Empty;
+
+ ArrayData rperf;
+
+ if (vperf) {
+ ObjectLock olock(vperf);
+ for (const Value& vp : vperf) {
+ Value p;
+
+ if (vp.IsObjectType<Dictionary>()) {
+ PerfdataValue::Ptr val = new PerfdataValue();
+ Deserialize(val, vp, true);
+ rperf.push_back(val);
+ } else
+ rperf.push_back(vp);
+ }
+ }
+
+ cr->SetPerformanceData(new Array(std::move(rperf)));
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable) && endpoint != checkable->GetCommandEndpoint()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'check result' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ if (!checkable->IsPaused() && Zone::GetLocalZone() == checkable->GetZone() && endpoint == checkable->GetCommandEndpoint())
+ checkable->ProcessCheckResult(cr);
+ else
+ checkable->ProcessCheckResult(cr, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::NextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("next_check", checkable->GetNextCheck());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetNextCheck");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::NextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'next check changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'next check changed' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ double nextCheck = params->Get("next_check");
+
+ if (nextCheck < Application::GetStartTime() + 60)
+ return Empty;
+
+ checkable->SetNextCheck(params->Get("next_check"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::LastCheckStartedChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("last_check_started", checkable->GetLastCheckStarted());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetLastCheckStarted");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::LastCheckStartedChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last_check_started changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last_check_started changed' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetLastCheckStarted(params->Get("last_check_started"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::StateBeforeSuppressionChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("state_before_suppression", checkable->GetStateBeforeSuppression());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetStateBeforeSuppression");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::StateBeforeSuppressionChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'state before suppression changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'state before suppression changed' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetStateBeforeSuppression(ServiceState(int(params->Get("state_before_suppression"))), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::SuppressedNotificationsChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("suppressed_notifications", checkable->GetSuppressedNotifications());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetSuppressedNotifications");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::SuppressedNotificationsChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'suppressed notifications changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'suppressed notifications changed' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetSuppressedNotifications(params->Get("suppressed_notifications"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::SuppressedNotificationTypesChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+ params->Set("suppressed_notifications", notification->GetSuppressedNotifications());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetSuppressedNotificationTypes");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::SuppressedNotificationTypesChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'suppressed notifications changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ auto notification (Notification::GetByName(params->Get("notification")));
+
+ if (!notification)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'suppressed notification types changed' message for notification '" << notification->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ notification->SetSuppressedNotifications(params->Get("suppressed_notifications"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::NextNotificationChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+ params->Set("next_notification", notification->GetNextNotification());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetNextNotification");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::NextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'next notification changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Notification::Ptr notification = Notification::GetByName(params->Get("notification"));
+
+ if (!notification)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(notification)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'next notification changed' message for notification '" << notification->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ double nextNotification = params->Get("next_notification");
+
+ if (nextNotification < Utility::GetTime())
+ return Empty;
+
+ notification->SetNextNotification(nextNotification, false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin)
+{
+ auto listener (ApiListener::GetInstance());
+
+ if (!listener) {
+ return;
+ }
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+ params->Set("user", user);
+ params->Set("state", state);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::UpdateLastNotifiedStatePerUser");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ auto endpoint (origin->FromClient->GetEndpoint());
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user updated' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user updated' message from '"
+ << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+
+ return Empty;
+ }
+
+ auto notification (Notification::GetByName(params->Get("notification")));
+
+ if (!notification) {
+ return Empty;
+ }
+
+ auto state (params->Get("state"));
+
+ if (!state.IsNumber()) {
+ return Empty;
+ }
+
+ notification->GetLastNotifiedStatePerUser()->Set(params->Get("user"), state);
+ Notification::OnLastNotifiedStatePerUserUpdated(notification, params->Get("user"), state, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::LastNotifiedStatePerUserClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin)
+{
+ auto listener (ApiListener::GetInstance());
+
+ if (!listener) {
+ return;
+ }
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ClearLastNotifiedStatePerUser");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::LastNotifiedStatePerUserClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ auto endpoint (origin->FromClient->GetEndpoint());
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user cleared' message from '"
+ << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user cleared' message from '"
+ << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+
+ return Empty;
+ }
+
+ auto notification (Notification::GetByName(params->Get("notification")));
+
+ if (!notification) {
+ return Empty;
+ }
+
+ notification->GetLastNotifiedStatePerUser()->Clear();
+ Notification::OnLastNotifiedStatePerUserCleared(notification, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("forced", checkable->GetForceNextCheck());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetForceNextCheck");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::ForceNextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'force next check changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'force next check' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetForceNextCheck(params->Get("forced"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::ForceNextNotificationChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("forced", checkable->GetForceNextNotification());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetForceNextNotification");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::ForceNextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'force next notification changed' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'force next notification' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->SetForceNextNotification(params->Get("forced"), false, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::AcknowledgementSetHandler(const Checkable::Ptr& checkable,
+ const String& author, const String& comment, AcknowledgementType type,
+ bool notify, bool persistent, double changeTime, double expiry, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("author", author);
+ params->Set("comment", comment);
+ params->Set("acktype", type);
+ params->Set("notify", notify);
+ params->Set("persistent", persistent);
+ params->Set("expiry", expiry);
+ params->Set("change_time", changeTime);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetAcknowledgement");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::AcknowledgementSetAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'acknowledgement set' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'acknowledgement set' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ ObjectLock oLock (checkable);
+
+ if (checkable->IsAcknowledged()) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'acknowledgement set' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Checkable is already acknowledged.";
+ return Empty;
+ }
+
+ checkable->AcknowledgeProblem(params->Get("author"), params->Get("comment"),
+ static_cast<AcknowledgementType>(static_cast<int>(params->Get("acktype"))),
+ params->Get("notify"), params->Get("persistent"), params->Get("change_time"), params->Get("expiry"), origin);
+
+ return Empty;
+}
+
+void ClusterEvents::AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("author", removedBy);
+ params->Set("change_time", changeTime);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ClearAcknowledgement");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, message, true);
+}
+
+Value ClusterEvents::AcknowledgementClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'acknowledgement cleared' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'acknowledgement cleared' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ checkable->ClearAcknowledgement(params->Get("author"), params->Get("change_time"), origin);
+
+ return Empty;
+}
+
+Value ClusterEvents::ExecuteCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ if (!origin->IsLocal()) {
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ /* Discard messages from anonymous clients */
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents") << "Discarding 'execute command' message from '"
+ << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Zone::Ptr originZone = endpoint->GetZone();
+
+ Zone::Ptr localZone = Zone::GetLocalZone();
+ bool fromLocalZone = originZone == localZone;
+
+ Zone::Ptr parentZone = localZone->GetParent();
+ bool fromParentZone = parentZone && originZone == parentZone;
+
+ if (!fromLocalZone && !fromParentZone) {
+ Log(LogNotice, "ClusterEvents") << "Discarding 'execute command' message from '"
+ << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+ }
+
+ String executionUuid = params->Get("source");
+
+ if (params->Contains("endpoint")) {
+ Endpoint::Ptr execEndpoint = Endpoint::GetByName(params->Get("endpoint"));
+
+ if (!execEndpoint) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'execute command' message " << executionUuid
+ << ": Endpoint " << params->Get("endpoint") << " does not exist";
+ return Empty;
+ }
+
+ if (execEndpoint != Endpoint::GetLocalEndpoint()) {
+ Zone::Ptr endpointZone = execEndpoint->GetZone();
+ Zone::Ptr localZone = Zone::GetLocalZone();
+
+ if (!endpointZone->IsChildOf(localZone)) {
+ return Empty;
+ }
+
+ /* Check if the child endpoints have Icinga version >= 2.13 */
+ for (const Zone::Ptr &zone : ConfigType::GetObjectsByType<Zone>()) {
+ /* Fetch immediate child zone members */
+ if (zone->GetParent() == localZone && zone->CanAccessObject(endpointZone)) {
+ std::set<Endpoint::Ptr> endpoints = zone->GetEndpoints();
+
+ for (const Endpoint::Ptr &childEndpoint : endpoints) {
+ if (!(childEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand)) {
+ double now = Utility::GetTime();
+ Dictionary::Ptr executedParams = new Dictionary();
+ executedParams->Set("execution", executionUuid);
+ executedParams->Set("host", params->Get("host"));
+
+ if (params->Contains("service"))
+ executedParams->Set("service", params->Get("service"));
+
+ executedParams->Set("exit", 126);
+ executedParams->Set("output",
+ "Endpoint '" + childEndpoint->GetName() + "' doesn't support executing arbitrary commands.");
+ executedParams->Set("start", now);
+ executedParams->Set("end", now);
+
+ Dictionary::Ptr executedMessage = new Dictionary();
+ executedMessage->Set("jsonrpc", "2.0");
+ executedMessage->Set("method", "event::ExecutedCommand");
+ executedMessage->Set("params", executedParams);
+
+ listener->RelayMessage(nullptr, nullptr, executedMessage, true);
+ return Empty;
+ }
+ }
+
+ Checkable::Ptr checkable;
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+ if (!host) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'execute command' message " << executionUuid
+ << ": host " << params->Get("host") << " does not exist";
+ return Empty;
+ }
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable) {
+ String checkableName = host->GetName();
+ if (params->Contains("service"))
+ checkableName += "!" + params->Get("service");
+
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'execute command' message " << executionUuid
+ << ": " << checkableName << " does not exist";
+ return Empty;
+ }
+
+ /* Return an error when the endpointZone is different than the child zone and
+ * the child zone can't access the checkable.
+ * The zones are checked to allow for the case where command_endpoint is specified in the checkable
+ * but checkable is not actually present in the agent.
+ */
+ if (!zone->CanAccessObject(checkable) && zone != endpointZone) {
+ double now = Utility::GetTime();
+ Dictionary::Ptr executedParams = new Dictionary();
+ executedParams->Set("execution", executionUuid);
+ executedParams->Set("host", params->Get("host"));
+
+ if (params->Contains("service"))
+ executedParams->Set("service", params->Get("service"));
+
+ executedParams->Set("exit", 126);
+ executedParams->Set(
+ "output",
+ "Zone '" + zone->GetName() + "' cannot access to checkable '" + checkable->GetName() + "'."
+ );
+ executedParams->Set("start", now);
+ executedParams->Set("end", now);
+
+ Dictionary::Ptr executedMessage = new Dictionary();
+ executedMessage->Set("jsonrpc", "2.0");
+ executedMessage->Set("method", "event::ExecutedCommand");
+ executedMessage->Set("params", executedParams);
+
+ listener->RelayMessage(nullptr, nullptr, executedMessage, true);
+ return Empty;
+ }
+ }
+ }
+
+ Dictionary::Ptr execMessage = new Dictionary();
+ execMessage->Set("jsonrpc", "2.0");
+ execMessage->Set("method", "event::ExecuteCommand");
+ execMessage->Set("params", params);
+
+ listener->RelayMessage(origin, endpointZone, execMessage, true);
+ return Empty;
+ }
+ }
+
+ EnqueueCheck(origin, params);
+
+ return Empty;
+}
+
+void ClusterEvents::SendNotificationsHandler(const Checkable::Ptr& checkable, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr message = MakeCheckResultMessage(checkable, cr);
+ message->Set("method", "event::SendNotifications");
+
+ Dictionary::Ptr params = message->Get("params");
+ params->Set("type", type);
+ params->Set("author", author);
+ params->Set("text", text);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::SendNotificationsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'send notification' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'send custom notification' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ CheckResult::Ptr cr;
+ Array::Ptr vperf;
+
+ if (params->Contains("cr")) {
+ cr = new CheckResult();
+ Dictionary::Ptr vcr = params->Get("cr");
+
+ if (vcr && vcr->Contains("performance_data")) {
+ vperf = vcr->Get("performance_data");
+
+ if (vperf)
+ vcr->Remove("performance_data");
+
+ Deserialize(cr, vcr, true);
+ }
+ }
+
+ NotificationType type = static_cast<NotificationType>(static_cast<int>(params->Get("type")));
+ String author = params->Get("author");
+ String text = params->Get("text");
+
+ Checkable::OnNotificationsRequested(checkable, type, cr, author, text, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::NotificationSentUserHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const User::Ptr& user,
+ NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const String& command,
+ const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("notification", notification->GetName());
+ params->Set("user", user->GetName());
+ params->Set("type", notificationType);
+ params->Set("cr", Serialize(cr));
+ params->Set("author", author);
+ params->Set("text", commentText);
+ params->Set("command", command);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::NotificationSentUser");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::NotificationSentUserAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'sent notification to user' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'send notification to user' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ CheckResult::Ptr cr;
+ Array::Ptr vperf;
+
+ if (params->Contains("cr")) {
+ cr = new CheckResult();
+ Dictionary::Ptr vcr = params->Get("cr");
+
+ if (vcr && vcr->Contains("performance_data")) {
+ vperf = vcr->Get("performance_data");
+
+ if (vperf)
+ vcr->Remove("performance_data");
+
+ Deserialize(cr, vcr, true);
+ }
+ }
+
+ NotificationType type = static_cast<NotificationType>(static_cast<int>(params->Get("type")));
+ String author = params->Get("author");
+ String text = params->Get("text");
+
+ Notification::Ptr notification = Notification::GetByName(params->Get("notification"));
+
+ if (!notification)
+ return Empty;
+
+ User::Ptr user = User::GetByName(params->Get("user"));
+
+ if (!user)
+ return Empty;
+
+ String command = params->Get("command");
+
+ Checkable::OnNotificationSentToUser(notification, checkable, user, type, cr, author, text, command, origin);
+
+ return Empty;
+}
+
+void ClusterEvents::NotificationSentToAllUsersHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("host", host->GetName());
+ if (service)
+ params->Set("service", service->GetShortName());
+ params->Set("notification", notification->GetName());
+
+ ArrayData ausers;
+ for (const User::Ptr& user : users) {
+ ausers.push_back(user->GetName());
+ }
+ params->Set("users", new Array(std::move(ausers)));
+
+ params->Set("type", notificationType);
+ params->Set("cr", Serialize(cr));
+ params->Set("author", author);
+ params->Set("text", commentText);
+
+ params->Set("last_notification", notification->GetLastNotification());
+ params->Set("next_notification", notification->GetNextNotification());
+ params->Set("notification_number", notification->GetNotificationNumber());
+ params->Set("last_problem_notification", notification->GetLastProblemNotification());
+ params->Set("no_more_notifications", notification->GetNoMoreNotifications());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::NotificationSentToAllUsers");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, nullptr, message, true);
+}
+
+Value ClusterEvents::NotificationSentToAllUsersAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'sent notification to all users' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'sent notification to all users' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ CheckResult::Ptr cr;
+ Array::Ptr vperf;
+
+ if (params->Contains("cr")) {
+ cr = new CheckResult();
+ Dictionary::Ptr vcr = params->Get("cr");
+
+ if (vcr && vcr->Contains("performance_data")) {
+ vperf = vcr->Get("performance_data");
+
+ if (vperf)
+ vcr->Remove("performance_data");
+
+ Deserialize(cr, vcr, true);
+ }
+ }
+
+ NotificationType type = static_cast<NotificationType>(static_cast<int>(params->Get("type")));
+ String author = params->Get("author");
+ String text = params->Get("text");
+
+ Notification::Ptr notification = Notification::GetByName(params->Get("notification"));
+
+ if (!notification)
+ return Empty;
+
+ Array::Ptr ausers = params->Get("users");
+
+ if (!ausers)
+ return Empty;
+
+ std::set<User::Ptr> users;
+
+ {
+ ObjectLock olock(ausers);
+ for (const String& auser : ausers) {
+ User::Ptr user = User::GetByName(auser);
+
+ if (!user)
+ continue;
+
+ users.insert(user);
+ }
+ }
+
+ notification->SetLastNotification(params->Get("last_notification"));
+ notification->SetNextNotification(params->Get("next_notification"));
+ notification->SetNotificationNumber(params->Get("notification_number"));
+ notification->SetLastProblemNotification(params->Get("last_problem_notification"));
+ notification->SetNoMoreNotifications(params->Get("no_more_notifications"));
+
+ ArrayData notifiedProblemUsers;
+ for (const User::Ptr& user : users) {
+ notifiedProblemUsers.push_back(user->GetName());
+ }
+
+ notification->SetNotifiedProblemUsers(new Array(std::move(notifiedProblemUsers)));
+
+ Checkable::OnNotificationSentToAllUsers(notification, checkable, users, type, cr, author, text, origin);
+
+ return Empty;
+}
+
+Value ClusterEvents::ExecutedCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ Endpoint::Ptr endpoint;
+
+ if (origin->FromClient) {
+ endpoint = origin->FromClient->GetEndpoint();
+ } else if (origin->IsLocal()) {
+ endpoint = Endpoint::GetLocalEndpoint();
+ }
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message from '" << origin->FromClient->GetIdentity()
+ << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ ObjectLock oLock (checkable);
+
+ if (!params->Contains("execution")) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Execution UUID missing.";
+ return Empty;
+ }
+
+ String uuid = params->Get("execution");
+
+ Dictionary::Ptr executions = checkable->GetExecutions();
+
+ if (!executions) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Execution '" << uuid << "' missing.";
+ return Empty;
+ }
+
+ Dictionary::Ptr execution = executions->Get(uuid);
+
+ if (!execution) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Execution '" << uuid << "' missing.";
+ return Empty;
+ }
+
+ Endpoint::Ptr command_endpoint = Endpoint::GetByName(execution->Get("endpoint"));
+ if (!command_endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message from '" << origin->FromClient->GetIdentity()
+ << "': Command endpoint does not exists.";
+
+ return Empty;
+ }
+
+ if (origin->FromZone && !command_endpoint->GetZone()->IsChildOf(origin->FromZone)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ if (params->Contains("exit"))
+ execution->Set("exit", params->Get("exit"));
+
+ if (params->Contains("output"))
+ execution->Set("output", params->Get("output"));
+
+ if (params->Contains("start"))
+ execution->Set("start", params->Get("start"));
+
+ if (params->Contains("end"))
+ execution->Set("end", params->Get("end"));
+
+ execution->Remove("pending");
+
+ /* Broadcast the update */
+ Dictionary::Ptr executionsToBroadcast = new Dictionary();
+ executionsToBroadcast->Set(uuid, execution);
+ Dictionary::Ptr updateParams = new Dictionary();
+ updateParams->Set("host", host->GetName());
+
+ if (params->Contains("service"))
+ updateParams->Set("service", params->Get("service"));
+
+ updateParams->Set("executions", executionsToBroadcast);
+
+ Dictionary::Ptr updateMessage = new Dictionary();
+ updateMessage->Set("jsonrpc", "2.0");
+ updateMessage->Set("method", "event::UpdateExecutions");
+ updateMessage->Set("params", updateParams);
+
+ listener->RelayMessage(nullptr, checkable, updateMessage, true);
+
+ return Empty;
+}
+
+Value ClusterEvents::UpdateExecutionsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message from '" << origin->FromClient->GetIdentity()
+ << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ Host::Ptr host = Host::GetByName(params->Get("host"));
+
+ if (!host)
+ return Empty;
+
+ Checkable::Ptr checkable;
+
+ if (params->Contains("service"))
+ checkable = host->GetServiceByShortName(params->Get("service"));
+ else
+ checkable = host;
+
+ if (!checkable)
+ return Empty;
+
+ ObjectLock oLock (checkable);
+
+ if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName()
+ << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+ return Empty;
+ }
+
+ Dictionary::Ptr executions = checkable->GetExecutions();
+
+ if (!executions)
+ executions = new Dictionary();
+
+ Dictionary::Ptr newExecutions = params->Get("executions");
+ newExecutions->CopyTo(executions);
+ checkable->SetExecutions(executions);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ Dictionary::Ptr updateMessage = new Dictionary();
+ updateMessage->Set("jsonrpc", "2.0");
+ updateMessage->Set("method", "event::UpdateExecutions");
+ updateMessage->Set("params", params);
+
+ listener->RelayMessage(origin, checkable, updateMessage, true);
+
+ return Empty;
+}
+
+void ClusterEvents::SetRemovalInfoHandler(const ConfigObject::Ptr& obj, const String& removedBy, double removeTime,
+ const MessageOrigin::Ptr& origin)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("object_type", obj->GetReflectionType()->GetName());
+ params->Set("object_name", obj->GetName());
+ params->Set("removed_by", removedBy);
+ params->Set("remove_time", removeTime);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::SetRemovalInfo");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, obj, message, true);
+}
+
+Value ClusterEvents::SetRemovalInfoAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone))) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'set removal info' message from '" << origin->FromClient->GetIdentity()
+ << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ String objectType = params->Get("object_type");
+ String objectName = params->Get("object_name");
+ String removedBy = params->Get("removed_by");
+ double removeTime = params->Get("remove_time");
+
+ if (objectType == Comment::GetTypeName()) {
+ Comment::Ptr comment = Comment::GetByName(objectName);
+
+ if (comment) {
+ comment->SetRemovalInfo(removedBy, removeTime, origin);
+ }
+ } else if (objectType == Downtime::GetTypeName()) {
+ Downtime::Ptr downtime = Downtime::GetByName(objectName);
+
+ if (downtime) {
+ downtime->SetRemovalInfo(removedBy, removeTime, origin);
+ }
+ } else {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'set removal info' message from '" << origin->FromClient->GetIdentity()
+ << "': Unknown object type.";
+ }
+
+ return Empty;
+}
diff --git a/lib/icinga/clusterevents.hpp b/lib/icinga/clusterevents.hpp
new file mode 100644
index 0000000..8daf86a
--- /dev/null
+++ b/lib/icinga/clusterevents.hpp
@@ -0,0 +1,102 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CLUSTEREVENTS_H
+#define CLUSTEREVENTS_H
+
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+class ClusterEvents
+{
+public:
+ static void StaticInitialize();
+
+ static void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MessageOrigin::Ptr& origin);
+ static Value CheckResultAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void NextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value NextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void LastCheckStartedChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value LastCheckStartedChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void StateBeforeSuppressionChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value StateBeforeSuppressionChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void SuppressedNotificationsChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value SuppressedNotificationsChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void SuppressedNotificationTypesChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
+ static Value SuppressedNotificationTypesChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void NextNotificationChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
+ static Value NextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin);
+ static Value LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void LastNotifiedStatePerUserClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
+ static Value LastNotifiedStatePerUserClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value ForceNextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void ForceNextNotificationChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
+ static Value ForceNextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type,
+ bool notify, bool persistent, double changeTime, double expiry, const MessageOrigin::Ptr& origin);
+ static Value AcknowledgementSetAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, const MessageOrigin::Ptr& origin);
+ static Value AcknowledgementClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static Value ExecuteCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static Dictionary::Ptr MakeCheckResultMessage(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
+
+ static void SendNotificationsHandler(const Checkable::Ptr& checkable, NotificationType type,
+ const CheckResult::Ptr& cr, const String& author, const String& text, const MessageOrigin::Ptr& origin);
+ static Value SendNotificationsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void NotificationSentUserHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const User::Ptr& user,
+ NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const String& command, const MessageOrigin::Ptr& origin);
+ static Value NotificationSentUserAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void NotificationSentToAllUsersHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const MessageOrigin::Ptr& origin);
+ static Value NotificationSentToAllUsersAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static Value ExecutedCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static Value UpdateExecutionsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void SetRemovalInfoHandler(const ConfigObject::Ptr& obj, const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin);
+ static Value SetRemovalInfoAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static int GetCheckRequestQueueSize();
+ static void LogRemoteCheckQueueInformation();
+
+private:
+ static std::mutex m_Mutex;
+ static std::deque<std::function<void ()>> m_CheckRequestQueue;
+ static bool m_CheckSchedulerRunning;
+ static int m_ChecksExecutedDuringInterval;
+ static int m_ChecksDroppedDuringInterval;
+ static Timer::Ptr m_LogTimer;
+
+ static void RemoteCheckThreadProc();
+ static void EnqueueCheck(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static void ExecuteCheckFromQueue(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+};
+
+}
+
+#endif /* CLUSTEREVENTS_H */
diff --git a/lib/icinga/command.cpp b/lib/icinga/command.cpp
new file mode 100644
index 0000000..8e0f357
--- /dev/null
+++ b/lib/icinga/command.cpp
@@ -0,0 +1,68 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/command.hpp"
+#include "icinga/command-ti.cpp"
+#include "icinga/macroprocessor.hpp"
+#include "base/exception.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Command);
+
+void Command::Validate(int types, const ValidationUtils& utils)
+{
+ ObjectImpl<Command>::Validate(types, utils);
+
+ Dictionary::Ptr arguments = GetArguments();
+
+ if (!(types & FAConfig))
+ return;
+
+ if (arguments) {
+ if (!GetCommandLine().IsObjectType<Array>())
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "command" }, "Attribute 'command' must be an array if the 'arguments' attribute is set."));
+
+ ObjectLock olock(arguments);
+ for (const Dictionary::Pair& kv : arguments) {
+ const Value& arginfo = kv.second;
+ Value argval;
+
+ if (arginfo.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr argdict = arginfo;
+
+ if (argdict->Contains("value")) {
+ Value argvalue = argdict->Get("value");
+
+ if (argvalue.IsString() && !MacroProcessor::ValidateMacroString(argvalue))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "arguments", kv.first, "value" }, "Validation failed: Closing $ not found in macro format string '" + argvalue + "'."));
+ }
+
+ if (argdict->Contains("set_if")) {
+ Value argsetif = argdict->Get("set_if");
+
+ if (argsetif.IsString() && !MacroProcessor::ValidateMacroString(argsetif))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "arguments", kv.first, "set_if" }, "Closing $ not found in macro format string '" + argsetif + "'."));
+ }
+ } else if (arginfo.IsString()) {
+ if (!MacroProcessor::ValidateMacroString(arginfo))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "arguments", kv.first }, "Closing $ not found in macro format string '" + arginfo + "'."));
+ }
+ }
+ }
+
+ Dictionary::Ptr env = GetEnv();
+
+ if (env) {
+ ObjectLock olock(env);
+ for (const Dictionary::Pair& kv : env) {
+ const Value& envval = kv.second;
+
+ if (!envval.IsString() || envval.IsEmpty())
+ continue;
+
+ if (!MacroProcessor::ValidateMacroString(envval))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "env", kv.first }, "Closing $ not found in macro format string '" + envval + "'."));
+ }
+ }
+}
diff --git a/lib/icinga/command.hpp b/lib/icinga/command.hpp
new file mode 100644
index 0000000..19bb050
--- /dev/null
+++ b/lib/icinga/command.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMMAND_H
+#define COMMAND_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/command-ti.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+/**
+ * A command.
+ *
+ * @ingroup icinga
+ */
+class Command : public ObjectImpl<Command>
+{
+public:
+ DECLARE_OBJECT(Command);
+
+ //virtual Dictionary::Ptr Execute(const Object::Ptr& context) = 0;
+
+ void Validate(int types, const ValidationUtils& utils) override;
+};
+
+}
+
+#endif /* COMMAND_H */
diff --git a/lib/icinga/command.ti b/lib/icinga/command.ti
new file mode 100644
index 0000000..2275955
--- /dev/null
+++ b/lib/icinga/command.ti
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "base/function.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+abstract class Command : CustomVarObject
+{
+ [config] Value command (CommandLine);
+ [config, signal_with_old_value] Value arguments;
+ [config] int timeout {
+ default {{{ return 60; }}}
+ };
+ [config, signal_with_old_value] Dictionary::Ptr env;
+ [config, required] Function::Ptr execute;
+};
+
+validator Command {
+ String command;
+ Function command;
+ Array command {
+ String "*";
+ Function "*";
+ };
+
+ Dictionary arguments {
+ String "*";
+ Function "*";
+ Dictionary "*" {
+ String key;
+ String value;
+ Function value;
+ String description;
+ Number "required";
+ Number skip_key;
+ Number repeat_key;
+ String set_if;
+ Function set_if;
+ Number order;
+ String separator;
+ };
+ };
+
+ Dictionary env {
+ String "*";
+ Function "*";
+ };
+};
+
+}
diff --git a/lib/icinga/comment.cpp b/lib/icinga/comment.cpp
new file mode 100644
index 0000000..9c0b923
--- /dev/null
+++ b/lib/icinga/comment.cpp
@@ -0,0 +1,258 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/comment.hpp"
+#include "icinga/comment-ti.cpp"
+#include "icinga/host.hpp"
+#include "remote/configobjectutility.hpp"
+#include "base/utility.hpp"
+#include "base/configtype.hpp"
+#include "base/timer.hpp"
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+static int l_NextCommentID = 1;
+static std::mutex l_CommentMutex;
+static std::map<int, String> l_LegacyCommentsCache;
+static Timer::Ptr l_CommentsExpireTimer;
+
+boost::signals2::signal<void (const Comment::Ptr&)> Comment::OnCommentAdded;
+boost::signals2::signal<void (const Comment::Ptr&)> Comment::OnCommentRemoved;
+boost::signals2::signal<void (const Comment::Ptr&, const String&, double, const MessageOrigin::Ptr&)> Comment::OnRemovalInfoChanged;
+
+REGISTER_TYPE(Comment);
+
+String CommentNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Comment::Ptr comment = dynamic_pointer_cast<Comment>(context);
+
+ if (!comment)
+ return "";
+
+ String name = comment->GetHostName();
+
+ if (!comment->GetServiceName().IsEmpty())
+ name += "!" + comment->GetServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr CommentNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Comment name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void Comment::OnAllConfigLoaded()
+{
+ ConfigObject::OnAllConfigLoaded();
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+
+ if (GetServiceName().IsEmpty())
+ m_Checkable = host;
+ else
+ m_Checkable = host->GetServiceByShortName(GetServiceName());
+
+ if (!m_Checkable)
+ BOOST_THROW_EXCEPTION(ScriptError("Comment '" + GetName() + "' references a host/service which doesn't exist.", GetDebugInfo()));
+}
+
+void Comment::Start(bool runtimeCreated)
+{
+ ObjectImpl<Comment>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, [this]() {
+ l_CommentsExpireTimer = Timer::Create();
+ l_CommentsExpireTimer->SetInterval(60);
+ l_CommentsExpireTimer->OnTimerExpired.connect([](const Timer * const&) { CommentsExpireTimerHandler(); });
+ l_CommentsExpireTimer->Start();
+ });
+
+ {
+ std::unique_lock<std::mutex> lock(l_CommentMutex);
+
+ SetLegacyId(l_NextCommentID);
+ l_LegacyCommentsCache[l_NextCommentID] = GetName();
+ l_NextCommentID++;
+ }
+
+ GetCheckable()->RegisterComment(this);
+
+ if (runtimeCreated)
+ OnCommentAdded(this);
+}
+
+void Comment::Stop(bool runtimeRemoved)
+{
+ GetCheckable()->UnregisterComment(this);
+
+ if (runtimeRemoved)
+ OnCommentRemoved(this);
+
+ ObjectImpl<Comment>::Stop(runtimeRemoved);
+}
+
+Checkable::Ptr Comment::GetCheckable() const
+{
+ return static_pointer_cast<Checkable>(m_Checkable);
+}
+
+bool Comment::IsExpired() const
+{
+ double expire_time = GetExpireTime();
+
+ return (expire_time != 0 && expire_time < Utility::GetTime());
+}
+
+int Comment::GetNextCommentID()
+{
+ std::unique_lock<std::mutex> lock(l_CommentMutex);
+
+ return l_NextCommentID;
+}
+
+String Comment::AddComment(const Checkable::Ptr& checkable, CommentType entryType, const String& author,
+ const String& text, bool persistent, double expireTime, bool sticky, const String& id, const MessageOrigin::Ptr& origin)
+{
+ String fullName;
+
+ if (id.IsEmpty())
+ fullName = checkable->GetName() + "!" + Utility::NewUniqueID();
+ else
+ fullName = id;
+
+ Dictionary::Ptr attrs = new Dictionary();
+
+ attrs->Set("author", author);
+ attrs->Set("text", text);
+ attrs->Set("persistent", persistent);
+ attrs->Set("expire_time", expireTime);
+ attrs->Set("entry_type", entryType);
+ attrs->Set("sticky", sticky);
+ attrs->Set("entry_time", Utility::GetTime());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ attrs->Set("host_name", host->GetName());
+ if (service)
+ attrs->Set("service_name", service->GetShortName());
+
+ String zone = checkable->GetZoneName();
+
+ if (!zone.IsEmpty())
+ attrs->Set("zone", zone);
+
+ String config = ConfigObjectUtility::CreateObjectConfig(Comment::TypeInstance, fullName, true, nullptr, attrs);
+
+ Array::Ptr errors = new Array();
+
+ if (!ConfigObjectUtility::CreateObject(Comment::TypeInstance, fullName, config, errors, nullptr)) {
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "Comment", error);
+ }
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not create comment."));
+ }
+
+ Comment::Ptr comment = Comment::GetByName(fullName);
+
+ if (!comment)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not create comment."));
+
+ Log(LogNotice, "Comment")
+ << "Added comment '" << comment->GetName() << "'.";
+
+ return fullName;
+}
+
+void Comment::RemoveComment(const String& id, bool removedManually, const String& removedBy,
+ const MessageOrigin::Ptr& origin)
+{
+ Comment::Ptr comment = Comment::GetByName(id);
+
+ if (!comment || comment->GetPackage() != "_api")
+ return;
+
+ Log(LogNotice, "Comment")
+ << "Removed comment '" << comment->GetName() << "' from object '" << comment->GetCheckable()->GetName() << "'.";
+
+ if (removedManually) {
+ comment->SetRemovalInfo(removedBy, Utility::GetTime());
+ }
+
+ Array::Ptr errors = new Array();
+
+ if (!ConfigObjectUtility::DeleteObject(comment, false, errors, nullptr)) {
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "Comment", error);
+ }
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not remove comment."));
+ }
+}
+
+void Comment::SetRemovalInfo(const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin) {
+ {
+ ObjectLock olock(this);
+
+ SetRemovedBy(removedBy, false, origin);
+ SetRemoveTime(removeTime, false, origin);
+ }
+
+ OnRemovalInfoChanged(this, removedBy, removeTime, origin);
+}
+
+String Comment::GetCommentIDFromLegacyID(int id)
+{
+ std::unique_lock<std::mutex> lock(l_CommentMutex);
+
+ auto it = l_LegacyCommentsCache.find(id);
+
+ if (it == l_LegacyCommentsCache.end())
+ return Empty;
+
+ return it->second;
+}
+
+void Comment::CommentsExpireTimerHandler()
+{
+ std::vector<Comment::Ptr> comments;
+
+ for (const Comment::Ptr& comment : ConfigType::GetObjectsByType<Comment>()) {
+ comments.push_back(comment);
+ }
+
+ for (const Comment::Ptr& comment : comments) {
+ /* Only remove comments which are activated after daemon start. */
+ if (comment->IsActive() && comment->IsExpired()) {
+ /* Do not remove persistent comments from an acknowledgement */
+ if (comment->GetEntryType() == CommentAcknowledgement && comment->GetPersistent())
+ continue;
+
+ RemoveComment(comment->GetName());
+ }
+ }
+}
diff --git a/lib/icinga/comment.hpp b/lib/icinga/comment.hpp
new file mode 100644
index 0000000..6532084
--- /dev/null
+++ b/lib/icinga/comment.hpp
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMMENT_H
+#define COMMENT_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/comment-ti.hpp"
+#include "icinga/checkable-ti.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+/**
+ * A comment.
+ *
+ * @ingroup icinga
+ */
+class Comment final : public ObjectImpl<Comment>
+{
+public:
+ DECLARE_OBJECT(Comment);
+ DECLARE_OBJECTNAME(Comment);
+
+ static boost::signals2::signal<void (const Comment::Ptr&)> OnCommentAdded;
+ static boost::signals2::signal<void (const Comment::Ptr&)> OnCommentRemoved;
+ static boost::signals2::signal<void (const Comment::Ptr&, const String&, double, const MessageOrigin::Ptr&)> OnRemovalInfoChanged;
+
+ intrusive_ptr<Checkable> GetCheckable() const;
+
+ bool IsExpired() const;
+
+ void SetRemovalInfo(const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin = nullptr);
+
+ static int GetNextCommentID();
+
+ static String AddComment(const intrusive_ptr<Checkable>& checkable, CommentType entryType,
+ const String& author, const String& text, bool persistent, double expireTime, bool sticky = false,
+ const String& id = String(), const MessageOrigin::Ptr& origin = nullptr);
+
+ static void RemoveComment(const String& id, bool removedManually = false, const String& removedBy = "",
+ const MessageOrigin::Ptr& origin = nullptr);
+
+ static String GetCommentIDFromLegacyID(int id);
+
+protected:
+ void OnAllConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ ObjectImpl<Checkable>::Ptr m_Checkable;
+
+ static void CommentsExpireTimerHandler();
+};
+
+}
+
+#endif /* COMMENT_H */
diff --git a/lib/icinga/comment.ti b/lib/icinga/comment.ti
new file mode 100644
index 0000000..b8ad6f7
--- /dev/null
+++ b/lib/icinga/comment.ti
@@ -0,0 +1,80 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/utility.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+/**
+ * The type of a service comment.
+ *
+ * @ingroup icinga
+ */
+enum CommentType
+{
+ CommentUser = 1,
+ CommentAcknowledgement = 4
+};
+
+class CommentNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Comment : ConfigObject < CommentNameComposer
+{
+ load_after Host;
+ load_after Service;
+
+ [config, no_user_modify, protected, required, navigation(host)] name(Host) host_name {
+ navigate {{{
+ return Host::GetByName(GetHostName());
+ }}}
+ };
+ [config, no_user_modify, protected, navigation(service)] String service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+ return host->GetServiceByShortName(GetServiceName());
+ }}}
+ };
+
+ [config] Timestamp entry_time {
+ default {{{ return Utility::GetTime(); }}}
+ };
+ [config, enum] CommentType entry_type {
+ default {{{ return CommentUser; }}}
+ };
+ [config, no_user_view, no_user_modify] bool sticky;
+ [config, required] String author;
+ [config, required] String text;
+ [config] bool persistent;
+ [config] Timestamp expire_time;
+ [state] int legacy_id;
+
+ [no_user_view, no_user_modify] String removed_by;
+ [no_user_view, no_user_modify] Timestamp remove_time;
+};
+
+}
diff --git a/lib/icinga/compatutility.cpp b/lib/icinga/compatutility.cpp
new file mode 100644
index 0000000..95aed43
--- /dev/null
+++ b/lib/icinga/compatutility.cpp
@@ -0,0 +1,302 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/compatutility.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/service.hpp"
+#include "base/utility.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/join.hpp>
+
+using namespace icinga;
+
+/* Used in DB IDO and Livestatus. */
+String CompatUtility::GetCommandLine(const Command::Ptr& command)
+{
+ Value commandLine = command->GetCommandLine();
+
+ String result;
+ if (commandLine.IsObjectType<Array>()) {
+ Array::Ptr args = commandLine;
+
+ ObjectLock olock(args);
+ for (const String& arg : args) {
+ // This is obviously incorrect for non-trivial cases.
+ result += " \"" + EscapeString(arg) + "\"";
+ }
+ } else if (!commandLine.IsEmpty()) {
+ result = EscapeString(Convert::ToString(commandLine));
+ } else {
+ result = "<internal>";
+ }
+
+ return result;
+}
+
+String CompatUtility::GetCommandNamePrefix(const Command::Ptr& command)
+/* Helper. */
+{
+ if (!command)
+ return Empty;
+
+ String prefix;
+ if (command->GetReflectionType() == CheckCommand::TypeInstance)
+ prefix = "check_";
+ else if (command->GetReflectionType() == NotificationCommand::TypeInstance)
+ prefix = "notification_";
+ else if (command->GetReflectionType() == EventCommand::TypeInstance)
+ prefix = "event_";
+
+ return prefix;
+}
+
+String CompatUtility::GetCommandName(const Command::Ptr& command)
+/* Used in DB IDO and Livestatus. */
+{
+ if (!command)
+ return Empty;
+
+ return GetCommandNamePrefix(command) + command->GetName();
+}
+
+/* Used in DB IDO and Livestatus. */
+String CompatUtility::GetCheckableCommandArgs(const Checkable::Ptr& checkable)
+{
+ CheckCommand::Ptr command = checkable->GetCheckCommand();
+
+ Dictionary::Ptr args = new Dictionary();
+
+ if (command) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+ String command_line = GetCommandLine(command);
+
+ Dictionary::Ptr command_vars = command->GetVars();
+
+ if (command_vars) {
+ ObjectLock olock(command_vars);
+ for (const Dictionary::Pair& kv : command_vars) {
+ String macro = "$" + kv.first + "$"; // this is too simple
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+
+ }
+ }
+
+ Dictionary::Ptr host_vars = host->GetVars();
+
+ if (host_vars) {
+ ObjectLock olock(host_vars);
+ for (const Dictionary::Pair& kv : host_vars) {
+ String macro = "$" + kv.first + "$"; // this is too simple
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+ macro = "$host.vars." + kv.first + "$";
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+ }
+ }
+
+ if (service) {
+ Dictionary::Ptr service_vars = service->GetVars();
+
+ if (service_vars) {
+ ObjectLock olock(service_vars);
+ for (const Dictionary::Pair& kv : service_vars) {
+ String macro = "$" + kv.first + "$"; // this is too simple
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+ macro = "$service.vars." + kv.first + "$";
+ if (command_line.Contains(macro))
+ args->Set(kv.first, kv.second);
+ }
+ }
+ }
+
+ String arg_string;
+ ObjectLock olock(args);
+ for (const Dictionary::Pair& kv : args) {
+ arg_string += Convert::ToString(kv.first) + "=" + Convert::ToString(kv.second) + "!";
+ }
+ return arg_string;
+ }
+
+ return Empty;
+}
+
+/* Used in DB IDO and Livestatus. */
+int CompatUtility::GetCheckableNotificationLastNotification(const Checkable::Ptr& checkable)
+{
+ double last_notification = 0.0;
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ if (notification->GetLastNotification() > last_notification)
+ last_notification = notification->GetLastNotification();
+ }
+
+ return static_cast<int>(last_notification);
+}
+
+/* Used in DB IDO and Livestatus. */
+int CompatUtility::GetCheckableNotificationNextNotification(const Checkable::Ptr& checkable)
+{
+ double next_notification = 0.0;
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ if (next_notification == 0 || notification->GetNextNotification() < next_notification)
+ next_notification = notification->GetNextNotification();
+ }
+
+ return static_cast<int>(next_notification);
+}
+
+/* Used in DB IDO and Livestatus. */
+int CompatUtility::GetCheckableNotificationNotificationNumber(const Checkable::Ptr& checkable)
+{
+ int notification_number = 0;
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ if (notification->GetNotificationNumber() > notification_number)
+ notification_number = notification->GetNotificationNumber();
+ }
+
+ return notification_number;
+}
+
+/* Used in DB IDO and Livestatus. */
+double CompatUtility::GetCheckableNotificationNotificationInterval(const Checkable::Ptr& checkable)
+{
+ double notification_interval = -1;
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ if (notification_interval == -1 || notification->GetInterval() < notification_interval)
+ notification_interval = notification->GetInterval();
+ }
+
+ if (notification_interval == -1)
+ notification_interval = 60;
+
+ return notification_interval / 60.0;
+}
+
+/* Helper. */
+int CompatUtility::GetCheckableNotificationTypeFilter(const Checkable::Ptr& checkable)
+{
+ unsigned long notification_type_filter = 0;
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ ObjectLock olock(notification);
+
+ notification_type_filter |= notification->GetTypeFilter();
+ }
+
+ return notification_type_filter;
+}
+
+/* Helper. */
+int CompatUtility::GetCheckableNotificationStateFilter(const Checkable::Ptr& checkable)
+{
+ unsigned long notification_state_filter = 0;
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ ObjectLock olock(notification);
+
+ notification_state_filter |= notification->GetStateFilter();
+ }
+
+ return notification_state_filter;
+}
+
+/* Used in DB IDO and Livestatus. */
+std::set<User::Ptr> CompatUtility::GetCheckableNotificationUsers(const Checkable::Ptr& checkable)
+{
+ /* Service -> Notifications -> (Users + UserGroups -> Users) */
+ std::set<User::Ptr> allUsers;
+ std::set<User::Ptr> users;
+
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ ObjectLock olock(notification);
+
+ users = notification->GetUsers();
+
+ std::copy(users.begin(), users.end(), std::inserter(allUsers, allUsers.begin()));
+
+ for (const UserGroup::Ptr& ug : notification->GetUserGroups()) {
+ std::set<User::Ptr> members = ug->GetMembers();
+ std::copy(members.begin(), members.end(), std::inserter(allUsers, allUsers.begin()));
+ }
+ }
+
+ return allUsers;
+}
+
+/* Used in DB IDO and Livestatus. */
+std::set<UserGroup::Ptr> CompatUtility::GetCheckableNotificationUserGroups(const Checkable::Ptr& checkable)
+{
+ std::set<UserGroup::Ptr> usergroups;
+ /* Service -> Notifications -> UserGroups */
+ for (const Notification::Ptr& notification : checkable->GetNotifications()) {
+ ObjectLock olock(notification);
+
+ for (const UserGroup::Ptr& ug : notification->GetUserGroups()) {
+ usergroups.insert(ug);
+ }
+ }
+
+ return usergroups;
+}
+
+/* Used in DB IDO, Livestatus, CompatLogger, GelfWriter, IcingaDB. */
+String CompatUtility::GetCheckResultOutput(const CheckResult::Ptr& cr)
+{
+ if (!cr)
+ return Empty;
+
+ String output;
+
+ String raw_output = cr->GetOutput();
+
+ size_t line_end = raw_output.Find("\n");
+
+ return raw_output.SubStr(0, line_end);
+}
+
+/* Used in DB IDO, Livestatus and IcingaDB. */
+String CompatUtility::GetCheckResultLongOutput(const CheckResult::Ptr& cr)
+{
+ if (!cr)
+ return Empty;
+
+ String long_output;
+ String output;
+
+ String raw_output = cr->GetOutput();
+
+ size_t line_end = raw_output.Find("\n");
+
+ if (line_end > 0 && line_end != String::NPos) {
+ long_output = raw_output.SubStr(line_end+1, raw_output.GetLength());
+ return EscapeString(long_output);
+ }
+
+ return Empty;
+}
+
+/* Helper for DB IDO and Livestatus. */
+String CompatUtility::EscapeString(const String& str)
+{
+ String result = str;
+ boost::algorithm::replace_all(result, "\n", "\\n");
+ return result;
+}
+
+/* Used in ExternalCommandListener. */
+String CompatUtility::UnEscapeString(const String& str)
+{
+ String result = str;
+ boost::algorithm::replace_all(result, "\\n", "\n");
+ return result;
+}
diff --git a/lib/icinga/compatutility.hpp b/lib/icinga/compatutility.hpp
new file mode 100644
index 0000000..7b96fb3
--- /dev/null
+++ b/lib/icinga/compatutility.hpp
@@ -0,0 +1,56 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef COMPATUTILITY_H
+#define COMPATUTILITY_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/host.hpp"
+#include "icinga/command.hpp"
+
+namespace icinga
+{
+
+/**
+ * Compatibility utility functions.
+ *
+ * @ingroup icinga
+ */
+class CompatUtility
+{
+public:
+ /* command */
+ static String GetCommandLine(const Command::Ptr& command);
+ static String GetCommandName(const Command::Ptr& command);
+
+ /* service */
+ static String GetCheckableCommandArgs(const Checkable::Ptr& checkable);
+
+ /* notification */
+ static int GetCheckableNotificationsEnabled(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationLastNotification(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationNextNotification(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationNotificationNumber(const Checkable::Ptr& checkable);
+ static double GetCheckableNotificationNotificationInterval(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationTypeFilter(const Checkable::Ptr& checkable);
+ static int GetCheckableNotificationStateFilter(const Checkable::Ptr& checkable);
+
+ static std::set<User::Ptr> GetCheckableNotificationUsers(const Checkable::Ptr& checkable);
+ static std::set<UserGroup::Ptr> GetCheckableNotificationUserGroups(const Checkable::Ptr& checkable);
+
+ /* check result */
+ static String GetCheckResultOutput(const CheckResult::Ptr& cr);
+ static String GetCheckResultLongOutput(const CheckResult::Ptr& cr);
+
+ /* misc */
+ static String EscapeString(const String& str);
+ static String UnEscapeString(const String& str);
+
+private:
+ CompatUtility();
+
+ static String GetCommandNamePrefix(const Command::Ptr& command);
+};
+
+}
+
+#endif /* COMPATUTILITY_H */
diff --git a/lib/icinga/customvarobject.cpp b/lib/icinga/customvarobject.cpp
new file mode 100644
index 0000000..fc1fd27
--- /dev/null
+++ b/lib/icinga/customvarobject.cpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "icinga/customvarobject-ti.cpp"
+#include "icinga/macroprocessor.hpp"
+#include "base/logger.hpp"
+#include "base/function.hpp"
+#include "base/exception.hpp"
+#include "base/objectlock.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(CustomVarObject);
+
+void CustomVarObject::ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ MacroProcessor::ValidateCustomVars(this, lvalue());
+}
+
+int icinga::FilterArrayToInt(const Array::Ptr& typeFilters, const std::map<String, int>& filterMap, int defaultValue)
+{
+ int resultTypeFilter;
+
+ if (!typeFilters)
+ return defaultValue;
+
+ resultTypeFilter = 0;
+
+ ObjectLock olock(typeFilters);
+ for (const Value& typeFilter : typeFilters) {
+ if (typeFilter.IsNumber()) {
+ resultTypeFilter = resultTypeFilter | typeFilter;
+ continue;
+ }
+
+ if (!typeFilter.IsString())
+ return -1;
+
+ auto it = filterMap.find(typeFilter);
+
+ if (it == filterMap.end())
+ return -1;
+
+ resultTypeFilter = resultTypeFilter | it->second;
+ }
+
+ return resultTypeFilter;
+}
+
diff --git a/lib/icinga/customvarobject.hpp b/lib/icinga/customvarobject.hpp
new file mode 100644
index 0000000..e10ef32
--- /dev/null
+++ b/lib/icinga/customvarobject.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CUSTOMVAROBJECT_H
+#define CUSTOMVAROBJECT_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/customvarobject-ti.hpp"
+#include "base/configobject.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+/**
+ * An object with custom variable attribute.
+ *
+ * @ingroup icinga
+ */
+class CustomVarObject : public ObjectImpl<CustomVarObject>
+{
+public:
+ DECLARE_OBJECT(CustomVarObject);
+
+ void ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) final;
+};
+
+int FilterArrayToInt(const Array::Ptr& typeFilters, const std::map<String, int>& filterMap, int defaultValue);
+
+}
+
+#endif /* CUSTOMVAROBJECT_H */
diff --git a/lib/icinga/customvarobject.ti b/lib/icinga/customvarobject.ti
new file mode 100644
index 0000000..3e40f66
--- /dev/null
+++ b/lib/icinga/customvarobject.ti
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+abstract class CustomVarObject : ConfigObject
+{
+ [config, signal_with_old_value] Dictionary::Ptr vars;
+};
+
+}
diff --git a/lib/icinga/dependency-apply.cpp b/lib/icinga/dependency-apply.cpp
new file mode 100644
index 0000000..8681c43
--- /dev/null
+++ b/lib/icinga/dependency-apply.cpp
@@ -0,0 +1,161 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/dependency.hpp"
+#include "icinga/service.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE([]() {
+ ApplyRule::RegisterType("Dependency", { "Host", "Service" });
+});
+
+bool Dependency::EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter)
+{
+ if (!skipFilter && !rule.EvaluateFilter(frame))
+ return false;
+
+ auto& di (rule.GetDebugInfo());
+
+#ifdef _DEBUG
+ Log(LogDebug, "Dependency")
+ << "Applying dependency '" << name << "' to object '" << checkable->GetName() << "' for rule " << di;
+#endif /* _DEBUG */
+
+ ConfigItemBuilder builder{di};
+ builder.SetType(Dependency::TypeInstance);
+ builder.SetName(name);
+ builder.SetScope(frame.Locals->ShallowClone());
+ builder.SetIgnoreOnError(rule.GetIgnoreOnError());
+
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "parent_host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "child_host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+
+ if (service)
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "child_service_name"), OpSetLiteral, MakeLiteral(service->GetShortName()), di));
+
+ String zone = checkable->GetZoneName();
+
+ if (!zone.IsEmpty())
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "zone"), OpSetLiteral, MakeLiteral(zone), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "package"), OpSetLiteral, MakeLiteral(rule.GetPackage()), di));
+
+ builder.AddExpression(new OwnedExpression(rule.GetExpression()));
+
+ ConfigItem::Ptr dependencyItem = builder.Compile();
+ dependencyItem->Register();
+
+ return true;
+}
+
+bool Dependency::EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter)
+{
+ auto& di (rule.GetDebugInfo());
+
+ CONTEXT("Evaluating 'apply' rule (" << di << ")");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ScriptFrame frame(true);
+ if (rule.GetScope())
+ rule.GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+ if (service)
+ frame.Locals->Set("service", service);
+
+ Value vinstances;
+
+ if (rule.GetFTerm()) {
+ try {
+ vinstances = rule.GetFTerm()->Evaluate(frame);
+ } catch (const std::exception&) {
+ /* Silently ignore errors here and assume there are no instances. */
+ return false;
+ }
+ } else {
+ vinstances = new Array({ "" });
+ }
+
+ bool match = false;
+
+ if (vinstances.IsObjectType<Array>()) {
+ if (!rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Dictionary iterator requires value to be a dictionary.", di));
+
+ Array::Ptr arr = vinstances;
+
+ ObjectLock olock(arr);
+ for (const Value& instance : arr) {
+ String name = rule.GetName();
+
+ if (!rule.GetFKVar().IsEmpty()) {
+ frame.Locals->Set(rule.GetFKVar(), instance);
+ name += instance;
+ }
+
+ if (EvaluateApplyRuleInstance(checkable, name, frame, rule, skipFilter))
+ match = true;
+ }
+ } else if (vinstances.IsObjectType<Dictionary>()) {
+ if (rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Array iterator requires value to be an array.", di));
+
+ Dictionary::Ptr dict = vinstances;
+
+ for (const String& key : dict->GetKeys()) {
+ frame.Locals->Set(rule.GetFKVar(), key);
+ frame.Locals->Set(rule.GetFVVar(), dict->Get(key));
+
+ if (EvaluateApplyRuleInstance(checkable, rule.GetName() + key, frame, rule, skipFilter))
+ match = true;
+ }
+ }
+
+ return match;
+}
+
+void Dependency::EvaluateApplyRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating 'apply' rules for host '" << host->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Dependency::TypeInstance, Host::TypeInstance)) {
+ if (EvaluateApplyRule(host, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedHostRules(Dependency::TypeInstance, host->GetName())) {
+ if (EvaluateApplyRule(host, *rule, true))
+ rule->AddMatch();
+ }
+}
+
+void Dependency::EvaluateApplyRules(const Service::Ptr& service)
+{
+ CONTEXT("Evaluating 'apply' rules for service '" << service->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Dependency::TypeInstance, Service::TypeInstance)) {
+ if (EvaluateApplyRule(service, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedServiceRules(Dependency::TypeInstance, service->GetHost()->GetName(), service->GetShortName())) {
+ if (EvaluateApplyRule(service, *rule, true))
+ rule->AddMatch();
+ }
+}
diff --git a/lib/icinga/dependency.cpp b/lib/icinga/dependency.cpp
new file mode 100644
index 0000000..2843b90
--- /dev/null
+++ b/lib/icinga/dependency.cpp
@@ -0,0 +1,325 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/dependency.hpp"
+#include "icinga/dependency-ti.cpp"
+#include "icinga/service.hpp"
+#include "base/configobject.hpp"
+#include "base/initialize.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include <map>
+#include <sstream>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(Dependency);
+
+bool Dependency::m_AssertNoCyclesForIndividualDeps = false;
+
+struct DependencyCycleNode
+{
+ bool Visited = false;
+ bool OnStack = false;
+};
+
+struct DependencyStackFrame
+{
+ ConfigObject::Ptr Node;
+ bool Implicit;
+
+ inline DependencyStackFrame(ConfigObject::Ptr node, bool implicit = false) : Node(std::move(node)), Implicit(implicit)
+ { }
+};
+
+struct DependencyCycleGraph
+{
+ std::map<Checkable::Ptr, DependencyCycleNode> Nodes;
+ std::vector<DependencyStackFrame> Stack;
+};
+
+static void AssertNoDependencyCycle(const Checkable::Ptr& checkable, DependencyCycleGraph& graph, bool implicit = false);
+
+static void AssertNoParentDependencyCycle(const Checkable::Ptr& parent, DependencyCycleGraph& graph, bool implicit)
+{
+ if (graph.Nodes[parent].OnStack) {
+ std::ostringstream oss;
+ oss << "Dependency cycle:\n";
+
+ for (auto& frame : graph.Stack) {
+ oss << frame.Node->GetReflectionType()->GetName() << " '" << frame.Node->GetName() << "'";
+
+ if (frame.Implicit) {
+ oss << " (implicit)";
+ }
+
+ oss << "\n-> ";
+ }
+
+ oss << parent->GetReflectionType()->GetName() << " '" << parent->GetName() << "'";
+
+ if (implicit) {
+ oss << " (implicit)";
+ }
+
+ BOOST_THROW_EXCEPTION(ScriptError(oss.str()));
+ }
+
+ AssertNoDependencyCycle(parent, graph, implicit);
+}
+
+static void AssertNoDependencyCycle(const Checkable::Ptr& checkable, DependencyCycleGraph& graph, bool implicit)
+{
+ auto& node (graph.Nodes[checkable]);
+
+ if (!node.Visited) {
+ node.Visited = true;
+ node.OnStack = true;
+ graph.Stack.emplace_back(checkable, implicit);
+
+ for (auto& dep : checkable->GetDependencies()) {
+ graph.Stack.emplace_back(dep);
+ AssertNoParentDependencyCycle(dep->GetParent(), graph, false);
+ graph.Stack.pop_back();
+ }
+
+ {
+ auto service (dynamic_pointer_cast<Service>(checkable));
+
+ if (service) {
+ AssertNoParentDependencyCycle(service->GetHost(), graph, true);
+ }
+ }
+
+ graph.Stack.pop_back();
+ node.OnStack = false;
+ }
+}
+
+void Dependency::AssertNoCycles()
+{
+ DependencyCycleGraph graph;
+
+ for (auto& host : ConfigType::GetObjectsByType<Host>()) {
+ AssertNoDependencyCycle(host, graph);
+ }
+
+ for (auto& service : ConfigType::GetObjectsByType<Service>()) {
+ AssertNoDependencyCycle(service, graph);
+ }
+
+ m_AssertNoCyclesForIndividualDeps = true;
+}
+
+String DependencyNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Dependency::Ptr dependency = dynamic_pointer_cast<Dependency>(context);
+
+ if (!dependency)
+ return "";
+
+ String name = dependency->GetChildHostName();
+
+ if (!dependency->GetChildServiceName().IsEmpty())
+ name += "!" + dependency->GetChildServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr DependencyNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Dependency name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("child_host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("child_service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void Dependency::OnConfigLoaded()
+{
+ Value defaultFilter;
+
+ if (GetParentServiceName().IsEmpty())
+ defaultFilter = StateFilterUp;
+ else
+ defaultFilter = StateFilterOK | StateFilterWarning;
+
+ SetStateFilter(FilterArrayToInt(GetStates(), Notification::GetStateFilterMap(), defaultFilter));
+}
+
+void Dependency::OnAllConfigLoaded()
+{
+ ObjectImpl<Dependency>::OnAllConfigLoaded();
+
+ Host::Ptr childHost = Host::GetByName(GetChildHostName());
+
+ if (childHost) {
+ if (GetChildServiceName().IsEmpty())
+ m_Child = childHost;
+ else
+ m_Child = childHost->GetServiceByShortName(GetChildServiceName());
+ }
+
+ if (!m_Child)
+ BOOST_THROW_EXCEPTION(ScriptError("Dependency '" + GetName() + "' references a child host/service which doesn't exist.", GetDebugInfo()));
+
+ Host::Ptr parentHost = Host::GetByName(GetParentHostName());
+
+ if (parentHost) {
+ if (GetParentServiceName().IsEmpty())
+ m_Parent = parentHost;
+ else
+ m_Parent = parentHost->GetServiceByShortName(GetParentServiceName());
+ }
+
+ if (!m_Parent)
+ BOOST_THROW_EXCEPTION(ScriptError("Dependency '" + GetName() + "' references a parent host/service which doesn't exist.", GetDebugInfo()));
+
+ m_Child->AddDependency(this);
+ m_Parent->AddReverseDependency(this);
+
+ if (m_AssertNoCyclesForIndividualDeps) {
+ DependencyCycleGraph graph;
+
+ try {
+ AssertNoDependencyCycle(m_Parent, graph);
+ } catch (...) {
+ m_Child->RemoveDependency(this);
+ m_Parent->RemoveReverseDependency(this);
+ throw;
+ }
+ }
+}
+
+void Dependency::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<Dependency>::Stop(runtimeRemoved);
+
+ GetChild()->RemoveDependency(this);
+ GetParent()->RemoveReverseDependency(this);
+}
+
+bool Dependency::IsAvailable(DependencyType dt) const
+{
+ Checkable::Ptr parent = GetParent();
+
+ Host::Ptr parentHost;
+ Service::Ptr parentService;
+ tie(parentHost, parentService) = GetHostService(parent);
+
+ /* ignore if it's the same checkable object */
+ if (parent == GetChild()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Parent and child " << (parentService ? "service" : "host") << " are identical.";
+ return true;
+ }
+
+ /* ignore pending */
+ if (!parent->GetLastCheckResult()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Parent " << (parentService ? "service" : "host") << " '" << parent->GetName() << "' hasn't been checked yet.";
+ return true;
+ }
+
+ if (GetIgnoreSoftStates()) {
+ /* ignore soft states */
+ if (parent->GetStateType() == StateTypeSoft) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Parent " << (parentService ? "service" : "host") << " '" << parent->GetName() << "' is in a soft state.";
+ return true;
+ }
+ } else {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' failed: Parent " << (parentService ? "service" : "host") << " '" << parent->GetName() << "' is in a soft state.";
+ }
+
+ int state;
+
+ if (parentService)
+ state = ServiceStateToFilter(parentService->GetState());
+ else
+ state = HostStateToFilter(parentHost->GetState());
+
+ /* check state */
+ if (state & GetStateFilter()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Parent " << (parentService ? "service" : "host") << " '" << parent->GetName() << "' matches state filter.";
+ return true;
+ }
+
+ /* ignore if not in time period */
+ TimePeriod::Ptr tp = GetPeriod();
+ if (tp && !tp->IsInside(Utility::GetTime())) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Outside time period.";
+ return true;
+ }
+
+ if (dt == DependencyCheckExecution && !GetDisableChecks()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Checks are not disabled.";
+ return true;
+ } else if (dt == DependencyNotification && !GetDisableNotifications()) {
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' passed: Notifications are not disabled";
+ return true;
+ }
+
+ Log(LogNotice, "Dependency")
+ << "Dependency '" << GetName() << "' failed. Parent "
+ << (parentService ? "service" : "host") << " '" << parent->GetName() << "' is "
+ << (parentService ? Service::StateToString(parentService->GetState()) : Host::StateToString(parentHost->GetState()));
+
+ return false;
+}
+
+Checkable::Ptr Dependency::GetChild() const
+{
+ return m_Child;
+}
+
+Checkable::Ptr Dependency::GetParent() const
+{
+ return m_Parent;
+}
+
+TimePeriod::Ptr Dependency::GetPeriod() const
+{
+ return TimePeriod::GetByName(GetPeriodRaw());
+}
+
+void Dependency::ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Dependency>::ValidateStates(lvalue, utils);
+
+ int sfilter = FilterArrayToInt(lvalue(), Notification::GetStateFilterMap(), 0);
+
+ if (GetParentServiceName().IsEmpty() && (sfilter & ~(StateFilterUp | StateFilterDown)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid for host dependency."));
+
+ if (!GetParentServiceName().IsEmpty() && (sfilter & ~(StateFilterOK | StateFilterWarning | StateFilterCritical | StateFilterUnknown)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid for service dependency."));
+}
+
+void Dependency::SetParent(intrusive_ptr<Checkable> parent)
+{
+ m_Parent = parent;
+}
+
+void Dependency::SetChild(intrusive_ptr<Checkable> child)
+{
+ m_Child = child;
+}
diff --git a/lib/icinga/dependency.hpp b/lib/icinga/dependency.hpp
new file mode 100644
index 0000000..6cebfaa
--- /dev/null
+++ b/lib/icinga/dependency.hpp
@@ -0,0 +1,62 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DEPENDENCY_H
+#define DEPENDENCY_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/dependency-ti.hpp"
+
+namespace icinga
+{
+
+class ApplyRule;
+struct ScriptFrame;
+class Host;
+class Service;
+
+/**
+ * A service dependency..
+ *
+ * @ingroup icinga
+ */
+class Dependency final : public ObjectImpl<Dependency>
+{
+public:
+ DECLARE_OBJECT(Dependency);
+ DECLARE_OBJECTNAME(Dependency);
+
+ intrusive_ptr<Checkable> GetParent() const;
+ intrusive_ptr<Checkable> GetChild() const;
+
+ TimePeriod::Ptr GetPeriod() const;
+
+ bool IsAvailable(DependencyType dt) const;
+
+ void ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+ static void EvaluateApplyRules(const intrusive_ptr<Host>& host);
+ static void EvaluateApplyRules(const intrusive_ptr<Service>& service);
+ static void AssertNoCycles();
+
+ /* Note: Only use them for unit test mocks. Prefer OnConfigLoaded(). */
+ void SetParent(intrusive_ptr<Checkable> parent);
+ void SetChild(intrusive_ptr<Checkable> child);
+
+protected:
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ Checkable::Ptr m_Parent;
+ Checkable::Ptr m_Child;
+
+ static bool m_AssertNoCyclesForIndividualDeps;
+
+ static bool EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter);
+ static bool EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter = false);
+};
+
+}
+
+#endif /* DEPENDENCY_H */
diff --git a/lib/icinga/dependency.ti b/lib/icinga/dependency.ti
new file mode 100644
index 0000000..41de7ba
--- /dev/null
+++ b/lib/icinga/dependency.ti
@@ -0,0 +1,101 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "icinga/checkable.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class DependencyNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Dependency : CustomVarObject < DependencyNameComposer
+{
+ load_after Host;
+ load_after Service;
+
+ [config, no_user_modify, required, navigation(child_host)] name(Host) child_host_name {
+ navigate {{{
+ return Host::GetByName(GetChildHostName());
+ }}}
+ };
+
+ [config, no_user_modify, navigation(child_service)] String child_service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetChildHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetChildHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetChildServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetChildHostName());
+ return host->GetServiceByShortName(GetChildServiceName());
+ }}}
+ };
+
+ [config, no_user_modify, required, navigation(parent_host)] name(Host) parent_host_name {
+ navigate {{{
+ return Host::GetByName(GetParentHostName());
+ }}}
+ };
+
+ [config, no_user_modify, navigation(parent_service)] String parent_service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetParentHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetParentHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetParentServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetParentHostName());
+ return host->GetServiceByShortName(GetParentServiceName());
+ }}}
+ };
+
+ [config] String redundancy_group;
+
+ [config, navigation] name(TimePeriod) period (PeriodRaw) {
+ navigate {{{
+ return TimePeriod::GetByName(GetPeriodRaw());
+ }}}
+ };
+
+ [config] array(Value) states;
+ [no_user_view, no_user_modify] int state_filter_real (StateFilter);
+
+ [config] bool ignore_soft_states {
+ default {{{ return true; }}}
+ };
+
+ [config] bool disable_checks;
+ [config] bool disable_notifications {
+ default {{{ return true; }}}
+ };
+};
+
+}
diff --git a/lib/icinga/downtime.cpp b/lib/icinga/downtime.cpp
new file mode 100644
index 0000000..2178953
--- /dev/null
+++ b/lib/icinga/downtime.cpp
@@ -0,0 +1,584 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/downtime.hpp"
+#include "icinga/downtime-ti.cpp"
+#include "icinga/host.hpp"
+#include "icinga/scheduleddowntime.hpp"
+#include "remote/configobjectutility.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/timer.hpp"
+#include <boost/thread/once.hpp>
+#include <cmath>
+#include <utility>
+
+using namespace icinga;
+
+static int l_NextDowntimeID = 1;
+static std::mutex l_DowntimeMutex;
+static std::map<int, String> l_LegacyDowntimesCache;
+static Timer::Ptr l_DowntimesOrphanedTimer;
+static Timer::Ptr l_DowntimesStartTimer;
+
+boost::signals2::signal<void (const Downtime::Ptr&)> Downtime::OnDowntimeAdded;
+boost::signals2::signal<void (const Downtime::Ptr&)> Downtime::OnDowntimeRemoved;
+boost::signals2::signal<void (const Downtime::Ptr&)> Downtime::OnDowntimeStarted;
+boost::signals2::signal<void (const Downtime::Ptr&)> Downtime::OnDowntimeTriggered;
+boost::signals2::signal<void (const Downtime::Ptr&, const String&, double, const MessageOrigin::Ptr&)> Downtime::OnRemovalInfoChanged;
+
+REGISTER_TYPE(Downtime);
+
+INITIALIZE_ONCE(&Downtime::StaticInitialize);
+
+void Downtime::StaticInitialize()
+{
+ ScriptGlobal::Set("Icinga.DowntimeNoChildren", "DowntimeNoChildren");
+ ScriptGlobal::Set("Icinga.DowntimeTriggeredChildren", "DowntimeTriggeredChildren");
+ ScriptGlobal::Set("Icinga.DowntimeNonTriggeredChildren", "DowntimeNonTriggeredChildren");
+}
+
+String DowntimeNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Downtime::Ptr downtime = dynamic_pointer_cast<Downtime>(context);
+
+ if (!downtime)
+ return "";
+
+ String name = downtime->GetHostName();
+
+ if (!downtime->GetServiceName().IsEmpty())
+ name += "!" + downtime->GetServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr DowntimeNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Downtime name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void Downtime::OnAllConfigLoaded()
+{
+ ObjectImpl<Downtime>::OnAllConfigLoaded();
+
+ if (GetServiceName().IsEmpty())
+ m_Checkable = Host::GetByName(GetHostName());
+ else
+ m_Checkable = Service::GetByNamePair(GetHostName(), GetServiceName());
+
+ if (!m_Checkable)
+ BOOST_THROW_EXCEPTION(ScriptError("Downtime '" + GetName() + "' references a host/service which doesn't exist.", GetDebugInfo()));
+}
+
+void Downtime::Start(bool runtimeCreated)
+{
+ ObjectImpl<Downtime>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, [this]() {
+ l_DowntimesStartTimer = Timer::Create();
+ l_DowntimesStartTimer->SetInterval(5);
+ l_DowntimesStartTimer->OnTimerExpired.connect([](const Timer * const&){ DowntimesStartTimerHandler(); });
+ l_DowntimesStartTimer->Start();
+
+ l_DowntimesOrphanedTimer = Timer::Create();
+ l_DowntimesOrphanedTimer->SetInterval(60);
+ l_DowntimesOrphanedTimer->OnTimerExpired.connect([](const Timer * const&) { DowntimesOrphanedTimerHandler(); });
+ l_DowntimesOrphanedTimer->Start();
+ });
+
+ {
+ std::unique_lock<std::mutex> lock(l_DowntimeMutex);
+
+ SetLegacyId(l_NextDowntimeID);
+ l_LegacyDowntimesCache[l_NextDowntimeID] = GetName();
+ l_NextDowntimeID++;
+ }
+
+ Checkable::Ptr checkable = GetCheckable();
+
+ checkable->RegisterDowntime(this);
+
+ Downtime::Ptr parent = GetByName(GetParent());
+
+ if (parent)
+ parent->RegisterChild(this);
+
+ if (runtimeCreated)
+ OnDowntimeAdded(this);
+
+ /* if this object is already in a NOT-OK state trigger
+ * this downtime now *after* it has been added (important
+ * for DB IDO, etc.)
+ */
+ if (!GetFixed() && !checkable->IsStateOK(checkable->GetStateRaw())) {
+ Log(LogNotice, "Downtime")
+ << "Checkable '" << checkable->GetName() << "' already in a NOT-OK state."
+ << " Triggering downtime now.";
+
+ TriggerDowntime(std::fmax(std::fmax(GetStartTime(), GetEntryTime()), checkable->GetLastStateChange()));
+ }
+
+ if (GetFixed() && CanBeTriggered()) {
+ /* Send notifications. */
+ OnDowntimeStarted(this);
+
+ /* Trigger fixed downtime immediately. */
+ TriggerDowntime(std::fmax(GetStartTime(), GetEntryTime()));
+ }
+}
+
+void Downtime::Stop(bool runtimeRemoved)
+{
+ GetCheckable()->UnregisterDowntime(this);
+
+ Downtime::Ptr parent = GetByName(GetParent());
+
+ if (parent)
+ parent->UnregisterChild(this);
+
+ if (runtimeRemoved)
+ OnDowntimeRemoved(this);
+
+ ObjectImpl<Downtime>::Stop(runtimeRemoved);
+}
+
+void Downtime::Pause()
+{
+ if (m_CleanupTimer) {
+ m_CleanupTimer->Stop();
+ }
+
+ ObjectImpl<Downtime>::Pause();
+}
+
+void Downtime::Resume()
+{
+ ObjectImpl<Downtime>::Resume();
+ SetupCleanupTimer();
+}
+
+Checkable::Ptr Downtime::GetCheckable() const
+{
+ return static_pointer_cast<Checkable>(m_Checkable);
+}
+
+bool Downtime::IsInEffect() const
+{
+ double now = Utility::GetTime();
+
+ if (GetFixed()) {
+ /* fixed downtimes are in effect during the entire [start..end) interval */
+ return (now >= GetStartTime() && now < GetEndTime());
+ }
+
+ double triggerTime = GetTriggerTime();
+
+ if (triggerTime == 0)
+ /* flexible downtime has not been triggered yet */
+ return false;
+
+ return (now < triggerTime + GetDuration());
+}
+
+bool Downtime::IsTriggered() const
+{
+ double now = Utility::GetTime();
+
+ double triggerTime = GetTriggerTime();
+
+ return (triggerTime > 0 && triggerTime <= now);
+}
+
+bool Downtime::IsExpired() const
+{
+ double now = Utility::GetTime();
+
+ if (GetFixed())
+ return (GetEndTime() < now);
+ else {
+ /* triggered flexible downtime not in effect anymore */
+ if (IsTriggered() && !IsInEffect())
+ return true;
+ /* flexible downtime never triggered */
+ else if (!IsTriggered() && (GetEndTime() < now))
+ return true;
+ else
+ return false;
+ }
+}
+
+bool Downtime::HasValidConfigOwner() const
+{
+ if (!ScheduledDowntime::AllConfigIsLoaded()) {
+ return true;
+ }
+
+ String configOwner = GetConfigOwner();
+ return configOwner.IsEmpty() || Zone::GetByName(GetAuthoritativeZone()) != Zone::GetLocalZone() || GetObject<ScheduledDowntime>(configOwner);
+}
+
+int Downtime::GetNextDowntimeID()
+{
+ std::unique_lock<std::mutex> lock(l_DowntimeMutex);
+
+ return l_NextDowntimeID;
+}
+
+Downtime::Ptr Downtime::AddDowntime(const Checkable::Ptr& checkable, const String& author,
+ const String& comment, double startTime, double endTime, bool fixed,
+ const String& triggeredBy, double duration,
+ const String& scheduledDowntime, const String& scheduledBy, const String& parent,
+ const String& id, const MessageOrigin::Ptr& origin)
+{
+ String fullName;
+
+ if (id.IsEmpty())
+ fullName = checkable->GetName() + "!" + Utility::NewUniqueID();
+ else
+ fullName = id;
+
+ Dictionary::Ptr attrs = new Dictionary();
+
+ attrs->Set("author", author);
+ attrs->Set("comment", comment);
+ attrs->Set("start_time", startTime);
+ attrs->Set("end_time", endTime);
+ attrs->Set("fixed", fixed);
+ attrs->Set("duration", duration);
+ attrs->Set("triggered_by", triggeredBy);
+ attrs->Set("scheduled_by", scheduledBy);
+ attrs->Set("parent", parent);
+ attrs->Set("config_owner", scheduledDowntime);
+ attrs->Set("entry_time", Utility::GetTime());
+
+ if (!scheduledDowntime.IsEmpty()) {
+ auto localZone (Zone::GetLocalZone());
+
+ if (localZone) {
+ attrs->Set("authoritative_zone", localZone->GetName());
+ }
+
+ auto sd (ScheduledDowntime::GetByName(scheduledDowntime));
+
+ if (sd) {
+ attrs->Set("config_owner_hash", sd->HashDowntimeOptions());
+ }
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ attrs->Set("host_name", host->GetName());
+ if (service)
+ attrs->Set("service_name", service->GetShortName());
+
+ String zone;
+
+ if (!scheduledDowntime.IsEmpty()) {
+ auto sdt (ScheduledDowntime::GetByName(scheduledDowntime));
+
+ if (sdt) {
+ auto sdtZone (sdt->GetZone());
+
+ if (sdtZone) {
+ zone = sdtZone->GetName();
+ }
+ }
+ }
+
+ if (zone.IsEmpty()) {
+ zone = checkable->GetZoneName();
+ }
+
+ if (!zone.IsEmpty())
+ attrs->Set("zone", zone);
+
+ String config = ConfigObjectUtility::CreateObjectConfig(Downtime::TypeInstance, fullName, true, nullptr, attrs);
+
+ Array::Ptr errors = new Array();
+
+ if (!ConfigObjectUtility::CreateObject(Downtime::TypeInstance, fullName, config, errors, nullptr)) {
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "Downtime", error);
+ }
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not create downtime."));
+ }
+
+ if (!triggeredBy.IsEmpty()) {
+ Downtime::Ptr parentDowntime = Downtime::GetByName(triggeredBy);
+ Array::Ptr triggers = parentDowntime->GetTriggers();
+
+ ObjectLock olock(triggers);
+ if (!triggers->Contains(fullName))
+ triggers->Add(fullName);
+ }
+
+ Downtime::Ptr downtime = Downtime::GetByName(fullName);
+
+ if (!downtime)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not create downtime object."));
+
+ Log(LogInformation, "Downtime")
+ << "Added downtime '" << downtime->GetName()
+ << "' between '" << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", startTime)
+ << "' and '" << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", endTime) << "', author: '"
+ << author << "', " << (fixed ? "fixed" : "flexible with " + Convert::ToString(duration) + "s duration");
+
+ return downtime;
+}
+
+void Downtime::RemoveDowntime(const String& id, bool includeChildren, bool cancelled, bool expired,
+ const String& removedBy, const MessageOrigin::Ptr& origin)
+{
+ Downtime::Ptr downtime = Downtime::GetByName(id);
+
+ if (!downtime || downtime->GetPackage() != "_api")
+ return;
+
+ String config_owner = downtime->GetConfigOwner();
+
+ if (!config_owner.IsEmpty() && !expired) {
+ BOOST_THROW_EXCEPTION(invalid_downtime_removal_error("Cannot remove downtime '" + downtime->GetName() +
+ "'. It is owned by scheduled downtime object '" + config_owner + "'"));
+ }
+
+ if (includeChildren) {
+ for (const Downtime::Ptr& child : downtime->GetChildren()) {
+ Downtime::RemoveDowntime(child->GetName(), true, true);
+ }
+ }
+
+ if (cancelled) {
+ downtime->SetRemovalInfo(removedBy, Utility::GetTime());
+ }
+
+ Array::Ptr errors = new Array();
+
+ if (!ConfigObjectUtility::DeleteObject(downtime, false, errors, nullptr)) {
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "Downtime", error);
+ }
+
+ BOOST_THROW_EXCEPTION(std::runtime_error("Could not remove downtime."));
+ }
+
+ String reason;
+
+ if (expired) {
+ reason = "expired at " + Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", downtime->GetEndTime());
+ } else if (cancelled) {
+ reason = "cancelled by user";
+ } else {
+ reason = "<unknown>";
+ }
+
+ Log msg (LogInformation, "Downtime");
+
+ msg << "Removed downtime '" << downtime->GetName() << "' from checkable";
+
+ {
+ auto checkable (downtime->GetCheckable());
+
+ if (checkable) {
+ msg << " '" << checkable->GetName() << "'";
+ }
+ }
+
+ msg << " (Reason: " << reason << ").";
+}
+
+void Downtime::RegisterChild(const Downtime::Ptr& downtime)
+{
+ std::unique_lock<std::mutex> lock(m_ChildrenMutex);
+ m_Children.insert(downtime);
+}
+
+void Downtime::UnregisterChild(const Downtime::Ptr& downtime)
+{
+ std::unique_lock<std::mutex> lock(m_ChildrenMutex);
+ m_Children.erase(downtime);
+}
+
+std::set<Downtime::Ptr> Downtime::GetChildren() const
+{
+ std::unique_lock<std::mutex> lock(m_ChildrenMutex);
+ return m_Children;
+}
+
+bool Downtime::CanBeTriggered()
+{
+ if (IsInEffect() && IsTriggered())
+ return false;
+
+ if (IsExpired())
+ return false;
+
+ double now = Utility::GetTime();
+
+ if (now < GetStartTime() || now > GetEndTime())
+ return false;
+
+ return true;
+}
+
+void Downtime::SetupCleanupTimer()
+{
+ if (!m_CleanupTimer) {
+ m_CleanupTimer = Timer::Create();
+
+ auto name (GetName());
+
+ m_CleanupTimer->OnTimerExpired.connect([name=std::move(name)](const Timer * const&) {
+ auto downtime (Downtime::GetByName(name));
+
+ if (downtime && downtime->IsExpired()) {
+ RemoveDowntime(name, false, false, true);
+ }
+ });
+ }
+
+ auto triggerTime (GetTriggerTime());
+
+ m_CleanupTimer->Reschedule((GetFixed() || triggerTime <= 0 ? GetEndTime() : triggerTime + GetDuration()) + 0.1);
+ m_CleanupTimer->Start();
+}
+
+void Downtime::TriggerDowntime(double triggerTime)
+{
+ if (!CanBeTriggered())
+ return;
+
+ Checkable::Ptr checkable = GetCheckable();
+
+ Log(LogInformation, "Downtime")
+ << "Triggering downtime '" << GetName() << "' for checkable '" << checkable->GetName() << "'.";
+
+ if (GetTriggerTime() == 0) {
+ SetTriggerTime(triggerTime);
+ }
+
+ {
+ ObjectLock olock (this);
+ SetupCleanupTimer();
+ }
+
+ Array::Ptr triggers = GetTriggers();
+
+ {
+ ObjectLock olock(triggers);
+ for (const String& triggerName : triggers) {
+ Downtime::Ptr downtime = Downtime::GetByName(triggerName);
+
+ if (!downtime)
+ continue;
+
+ downtime->TriggerDowntime(triggerTime);
+ }
+ }
+
+ OnDowntimeTriggered(this);
+}
+
+void Downtime::SetRemovalInfo(const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin) {
+ {
+ ObjectLock olock(this);
+
+ SetRemovedBy(removedBy, false, origin);
+ SetRemoveTime(removeTime, false, origin);
+ }
+
+ OnRemovalInfoChanged(this, removedBy, removeTime, origin);
+}
+
+String Downtime::GetDowntimeIDFromLegacyID(int id)
+{
+ std::unique_lock<std::mutex> lock(l_DowntimeMutex);
+
+ auto it = l_LegacyDowntimesCache.find(id);
+
+ if (it == l_LegacyDowntimesCache.end())
+ return Empty;
+
+ return it->second;
+}
+
+void Downtime::DowntimesStartTimerHandler()
+{
+ /* Start fixed downtimes. Flexible downtimes will be triggered on-demand. */
+ for (const Downtime::Ptr& downtime : ConfigType::GetObjectsByType<Downtime>()) {
+ if (downtime->IsActive() &&
+ downtime->CanBeTriggered() &&
+ downtime->GetFixed()) {
+ /* Send notifications. */
+ OnDowntimeStarted(downtime);
+
+ /* Trigger fixed downtime immediately. */
+ downtime->TriggerDowntime(std::fmax(downtime->GetStartTime(), downtime->GetEntryTime()));
+ }
+ }
+}
+
+void Downtime::DowntimesOrphanedTimerHandler()
+{
+ for (const Downtime::Ptr& downtime : ConfigType::GetObjectsByType<Downtime>()) {
+ /* Only remove downtimes which are activated after daemon start. */
+ if (downtime->IsActive() && !downtime->HasValidConfigOwner())
+ RemoveDowntime(downtime->GetName(), false, false, true);
+ }
+}
+
+void Downtime::ValidateStartTime(const Lazy<Timestamp>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Downtime>::ValidateStartTime(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "start_time" }, "Start time must be greater than 0."));
+}
+
+void Downtime::ValidateEndTime(const Lazy<Timestamp>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Downtime>::ValidateEndTime(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "end_time" }, "End time must be greater than 0."));
+}
+
+DowntimeChildOptions Downtime::ChildOptionsFromValue(const Value& options)
+{
+ if (options == "DowntimeNoChildren")
+ return DowntimeNoChildren;
+ else if (options == "DowntimeTriggeredChildren")
+ return DowntimeTriggeredChildren;
+ else if (options == "DowntimeNonTriggeredChildren")
+ return DowntimeNonTriggeredChildren;
+ else if (options.IsNumber()) {
+ int number = options;
+ if (number >= 0 && number <= 2)
+ return static_cast<DowntimeChildOptions>(number);
+ }
+
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid child option specified"));
+}
diff --git a/lib/icinga/downtime.hpp b/lib/icinga/downtime.hpp
new file mode 100644
index 0000000..15aa0af
--- /dev/null
+++ b/lib/icinga/downtime.hpp
@@ -0,0 +1,99 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DOWNTIME_H
+#define DOWNTIME_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/downtime-ti.hpp"
+#include "icinga/checkable-ti.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+enum DowntimeChildOptions
+{
+ DowntimeNoChildren,
+ DowntimeTriggeredChildren,
+ DowntimeNonTriggeredChildren
+};
+
+/**
+ * A downtime.
+ *
+ * @ingroup icinga
+ */
+class Downtime final : public ObjectImpl<Downtime>
+{
+public:
+ DECLARE_OBJECT(Downtime);
+ DECLARE_OBJECTNAME(Downtime);
+
+ static boost::signals2::signal<void (const Downtime::Ptr&)> OnDowntimeAdded;
+ static boost::signals2::signal<void (const Downtime::Ptr&)> OnDowntimeRemoved;
+ static boost::signals2::signal<void (const Downtime::Ptr&)> OnDowntimeStarted;
+ static boost::signals2::signal<void (const Downtime::Ptr&)> OnDowntimeTriggered;
+ static boost::signals2::signal<void (const Downtime::Ptr&, const String&, double, const MessageOrigin::Ptr&)> OnRemovalInfoChanged;
+
+ intrusive_ptr<Checkable> GetCheckable() const;
+
+ bool IsInEffect() const;
+ bool IsTriggered() const;
+ bool IsExpired() const;
+ bool HasValidConfigOwner() const;
+
+ static void StaticInitialize();
+
+ static int GetNextDowntimeID();
+
+ static Ptr AddDowntime(const intrusive_ptr<Checkable>& checkable, const String& author,
+ const String& comment, double startTime, double endTime, bool fixed,
+ const String& triggeredBy, double duration, const String& scheduledDowntime = String(),
+ const String& scheduledBy = String(), const String& parent = String(), const String& id = String(),
+ const MessageOrigin::Ptr& origin = nullptr);
+
+ static void RemoveDowntime(const String& id, bool includeChildren, bool cancelled, bool expired = false,
+ const String& removedBy = "", const MessageOrigin::Ptr& origin = nullptr);
+
+ void RegisterChild(const Downtime::Ptr& downtime);
+ void UnregisterChild(const Downtime::Ptr& downtime);
+ std::set<Downtime::Ptr> GetChildren() const;
+
+ void TriggerDowntime(double triggerTime);
+ void SetRemovalInfo(const String& removedBy, double removeTime, const MessageOrigin::Ptr& origin = nullptr);
+
+ void OnAllConfigLoaded() override;
+
+ static String GetDowntimeIDFromLegacyID(int id);
+
+ static DowntimeChildOptions ChildOptionsFromValue(const Value& options);
+
+protected:
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+ void Pause() override;
+ void Resume() override;
+
+ void ValidateStartTime(const Lazy<Timestamp>& lvalue, const ValidationUtils& utils) override;
+ void ValidateEndTime(const Lazy<Timestamp>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ ObjectImpl<Checkable>::Ptr m_Checkable;
+
+ std::set<Downtime::Ptr> m_Children;
+ mutable std::mutex m_ChildrenMutex;
+
+ Timer::Ptr m_CleanupTimer;
+
+ bool CanBeTriggered();
+
+ void SetupCleanupTimer();
+
+ static void DowntimesStartTimerHandler();
+ static void DowntimesOrphanedTimerHandler();
+};
+
+}
+
+#endif /* DOWNTIME_H */
diff --git a/lib/icinga/downtime.ti b/lib/icinga/downtime.ti
new file mode 100644
index 0000000..21e9731
--- /dev/null
+++ b/lib/icinga/downtime.ti
@@ -0,0 +1,82 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/utility.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class DowntimeNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Downtime : ConfigObject < DowntimeNameComposer
+{
+ activation_priority -10;
+
+ load_after Host;
+ load_after Service;
+
+ [config, no_user_modify, required, navigation(host)] name(Host) host_name {
+ navigate {{{
+ return Host::GetByName(GetHostName());
+ }}}
+ };
+ [config, no_user_modify, navigation(service)] String service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+ return host->GetServiceByShortName(GetServiceName());
+ }}}
+ };
+
+ [config] Timestamp entry_time {
+ default {{{ return Utility::GetTime(); }}}
+ };
+ [config, required] String author;
+ [config, required] String comment;
+ [config] Timestamp start_time;
+ [config] Timestamp end_time;
+ [state] Timestamp trigger_time;
+ [config] bool fixed;
+ [config] Timestamp duration;
+ [config] String triggered_by;
+ [config] String scheduled_by;
+ [config] String parent;
+ [state] Array::Ptr triggers {
+ default {{{ return new Array(); }}}
+ };
+ [state] int legacy_id;
+ [state] Timestamp remove_time;
+ [no_storage] bool was_cancelled {
+ get {{{ return GetRemoveTime() > 0; }}}
+ };
+ [config] String config_owner;
+ [config] String config_owner_hash;
+ [config] String authoritative_zone;
+
+ [no_user_view, no_user_modify] String removed_by;
+};
+
+}
diff --git a/lib/icinga/envresolver.cpp b/lib/icinga/envresolver.cpp
new file mode 100644
index 0000000..633255c
--- /dev/null
+++ b/lib/icinga/envresolver.cpp
@@ -0,0 +1,20 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#include "base/string.hpp"
+#include "base/value.hpp"
+#include "icinga/envresolver.hpp"
+#include "icinga/checkresult.hpp"
+#include <cstdlib>
+
+using namespace icinga;
+
+bool EnvResolver::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const
+{
+ auto value (getenv(macro.CStr()));
+
+ if (value) {
+ *result = value;
+ }
+
+ return value;
+}
diff --git a/lib/icinga/envresolver.hpp b/lib/icinga/envresolver.hpp
new file mode 100644
index 0000000..b3f0076
--- /dev/null
+++ b/lib/icinga/envresolver.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#ifndef ENVRESOLVER_H
+#define ENVRESOLVER_H
+
+#include "base/object.hpp"
+#include "base/string.hpp"
+#include "base/value.hpp"
+#include "icinga/macroresolver.hpp"
+#include "icinga/checkresult.hpp"
+
+namespace icinga
+{
+
+/**
+ * Resolves env var names.
+ *
+ * @ingroup icinga
+ */
+class EnvResolver final : public Object, public MacroResolver
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EnvResolver);
+
+ bool ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const override;
+};
+
+}
+
+#endif /* ENVRESOLVER_H */
diff --git a/lib/icinga/eventcommand.cpp b/lib/icinga/eventcommand.cpp
new file mode 100644
index 0000000..39f2d31
--- /dev/null
+++ b/lib/icinga/eventcommand.cpp
@@ -0,0 +1,20 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/eventcommand.hpp"
+#include "icinga/eventcommand-ti.cpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(EventCommand);
+
+thread_local EventCommand::Ptr EventCommand::ExecuteOverride;
+
+void EventCommand::Execute(const Checkable::Ptr& checkable,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ GetExecute()->Invoke({
+ checkable,
+ resolvedMacros,
+ useResolvedMacros
+ });
+}
diff --git a/lib/icinga/eventcommand.hpp b/lib/icinga/eventcommand.hpp
new file mode 100644
index 0000000..67997e6
--- /dev/null
+++ b/lib/icinga/eventcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EVENTCOMMAND_H
+#define EVENTCOMMAND_H
+
+#include "icinga/eventcommand-ti.hpp"
+#include "icinga/checkable.hpp"
+
+namespace icinga
+{
+
+/**
+ * An event handler command.
+ *
+ * @ingroup icinga
+ */
+class EventCommand final : public ObjectImpl<EventCommand>
+{
+public:
+ DECLARE_OBJECT(EventCommand);
+ DECLARE_OBJECTNAME(EventCommand);
+
+ static thread_local EventCommand::Ptr ExecuteOverride;
+
+ void Execute(const Checkable::Ptr& checkable,
+ const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false);
+};
+
+}
+
+#endif /* EVENTCOMMAND_H */
diff --git a/lib/icinga/eventcommand.ti b/lib/icinga/eventcommand.ti
new file mode 100644
index 0000000..a166d1e
--- /dev/null
+++ b/lib/icinga/eventcommand.ti
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/command.hpp"
+
+
+library icinga;
+
+namespace icinga
+{
+
+class EventCommand : Command
+{
+};
+
+}
diff --git a/lib/icinga/externalcommandprocessor.cpp b/lib/icinga/externalcommandprocessor.cpp
new file mode 100644
index 0000000..9850da0
--- /dev/null
+++ b/lib/icinga/externalcommandprocessor.cpp
@@ -0,0 +1,2281 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/externalcommandprocessor.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/user.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/compatutility.hpp"
+#include "remote/apifunction.hpp"
+#include "base/convert.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include <fstream>
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+boost::signals2::signal<void(double, const String&, const std::vector<String>&)> ExternalCommandProcessor::OnNewExternalCommand;
+
+void ExternalCommandProcessor::Execute(const String& line)
+{
+ if (line.IsEmpty())
+ return;
+
+ if (line[0] != '[')
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing timestamp in command: " + line));
+
+ size_t pos = line.FindFirstOf("]");
+
+ if (pos == String::NPos)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing timestamp in command: " + line));
+
+ String timestamp = line.SubStr(1, pos - 1);
+ String args = line.SubStr(pos + 2, String::NPos);
+
+ double ts = Convert::ToDouble(timestamp);
+
+ if (ts == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid timestamp in command: " + line));
+
+ std::vector<String> argv = args.Split(";");
+
+ if (argv.empty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing arguments in command: " + line));
+
+ std::vector<String> argvExtra(argv.begin() + 1, argv.end());
+ Execute(ts, argv[0], argvExtra);
+}
+
+void ExternalCommandProcessor::Execute(double time, const String& command, const std::vector<String>& arguments)
+{
+ ExternalCommandInfo eci;
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, []() {
+ RegisterCommands();
+ });
+
+ {
+ std::unique_lock<std::mutex> lock(GetMutex());
+
+ auto it = GetCommands().find(command);
+
+ if (it == GetCommands().end())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The external command '" + command + "' does not exist."));
+
+ eci = it->second;
+ }
+
+ if (arguments.size() < eci.MinArgs)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Expected " + Convert::ToString(eci.MinArgs) + " arguments"));
+
+ size_t argnum = std::min(arguments.size(), eci.MaxArgs);
+
+ std::vector<String> realArguments;
+ realArguments.resize(argnum);
+
+ if (argnum > 0) {
+ std::copy(arguments.begin(), arguments.begin() + argnum - 1, realArguments.begin());
+
+ String last_argument;
+ for (std::vector<String>::size_type i = argnum - 1; i < arguments.size(); i++) {
+ if (!last_argument.IsEmpty())
+ last_argument += ";";
+
+ last_argument += arguments[i];
+ }
+
+ realArguments[argnum - 1] = last_argument;
+ }
+
+ OnNewExternalCommand(time, command, realArguments);
+
+ eci.Callback(time, realArguments);
+}
+
+void ExternalCommandProcessor::RegisterCommand(const String& command, const ExternalCommandCallback& callback, size_t minArgs, size_t maxArgs)
+{
+ std::unique_lock<std::mutex> lock(GetMutex());
+ ExternalCommandInfo eci;
+ eci.Callback = callback;
+ eci.MinArgs = minArgs;
+ eci.MaxArgs = (maxArgs == UINT_MAX) ? minArgs : maxArgs;
+ GetCommands()[command] = eci;
+}
+
+void ExternalCommandProcessor::RegisterCommands()
+{
+ RegisterCommand("PROCESS_HOST_CHECK_RESULT", &ExternalCommandProcessor::ProcessHostCheckResult, 3);
+ RegisterCommand("PROCESS_SERVICE_CHECK_RESULT", &ExternalCommandProcessor::ProcessServiceCheckResult, 4);
+ RegisterCommand("SCHEDULE_HOST_CHECK", &ExternalCommandProcessor::ScheduleHostCheck, 2);
+ RegisterCommand("SCHEDULE_FORCED_HOST_CHECK", &ExternalCommandProcessor::ScheduleForcedHostCheck, 2);
+ RegisterCommand("SCHEDULE_SVC_CHECK", &ExternalCommandProcessor::ScheduleSvcCheck, 3);
+ RegisterCommand("SCHEDULE_FORCED_SVC_CHECK", &ExternalCommandProcessor::ScheduleForcedSvcCheck, 3);
+ RegisterCommand("ENABLE_HOST_CHECK", &ExternalCommandProcessor::EnableHostCheck, 1);
+ RegisterCommand("DISABLE_HOST_CHECK", &ExternalCommandProcessor::DisableHostCheck, 1);
+ RegisterCommand("ENABLE_SVC_CHECK", &ExternalCommandProcessor::EnableSvcCheck, 2);
+ RegisterCommand("DISABLE_SVC_CHECK", &ExternalCommandProcessor::DisableSvcCheck, 2);
+ RegisterCommand("SHUTDOWN_PROCESS", &ExternalCommandProcessor::ShutdownProcess);
+ RegisterCommand("RESTART_PROCESS", &ExternalCommandProcessor::RestartProcess);
+ RegisterCommand("SCHEDULE_FORCED_HOST_SVC_CHECKS", &ExternalCommandProcessor::ScheduleForcedHostSvcChecks, 2);
+ RegisterCommand("SCHEDULE_HOST_SVC_CHECKS", &ExternalCommandProcessor::ScheduleHostSvcChecks, 2);
+ RegisterCommand("ENABLE_HOST_SVC_CHECKS", &ExternalCommandProcessor::EnableHostSvcChecks, 1);
+ RegisterCommand("DISABLE_HOST_SVC_CHECKS", &ExternalCommandProcessor::DisableHostSvcChecks, 1);
+ RegisterCommand("ACKNOWLEDGE_SVC_PROBLEM", &ExternalCommandProcessor::AcknowledgeSvcProblem, 7);
+ RegisterCommand("ACKNOWLEDGE_SVC_PROBLEM_EXPIRE", &ExternalCommandProcessor::AcknowledgeSvcProblemExpire, 8);
+ RegisterCommand("REMOVE_SVC_ACKNOWLEDGEMENT", &ExternalCommandProcessor::RemoveSvcAcknowledgement, 2);
+ RegisterCommand("ACKNOWLEDGE_HOST_PROBLEM", &ExternalCommandProcessor::AcknowledgeHostProblem, 6);
+ RegisterCommand("ACKNOWLEDGE_HOST_PROBLEM_EXPIRE", &ExternalCommandProcessor::AcknowledgeHostProblemExpire, 7);
+ RegisterCommand("REMOVE_HOST_ACKNOWLEDGEMENT", &ExternalCommandProcessor::RemoveHostAcknowledgement, 1);
+ RegisterCommand("DISABLE_HOST_FLAP_DETECTION", &ExternalCommandProcessor::DisableHostFlapping, 1);
+ RegisterCommand("ENABLE_HOST_FLAP_DETECTION", &ExternalCommandProcessor::EnableHostFlapping, 1);
+ RegisterCommand("DISABLE_SVC_FLAP_DETECTION", &ExternalCommandProcessor::DisableSvcFlapping, 2);
+ RegisterCommand("ENABLE_SVC_FLAP_DETECTION", &ExternalCommandProcessor::EnableSvcFlapping, 2);
+ RegisterCommand("ENABLE_HOSTGROUP_SVC_CHECKS", &ExternalCommandProcessor::EnableHostgroupSvcChecks, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_SVC_CHECKS", &ExternalCommandProcessor::DisableHostgroupSvcChecks, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_SVC_CHECKS", &ExternalCommandProcessor::EnableServicegroupSvcChecks, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_SVC_CHECKS", &ExternalCommandProcessor::DisableServicegroupSvcChecks, 1);
+ RegisterCommand("ENABLE_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::EnablePassiveHostChecks, 1);
+ RegisterCommand("DISABLE_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::DisablePassiveHostChecks, 1);
+ RegisterCommand("ENABLE_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::EnablePassiveSvcChecks, 2);
+ RegisterCommand("DISABLE_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::DisablePassiveSvcChecks, 2);
+ RegisterCommand("ENABLE_SERVICEGROUP_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::EnableServicegroupPassiveSvcChecks, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::DisableServicegroupPassiveSvcChecks, 1);
+ RegisterCommand("ENABLE_HOSTGROUP_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::EnableHostgroupPassiveSvcChecks, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_PASSIVE_SVC_CHECKS", &ExternalCommandProcessor::DisableHostgroupPassiveSvcChecks, 1);
+ RegisterCommand("PROCESS_FILE", &ExternalCommandProcessor::ProcessFile, 2);
+ RegisterCommand("SCHEDULE_SVC_DOWNTIME", &ExternalCommandProcessor::ScheduleSvcDowntime, 9);
+ RegisterCommand("DEL_SVC_DOWNTIME", &ExternalCommandProcessor::DelSvcDowntime, 1);
+ RegisterCommand("SCHEDULE_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleHostDowntime, 8);
+ RegisterCommand("SCHEDULE_AND_PROPAGATE_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleAndPropagateHostDowntime, 8);
+ RegisterCommand("SCHEDULE_AND_PROPAGATE_TRIGGERED_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleAndPropagateTriggeredHostDowntime, 8);
+ RegisterCommand("DEL_HOST_DOWNTIME", &ExternalCommandProcessor::DelHostDowntime, 1);
+ RegisterCommand("DEL_DOWNTIME_BY_HOST_NAME", &ExternalCommandProcessor::DelDowntimeByHostName, 1, 4);
+ RegisterCommand("SCHEDULE_HOST_SVC_DOWNTIME", &ExternalCommandProcessor::ScheduleHostSvcDowntime, 8);
+ RegisterCommand("SCHEDULE_HOSTGROUP_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleHostgroupHostDowntime, 8);
+ RegisterCommand("SCHEDULE_HOSTGROUP_SVC_DOWNTIME", &ExternalCommandProcessor::ScheduleHostgroupSvcDowntime, 8);
+ RegisterCommand("SCHEDULE_SERVICEGROUP_HOST_DOWNTIME", &ExternalCommandProcessor::ScheduleServicegroupHostDowntime, 8);
+ RegisterCommand("SCHEDULE_SERVICEGROUP_SVC_DOWNTIME", &ExternalCommandProcessor::ScheduleServicegroupSvcDowntime, 8);
+ RegisterCommand("ADD_HOST_COMMENT", &ExternalCommandProcessor::AddHostComment, 4);
+ RegisterCommand("DEL_HOST_COMMENT", &ExternalCommandProcessor::DelHostComment, 1);
+ RegisterCommand("ADD_SVC_COMMENT", &ExternalCommandProcessor::AddSvcComment, 5);
+ RegisterCommand("DEL_SVC_COMMENT", &ExternalCommandProcessor::DelSvcComment, 1);
+ RegisterCommand("DEL_ALL_HOST_COMMENTS", &ExternalCommandProcessor::DelAllHostComments, 1);
+ RegisterCommand("DEL_ALL_SVC_COMMENTS", &ExternalCommandProcessor::DelAllSvcComments, 2);
+ RegisterCommand("SEND_CUSTOM_HOST_NOTIFICATION", &ExternalCommandProcessor::SendCustomHostNotification, 4);
+ RegisterCommand("SEND_CUSTOM_SVC_NOTIFICATION", &ExternalCommandProcessor::SendCustomSvcNotification, 5);
+ RegisterCommand("DELAY_HOST_NOTIFICATION", &ExternalCommandProcessor::DelayHostNotification, 2);
+ RegisterCommand("DELAY_SVC_NOTIFICATION", &ExternalCommandProcessor::DelaySvcNotification, 3);
+ RegisterCommand("ENABLE_HOST_NOTIFICATIONS", &ExternalCommandProcessor::EnableHostNotifications, 1);
+ RegisterCommand("DISABLE_HOST_NOTIFICATIONS", &ExternalCommandProcessor::DisableHostNotifications, 1);
+ RegisterCommand("ENABLE_SVC_NOTIFICATIONS", &ExternalCommandProcessor::EnableSvcNotifications, 2);
+ RegisterCommand("DISABLE_SVC_NOTIFICATIONS", &ExternalCommandProcessor::DisableSvcNotifications, 2);
+ RegisterCommand("ENABLE_HOST_SVC_NOTIFICATIONS", &ExternalCommandProcessor::EnableHostSvcNotifications, 1);
+ RegisterCommand("DISABLE_HOST_SVC_NOTIFICATIONS", &ExternalCommandProcessor::DisableHostSvcNotifications, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_HOST_CHECKS", &ExternalCommandProcessor::DisableHostgroupHostChecks, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::DisableHostgroupPassiveHostChecks, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_HOST_CHECKS", &ExternalCommandProcessor::DisableServicegroupHostChecks, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::DisableServicegroupPassiveHostChecks, 1);
+ RegisterCommand("ENABLE_HOSTGROUP_HOST_CHECKS", &ExternalCommandProcessor::EnableHostgroupHostChecks, 1);
+ RegisterCommand("ENABLE_HOSTGROUP_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::EnableHostgroupPassiveHostChecks, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_HOST_CHECKS", &ExternalCommandProcessor::EnableServicegroupHostChecks, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_PASSIVE_HOST_CHECKS", &ExternalCommandProcessor::EnableServicegroupPassiveHostChecks, 1);
+ RegisterCommand("ENABLE_NOTIFICATIONS", &ExternalCommandProcessor::EnableNotifications);
+ RegisterCommand("DISABLE_NOTIFICATIONS", &ExternalCommandProcessor::DisableNotifications);
+ RegisterCommand("ENABLE_FLAP_DETECTION", &ExternalCommandProcessor::EnableFlapDetection);
+ RegisterCommand("DISABLE_FLAP_DETECTION", &ExternalCommandProcessor::DisableFlapDetection);
+ RegisterCommand("ENABLE_EVENT_HANDLERS", &ExternalCommandProcessor::EnableEventHandlers);
+ RegisterCommand("DISABLE_EVENT_HANDLERS", &ExternalCommandProcessor::DisableEventHandlers);
+ RegisterCommand("ENABLE_PERFORMANCE_DATA", &ExternalCommandProcessor::EnablePerformanceData);
+ RegisterCommand("DISABLE_PERFORMANCE_DATA", &ExternalCommandProcessor::DisablePerformanceData);
+ RegisterCommand("START_EXECUTING_SVC_CHECKS", &ExternalCommandProcessor::StartExecutingSvcChecks);
+ RegisterCommand("STOP_EXECUTING_SVC_CHECKS", &ExternalCommandProcessor::StopExecutingSvcChecks);
+ RegisterCommand("START_EXECUTING_HOST_CHECKS", &ExternalCommandProcessor::StartExecutingHostChecks);
+ RegisterCommand("STOP_EXECUTING_HOST_CHECKS", &ExternalCommandProcessor::StopExecutingHostChecks);
+ RegisterCommand("CHANGE_NORMAL_SVC_CHECK_INTERVAL", &ExternalCommandProcessor::ChangeNormalSvcCheckInterval, 3);
+ RegisterCommand("CHANGE_NORMAL_HOST_CHECK_INTERVAL", &ExternalCommandProcessor::ChangeNormalHostCheckInterval, 2);
+ RegisterCommand("CHANGE_RETRY_SVC_CHECK_INTERVAL", &ExternalCommandProcessor::ChangeRetrySvcCheckInterval, 3);
+ RegisterCommand("CHANGE_RETRY_HOST_CHECK_INTERVAL", &ExternalCommandProcessor::ChangeRetryHostCheckInterval, 2);
+ RegisterCommand("ENABLE_HOST_EVENT_HANDLER", &ExternalCommandProcessor::EnableHostEventHandler, 1);
+ RegisterCommand("DISABLE_HOST_EVENT_HANDLER", &ExternalCommandProcessor::DisableHostEventHandler, 1);
+ RegisterCommand("ENABLE_SVC_EVENT_HANDLER", &ExternalCommandProcessor::EnableSvcEventHandler, 2);
+ RegisterCommand("DISABLE_SVC_EVENT_HANDLER", &ExternalCommandProcessor::DisableSvcEventHandler, 2);
+ RegisterCommand("CHANGE_HOST_EVENT_HANDLER", &ExternalCommandProcessor::ChangeHostEventHandler, 2);
+ RegisterCommand("CHANGE_SVC_EVENT_HANDLER", &ExternalCommandProcessor::ChangeSvcEventHandler, 3);
+ RegisterCommand("CHANGE_HOST_CHECK_COMMAND", &ExternalCommandProcessor::ChangeHostCheckCommand, 2);
+ RegisterCommand("CHANGE_SVC_CHECK_COMMAND", &ExternalCommandProcessor::ChangeSvcCheckCommand, 3);
+ RegisterCommand("CHANGE_MAX_HOST_CHECK_ATTEMPTS", &ExternalCommandProcessor::ChangeMaxHostCheckAttempts, 2);
+ RegisterCommand("CHANGE_MAX_SVC_CHECK_ATTEMPTS", &ExternalCommandProcessor::ChangeMaxSvcCheckAttempts, 3);
+ RegisterCommand("CHANGE_HOST_CHECK_TIMEPERIOD", &ExternalCommandProcessor::ChangeHostCheckTimeperiod, 2);
+ RegisterCommand("CHANGE_SVC_CHECK_TIMEPERIOD", &ExternalCommandProcessor::ChangeSvcCheckTimeperiod, 3);
+ RegisterCommand("CHANGE_CUSTOM_HOST_VAR", &ExternalCommandProcessor::ChangeCustomHostVar, 3);
+ RegisterCommand("CHANGE_CUSTOM_SVC_VAR", &ExternalCommandProcessor::ChangeCustomSvcVar, 4);
+ RegisterCommand("CHANGE_CUSTOM_USER_VAR", &ExternalCommandProcessor::ChangeCustomUserVar, 3);
+ RegisterCommand("CHANGE_CUSTOM_CHECKCOMMAND_VAR", &ExternalCommandProcessor::ChangeCustomCheckcommandVar, 3);
+ RegisterCommand("CHANGE_CUSTOM_EVENTCOMMAND_VAR", &ExternalCommandProcessor::ChangeCustomEventcommandVar, 3);
+ RegisterCommand("CHANGE_CUSTOM_NOTIFICATIONCOMMAND_VAR", &ExternalCommandProcessor::ChangeCustomNotificationcommandVar, 3);
+
+ RegisterCommand("ENABLE_HOSTGROUP_HOST_NOTIFICATIONS", &ExternalCommandProcessor::EnableHostgroupHostNotifications, 1);
+ RegisterCommand("ENABLE_HOSTGROUP_SVC_NOTIFICATIONS", &ExternalCommandProcessor::EnableHostgroupSvcNotifications, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_HOST_NOTIFICATIONS", &ExternalCommandProcessor::DisableHostgroupHostNotifications, 1);
+ RegisterCommand("DISABLE_HOSTGROUP_SVC_NOTIFICATIONS", &ExternalCommandProcessor::DisableHostgroupSvcNotifications, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_HOST_NOTIFICATIONS", &ExternalCommandProcessor::EnableServicegroupHostNotifications, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_HOST_NOTIFICATIONS", &ExternalCommandProcessor::DisableServicegroupHostNotifications, 1);
+ RegisterCommand("ENABLE_SERVICEGROUP_SVC_NOTIFICATIONS", &ExternalCommandProcessor::EnableServicegroupSvcNotifications, 1);
+ RegisterCommand("DISABLE_SERVICEGROUP_SVC_NOTIFICATIONS", &ExternalCommandProcessor::DisableServicegroupSvcNotifications, 1);
+}
+
+void ExternalCommandProcessor::ExecuteFromFile(const String& line, std::deque< std::vector<String> >& file_queue)
+{
+ if (line.IsEmpty())
+ return;
+
+ if (line[0] != '[')
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing timestamp in command: " + line));
+
+ size_t pos = line.FindFirstOf("]");
+
+ if (pos == String::NPos)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing timestamp in command: " + line));
+
+ String timestamp = line.SubStr(1, pos - 1);
+ String args = line.SubStr(pos + 2, String::NPos);
+
+ double ts = Convert::ToDouble(timestamp);
+
+ if (ts == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid timestamp in command: " + line));
+
+ std::vector<String> argv = args.Split(";");
+
+ if (argv.empty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Missing arguments in command: " + line));
+
+ std::vector<String> argvExtra(argv.begin() + 1, argv.end());
+
+ if (argv[0] == "PROCESS_FILE") {
+ Log(LogDebug, "ExternalCommandProcessor")
+ << "Enqueing external command file " << argvExtra[0];
+ file_queue.push_back(argvExtra);
+ } else {
+ Execute(ts, argv[0], argvExtra);
+ }
+}
+
+void ExternalCommandProcessor::ProcessHostCheckResult(double time, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot process passive host check result for non-existent host '" + arguments[0] + "'"));
+
+ if (!host->GetEnablePassiveChecks())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Got passive check result for host '" + arguments[0] + "' which has passive checks disabled."));
+
+ if (!host->IsReachable(DependencyCheckExecution)) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring passive check result for unreachable host '" << arguments[0] << "'";
+ return;
+ }
+
+ int exitStatus = Convert::ToDouble(arguments[1]);
+ CheckResult::Ptr result = new CheckResult();
+ std::pair<String, String> co = PluginUtility::ParseCheckOutput(arguments[2]);
+ result->SetOutput(co.first);
+ result->SetPerformanceData(PluginUtility::SplitPerfdata(co.second));
+
+ ServiceState state;
+
+ if (exitStatus == 0)
+ state = ServiceOK;
+ else if (exitStatus == 1)
+ state = ServiceCritical;
+ else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid status code: " + arguments[1]));
+
+ result->SetState(state);
+
+ result->SetScheduleStart(time);
+ result->SetScheduleEnd(time);
+ result->SetExecutionStart(time);
+ result->SetExecutionEnd(time);
+
+ /* Mark this check result as passive. */
+ result->SetActive(false);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Processing passive check result for host '" << arguments[0] << "'";
+
+ host->ProcessCheckResult(result);
+}
+
+void ExternalCommandProcessor::ProcessServiceCheckResult(double time, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot process passive service check result for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (!service->GetEnablePassiveChecks())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Got passive check result for service '" + arguments[1] + "' which has passive checks disabled."));
+
+ if (!service->IsReachable(DependencyCheckExecution)) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring passive check result for unreachable service '" << arguments[1] << "'";
+ return;
+ }
+
+ int exitStatus = Convert::ToDouble(arguments[2]);
+ CheckResult::Ptr result = new CheckResult();
+ String output = CompatUtility::UnEscapeString(arguments[3]);
+ std::pair<String, String> co = PluginUtility::ParseCheckOutput(output);
+ result->SetOutput(co.first);
+ result->SetPerformanceData(PluginUtility::SplitPerfdata(co.second));
+ result->SetState(PluginUtility::ExitStatusToState(exitStatus));
+
+ result->SetScheduleStart(time);
+ result->SetScheduleEnd(time);
+ result->SetExecutionStart(time);
+ result->SetExecutionEnd(time);
+
+ /* Mark this check result as passive. */
+ result->SetActive(false);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Processing passive check result for service '" << arguments[1] << "'";
+
+ service->ProcessCheckResult(result);
+}
+
+void ExternalCommandProcessor::ScheduleHostCheck(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule host check for non-existent host '" + arguments[0] + "'"));
+
+ double planned_check = Convert::ToDouble(arguments[1]);
+
+ if (planned_check > host->GetNextCheck()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring reschedule request for host '"
+ << arguments[0] << "' (next check is already sooner than requested check time)";
+ return;
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for host '" << arguments[0] << "'";
+
+ if (planned_check < Utility::GetTime())
+ planned_check = Utility::GetTime();
+
+ host->SetNextCheck(planned_check);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(host);
+}
+
+void ExternalCommandProcessor::ScheduleForcedHostCheck(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule forced host check for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for host '" << arguments[0] << "'";
+
+ host->SetForceNextCheck(true);
+ host->SetNextCheck(Convert::ToDouble(arguments[1]));
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(host);
+}
+
+void ExternalCommandProcessor::ScheduleSvcCheck(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule service check for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ double planned_check = Convert::ToDouble(arguments[2]);
+
+ if (planned_check > service->GetNextCheck()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring reschedule request for service '"
+ << arguments[1] << "' (next check is already sooner than requested check time)";
+ return;
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for service '" << arguments[1] << "'";
+
+ if (planned_check < Utility::GetTime())
+ planned_check = Utility::GetTime();
+
+ service->SetNextCheck(planned_check);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(service);
+}
+
+void ExternalCommandProcessor::ScheduleForcedSvcCheck(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule forced service check for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for service '" << arguments[1] << "'";
+
+ service->SetForceNextCheck(true);
+ service->SetNextCheck(Convert::ToDouble(arguments[2]));
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(service);
+}
+
+void ExternalCommandProcessor::EnableHostCheck(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host checks for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_active_checks", true);
+}
+
+void ExternalCommandProcessor::DisableHostCheck(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host check non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_active_checks", false);
+}
+
+void ExternalCommandProcessor::EnableSvcCheck(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service check for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_active_checks", true);
+}
+
+void ExternalCommandProcessor::DisableSvcCheck(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service check for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_active_checks", false);
+}
+
+void ExternalCommandProcessor::ShutdownProcess(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Shutting down Icinga via external command.");
+ Application::RequestShutdown();
+}
+
+void ExternalCommandProcessor::RestartProcess(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Restarting Icinga via external command.");
+ Application::RequestRestart();
+}
+
+void ExternalCommandProcessor::ScheduleForcedHostSvcChecks(double, const std::vector<String>& arguments)
+{
+ double planned_check = Convert::ToDouble(arguments[1]);
+
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule forced host service checks for non-existent host '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for service '" << service->GetName() << "'";
+
+ service->SetNextCheck(planned_check);
+ service->SetForceNextCheck(true);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(service);
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostSvcChecks(double, const std::vector<String>& arguments)
+{
+ double planned_check = Convert::ToDouble(arguments[1]);
+
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot reschedule host service checks for non-existent host '" + arguments[0] + "'"));
+
+ if (planned_check < Utility::GetTime())
+ planned_check = Utility::GetTime();
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (planned_check > service->GetNextCheck()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Ignoring reschedule request for service '"
+ << service->GetName() << "' (next check is already sooner than requested check time)";
+ continue;
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Rescheduling next check for service '" << service->GetName() << "'";
+
+ service->SetNextCheck(planned_check);
+
+ /* trigger update event for DB IDO */
+ Checkable::OnNextCheckUpdated(service);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostSvcChecks(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host service checks for non-existent host '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostSvcChecks(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host service checks for non-existent host '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::AcknowledgeSvcProblem(double, const std::vector<String>& arguments)
+{
+ bool sticky = (Convert::ToLong(arguments[2]) == 2 ? true : false);
+ bool notify = (Convert::ToLong(arguments[3]) > 0 ? true : false);
+ bool persistent = (Convert::ToLong(arguments[4]) > 0 ? true : false);
+
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+ ObjectLock oLock (service);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot acknowledge service problem for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (service->GetState() == ServiceOK)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The service '" + arguments[1] + "' is OK."));
+
+ if (service->IsAcknowledged()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The service '" + arguments[1] + "' is already acknowledged."));
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Setting acknowledgement for service '" << service->GetName() << "'" << (notify ? "" : ". Disabled notification");
+
+ Comment::AddComment(service, CommentAcknowledgement, arguments[5], arguments[6], persistent, 0, sticky);
+ service->AcknowledgeProblem(arguments[5], arguments[6], sticky ? AcknowledgementSticky : AcknowledgementNormal, notify, persistent);
+}
+
+void ExternalCommandProcessor::AcknowledgeSvcProblemExpire(double, const std::vector<String>& arguments)
+{
+ bool sticky = (Convert::ToLong(arguments[2]) == 2 ? true : false);
+ bool notify = (Convert::ToLong(arguments[3]) > 0 ? true : false);
+ bool persistent = (Convert::ToLong(arguments[4]) > 0 ? true : false);
+ double timestamp = Convert::ToDouble(arguments[5]);
+
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+ ObjectLock oLock (service);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot acknowledge service problem with expire time for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (service->GetState() == ServiceOK)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The service '" + arguments[1] + "' is OK."));
+
+ if (timestamp != 0 && timestamp <= Utility::GetTime())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Acknowledgement expire time must be in the future for service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (service->IsAcknowledged()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The service '" + arguments[1] + "' is already acknowledged."));
+ }
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Setting timed acknowledgement for service '" << service->GetName() << "'" << (notify ? "" : ". Disabled notification");
+
+ Comment::AddComment(service, CommentAcknowledgement, arguments[6], arguments[7], persistent, timestamp, sticky);
+ service->AcknowledgeProblem(arguments[6], arguments[7], sticky ? AcknowledgementSticky : AcknowledgementNormal, notify, persistent, timestamp);
+}
+
+void ExternalCommandProcessor::RemoveSvcAcknowledgement(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot remove service acknowledgement for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing acknowledgement for service '" << service->GetName() << "'";
+
+ {
+ ObjectLock olock(service);
+ service->ClearAcknowledgement("");
+ }
+
+ service->RemoveAckComments();
+}
+
+void ExternalCommandProcessor::AcknowledgeHostProblem(double, const std::vector<String>& arguments)
+{
+ bool sticky = (Convert::ToLong(arguments[1]) == 2 ? true : false);
+ bool notify = (Convert::ToLong(arguments[2]) > 0 ? true : false);
+ bool persistent = (Convert::ToLong(arguments[3]) > 0 ? true : false);
+
+ Host::Ptr host = Host::GetByName(arguments[0]);
+ ObjectLock oLock (host);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot acknowledge host problem for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Setting acknowledgement for host '" << host->GetName() << "'" << (notify ? "" : ". Disabled notification");
+
+ if (host->GetState() == HostUp)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The host '" + arguments[0] + "' is OK."));
+
+ if (host->IsAcknowledged()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The host '" + arguments[1] + "' is already acknowledged."));
+ }
+
+ Comment::AddComment(host, CommentAcknowledgement, arguments[4], arguments[5], persistent, 0, sticky);
+ host->AcknowledgeProblem(arguments[4], arguments[5], sticky ? AcknowledgementSticky : AcknowledgementNormal, notify, persistent);
+}
+
+void ExternalCommandProcessor::AcknowledgeHostProblemExpire(double, const std::vector<String>& arguments)
+{
+ bool sticky = (Convert::ToLong(arguments[1]) == 2 ? true : false);
+ bool notify = (Convert::ToLong(arguments[2]) > 0 ? true : false);
+ bool persistent = (Convert::ToLong(arguments[3]) > 0 ? true : false);
+ double timestamp = Convert::ToDouble(arguments[4]);
+
+ Host::Ptr host = Host::GetByName(arguments[0]);
+ ObjectLock oLock (host);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot acknowledge host problem with expire time for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Setting timed acknowledgement for host '" << host->GetName() << "'" << (notify ? "" : ". Disabled notification");
+
+ if (host->GetState() == HostUp)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The host '" + arguments[0] + "' is OK."));
+
+ if (timestamp != 0 && timestamp <= Utility::GetTime())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Acknowledgement expire time must be in the future for host '" + arguments[0] + "'"));
+
+ if (host->IsAcknowledged()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("The host '" + arguments[1] + "' is already acknowledged."));
+ }
+
+ Comment::AddComment(host, CommentAcknowledgement, arguments[5], arguments[6], persistent, timestamp, sticky);
+ host->AcknowledgeProblem(arguments[5], arguments[6], sticky ? AcknowledgementSticky : AcknowledgementNormal, notify, persistent, timestamp);
+}
+
+void ExternalCommandProcessor::RemoveHostAcknowledgement(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot remove acknowledgement for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing acknowledgement for host '" << host->GetName() << "'";
+
+ {
+ ObjectLock olock(host);
+ host->ClearAcknowledgement("");
+ }
+ host->RemoveAckComments();
+}
+
+void ExternalCommandProcessor::EnableHostgroupSvcChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable hostgroup service checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", true);
+ }
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupSvcChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable hostgroup service checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", false);
+ }
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupSvcChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable servicegroup service checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupSvcChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable servicegroup service checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_active_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::EnablePassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable passive host checks for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_passive_checks", true);
+}
+
+void ExternalCommandProcessor::DisablePassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable passive host checks for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_passive_checks", false);
+}
+
+void ExternalCommandProcessor::EnablePassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service checks for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_passive_checks", true);
+}
+
+void ExternalCommandProcessor::DisablePassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service checks for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_passive_checks", false);
+}
+
+void ExternalCommandProcessor::EnableServicegroupPassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable servicegroup passive service checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_passive_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupPassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable servicegroup passive service checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_passive_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostgroupPassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable hostgroup passive service checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_passive_checks", true);
+ }
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupPassiveSvcChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable hostgroup passive service checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_passive_checks", false);
+ }
+ }
+}
+
+void ExternalCommandProcessor::ProcessFile(double, const std::vector<String>& arguments)
+{
+ std::deque< std::vector<String> > file_queue;
+ file_queue.push_back(arguments);
+
+ while (!file_queue.empty()) {
+ std::vector<String> argument = file_queue.front();
+ file_queue.pop_front();
+
+ String file = argument[0];
+ int to_delete = Convert::ToLong(argument[1]);
+
+ std::ifstream ifp;
+ ifp.exceptions(std::ifstream::badbit);
+
+ ifp.open(file.CStr(), std::ifstream::in);
+
+ while (ifp.good()) {
+ std::string line;
+ std::getline(ifp, line);
+
+ try {
+ Log(LogNotice, "compat")
+ << "Executing external command: " << line;
+
+ ExecuteFromFile(line, file_queue);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ExternalCommandProcessor")
+ << "External command failed: " << DiagnosticInformation(ex);
+ }
+ }
+
+ ifp.close();
+
+ if (to_delete > 0)
+ (void) unlink(file.CStr());
+ }
+}
+
+void ExternalCommandProcessor::ScheduleSvcDowntime(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule service downtime for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[5]);
+ int is_fixed = Convert::ToLong(arguments[4]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for service " << service->GetName();
+ (void) Downtime::AddDowntime(service, arguments[7], arguments[8],
+ Convert::ToDouble(arguments[2]), Convert::ToDouble(arguments[3]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[6]));
+}
+
+void ExternalCommandProcessor::DelSvcDowntime(double, const std::vector<String>& arguments)
+{
+ int id = Convert::ToLong(arguments[0]);
+ String rid = Downtime::GetDowntimeIDFromLegacyID(id);
+
+ try {
+ Downtime::RemoveDowntime(rid, false, true);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removed downtime ID " << arguments[0];
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ExternalCommandProcessor") << error.what();
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostDowntime(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule host downtime for non-existent host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+}
+
+void ExternalCommandProcessor::ScheduleAndPropagateHostDowntime(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule and propagate host downtime for non-existent host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+
+ /* Schedule downtime for all child hosts */
+ for (const Checkable::Ptr& child : host->GetAllChildren()) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(child);
+
+ /* ignore all service children */
+ if (service)
+ continue;
+
+ (void) Downtime::AddDowntime(child, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleAndPropagateTriggeredHostDowntime(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule and propagate triggered host downtime for non-existent host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ Downtime::Ptr parentDowntime = Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+
+ /* Schedule downtime for all child hosts and explicitely trigger them through the parent host's downtime */
+ for (const Checkable::Ptr& child : host->GetAllChildren()) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(child);
+
+ /* ignore all service children */
+ if (service)
+ continue;
+
+ (void) Downtime::AddDowntime(child, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), parentDowntime->GetName(), Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::DelHostDowntime(double, const std::vector<String>& arguments)
+{
+ int id = Convert::ToLong(arguments[0]);
+ String rid = Downtime::GetDowntimeIDFromLegacyID(id);
+
+ try {
+ Downtime::RemoveDowntime(rid, false, true);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removed downtime ID " << arguments[0];
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ExternalCommandProcessor") << error.what();
+ }
+}
+
+void ExternalCommandProcessor::DelDowntimeByHostName(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule host services downtime for non-existent host '" + arguments[0] + "'"));
+
+ String serviceName;
+ if (arguments.size() >= 2)
+ serviceName = arguments[1];
+
+ String startTime;
+ if (arguments.size() >= 3)
+ startTime = arguments[2];
+
+ String commentString;
+ if (arguments.size() >= 4)
+ commentString = arguments[3];
+
+ if (arguments.size() > 5)
+ Log(LogWarning, "ExternalCommandProcessor")
+ << ("Ignoring additional parameters for host '" + arguments[0] + "' downtime deletion.");
+
+ for (const Downtime::Ptr& downtime : host->GetDowntimes()) {
+ try {
+ String downtimeName = downtime->GetName();
+ Downtime::RemoveDowntime(downtimeName, false, true);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removed downtime '" << downtimeName << "'.";
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ExternalCommandProcessor") << error.what();
+ }
+ }
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ if (!serviceName.IsEmpty() && serviceName != service->GetName())
+ continue;
+
+ for (const Downtime::Ptr& downtime : service->GetDowntimes()) {
+ if (!startTime.IsEmpty() && downtime->GetStartTime() != Convert::ToDouble(startTime))
+ continue;
+
+ if (!commentString.IsEmpty() && downtime->GetComment() != commentString)
+ continue;
+
+ try {
+ String downtimeName = downtime->GetName();
+ Downtime::RemoveDowntime(downtimeName, false, true);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removed downtime '" << downtimeName << "'.";
+ } catch (const invalid_downtime_removal_error& error) {
+ Log(LogWarning, "ExternalCommandProcessor") << error.what();
+ }
+ }
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostSvcDowntime(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule host services downtime for non-existent host '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for service " << service->GetName();
+ (void) Downtime::AddDowntime(service, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostgroupHostDowntime(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule hostgroup host downtime for non-existent hostgroup '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleHostgroupSvcDowntime(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule hostgroup service downtime for non-existent hostgroup '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ /* Note: we can't just directly create downtimes for all the services by iterating
+ * over all hosts in the host group - otherwise we might end up creating multiple
+ * downtimes for some services. */
+
+ std::set<Service::Ptr> services;
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ services.insert(service);
+ }
+ }
+
+ for (const Service::Ptr& service : services) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for service " << service->GetName();
+ (void) Downtime::AddDowntime(service, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleServicegroupHostDowntime(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule servicegroup host downtime for non-existent servicegroup '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ /* Note: we can't just directly create downtimes for all the hosts by iterating
+ * over all services in the service group - otherwise we might end up creating multiple
+ * downtimes for some hosts. */
+
+ std::set<Host::Ptr> hosts;
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+ hosts.insert(host);
+ }
+
+ for (const Host::Ptr& host : hosts) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for host " << host->GetName();
+ (void) Downtime::AddDowntime(host, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::ScheduleServicegroupSvcDowntime(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot schedule servicegroup service downtime for non-existent servicegroup '" + arguments[0] + "'"));
+
+ String triggeredBy;
+ int triggeredByLegacy = Convert::ToLong(arguments[4]);
+ int is_fixed = Convert::ToLong(arguments[3]);
+ if (triggeredByLegacy != 0)
+ triggeredBy = Downtime::GetDowntimeIDFromLegacyID(triggeredByLegacy);
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating downtime for service " << service->GetName();
+ (void) Downtime::AddDowntime(service, arguments[6], arguments[7],
+ Convert::ToDouble(arguments[1]), Convert::ToDouble(arguments[2]),
+ Convert::ToBool(is_fixed), triggeredBy, Convert::ToDouble(arguments[5]));
+ }
+}
+
+void ExternalCommandProcessor::AddHostComment(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot add host comment for non-existent host '" + arguments[0] + "'"));
+
+ if (arguments[2].IsEmpty() || arguments[3].IsEmpty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Author and comment must not be empty"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating comment for host " << host->GetName();
+ (void) Comment::AddComment(host, CommentUser, arguments[2], arguments[3], false, 0);
+}
+
+void ExternalCommandProcessor::DelHostComment(double, const std::vector<String>& arguments)
+{
+ int id = Convert::ToLong(arguments[0]);
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing comment ID " << arguments[0];
+ String rid = Comment::GetCommentIDFromLegacyID(id);
+ Comment::RemoveComment(rid);
+}
+
+void ExternalCommandProcessor::AddSvcComment(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot add service comment for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (arguments[3].IsEmpty() || arguments[4].IsEmpty())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Author and comment must not be empty"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Creating comment for service " << service->GetName();
+ (void) Comment::AddComment(service, CommentUser, arguments[3], arguments[4], false, 0);
+}
+
+void ExternalCommandProcessor::DelSvcComment(double, const std::vector<String>& arguments)
+{
+ int id = Convert::ToLong(arguments[0]);
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing comment ID " << arguments[0];
+
+ String rid = Comment::GetCommentIDFromLegacyID(id);
+ Comment::RemoveComment(rid);
+}
+
+void ExternalCommandProcessor::DelAllHostComments(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot delete all host comments for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing all comments for host " << host->GetName();
+ host->RemoveAllComments();
+}
+
+void ExternalCommandProcessor::DelAllSvcComments(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot delete all service comments for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Removing all comments for service " << service->GetName();
+ service->RemoveAllComments();
+}
+
+void ExternalCommandProcessor::SendCustomHostNotification(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot send custom host notification for non-existent host '" + arguments[0] + "'"));
+
+ int options = Convert::ToLong(arguments[1]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Sending custom notification for host " << host->GetName();
+ if (options & 2) {
+ host->SetForceNextNotification(true);
+ }
+
+ Checkable::OnNotificationsRequested(host, NotificationCustom,
+ host->GetLastCheckResult(), arguments[2], arguments[3], nullptr);
+}
+
+void ExternalCommandProcessor::SendCustomSvcNotification(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot send custom service notification for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ int options = Convert::ToLong(arguments[2]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Sending custom notification for service " << service->GetName();
+
+ if (options & 2) {
+ service->SetForceNextNotification(true);
+ }
+
+ Service::OnNotificationsRequested(service, NotificationCustom,
+ service->GetLastCheckResult(), arguments[3], arguments[4], nullptr);
+}
+
+void ExternalCommandProcessor::DelayHostNotification(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot delay host notification for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Delaying notifications for host '" << host->GetName() << "'";
+
+ for (const Notification::Ptr& notification : host->GetNotifications()) {
+ notification->SetNextNotification(Convert::ToDouble(arguments[1]));
+ }
+}
+
+void ExternalCommandProcessor::DelaySvcNotification(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot delay service notification for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Delaying notifications for service " << service->GetName();
+
+ for (const Notification::Ptr& notification : service->GetNotifications()) {
+ notification->SetNextNotification(Convert::ToDouble(arguments[2]));
+ }
+}
+
+void ExternalCommandProcessor::EnableHostNotifications(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host notifications for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_notifications", true);
+}
+
+void ExternalCommandProcessor::DisableHostNotifications(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host notifications for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_notifications", false);
+}
+
+void ExternalCommandProcessor::EnableSvcNotifications(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service notifications for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_notifications", true);
+}
+
+void ExternalCommandProcessor::DisableSvcNotifications(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service notifications for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_notifications", false);
+}
+
+void ExternalCommandProcessor::EnableHostSvcNotifications(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable notifications for all services for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for all services on host '" << arguments[0] << "'";
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostSvcNotifications(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable notifications for all services for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for all services on host '" << arguments[0] << "'";
+
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupHostChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable hostgroup host checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_active_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupPassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable hostgroup passive host checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_passive_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupHostChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable servicegroup host checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling active checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_active_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupPassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable servicegroup passive host checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling passive checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_passive_checks", false);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostgroupHostChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable hostgroup host checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_active_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostgroupPassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable hostgroup passive host checks for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_passive_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupHostChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable servicegroup host checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling active checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_active_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupPassiveHostChecks(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable servicegroup passive host checks for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling passive checks for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_passive_checks", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostFlapping(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host flapping for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling flapping detection for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_flapping", true);
+}
+
+void ExternalCommandProcessor::DisableHostFlapping(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host flapping for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling flapping detection for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_flapping", false);
+}
+
+void ExternalCommandProcessor::EnableSvcFlapping(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service flapping for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling flapping detection for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_flapping", true);
+}
+
+void ExternalCommandProcessor::DisableSvcFlapping(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service flapping for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling flapping detection for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_flapping", false);
+}
+
+void ExternalCommandProcessor::EnableNotifications(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling notifications.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_notifications", true);
+}
+
+void ExternalCommandProcessor::DisableNotifications(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling notifications.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_notifications", false);
+}
+
+void ExternalCommandProcessor::EnableFlapDetection(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling flap detection.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_flapping", true);
+}
+
+void ExternalCommandProcessor::DisableFlapDetection(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling flap detection.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_flapping", false);
+}
+
+void ExternalCommandProcessor::EnableEventHandlers(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling event handlers.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_event_handlers", true);
+}
+
+void ExternalCommandProcessor::DisableEventHandlers(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling event handlers.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_event_handlers", false);
+}
+
+void ExternalCommandProcessor::EnablePerformanceData(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling performance data processing.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_perfdata", true);
+}
+
+void ExternalCommandProcessor::DisablePerformanceData(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling performance data processing.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_perfdata", false);
+}
+
+void ExternalCommandProcessor::StartExecutingSvcChecks(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling service checks.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_service_checks", true);
+}
+
+void ExternalCommandProcessor::StopExecutingSvcChecks(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling service checks.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_service_checks", false);
+}
+
+void ExternalCommandProcessor::StartExecutingHostChecks(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally enabling host checks.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_host_checks", true);
+}
+
+void ExternalCommandProcessor::StopExecutingHostChecks(double, const std::vector<String>&)
+{
+ Log(LogNotice, "ExternalCommandProcessor", "Globally disabling host checks.");
+
+ IcingaApplication::GetInstance()->ModifyAttribute("enable_host_checks", false);
+}
+
+void ExternalCommandProcessor::ChangeNormalSvcCheckInterval(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot update check interval for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ double interval = Convert::ToDouble(arguments[2]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Updating check interval for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("check_interval", interval * 60);
+}
+
+void ExternalCommandProcessor::ChangeNormalHostCheckInterval(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot update check interval for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Updating check interval for host '" << arguments[0] << "'";
+
+ double interval = Convert::ToDouble(arguments[1]);
+
+ host->ModifyAttribute("check_interval", interval * 60);
+}
+
+void ExternalCommandProcessor::ChangeRetrySvcCheckInterval(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot update retry interval for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ double interval = Convert::ToDouble(arguments[2]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Updating retry interval for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("retry_interval", interval * 60);
+}
+
+void ExternalCommandProcessor::ChangeRetryHostCheckInterval(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot update retry interval for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Updating retry interval for host '" << arguments[0] << "'";
+
+ double interval = Convert::ToDouble(arguments[1]);
+
+ host->ModifyAttribute("retry_interval", interval * 60);
+}
+
+void ExternalCommandProcessor::EnableHostEventHandler(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable event handler for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling event handler for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_event_handler", true);
+}
+
+void ExternalCommandProcessor::DisableHostEventHandler(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable event handler for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling event handler for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("enable_event_handler", false);
+}
+
+void ExternalCommandProcessor::EnableSvcEventHandler(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable event handler for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling event handler for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("enable_event_handler", true);
+}
+
+void ExternalCommandProcessor::DisableSvcEventHandler(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable event handler for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling event handler for service '" << arguments[1] + "'";
+
+ service->ModifyAttribute("enable_event_handler", false);
+}
+
+void ExternalCommandProcessor::ChangeHostEventHandler(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change event handler for non-existent host '" + arguments[0] + "'"));
+
+ if (arguments[1].IsEmpty()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Unsetting event handler for host '" << arguments[0] << "'";
+
+ host->ModifyAttribute("event_command", "");
+ } else {
+ EventCommand::Ptr command = EventCommand::GetByName(arguments[1]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Event command '" + arguments[1] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing event handler for host '" << arguments[0] << "' to '" << arguments[1] << "'";
+
+ host->ModifyAttribute("event_command", command->GetName());
+ }
+}
+
+void ExternalCommandProcessor::ChangeSvcEventHandler(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change event handler for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ if (arguments[2].IsEmpty()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Unsetting event handler for service '" << arguments[1] << "'";
+
+ service->ModifyAttribute("event_command", "");
+ } else {
+ EventCommand::Ptr command = EventCommand::GetByName(arguments[2]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Event command '" + arguments[2] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing event handler for service '" << arguments[1] << "' to '" << arguments[2] << "'";
+
+ service->ModifyAttribute("event_command", command->GetName());
+ }
+}
+
+void ExternalCommandProcessor::ChangeHostCheckCommand(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change check command for non-existent host '" + arguments[0] + "'"));
+
+ CheckCommand::Ptr command = CheckCommand::GetByName(arguments[1]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Check command '" + arguments[1] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing check command for host '" << arguments[0] << "' to '" << arguments[1] << "'";
+
+ host->ModifyAttribute("check_command", command->GetName());
+}
+
+void ExternalCommandProcessor::ChangeSvcCheckCommand(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change check command for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ CheckCommand::Ptr command = CheckCommand::GetByName(arguments[2]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Check command '" + arguments[2] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing check command for service '" << arguments[1] << "' to '" << arguments[2] << "'";
+
+ service->ModifyAttribute("check_command", command->GetName());
+}
+
+void ExternalCommandProcessor::ChangeMaxHostCheckAttempts(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change max check attempts for non-existent host '" + arguments[0] + "'"));
+
+ int attempts = Convert::ToLong(arguments[1]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing max check attempts for host '" << arguments[0] << "' to '" << arguments[1] << "'";
+
+ host->ModifyAttribute("max_check_attempts", attempts);
+}
+
+void ExternalCommandProcessor::ChangeMaxSvcCheckAttempts(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change max check attempts for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ int attempts = Convert::ToLong(arguments[2]);
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing max check attempts for service '" << arguments[1] << "' to '" << arguments[2] << "'";
+
+ service->ModifyAttribute("max_check_attempts", attempts);
+}
+
+void ExternalCommandProcessor::ChangeHostCheckTimeperiod(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change check period for non-existent host '" + arguments[0] + "'"));
+
+ TimePeriod::Ptr tp = TimePeriod::GetByName(arguments[1]);
+
+ if (!tp)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Time period '" + arguments[1] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing check period for host '" << arguments[0] << "' to '" << arguments[1] << "'";
+
+ host->ModifyAttribute("check_period", tp->GetName());
+}
+
+void ExternalCommandProcessor::ChangeSvcCheckTimeperiod(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change check period for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ TimePeriod::Ptr tp = TimePeriod::GetByName(arguments[2]);
+
+ if (!tp)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Time period '" + arguments[2] + "' does not exist."));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing check period for service '" << arguments[1] << "' to '" << arguments[2] << "'";
+
+ service->ModifyAttribute("check_period", tp->GetName());
+}
+
+void ExternalCommandProcessor::ChangeCustomHostVar(double, const std::vector<String>& arguments)
+{
+ Host::Ptr host = Host::GetByName(arguments[0]);
+
+ if (!host)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing custom var '" << arguments[1] << "' for host '" << arguments[0] << "' to value '" << arguments[2] << "'";
+
+ host->ModifyAttribute("vars." + arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomSvcVar(double, const std::vector<String>& arguments)
+{
+ Service::Ptr service = Service::GetByNamePair(arguments[0], arguments[1]);
+
+ if (!service)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent service '" + arguments[1] + "' on host '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing custom var '" << arguments[2] << "' for service '" << arguments[1] << "' on host '"
+ << arguments[0] << "' to value '" << arguments[3] << "'";
+
+ service->ModifyAttribute("vars." + arguments[2], arguments[3]);
+}
+
+void ExternalCommandProcessor::ChangeCustomUserVar(double, const std::vector<String>& arguments)
+{
+ User::Ptr user = User::GetByName(arguments[0]);
+
+ if (!user)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent user '" + arguments[0] + "'"));
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing custom var '" << arguments[1] << "' for user '" << arguments[0] << "' to value '" << arguments[2] << "'";
+
+ user->ModifyAttribute("vars." + arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomCheckcommandVar(double, const std::vector<String>& arguments)
+{
+ CheckCommand::Ptr command = CheckCommand::GetByName(arguments[0]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent command '" + arguments[0] + "'"));
+
+ ChangeCustomCommandVarInternal(command, arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomEventcommandVar(double, const std::vector<String>& arguments)
+{
+ EventCommand::Ptr command = EventCommand::GetByName(arguments[0]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent command '" + arguments[0] + "'"));
+
+ ChangeCustomCommandVarInternal(command, arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomNotificationcommandVar(double, const std::vector<String>& arguments)
+{
+ NotificationCommand::Ptr command = NotificationCommand::GetByName(arguments[0]);
+
+ if (!command)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot change custom var for non-existent command '" + arguments[0] + "'"));
+
+ ChangeCustomCommandVarInternal(command, arguments[1], arguments[2]);
+}
+
+void ExternalCommandProcessor::ChangeCustomCommandVarInternal(const Command::Ptr& command, const String& name, const Value& value)
+{
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Changing custom var '" << name << "' for command '" << command->GetName() << "' to value '" << value << "'";
+
+ command->ModifyAttribute("vars." + name, value);
+}
+
+void ExternalCommandProcessor::EnableHostgroupHostNotifications(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host notifications for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_notifications", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableHostgroupSvcNotifications(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service notifications for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", true);
+ }
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupHostNotifications(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host notifications for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_notifications", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableHostgroupSvcNotifications(double, const std::vector<String>& arguments)
+{
+ HostGroup::Ptr hg = HostGroup::GetByName(arguments[0]);
+
+ if (!hg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service notifications for non-existent hostgroup '" + arguments[0] + "'"));
+
+ for (const Host::Ptr& host : hg->GetMembers()) {
+ for (const Service::Ptr& service : host->GetServices()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", false);
+ }
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupHostNotifications(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable host notifications for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_notifications", true);
+ }
+}
+
+void ExternalCommandProcessor::EnableServicegroupSvcNotifications(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot enable service notifications for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Enabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", true);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupHostNotifications(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable host notifications for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Host::Ptr host = service->GetHost();
+
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for host '" << host->GetName() << "'";
+
+ host->ModifyAttribute("enable_notifications", false);
+ }
+}
+
+void ExternalCommandProcessor::DisableServicegroupSvcNotifications(double, const std::vector<String>& arguments)
+{
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(arguments[0]);
+
+ if (!sg)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot disable service notifications for non-existent servicegroup '" + arguments[0] + "'"));
+
+ for (const Service::Ptr& service : sg->GetMembers()) {
+ Log(LogNotice, "ExternalCommandProcessor")
+ << "Disabling notifications for service '" << service->GetName() << "'";
+
+ service->ModifyAttribute("enable_notifications", false);
+ }
+}
+
+std::mutex& ExternalCommandProcessor::GetMutex()
+{
+ static std::mutex mtx;
+ return mtx;
+}
+
+std::map<String, ExternalCommandInfo>& ExternalCommandProcessor::GetCommands()
+{
+ static std::map<String, ExternalCommandInfo> commands;
+ return commands;
+}
+
diff --git a/lib/icinga/externalcommandprocessor.hpp b/lib/icinga/externalcommandprocessor.hpp
new file mode 100644
index 0000000..a7c5a30
--- /dev/null
+++ b/lib/icinga/externalcommandprocessor.hpp
@@ -0,0 +1,169 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EXTERNALCOMMANDPROCESSOR_H
+#define EXTERNALCOMMANDPROCESSOR_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/command.hpp"
+#include "base/string.hpp"
+#include <boost/signals2.hpp>
+#include <vector>
+
+namespace icinga
+{
+
+typedef std::function<void (double, const std::vector<String>& arguments)> ExternalCommandCallback;
+
+struct ExternalCommandInfo
+{
+ ExternalCommandCallback Callback;
+ size_t MinArgs;
+ size_t MaxArgs;
+};
+
+class ExternalCommandProcessor {
+public:
+ static void Execute(const String& line);
+ static void Execute(double time, const String& command, const std::vector<String>& arguments);
+
+ static boost::signals2::signal<void(double, const String&, const std::vector<String>&)> OnNewExternalCommand;
+
+private:
+ ExternalCommandProcessor();
+
+ static void ExecuteFromFile(const String& line, std::deque< std::vector<String> >& file_queue);
+
+ static void ProcessHostCheckResult(double time, const std::vector<String>& arguments);
+ static void ProcessServiceCheckResult(double time, const std::vector<String>& arguments);
+ static void ScheduleHostCheck(double time, const std::vector<String>& arguments);
+ static void ScheduleForcedHostCheck(double time, const std::vector<String>& arguments);
+ static void ScheduleSvcCheck(double time, const std::vector<String>& arguments);
+ static void ScheduleForcedSvcCheck(double time, const std::vector<String>& arguments);
+ static void EnableHostCheck(double time, const std::vector<String>& arguments);
+ static void DisableHostCheck(double time, const std::vector<String>& arguments);
+ static void EnableSvcCheck(double time, const std::vector<String>& arguments);
+ static void DisableSvcCheck(double time, const std::vector<String>& arguments);
+ static void ShutdownProcess(double time, const std::vector<String>& arguments);
+ static void RestartProcess(double time, const std::vector<String>& arguments);
+ static void ScheduleForcedHostSvcChecks(double time, const std::vector<String>& arguments);
+ static void ScheduleHostSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnableHostSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableHostSvcChecks(double time, const std::vector<String>& arguments);
+ static void AcknowledgeSvcProblem(double time, const std::vector<String>& arguments);
+ static void AcknowledgeSvcProblemExpire(double time, const std::vector<String>& arguments);
+ static void RemoveSvcAcknowledgement(double time, const std::vector<String>& arguments);
+ static void AcknowledgeHostProblem(double time, const std::vector<String>& arguments);
+ static void AcknowledgeHostProblemExpire(double time, const std::vector<String>& arguments);
+ static void RemoveHostAcknowledgement(double time, const std::vector<String>& arguments);
+ static void EnableHostgroupSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableHostgroupSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnableServicegroupSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableServicegroupSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnablePassiveHostChecks(double time, const std::vector<String>& arguments);
+ static void DisablePassiveHostChecks(double time, const std::vector<String>& arguments);
+ static void EnablePassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisablePassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnableServicegroupPassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableServicegroupPassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void EnableHostgroupPassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void DisableHostgroupPassiveSvcChecks(double time, const std::vector<String>& arguments);
+ static void ProcessFile(double time, const std::vector<String>& arguments);
+ static void ScheduleSvcDowntime(double time, const std::vector<String>& arguments);
+ static void DelSvcDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleHostDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleAndPropagateHostDowntime(double, const std::vector<String>& arguments);
+ static void ScheduleAndPropagateTriggeredHostDowntime(double, const std::vector<String>& arguments);
+ static void DelHostDowntime(double time, const std::vector<String>& arguments);
+ static void DelDowntimeByHostName(double, const std::vector<String>& arguments);
+ static void ScheduleHostSvcDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleHostgroupHostDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleHostgroupSvcDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleServicegroupHostDowntime(double time, const std::vector<String>& arguments);
+ static void ScheduleServicegroupSvcDowntime(double time, const std::vector<String>& arguments);
+ static void AddHostComment(double time, const std::vector<String>& arguments);
+ static void DelHostComment(double time, const std::vector<String>& arguments);
+ static void AddSvcComment(double time, const std::vector<String>& arguments);
+ static void DelSvcComment(double time, const std::vector<String>& arguments);
+ static void DelAllHostComments(double time, const std::vector<String>& arguments);
+ static void DelAllSvcComments(double time, const std::vector<String>& arguments);
+ static void SendCustomHostNotification(double time, const std::vector<String>& arguments);
+ static void SendCustomSvcNotification(double time, const std::vector<String>& arguments);
+ static void DelayHostNotification(double time, const std::vector<String>& arguments);
+ static void DelaySvcNotification(double time, const std::vector<String>& arguments);
+ static void EnableHostNotifications(double time, const std::vector<String>& arguments);
+ static void DisableHostNotifications(double time, const std::vector<String>& arguments);
+ static void EnableSvcNotifications(double time, const std::vector<String>& arguments);
+ static void DisableSvcNotifications(double time, const std::vector<String>& arguments);
+ static void EnableHostSvcNotifications(double, const std::vector<String>& arguments);
+ static void DisableHostSvcNotifications(double, const std::vector<String>& arguments);
+ static void DisableHostgroupHostChecks(double, const std::vector<String>& arguments);
+ static void DisableHostgroupPassiveHostChecks(double, const std::vector<String>& arguments);
+ static void DisableServicegroupHostChecks(double, const std::vector<String>& arguments);
+ static void DisableServicegroupPassiveHostChecks(double, const std::vector<String>& arguments);
+ static void EnableHostgroupHostChecks(double, const std::vector<String>& arguments);
+ static void EnableHostgroupPassiveHostChecks(double, const std::vector<String>& arguments);
+ static void EnableServicegroupHostChecks(double, const std::vector<String>& arguments);
+ static void EnableServicegroupPassiveHostChecks(double, const std::vector<String>& arguments);
+ static void EnableSvcFlapping(double time, const std::vector<String>& arguments);
+ static void DisableSvcFlapping(double time, const std::vector<String>& arguments);
+ static void EnableHostFlapping(double time, const std::vector<String>& arguments);
+ static void DisableHostFlapping(double time, const std::vector<String>& arguments);
+ static void EnableNotifications(double time, const std::vector<String>& arguments);
+ static void DisableNotifications(double time, const std::vector<String>& arguments);
+ static void EnableFlapDetection(double time, const std::vector<String>& arguments);
+ static void DisableFlapDetection(double time, const std::vector<String>& arguments);
+ static void EnableEventHandlers(double time, const std::vector<String>& arguments);
+ static void DisableEventHandlers(double time, const std::vector<String>& arguments);
+ static void EnablePerformanceData(double time, const std::vector<String>& arguments);
+ static void DisablePerformanceData(double time, const std::vector<String>& arguments);
+ static void StartExecutingSvcChecks(double time, const std::vector<String>& arguments);
+ static void StopExecutingSvcChecks(double time, const std::vector<String>& arguments);
+ static void StartExecutingHostChecks(double time, const std::vector<String>& arguments);
+ static void StopExecutingHostChecks(double time, const std::vector<String>& arguments);
+
+ static void ChangeNormalSvcCheckInterval(double time, const std::vector<String>& arguments);
+ static void ChangeNormalHostCheckInterval(double time, const std::vector<String>& arguments);
+ static void ChangeRetrySvcCheckInterval(double time, const std::vector<String>& arguments);
+ static void ChangeRetryHostCheckInterval(double time, const std::vector<String>& arguments);
+ static void EnableHostEventHandler(double time, const std::vector<String>& arguments);
+ static void DisableHostEventHandler(double time, const std::vector<String>& arguments);
+ static void EnableSvcEventHandler(double time, const std::vector<String>& arguments);
+ static void DisableSvcEventHandler(double time, const std::vector<String>& arguments);
+ static void ChangeHostEventHandler(double time, const std::vector<String>& arguments);
+ static void ChangeSvcEventHandler(double time, const std::vector<String>& arguments);
+ static void ChangeHostCheckCommand(double time, const std::vector<String>& arguments);
+ static void ChangeSvcCheckCommand(double time, const std::vector<String>& arguments);
+ static void ChangeMaxHostCheckAttempts(double time, const std::vector<String>& arguments);
+ static void ChangeMaxSvcCheckAttempts(double time, const std::vector<String>& arguments);
+ static void ChangeHostCheckTimeperiod(double time, const std::vector<String>& arguments);
+ static void ChangeSvcCheckTimeperiod(double time, const std::vector<String>& arguments);
+ static void ChangeCustomHostVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomSvcVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomUserVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomCheckcommandVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomEventcommandVar(double time, const std::vector<String>& arguments);
+ static void ChangeCustomNotificationcommandVar(double time, const std::vector<String>& arguments);
+
+ static void EnableHostgroupHostNotifications(double time, const std::vector<String>& arguments);
+ static void EnableHostgroupSvcNotifications(double time, const std::vector<String>& arguments);
+ static void DisableHostgroupHostNotifications(double time, const std::vector<String>& arguments);
+ static void DisableHostgroupSvcNotifications(double time, const std::vector<String>& arguments);
+ static void EnableServicegroupHostNotifications(double time, const std::vector<String>& arguments);
+ static void EnableServicegroupSvcNotifications(double time, const std::vector<String>& arguments);
+ static void DisableServicegroupHostNotifications(double time, const std::vector<String>& arguments);
+ static void DisableServicegroupSvcNotifications(double time, const std::vector<String>& arguments);
+
+private:
+ static void ChangeCustomCommandVarInternal(const Command::Ptr& command, const String& name, const Value& value);
+
+ static void RegisterCommand(const String& command, const ExternalCommandCallback& callback, size_t minArgs = 0, size_t maxArgs = UINT_MAX);
+ static void RegisterCommands();
+
+ static std::mutex& GetMutex();
+ static std::map<String, ExternalCommandInfo>& GetCommands();
+
+};
+
+}
+
+#endif /* EXTERNALCOMMANDPROCESSOR_H */
diff --git a/lib/icinga/host.cpp b/lib/icinga/host.cpp
new file mode 100644
index 0000000..36149d3
--- /dev/null
+++ b/lib/icinga/host.cpp
@@ -0,0 +1,330 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/host.hpp"
+#include "icinga/host-ti.cpp"
+#include "icinga/service.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/pluginutility.hpp"
+#include "icinga/scheduleddowntime.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/debug.hpp"
+#include "base/json.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Host);
+
+void Host::OnAllConfigLoaded()
+{
+ ObjectImpl<Host>::OnAllConfigLoaded();
+
+ String zoneName = GetZoneName();
+
+ if (!zoneName.IsEmpty()) {
+ Zone::Ptr zone = Zone::GetByName(zoneName);
+
+ if (zone && zone->IsGlobal())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Host '" + GetName() + "' cannot be put into global zone '" + zone->GetName() + "'."));
+ }
+
+ HostGroup::EvaluateObjectRules(this);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ HostGroup::Ptr hg = HostGroup::GetByName(name);
+
+ if (hg)
+ hg->ResolveGroupMembership(this, true);
+ }
+ }
+}
+
+void Host::CreateChildObjects(const Type::Ptr& childType)
+{
+ if (childType == ScheduledDowntime::TypeInstance)
+ ScheduledDowntime::EvaluateApplyRules(this);
+
+ if (childType == Notification::TypeInstance)
+ Notification::EvaluateApplyRules(this);
+
+ if (childType == Dependency::TypeInstance)
+ Dependency::EvaluateApplyRules(this);
+
+ if (childType == Service::TypeInstance)
+ Service::EvaluateApplyRules(this);
+}
+
+void Host::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<Host>::Stop(runtimeRemoved);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ HostGroup::Ptr hg = HostGroup::GetByName(name);
+
+ if (hg)
+ hg->ResolveGroupMembership(this, false);
+ }
+ }
+
+ // TODO: unregister slave services/notifications?
+}
+
+std::vector<Service::Ptr> Host::GetServices() const
+{
+ std::unique_lock<std::mutex> lock(m_ServicesMutex);
+
+ std::vector<Service::Ptr> services;
+ services.reserve(m_Services.size());
+ typedef std::pair<String, Service::Ptr> ServicePair;
+ for (const ServicePair& kv : m_Services) {
+ services.push_back(kv.second);
+ }
+
+ return services;
+}
+
+void Host::AddService(const Service::Ptr& service)
+{
+ std::unique_lock<std::mutex> lock(m_ServicesMutex);
+
+ m_Services[service->GetShortName()] = service;
+}
+
+void Host::RemoveService(const Service::Ptr& service)
+{
+ std::unique_lock<std::mutex> lock(m_ServicesMutex);
+
+ m_Services.erase(service->GetShortName());
+}
+
+int Host::GetTotalServices() const
+{
+ return GetServices().size();
+}
+
+Service::Ptr Host::GetServiceByShortName(const Value& name)
+{
+ if (name.IsScalar()) {
+ {
+ std::unique_lock<std::mutex> lock(m_ServicesMutex);
+
+ auto it = m_Services.find(name);
+
+ if (it != m_Services.end())
+ return it->second;
+ }
+
+ return nullptr;
+ } else if (name.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = name;
+ String short_name;
+
+ return Service::GetByNamePair(dict->Get("host"), dict->Get("service"));
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Host/Service name pair is invalid: " + JsonEncode(name)));
+ }
+}
+
+HostState Host::CalculateState(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ case ServiceWarning:
+ return HostUp;
+ default:
+ return HostDown;
+ }
+}
+
+HostState Host::GetState() const
+{
+ return CalculateState(GetStateRaw());
+}
+
+HostState Host::GetLastState() const
+{
+ return CalculateState(GetLastStateRaw());
+}
+
+HostState Host::GetLastHardState() const
+{
+ return CalculateState(GetLastHardStateRaw());
+}
+
+/* keep in sync with Service::GetSeverity()
+ * One could think it may be smart to use an enum and some bitmask math here.
+ * But the only thing the consuming icingaweb2 cares about is being able to
+ * sort by severity. It is therefore easier to keep them seperated here. */
+int Host::GetSeverity() const
+{
+ int severity = 0;
+
+ ObjectLock olock(this);
+ HostState state = GetState();
+
+ if (!HasBeenChecked()) {
+ severity = 16;
+ } else if (state == HostUp) {
+ severity = 0;
+ } else {
+ if (IsReachable())
+ severity = 64;
+ else
+ severity = 32;
+
+ if (IsAcknowledged())
+ severity += 512;
+ else if (IsInDowntime())
+ severity += 256;
+ else
+ severity += 2048;
+ }
+
+ olock.Unlock();
+
+ return severity;
+
+}
+
+bool Host::IsStateOK(ServiceState state) const
+{
+ return Host::CalculateState(state) == HostUp;
+}
+
+void Host::SaveLastState(ServiceState state, double timestamp)
+{
+ if (state == ServiceOK || state == ServiceWarning)
+ SetLastStateUp(timestamp);
+ else if (state == ServiceCritical)
+ SetLastStateDown(timestamp);
+}
+
+HostState Host::StateFromString(const String& state)
+{
+ if (state == "UP")
+ return HostUp;
+ else
+ return HostDown;
+}
+
+String Host::StateToString(HostState state)
+{
+ switch (state) {
+ case HostUp:
+ return "UP";
+ case HostDown:
+ return "DOWN";
+ default:
+ return "INVALID";
+ }
+}
+
+StateType Host::StateTypeFromString(const String& type)
+{
+ if (type == "SOFT")
+ return StateTypeSoft;
+ else
+ return StateTypeHard;
+}
+
+String Host::StateTypeToString(StateType type)
+{
+ if (type == StateTypeSoft)
+ return "SOFT";
+ else
+ return "HARD";
+}
+
+bool Host::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const
+{
+ if (macro == "state") {
+ *result = StateToString(GetState());
+ return true;
+ } else if (macro == "state_id") {
+ *result = GetState();
+ return true;
+ } else if (macro == "state_type") {
+ *result = StateTypeToString(GetStateType());
+ return true;
+ } else if (macro == "last_state") {
+ *result = StateToString(GetLastState());
+ return true;
+ } else if (macro == "last_state_id") {
+ *result = GetLastState();
+ return true;
+ } else if (macro == "last_state_type") {
+ *result = StateTypeToString(GetLastStateType());
+ return true;
+ } else if (macro == "last_state_change") {
+ *result = static_cast<long>(GetLastStateChange());
+ return true;
+ } else if (macro == "downtime_depth") {
+ *result = GetDowntimeDepth();
+ return true;
+ } else if (macro == "duration_sec") {
+ *result = Utility::GetTime() - GetLastStateChange();
+ return true;
+ } else if (macro == "num_services" || macro == "num_services_ok" || macro == "num_services_warning"
+ || macro == "num_services_unknown" || macro == "num_services_critical") {
+ int filter = -1;
+ int count = 0;
+
+ if (macro == "num_services_ok")
+ filter = ServiceOK;
+ else if (macro == "num_services_warning")
+ filter = ServiceWarning;
+ else if (macro == "num_services_unknown")
+ filter = ServiceUnknown;
+ else if (macro == "num_services_critical")
+ filter = ServiceCritical;
+
+ for (const Service::Ptr& service : GetServices()) {
+ if (filter != -1 && service->GetState() != filter)
+ continue;
+
+ count++;
+ }
+
+ *result = count;
+ return true;
+ }
+
+ CheckResult::Ptr cr = GetLastCheckResult();
+
+ if (cr) {
+ if (macro == "latency") {
+ *result = cr->CalculateLatency();
+ return true;
+ } else if (macro == "execution_time") {
+ *result = cr->CalculateExecutionTime();
+ return true;
+ } else if (macro == "output") {
+ *result = cr->GetOutput();
+ return true;
+ } else if (macro == "perfdata") {
+ *result = PluginUtility::FormatPerfdata(cr->GetPerformanceData());
+ return true;
+ } else if (macro == "check_source") {
+ *result = cr->GetCheckSource();
+ return true;
+ } else if (macro == "scheduling_source") {
+ *result = cr->GetSchedulingSource();
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/lib/icinga/host.hpp b/lib/icinga/host.hpp
new file mode 100644
index 0000000..d0d6c1a
--- /dev/null
+++ b/lib/icinga/host.hpp
@@ -0,0 +1,71 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HOST_H
+#define HOST_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/host-ti.hpp"
+#include "icinga/macroresolver.hpp"
+#include "icinga/checkresult.hpp"
+
+namespace icinga
+{
+
+class Service;
+
+/**
+ * An Icinga host.
+ *
+ * @ingroup icinga
+ */
+class Host final : public ObjectImpl<Host>, public MacroResolver
+{
+public:
+ DECLARE_OBJECT(Host);
+ DECLARE_OBJECTNAME(Host);
+
+ intrusive_ptr<Service> GetServiceByShortName(const Value& name);
+
+ std::vector<intrusive_ptr<Service> > GetServices() const;
+ void AddService(const intrusive_ptr<Service>& service);
+ void RemoveService(const intrusive_ptr<Service>& service);
+
+ int GetTotalServices() const;
+
+ static HostState CalculateState(ServiceState state);
+
+ HostState GetState() const override;
+ HostState GetLastState() const override;
+ HostState GetLastHardState() const override;
+ int GetSeverity() const override;
+
+ bool IsStateOK(ServiceState state) const override;
+ void SaveLastState(ServiceState state, double timestamp) override;
+
+ static HostState StateFromString(const String& state);
+ static String StateToString(HostState state);
+
+ static StateType StateTypeFromString(const String& state);
+ static String StateTypeToString(StateType state);
+
+ bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const override;
+
+ void OnAllConfigLoaded() override;
+
+protected:
+ void Stop(bool runtimeRemoved) override;
+
+ void CreateChildObjects(const Type::Ptr& childType) override;
+
+private:
+ mutable std::mutex m_ServicesMutex;
+ std::map<String, intrusive_ptr<Service> > m_Services;
+
+ static void RefreshServicesCache();
+};
+
+}
+
+#endif /* HOST_H */
+
+#include "icinga/service.hpp"
diff --git a/lib/icinga/host.ti b/lib/icinga/host.ti
new file mode 100644
index 0000000..f6624e3
--- /dev/null
+++ b/lib/icinga/host.ti
@@ -0,0 +1,48 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/customvarobject.hpp"
+#impl_include "icinga/hostgroup.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class Host : Checkable
+{
+ load_after ApiListener;
+ load_after Endpoint;
+ load_after Zone;
+
+ [config, no_user_modify, required, signal_with_old_value] array(name(HostGroup)) groups {
+ default {{{ return new Array(); }}}
+ };
+
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+
+ [config] String address;
+ [config] String address6;
+
+ [enum, no_storage] HostState "state" {
+ get;
+ };
+ [enum, no_storage] HostState last_state {
+ get;
+ };
+ [enum, no_storage] HostState last_hard_state {
+ get;
+ };
+ [state] Timestamp last_state_up;
+ [state] Timestamp last_state_down;
+};
+
+}
diff --git a/lib/icinga/hostgroup.cpp b/lib/icinga/hostgroup.cpp
new file mode 100644
index 0000000..a22f3b7
--- /dev/null
+++ b/lib/icinga/hostgroup.cpp
@@ -0,0 +1,108 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/hostgroup.hpp"
+#include "icinga/hostgroup-ti.cpp"
+#include "config/objectrule.hpp"
+#include "config/configitem.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(HostGroup);
+
+INITIALIZE_ONCE([]() {
+ ObjectRule::RegisterType("HostGroup");
+});
+
+bool HostGroup::EvaluateObjectRule(const Host::Ptr& host, const ConfigItem::Ptr& group)
+{
+ String groupName = group->GetName();
+
+ CONTEXT("Evaluating rule for group '" << groupName << "'");
+
+ ScriptFrame frame(true);
+ if (group->GetScope())
+ group->GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+
+ if (!group->GetFilter()->Evaluate(frame).GetValue().ToBool())
+ return false;
+
+ Log(LogDebug, "HostGroup")
+ << "Assigning membership for group '" << groupName << "' to host '" << host->GetName() << "'";
+
+ Array::Ptr groups = host->GetGroups();
+
+ if (groups && !groups->Contains(groupName))
+ groups->Add(groupName);
+
+ return true;
+}
+
+void HostGroup::EvaluateObjectRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating group memberships for host '" << host->GetName() << "'");
+
+ for (const ConfigItem::Ptr& group : ConfigItem::GetItems(HostGroup::TypeInstance))
+ {
+ if (!group->GetFilter())
+ continue;
+
+ EvaluateObjectRule(host, group);
+ }
+}
+
+std::set<Host::Ptr> HostGroup::GetMembers() const
+{
+ std::unique_lock<std::mutex> lock(m_HostGroupMutex);
+ return m_Members;
+}
+
+void HostGroup::AddMember(const Host::Ptr& host)
+{
+ host->AddGroup(GetName());
+
+ std::unique_lock<std::mutex> lock(m_HostGroupMutex);
+ m_Members.insert(host);
+}
+
+void HostGroup::RemoveMember(const Host::Ptr& host)
+{
+ std::unique_lock<std::mutex> lock(m_HostGroupMutex);
+ m_Members.erase(host);
+}
+
+bool HostGroup::ResolveGroupMembership(const Host::Ptr& host, bool add, int rstack) {
+
+ if (add && rstack > 20) {
+ Log(LogWarning, "HostGroup")
+ << "Too many nested groups for group '" << GetName() << "': Host '"
+ << host->GetName() << "' membership assignment failed.";
+
+ return false;
+ }
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups && groups->GetLength() > 0) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ HostGroup::Ptr group = HostGroup::GetByName(name);
+
+ if (group && !group->ResolveGroupMembership(host, add, rstack + 1))
+ return false;
+ }
+ }
+
+ if (add)
+ AddMember(host);
+ else
+ RemoveMember(host);
+
+ return true;
+}
diff --git a/lib/icinga/hostgroup.hpp b/lib/icinga/hostgroup.hpp
new file mode 100644
index 0000000..3ad5d26
--- /dev/null
+++ b/lib/icinga/hostgroup.hpp
@@ -0,0 +1,43 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HOSTGROUP_H
+#define HOSTGROUP_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/hostgroup-ti.hpp"
+#include "icinga/host.hpp"
+
+namespace icinga
+{
+
+class ConfigItem;
+
+/**
+ * An Icinga host group.
+ *
+ * @ingroup icinga
+ */
+class HostGroup final : public ObjectImpl<HostGroup>
+{
+public:
+ DECLARE_OBJECT(HostGroup);
+ DECLARE_OBJECTNAME(HostGroup);
+
+ std::set<Host::Ptr> GetMembers() const;
+ void AddMember(const Host::Ptr& host);
+ void RemoveMember(const Host::Ptr& host);
+
+ bool ResolveGroupMembership(const Host::Ptr& host, bool add = true, int rstack = 0);
+
+ static void EvaluateObjectRules(const Host::Ptr& host);
+
+private:
+ mutable std::mutex m_HostGroupMutex;
+ std::set<Host::Ptr> m_Members;
+
+ static bool EvaluateObjectRule(const Host::Ptr& host, const intrusive_ptr<ConfigItem>& item);
+};
+
+}
+
+#endif /* HOSTGROUP_H */
diff --git a/lib/icinga/hostgroup.ti b/lib/icinga/hostgroup.ti
new file mode 100644
index 0000000..b679344
--- /dev/null
+++ b/lib/icinga/hostgroup.ti
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class HostGroup : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+
+ [config, no_user_modify] array(name(HostGroup)) groups;
+ [config] String notes;
+ [config] String notes_url;
+ [config] String action_url;
+};
+
+}
diff --git a/lib/icinga/i2-icinga.hpp b/lib/icinga/i2-icinga.hpp
new file mode 100644
index 0000000..7163822
--- /dev/null
+++ b/lib/icinga/i2-icinga.hpp
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2ICINGA_H
+#define I2ICINGA_H
+
+/**
+ * @defgroup icinga Icinga library
+ *
+ * The Icinga library implements all Icinga-specific functionality that is
+ * common to all components (e.g. hosts, services, etc.).
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2ICINGA_H */
diff --git a/lib/icinga/icinga-itl.conf b/lib/icinga/icinga-itl.conf
new file mode 100644
index 0000000..22b688a
--- /dev/null
+++ b/lib/icinga/icinga-itl.conf
@@ -0,0 +1,15 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+System.assert(Internal.run_with_activation_context(function() {
+ template TimePeriod "legacy-timeperiod" use (LegacyTimePeriod = Internal.LegacyTimePeriod) default {
+ update = LegacyTimePeriod
+ }
+}))
+
+var methods = [
+ "LegacyTimePeriod"
+]
+
+for (method in methods) {
+ Internal.remove(method)
+}
diff --git a/lib/icinga/icingaapplication.cpp b/lib/icinga/icingaapplication.cpp
new file mode 100644
index 0000000..94ae0ed
--- /dev/null
+++ b/lib/icinga/icingaapplication.cpp
@@ -0,0 +1,321 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/icingaapplication.hpp"
+#include "icinga/icingaapplication-ti.cpp"
+#include "icinga/cib.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "config/configcompiler.hpp"
+#include "base/atomic-file.hpp"
+#include "base/configwriter.hpp"
+#include "base/configtype.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/debug.hpp"
+#include "base/utility.hpp"
+#include "base/timer.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/initialize.hpp"
+#include "base/statsfunction.hpp"
+#include "base/loader.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+static Timer::Ptr l_RetentionTimer;
+
+REGISTER_TYPE(IcingaApplication);
+/* Ensure that the priority is lower than the basic System namespace initialization in scriptframe.cpp. */
+INITIALIZE_ONCE_WITH_PRIORITY(&IcingaApplication::StaticInitialize, InitializePriority::InitIcingaApplication);
+
+static Namespace::Ptr l_IcingaNS;
+
+void IcingaApplication::StaticInitialize()
+{
+ /* Pre-fill global constants, can be overridden with user input later in icinga-app/icinga.cpp. */
+ String node_name = Utility::GetFQDN();
+
+ if (node_name.IsEmpty()) {
+ Log(LogNotice, "IcingaApplication", "No FQDN available. Trying Hostname.");
+ node_name = Utility::GetHostName();
+
+ if (node_name.IsEmpty()) {
+ Log(LogWarning, "IcingaApplication", "No FQDN nor Hostname available. Setting Nodename to 'localhost'.");
+ node_name = "localhost";
+ }
+ }
+
+ ScriptGlobal::Set("NodeName", node_name);
+
+ ScriptGlobal::Set("ReloadTimeout", 300);
+ ScriptGlobal::Set("MaxConcurrentChecks", 512);
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+ /* Ensure that the System namespace is already initialized. Otherwise this is a programming error. */
+ VERIFY(systemNS);
+
+ systemNS->Set("ApplicationType", "IcingaApplication", true);
+ systemNS->Set("ApplicationVersion", Application::GetAppVersion(), true);
+
+ Namespace::Ptr globalNS = ScriptGlobal::GetGlobals();
+ VERIFY(globalNS);
+
+ l_IcingaNS = new Namespace(true);
+ globalNS->Set("Icinga", l_IcingaNS, true);
+}
+
+INITIALIZE_ONCE_WITH_PRIORITY([]() {
+ l_IcingaNS->Freeze();
+}, InitializePriority::FreezeNamespaces);
+
+REGISTER_STATSFUNCTION(IcingaApplication, &IcingaApplication::StatsFunc);
+
+void IcingaApplication::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ DictionaryData nodes;
+
+ for (const IcingaApplication::Ptr& icingaapplication : ConfigType::GetObjectsByType<IcingaApplication>()) {
+ nodes.emplace_back(icingaapplication->GetName(), new Dictionary({
+ { "node_name", icingaapplication->GetNodeName() },
+ { "enable_notifications", icingaapplication->GetEnableNotifications() },
+ { "enable_event_handlers", icingaapplication->GetEnableEventHandlers() },
+ { "enable_flapping", icingaapplication->GetEnableFlapping() },
+ { "enable_host_checks", icingaapplication->GetEnableHostChecks() },
+ { "enable_service_checks", icingaapplication->GetEnableServiceChecks() },
+ { "enable_perfdata", icingaapplication->GetEnablePerfdata() },
+ { "environment", icingaapplication->GetEnvironment() },
+ { "pid", Utility::GetPid() },
+ { "program_start", Application::GetStartTime() },
+ { "version", Application::GetAppVersion() }
+ }));
+ }
+
+ status->Set("icingaapplication", new Dictionary(std::move(nodes)));
+}
+
+/**
+ * The entry point for the Icinga application.
+ *
+ * @returns An exit status.
+ */
+int IcingaApplication::Main()
+{
+ Log(LogDebug, "IcingaApplication", "In IcingaApplication::Main()");
+
+ /* periodically dump the program state */
+ l_RetentionTimer = Timer::Create();
+ l_RetentionTimer->SetInterval(300);
+ l_RetentionTimer->OnTimerExpired.connect([this](const Timer * const&) { DumpProgramState(); });
+ l_RetentionTimer->Start();
+
+ RunEventLoop();
+
+ Log(LogInformation, "IcingaApplication", "Icinga has shut down.");
+
+ return EXIT_SUCCESS;
+}
+
+void IcingaApplication::OnShutdown()
+{
+ {
+ ObjectLock olock(this);
+ l_RetentionTimer->Stop();
+ }
+
+ DumpProgramState();
+}
+
+static void PersistModAttrHelper(AtomicFile& fp, ConfigObject::Ptr& previousObject, const ConfigObject::Ptr& object, const String& attr, const Value& value)
+{
+ if (object != previousObject) {
+ if (previousObject) {
+ ConfigWriter::EmitRaw(fp, "\tobj.version = ");
+ ConfigWriter::EmitValue(fp, 0, previousObject->GetVersion());
+ ConfigWriter::EmitRaw(fp, "\n}\n\n");
+ }
+
+ ConfigWriter::EmitRaw(fp, "var obj = ");
+
+ Array::Ptr args1 = new Array({
+ object->GetReflectionType()->GetName(),
+ object->GetName()
+ });
+ ConfigWriter::EmitFunctionCall(fp, "get_object", args1);
+
+ ConfigWriter::EmitRaw(fp, "\nif (obj) {\n");
+ }
+
+ ConfigWriter::EmitRaw(fp, "\tobj.");
+
+ Array::Ptr args2 = new Array({
+ attr,
+ value
+ });
+ ConfigWriter::EmitFunctionCall(fp, "modify_attribute", args2);
+
+ ConfigWriter::EmitRaw(fp, "\n");
+
+ previousObject = object;
+}
+
+void IcingaApplication::DumpProgramState()
+{
+ ConfigObject::DumpObjects(Configuration::StatePath);
+ DumpModifiedAttributes();
+}
+
+void IcingaApplication::DumpModifiedAttributes()
+{
+ String path = Configuration::ModAttrPath;
+
+ try {
+ Utility::Glob(path + ".tmp.*", &Utility::Remove, GlobFile);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "IcingaApplication") << DiagnosticInformation(ex);
+ }
+
+ AtomicFile fp (path, 0644);
+
+ ConfigObject::Ptr previousObject;
+ ConfigObject::DumpModifiedAttributes([&fp, &previousObject](const ConfigObject::Ptr& object, const String& attr, const Value& value) {
+ PersistModAttrHelper(fp, previousObject, object, attr, value);
+ });
+
+ if (previousObject) {
+ ConfigWriter::EmitRaw(fp, "\tobj.version = ");
+ ConfigWriter::EmitValue(fp, 0, previousObject->GetVersion());
+ ConfigWriter::EmitRaw(fp, "\n}\n");
+ }
+
+ fp.Commit();
+}
+
+IcingaApplication::Ptr IcingaApplication::GetInstance()
+{
+ return static_pointer_cast<IcingaApplication>(Application::GetInstance());
+}
+
+bool IcingaApplication::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const
+{
+ double now = Utility::GetTime();
+
+ if (macro == "timet") {
+ *result = static_cast<long>(now);
+ return true;
+ } else if (macro == "long_date_time") {
+ *result = Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", now);
+ return true;
+ } else if (macro == "short_date_time") {
+ *result = Utility::FormatDateTime("%Y-%m-%d %H:%M:%S", now);
+ return true;
+ } else if (macro == "date") {
+ *result = Utility::FormatDateTime("%Y-%m-%d", now);
+ return true;
+ } else if (macro == "time") {
+ *result = Utility::FormatDateTime("%H:%M:%S %z", now);
+ return true;
+ } else if (macro == "uptime") {
+ *result = Utility::FormatDuration(Application::GetUptime());
+ return true;
+ }
+
+ if (macro.Contains("num_services")) {
+ ServiceStatistics ss = CIB::CalculateServiceStats();
+
+ if (macro == "num_services_ok") {
+ *result = ss.services_ok;
+ return true;
+ } else if (macro == "num_services_warning") {
+ *result = ss.services_warning;
+ return true;
+ } else if (macro == "num_services_critical") {
+ *result = ss.services_critical;
+ return true;
+ } else if (macro == "num_services_unknown") {
+ *result = ss.services_unknown;
+ return true;
+ } else if (macro == "num_services_pending") {
+ *result = ss.services_pending;
+ return true;
+ } else if (macro == "num_services_unreachable") {
+ *result = ss.services_unreachable;
+ return true;
+ } else if (macro == "num_services_flapping") {
+ *result = ss.services_flapping;
+ return true;
+ } else if (macro == "num_services_in_downtime") {
+ *result = ss.services_in_downtime;
+ return true;
+ } else if (macro == "num_services_acknowledged") {
+ *result = ss.services_acknowledged;
+ return true;
+ } else if (macro == "num_services_handled") {
+ *result = ss.services_handled;
+ return true;
+ } else if (macro == "num_services_problem") {
+ *result = ss.services_problem;
+ return true;
+ }
+ }
+ else if (macro.Contains("num_hosts")) {
+ HostStatistics hs = CIB::CalculateHostStats();
+
+ if (macro == "num_hosts_up") {
+ *result = hs.hosts_up;
+ return true;
+ } else if (macro == "num_hosts_down") {
+ *result = hs.hosts_down;
+ return true;
+ } else if (macro == "num_hosts_pending") {
+ *result = hs.hosts_pending;
+ return true;
+ } else if (macro == "num_hosts_unreachable") {
+ *result = hs.hosts_unreachable;
+ return true;
+ } else if (macro == "num_hosts_flapping") {
+ *result = hs.hosts_flapping;
+ return true;
+ } else if (macro == "num_hosts_in_downtime") {
+ *result = hs.hosts_in_downtime;
+ return true;
+ } else if (macro == "num_hosts_acknowledged") {
+ *result = hs.hosts_acknowledged;
+ return true;
+ } else if (macro == "num_hosts_handled") {
+ *result = hs.hosts_handled;
+ return true;
+ } else if (macro == "num_hosts_problem") {
+ *result = hs.hosts_problem;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+String IcingaApplication::GetNodeName() const
+{
+ return ScriptGlobal::Get("NodeName");
+}
+
+/* Intentionally kept here, since an agent may not have the CheckerComponent loaded. */
+int IcingaApplication::GetMaxConcurrentChecks() const
+{
+ return ScriptGlobal::Get("MaxConcurrentChecks");
+}
+
+String IcingaApplication::GetEnvironment() const
+{
+ return Application::GetAppEnvironment();
+}
+
+void IcingaApplication::SetEnvironment(const String& value, bool suppress_events, const Value& cookie)
+{
+ Application::SetAppEnvironment(value);
+}
+
+void IcingaApplication::ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ MacroProcessor::ValidateCustomVars(this, lvalue());
+}
diff --git a/lib/icinga/icingaapplication.hpp b/lib/icinga/icingaapplication.hpp
new file mode 100644
index 0000000..7888fa6
--- /dev/null
+++ b/lib/icinga/icingaapplication.hpp
@@ -0,0 +1,52 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ICINGAAPPLICATION_H
+#define ICINGAAPPLICATION_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/icingaapplication-ti.hpp"
+#include "icinga/macroresolver.hpp"
+
+namespace icinga
+{
+
+/**
+ * The Icinga application.
+ *
+ * @ingroup icinga
+ */
+class IcingaApplication final : public ObjectImpl<IcingaApplication>, public MacroResolver
+{
+public:
+ DECLARE_OBJECT(IcingaApplication);
+ DECLARE_OBJECTNAME(IcingaApplication);
+
+ static void StaticInitialize();
+
+ int Main() override;
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+
+ static IcingaApplication::Ptr GetInstance();
+
+ bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const override;
+
+ String GetNodeName() const;
+
+ int GetMaxConcurrentChecks() const;
+
+ String GetEnvironment() const override;
+ void SetEnvironment(const String& value, bool suppress_events = false, const Value& cookie = Empty) override;
+
+ void ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ void DumpProgramState();
+ void DumpModifiedAttributes();
+
+ void OnShutdown() override;
+};
+
+}
+
+#endif /* ICINGAAPPLICATION_H */
diff --git a/lib/icinga/icingaapplication.ti b/lib/icinga/icingaapplication.ti
new file mode 100644
index 0000000..1cdef74
--- /dev/null
+++ b/lib/icinga/icingaapplication.ti
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/application.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class IcingaApplication : Application
+{
+ activation_priority -50;
+
+ [config, no_storage, virtual] String environment {
+ get;
+ set;
+ default {{{ return Application::GetAppEnvironment(); }}}
+ };
+
+ [config] bool enable_notifications {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_event_handlers {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_flapping {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_host_checks {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_service_checks {
+ default {{{ return true; }}}
+ };
+ [config] bool enable_perfdata {
+ default {{{ return true; }}}
+ };
+ [config] Dictionary::Ptr vars;
+};
+
+}
diff --git a/lib/icinga/legacytimeperiod.cpp b/lib/icinga/legacytimeperiod.cpp
new file mode 100644
index 0000000..33e6665
--- /dev/null
+++ b/lib/icinga/legacytimeperiod.cpp
@@ -0,0 +1,644 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/legacytimeperiod.hpp"
+#include "base/function.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/debug.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, LegacyTimePeriod, &LegacyTimePeriod::ScriptFunc, "tp:begin:end");
+
+/**
+ * Returns the same as mktime() but does not modify its argument and takes a const pointer.
+ *
+ * @param t struct tm to convert to time_t
+ * @return time_t representing the timestamp given by t
+ */
+static time_t mktime_const(const tm *t) {
+ tm copy = *t;
+ return mktime(&copy);
+}
+
+bool LegacyTimePeriod::IsInTimeRange(const tm *begin, const tm *end, int stride, const tm *reference)
+{
+ time_t tsbegin, tsend, tsref;
+ tsbegin = mktime_const(begin);
+ tsend = mktime_const(end);
+ tsref = mktime_const(reference);
+
+ if (tsref < tsbegin || tsref > tsend)
+ return false;
+
+ int daynumber = (tsref - tsbegin) / (24 * 60 * 60);
+
+ if (stride > 1 && daynumber % stride > 0)
+ return false;
+
+ return true;
+}
+
+/**
+ * Update all day-related fields of reference (tm_year, tm_mon, tm_mday, tm_wday, tm_yday) to reference the n-th
+ * occurrence of a weekday (given by wday) in the month represented by the original value of reference.
+ *
+ * If n is negative, counting is done from the end of the month, so for example with wday=1 and n=-1, the result will be
+ * the last Monday in the month given by reference.
+ *
+ * @param wday Weekday (0 = Sunday, 1 = Monday, ..., 6 = Saturday, like tm_wday)
+ * @param n Search the n-th weekday (given by wday) in the month given by reference
+ * @param reference Input for the current month and output for the given day of that moth
+ */
+void LegacyTimePeriod::FindNthWeekday(int wday, int n, tm *reference)
+{
+ // Work on a copy to only update specific fields of reference (as documented).
+ tm t = *reference;
+
+ int dir, seen = 0;
+
+ if (n > 0) {
+ dir = 1;
+ } else {
+ n *= -1;
+ dir = -1;
+
+ /* Negative days are relative to the next month. */
+ t.tm_mon++;
+ }
+
+ ASSERT(n > 0);
+
+ t.tm_mday = 1;
+
+ for (;;) {
+ // Always operate on 00:00:00 with automatic DST detection, otherwise days could
+ // be skipped or counted twice if +-24 hours is not on the next or previous day.
+ t.tm_hour = 0;
+ t.tm_min = 0;
+ t.tm_sec = 0;
+ t.tm_isdst = -1;
+
+ mktime(&t);
+
+ if (t.tm_wday == wday) {
+ seen++;
+
+ if (seen == n)
+ break;
+ }
+
+ t.tm_mday += dir;
+ }
+
+ reference->tm_year = t.tm_year;
+ reference->tm_mon = t.tm_mon;
+ reference->tm_mday = t.tm_mday;
+ reference->tm_wday = t.tm_wday;
+ reference->tm_yday = t.tm_yday;
+}
+
+int LegacyTimePeriod::WeekdayFromString(const String& daydef)
+{
+ if (daydef == "sunday")
+ return 0;
+ else if (daydef == "monday")
+ return 1;
+ else if (daydef == "tuesday")
+ return 2;
+ else if (daydef == "wednesday")
+ return 3;
+ else if (daydef == "thursday")
+ return 4;
+ else if (daydef == "friday")
+ return 5;
+ else if (daydef == "saturday")
+ return 6;
+ else
+ return -1;
+}
+
+int LegacyTimePeriod::MonthFromString(const String& monthdef)
+{
+ if (monthdef == "january")
+ return 0;
+ else if (monthdef == "february")
+ return 1;
+ else if (monthdef == "march")
+ return 2;
+ else if (monthdef == "april")
+ return 3;
+ else if (monthdef == "may")
+ return 4;
+ else if (monthdef == "june")
+ return 5;
+ else if (monthdef == "july")
+ return 6;
+ else if (monthdef == "august")
+ return 7;
+ else if (monthdef == "september")
+ return 8;
+ else if (monthdef == "october")
+ return 9;
+ else if (monthdef == "november")
+ return 10;
+ else if (monthdef == "december")
+ return 11;
+ else
+ return -1;
+}
+
+boost::gregorian::date LegacyTimePeriod::GetEndOfMonthDay(int year, int month)
+{
+ boost::gregorian::date d(boost::gregorian::greg_year(year), boost::gregorian::greg_month(month), 1);
+
+ return d.end_of_month();
+}
+
+/**
+ * Finds the first day on or after the day given by reference and writes the beginning and end time of that day to
+ * the output parameters begin and end.
+ *
+ * @param timespec Day to find, for example "2021-10-20", "sunday", ...
+ * @param begin if != nullptr, set to 00:00:00 on that day
+ * @param end if != nullptr, set to 24:00:00 on that day (i.e. 00:00:00 of the next day)
+ * @param reference Time to begin the search at
+ */
+void LegacyTimePeriod::ParseTimeSpec(const String& timespec, tm *begin, tm *end, const tm *reference)
+{
+ /* YYYY-MM-DD */
+ if (timespec.GetLength() == 10 && timespec[4] == '-' && timespec[7] == '-') {
+ int year = Convert::ToLong(timespec.SubStr(0, 4));
+ int month = Convert::ToLong(timespec.SubStr(5, 2));
+ int day = Convert::ToLong(timespec.SubStr(8, 2));
+
+ if (month < 1 || month > 12)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid month in time specification: " + timespec));
+ if (day < 1 || day > 31)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid day in time specification: " + timespec));
+
+ if (begin) {
+ *begin = *reference;
+ begin->tm_year = year - 1900;
+ begin->tm_mon = month - 1;
+ begin->tm_mday = day;
+ begin->tm_hour = 0;
+ begin->tm_min = 0;
+ begin->tm_sec = 0;
+ begin->tm_isdst = -1;
+ }
+
+ if (end) {
+ *end = *reference;
+ end->tm_year = year - 1900;
+ end->tm_mon = month - 1;
+ end->tm_mday = day;
+ end->tm_hour = 24;
+ end->tm_min = 0;
+ end->tm_sec = 0;
+ end->tm_isdst = -1;
+ }
+
+ return;
+ }
+
+ std::vector<String> tokens = timespec.Split(" ");
+
+ int mon = -1;
+
+ if (tokens.size() > 1 && (tokens[0] == "day" || (mon = MonthFromString(tokens[0])) != -1)) {
+ if (mon == -1)
+ mon = reference->tm_mon;
+
+ int mday = Convert::ToLong(tokens[1]);
+
+ if (begin) {
+ *begin = *reference;
+ begin->tm_mon = mon;
+ begin->tm_mday = mday;
+ begin->tm_hour = 0;
+ begin->tm_min = 0;
+ begin->tm_sec = 0;
+ begin->tm_isdst = -1;
+
+ /* day -X: Negative days are relative to the next month. */
+ if (mday < 0) {
+ boost::gregorian::date d(GetEndOfMonthDay(reference->tm_year + 1900, mon + 1)); //TODO: Refactor this mess into full Boost.DateTime
+
+ //Depending on the number, we need to substract specific days (counting starts at 0).
+ d = d - boost::gregorian::days(mday * -1 - 1);
+
+ *begin = boost::gregorian::to_tm(d);
+ begin->tm_hour = 0;
+ begin->tm_min = 0;
+ begin->tm_sec = 0;
+ }
+ }
+
+ if (end) {
+ *end = *reference;
+ end->tm_mon = mon;
+ end->tm_mday = mday;
+ end->tm_hour = 24;
+ end->tm_min = 0;
+ end->tm_sec = 0;
+ end->tm_isdst = -1;
+
+ /* day -X: Negative days are relative to the next month. */
+ if (mday < 0) {
+ boost::gregorian::date d(GetEndOfMonthDay(reference->tm_year + 1900, mon + 1)); //TODO: Refactor this mess into full Boost.DateTime
+
+ //Depending on the number, we need to substract specific days (counting starts at 0).
+ d = d - boost::gregorian::days(mday * -1 - 1);
+
+ // End date is one day in the future, starting 00:00:00
+ d = d + boost::gregorian::days(1);
+
+ *end = boost::gregorian::to_tm(d);
+ end->tm_hour = 0;
+ end->tm_min = 0;
+ end->tm_sec = 0;
+ }
+ }
+
+ return;
+ }
+
+ int wday;
+
+ if (tokens.size() >= 1 && (wday = WeekdayFromString(tokens[0])) != -1) {
+ tm myref = *reference;
+ myref.tm_isdst = -1;
+
+ if (tokens.size() > 2) {
+ mon = MonthFromString(tokens[2]);
+
+ if (mon == -1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid month in time specification: " + timespec));
+
+ myref.tm_mon = mon;
+ }
+
+ int n = 0;
+
+ if (tokens.size() > 1)
+ n = Convert::ToLong(tokens[1]);
+
+ if (begin) {
+ *begin = myref;
+
+ if (tokens.size() > 1)
+ FindNthWeekday(wday, n, begin);
+ else
+ begin->tm_mday += (7 - begin->tm_wday + wday) % 7;
+
+ begin->tm_hour = 0;
+ begin->tm_min = 0;
+ begin->tm_sec = 0;
+ }
+
+ if (end) {
+ *end = myref;
+
+ if (tokens.size() > 1)
+ FindNthWeekday(wday, n, end);
+ else
+ end->tm_mday += (7 - end->tm_wday + wday) % 7;
+
+ end->tm_hour = 0;
+ end->tm_min = 0;
+ end->tm_sec = 0;
+ end->tm_mday++;
+ }
+
+ return;
+ }
+
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid time specification: " + timespec));
+}
+
+/**
+ * Parse a range of days.
+ *
+ * The input can have the following formats:
+ * begin
+ * begin - end
+ * begin / stride
+ * begin - end / stride
+ *
+ * @param timerange Text representation of a day range or a single day, for example "2021-10-20", "monday - friday", ...
+ * @param begin Output parameter set to 00:00:00 of the first day of the range
+ * @param end Output parameter set to 24:00:00 of the last day of the range (i.e. 00:00:00 of the day after)
+ * @param stride Output parameter for the stride (for every n-th day)
+ * @param reference Expand the range relative to this timestamp
+ */
+void LegacyTimePeriod::ParseTimeRange(const String& timerange, tm *begin, tm *end, int *stride, const tm *reference)
+{
+ String def = timerange;
+
+ /* Figure out the stride. */
+ size_t pos = def.FindFirstOf('/');
+
+ if (pos != String::NPos) {
+ String strStride = def.SubStr(pos + 1).Trim();
+ *stride = Convert::ToLong(strStride);
+
+ /* Remove the stride parameter from the definition. */
+ def = def.SubStr(0, pos);
+ } else {
+ *stride = 1; /* User didn't specify anything, assume default. */
+ }
+
+ /* Figure out whether the user has specified two dates. */
+ pos = def.Find("- ");
+
+ if (pos != String::NPos) {
+ String first = def.SubStr(0, pos).Trim();
+
+ String second = def.SubStr(pos + 1).Trim();
+
+ ParseTimeSpec(first, begin, nullptr, reference);
+
+ /* If the second definition starts with a number we need
+ * to add the first word from the first definition, e.g.:
+ * day 1 - 15 --> "day 15" */
+ bool is_number = true;
+ size_t xpos = second.FindFirstOf(' ');
+ String fword = second.SubStr(0, xpos);
+
+ try {
+ Convert::ToLong(fword);
+ } catch (...) {
+ is_number = false;
+ }
+
+ if (is_number) {
+ xpos = first.FindFirstOf(' ');
+ ASSERT(xpos != String::NPos);
+ second = first.SubStr(0, xpos + 1) + second;
+ }
+
+ ParseTimeSpec(second, nullptr, end, reference);
+ } else {
+ ParseTimeSpec(def, begin, end, reference);
+ }
+}
+
+bool LegacyTimePeriod::IsInDayDefinition(const String& daydef, const tm *reference)
+{
+ tm begin, end;
+ int stride;
+
+ ParseTimeRange(daydef, &begin, &end, &stride, reference);
+
+ Log(LogDebug, "LegacyTimePeriod")
+ << "ParseTimeRange: '" << daydef << "' => " << mktime(&begin)
+ << " -> " << mktime(&end) << ", stride: " << stride;
+
+ return IsInTimeRange(&begin, &end, stride, reference);
+}
+
+static inline
+void ProcessTimeRaw(const String& in, const tm *reference, tm *out)
+{
+ *out = *reference;
+
+ auto hd (in.Split(":"));
+
+ switch (hd.size()) {
+ case 2:
+ out->tm_sec = 0;
+ break;
+ case 3:
+ out->tm_sec = Convert::ToLong(hd[2]);
+ break;
+ default:
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid time specification: " + in));
+ }
+
+ out->tm_hour = Convert::ToLong(hd[0]);
+ out->tm_min = Convert::ToLong(hd[1]);
+}
+
+void LegacyTimePeriod::ProcessTimeRangeRaw(const String& timerange, const tm *reference, tm *begin, tm *end)
+{
+ std::vector<String> times = timerange.Split("-");
+
+ if (times.size() != 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid timerange: " + timerange));
+
+ ProcessTimeRaw(times[0], reference, begin);
+ ProcessTimeRaw(times[1], reference, end);
+
+ if (begin->tm_hour * 3600 + begin->tm_min * 60 + begin->tm_sec >=
+ end->tm_hour * 3600 + end->tm_min * 60 + end->tm_sec)
+ end->tm_hour += 24;
+}
+
+Dictionary::Ptr LegacyTimePeriod::ProcessTimeRange(const String& timestamp, const tm *reference)
+{
+ tm begin, end;
+
+ ProcessTimeRangeRaw(timestamp, reference, &begin, &end);
+
+ return new Dictionary({
+ { "begin", (long)mktime(&begin) },
+ { "end", (long)mktime(&end) }
+ });
+}
+
+/**
+ * Takes a list of timeranges end expands them to concrete timestamp based on a reference time.
+ *
+ * @param timeranges String of comma separated time ranges, for example "10:00-12:00", "12:15:30-12:23:43,16:00-18:00"
+ * @param reference Starting point for searching the segments
+ * @param result For each range, a dict with keys "begin" and "end" is added
+ */
+void LegacyTimePeriod::ProcessTimeRanges(const String& timeranges, const tm *reference, const Array::Ptr& result)
+{
+ std::vector<String> ranges = timeranges.Split(",");
+
+ for (const String& range : ranges) {
+ Dictionary::Ptr segment = ProcessTimeRange(range, reference);
+
+ if (segment->Get("begin") >= segment->Get("end"))
+ continue;
+
+ result->Add(segment);
+ }
+}
+
+Dictionary::Ptr LegacyTimePeriod::FindRunningSegment(const String& daydef, const String& timeranges, const tm *reference)
+{
+ tm begin, end, iter;
+ time_t tsend, tsiter, tsref;
+ int stride;
+
+ tsref = mktime_const(reference);
+
+ ParseTimeRange(daydef, &begin, &end, &stride, reference);
+
+ iter = begin;
+
+ tsend = mktime(&end);
+
+ do {
+ if (IsInTimeRange(&begin, &end, stride, &iter)) {
+ Array::Ptr segments = new Array();
+ ProcessTimeRanges(timeranges, &iter, segments);
+
+ Dictionary::Ptr bestSegment;
+ double bestEnd = 0.0;
+
+ ObjectLock olock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ double begin = segment->Get("begin");
+ double end = segment->Get("end");
+
+ if (begin >= tsref || end < tsref)
+ continue;
+
+ if (!bestSegment || end > bestEnd) {
+ bestSegment = segment;
+ bestEnd = end;
+ }
+ }
+
+ if (bestSegment)
+ return bestSegment;
+ }
+
+ iter.tm_mday++;
+ iter.tm_hour = 0;
+ iter.tm_min = 0;
+ iter.tm_sec = 0;
+ tsiter = mktime(&iter);
+ } while (tsiter < tsend);
+
+ return nullptr;
+}
+
+Dictionary::Ptr LegacyTimePeriod::FindNextSegment(const String& daydef, const String& timeranges, const tm *reference)
+{
+ tm begin, end, iter, ref;
+ time_t tsend, tsiter, tsref;
+ int stride;
+
+ for (int pass = 1; pass <= 2; pass++) {
+ if (pass == 1) {
+ ref = *reference;
+ } else {
+ ref = end;
+ ref.tm_mday++;
+ }
+
+ tsref = mktime(&ref);
+
+ ParseTimeRange(daydef, &begin, &end, &stride, &ref);
+
+ iter = begin;
+
+ tsend = mktime(&end);
+
+ do {
+ if (IsInTimeRange(&begin, &end, stride, &iter)) {
+ Array::Ptr segments = new Array();
+ ProcessTimeRanges(timeranges, &iter, segments);
+
+ Dictionary::Ptr bestSegment;
+ double bestBegin;
+
+ ObjectLock olock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ double begin = segment->Get("begin");
+
+ if (begin < tsref)
+ continue;
+
+ if (!bestSegment || begin < bestBegin) {
+ bestSegment = segment;
+ bestBegin = begin;
+ }
+ }
+
+ if (bestSegment)
+ return bestSegment;
+ }
+
+ iter.tm_mday++;
+ iter.tm_hour = 0;
+ iter.tm_min = 0;
+ iter.tm_sec = 0;
+ tsiter = mktime(&iter);
+ } while (tsiter < tsend);
+ }
+
+ return nullptr;
+}
+
+Array::Ptr LegacyTimePeriod::ScriptFunc(const TimePeriod::Ptr& tp, double begin, double end)
+{
+ Array::Ptr segments = new Array();
+
+ Dictionary::Ptr ranges = tp->GetRanges();
+
+ if (ranges) {
+ tm tm_begin = Utility::LocalTime(begin);
+
+ // Always evaluate time periods for full days as their ranges are given per day.
+ tm_begin.tm_hour = 0;
+ tm_begin.tm_min = 0;
+ tm_begin.tm_sec = 0;
+ tm_begin.tm_isdst = -1;
+
+ // Helper to move a struct tm to midnight of the next day for the loop below.
+ // Due to DST changes, this may move the time by something else than 24 hours.
+ auto advance_to_next_day = [](tm *t) {
+ t->tm_mday++;
+ t->tm_hour = 0;
+ t->tm_min = 0;
+ t->tm_sec = 0;
+ t->tm_isdst = -1;
+
+ // Normalize fields using mktime.
+ mktime(t);
+
+ // Reset tm_isdst so that future calls figure out the correct time zone after setting tm_hour/tm_min/tm_sec.
+ t->tm_isdst = -1;
+ };
+
+ for (tm reference = tm_begin; mktime_const(&reference) <= end; advance_to_next_day(&reference)) {
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "LegacyTimePeriod")
+ << "Checking reference time " << mktime_const(&reference);
+#endif /* I2_DEBUG */
+
+ ObjectLock olock(ranges);
+ for (const Dictionary::Pair& kv : ranges) {
+ if (!IsInDayDefinition(kv.first, &reference)) {
+#ifdef I2_DEBUG
+ Log(LogDebug, "LegacyTimePeriod")
+ << "Not in day definition '" << kv.first << "'.";
+#endif /* I2_DEBUG */
+ continue;
+ }
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "LegacyTimePeriod")
+ << "In day definition '" << kv.first << "'.";
+#endif /* I2_DEBUG */
+
+ ProcessTimeRanges(kv.second, &reference, segments);
+ }
+ }
+ }
+
+ Log(LogDebug, "LegacyTimePeriod")
+ << "Legacy timeperiod update returned " << segments->GetLength() << " segments.";
+
+ return segments;
+}
diff --git a/lib/icinga/legacytimeperiod.hpp b/lib/icinga/legacytimeperiod.hpp
new file mode 100644
index 0000000..001eb5c
--- /dev/null
+++ b/lib/icinga/legacytimeperiod.hpp
@@ -0,0 +1,45 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef LEGACYTIMEPERIOD_H
+#define LEGACYTIMEPERIOD_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/timeperiod.hpp"
+#include "base/dictionary.hpp"
+#include <boost/date_time/gregorian/gregorian.hpp>
+
+namespace icinga
+{
+
+/**
+ * Implements Icinga 1.x time periods.
+ *
+ * @ingroup icinga
+ */
+class LegacyTimePeriod
+{
+public:
+ static Array::Ptr ScriptFunc(const TimePeriod::Ptr& tp, double start, double end);
+
+ static bool IsInTimeRange(const tm *begin, const tm *end, int stride, const tm *reference);
+ static void FindNthWeekday(int wday, int n, tm *reference);
+ static int WeekdayFromString(const String& daydef);
+ static int MonthFromString(const String& monthdef);
+ static void ParseTimeSpec(const String& timespec, tm *begin, tm *end, const tm *reference);
+ static void ParseTimeRange(const String& timerange, tm *begin, tm *end, int *stride, const tm *reference);
+ static bool IsInDayDefinition(const String& daydef, const tm *reference);
+ static void ProcessTimeRangeRaw(const String& timerange, const tm *reference, tm *begin, tm *end);
+ static Dictionary::Ptr ProcessTimeRange(const String& timerange, const tm *reference);
+ static void ProcessTimeRanges(const String& timeranges, const tm *reference, const Array::Ptr& result);
+ static Dictionary::Ptr FindNextSegment(const String& daydef, const String& timeranges, const tm *reference);
+ static Dictionary::Ptr FindRunningSegment(const String& daydef, const String& timeranges, const tm *reference);
+
+private:
+ LegacyTimePeriod();
+
+ static boost::gregorian::date GetEndOfMonthDay(int year, int month);
+};
+
+}
+
+#endif /* LEGACYTIMEPERIOD_H */
diff --git a/lib/icinga/macroprocessor.cpp b/lib/icinga/macroprocessor.cpp
new file mode 100644
index 0000000..724a4f9
--- /dev/null
+++ b/lib/icinga/macroprocessor.cpp
@@ -0,0 +1,585 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/macroprocessor.hpp"
+#include "icinga/macroresolver.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/envresolver.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/configobject.hpp"
+#include "base/scriptframe.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/join.hpp>
+
+using namespace icinga;
+
+thread_local Dictionary::Ptr MacroResolver::OverrideMacros;
+
+Value MacroProcessor::ResolveMacros(const Value& str, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, String *missingMacro,
+ const MacroProcessor::EscapeCallback& escapeFn, const Dictionary::Ptr& resolvedMacros,
+ bool useResolvedMacros, int recursionLevel)
+{
+ if (useResolvedMacros)
+ REQUIRE_NOT_NULL(resolvedMacros);
+
+ Value result;
+
+ if (str.IsEmpty())
+ return Empty;
+
+ if (str.IsScalar()) {
+ result = InternalResolveMacros(str, resolvers, cr, missingMacro, escapeFn,
+ resolvedMacros, useResolvedMacros, recursionLevel + 1);
+ } else if (str.IsObjectType<Array>()) {
+ ArrayData resultArr;
+ Array::Ptr arr = str;
+
+ ObjectLock olock(arr);
+
+ for (const Value& arg : arr) {
+ /* Note: don't escape macros here. */
+ Value value = InternalResolveMacros(arg, resolvers, cr, missingMacro,
+ EscapeCallback(), resolvedMacros, useResolvedMacros, recursionLevel + 1);
+
+ if (value.IsObjectType<Array>())
+ resultArr.push_back(Utility::Join(value, ';'));
+ else
+ resultArr.push_back(value);
+ }
+
+ result = new Array(std::move(resultArr));
+ } else if (str.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr resultDict = new Dictionary();
+ Dictionary::Ptr dict = str;
+
+ ObjectLock olock(dict);
+
+ for (const Dictionary::Pair& kv : dict) {
+ /* Note: don't escape macros here. */
+ resultDict->Set(kv.first, InternalResolveMacros(kv.second, resolvers, cr, missingMacro,
+ EscapeCallback(), resolvedMacros, useResolvedMacros, recursionLevel + 1));
+ }
+
+ result = resultDict;
+ } else if (str.IsObjectType<Function>()) {
+ result = EvaluateFunction(str, resolvers, cr, escapeFn, resolvedMacros, useResolvedMacros, 0);
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Macro is not a string or array."));
+ }
+
+ return result;
+}
+
+static const EnvResolver::Ptr l_EnvResolver = new EnvResolver();
+
+static MacroProcessor::ResolverList GetDefaultResolvers()
+{
+ return {
+ { "icinga", IcingaApplication::GetInstance() },
+ { "env", l_EnvResolver, false }
+ };
+}
+
+bool MacroProcessor::ResolveMacro(const String& macro, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, Value *result, bool *recursive_macro)
+{
+ CONTEXT("Resolving macro '" << macro << "'");
+
+ *recursive_macro = false;
+
+ std::vector<String> tokens = macro.Split(".");
+
+ String objName;
+ if (tokens.size() > 1) {
+ objName = tokens[0];
+ tokens.erase(tokens.begin());
+ }
+
+ const auto defaultResolvers (GetDefaultResolvers());
+
+ for (auto resolverList : {&resolvers, &defaultResolvers}) {
+ for (auto& resolver : *resolverList) {
+ if (!objName.IsEmpty() && objName != resolver.Name)
+ continue;
+
+ if (objName.IsEmpty()) {
+ if (!resolver.ResolveShortMacros)
+ continue;
+
+ Dictionary::Ptr vars;
+ CustomVarObject::Ptr dobj = dynamic_pointer_cast<CustomVarObject>(resolver.Obj);
+
+ if (dobj) {
+ vars = dobj->GetVars();
+ } else {
+ auto app (dynamic_pointer_cast<IcingaApplication>(resolver.Obj));
+
+ if (app) {
+ vars = app->GetVars();
+ }
+ }
+
+ if (vars && vars->Contains(macro)) {
+ *result = vars->Get(macro);
+ *recursive_macro = true;
+ return true;
+ }
+ }
+
+ auto *mresolver = dynamic_cast<MacroResolver *>(resolver.Obj.get());
+
+ if (mresolver && mresolver->ResolveMacro(boost::algorithm::join(tokens, "."), cr, result))
+ return true;
+
+ Value ref = resolver.Obj;
+ bool valid = true;
+
+ for (const String& token : tokens) {
+ if (ref.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = ref;
+ if (dict->Contains(token)) {
+ ref = dict->Get(token);
+ continue;
+ } else {
+ valid = false;
+ break;
+ }
+ } else if (ref.IsObject()) {
+ Object::Ptr object = ref;
+
+ Type::Ptr type = object->GetReflectionType();
+
+ if (!type) {
+ valid = false;
+ break;
+ }
+
+ int field = type->GetFieldId(token);
+
+ if (field == -1) {
+ valid = false;
+ break;
+ }
+
+ ref = object->GetField(field);
+
+ Field fieldInfo = type->GetFieldInfo(field);
+
+ if (strcmp(fieldInfo.TypeName, "Timestamp") == 0)
+ ref = static_cast<long>(ref);
+ }
+ }
+
+ if (valid) {
+ if (tokens[0] == "vars" ||
+ tokens[0] == "action_url" ||
+ tokens[0] == "notes_url" ||
+ tokens[0] == "notes")
+ *recursive_macro = true;
+
+ *result = ref;
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+Value MacroProcessor::EvaluateFunction(const Function::Ptr& func, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, const MacroProcessor::EscapeCallback& escapeFn,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel)
+{
+ Dictionary::Ptr resolvers_this = new Dictionary();
+ const auto defaultResolvers (GetDefaultResolvers());
+
+ for (auto resolverList : {&resolvers, &defaultResolvers}) {
+ for (auto& resolver: *resolverList) {
+ resolvers_this->Set(resolver.Name, resolver.Obj);
+ }
+ }
+
+ auto internalResolveMacrosShim = [resolvers, cr, resolvedMacros, useResolvedMacros, recursionLevel](const std::vector<Value>& args) {
+ if (args.size() < 1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments for function"));
+
+ String missingMacro;
+
+ return MacroProcessor::InternalResolveMacros(args[0], resolvers, cr, &missingMacro, MacroProcessor::EscapeCallback(),
+ resolvedMacros, useResolvedMacros, recursionLevel);
+ };
+
+ resolvers_this->Set("macro", new Function("macro (temporary)", internalResolveMacrosShim, { "str" }));
+
+ auto internalResolveArgumentsShim = [resolvers, cr, resolvedMacros, useResolvedMacros, recursionLevel](const std::vector<Value>& args) {
+ if (args.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Too few arguments for function"));
+
+ return MacroProcessor::ResolveArguments(args[0], args[1], resolvers, cr,
+ resolvedMacros, useResolvedMacros, recursionLevel + 1);
+ };
+
+ resolvers_this->Set("resolve_arguments", new Function("resolve_arguments (temporary)", internalResolveArgumentsShim, { "command", "args" }));
+
+ return func->InvokeThis(resolvers_this);
+}
+
+Value MacroProcessor::InternalResolveMacros(const String& str, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, String *missingMacro,
+ const MacroProcessor::EscapeCallback& escapeFn, const Dictionary::Ptr& resolvedMacros,
+ bool useResolvedMacros, int recursionLevel)
+{
+ CONTEXT("Resolving macros for string '" << str << "'");
+
+ if (recursionLevel > 15)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Infinite recursion detected while resolving macros"));
+
+ size_t offset, pos_first, pos_second;
+ offset = 0;
+
+ Dictionary::Ptr resolvers_this;
+
+ String result = str;
+ while ((pos_first = result.FindFirstOf("$", offset)) != String::NPos) {
+ pos_second = result.FindFirstOf("$", pos_first + 1);
+
+ if (pos_second == String::NPos)
+ BOOST_THROW_EXCEPTION(std::runtime_error("Closing $ not found in macro format string."));
+
+ String name = result.SubStr(pos_first + 1, pos_second - pos_first - 1);
+
+ Value resolved_macro;
+ bool recursive_macro;
+ bool found;
+
+ if (useResolvedMacros) {
+ recursive_macro = false;
+ found = resolvedMacros->Contains(name);
+
+ if (found)
+ resolved_macro = resolvedMacros->Get(name);
+ } else
+ found = ResolveMacro(name, resolvers, cr, &resolved_macro, &recursive_macro);
+
+ /* $$ is an escape sequence for $. */
+ if (name.IsEmpty()) {
+ resolved_macro = "$";
+ found = true;
+ }
+
+ if (resolved_macro.IsObjectType<Function>()) {
+ resolved_macro = EvaluateFunction(resolved_macro, resolvers, cr, escapeFn,
+ resolvedMacros, useResolvedMacros, recursionLevel + 1);
+ }
+
+ if (!found) {
+ if (!missingMacro)
+ Log(LogWarning, "MacroProcessor")
+ << "Macro '" << name << "' is not defined.";
+ else
+ *missingMacro = name;
+ }
+
+ /* recursively resolve macros in the macro if it was a user macro */
+ if (recursive_macro) {
+ if (resolved_macro.IsObjectType<Array>()) {
+ Array::Ptr arr = resolved_macro;
+ ArrayData resolved_arr;
+
+ ObjectLock olock(arr);
+ for (const Value& value : arr) {
+ if (value.IsScalar()) {
+ resolved_arr.push_back(InternalResolveMacros(value,
+ resolvers, cr, missingMacro, EscapeCallback(), nullptr,
+ false, recursionLevel + 1));
+ } else
+ resolved_arr.push_back(value);
+ }
+
+ resolved_macro = new Array(std::move(resolved_arr));
+ } else if (resolved_macro.IsString()) {
+ resolved_macro = InternalResolveMacros(resolved_macro,
+ resolvers, cr, missingMacro, EscapeCallback(), nullptr,
+ false, recursionLevel + 1);
+ }
+ }
+
+ if (!useResolvedMacros && found && resolvedMacros)
+ resolvedMacros->Set(name, resolved_macro);
+
+ if (escapeFn)
+ resolved_macro = escapeFn(resolved_macro);
+
+ /* we're done if this is the only macro and there are no other non-macro parts in the string */
+ if (pos_first == 0 && pos_second == str.GetLength() - 1)
+ return resolved_macro;
+ else if (resolved_macro.IsObjectType<Array>())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Mixing both strings and non-strings in macros is not allowed."));
+
+ if (resolved_macro.IsObjectType<Array>()) {
+ /* don't allow mixing strings and arrays in macro strings */
+ if (pos_first != 0 || pos_second != str.GetLength() - 1)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Mixing both strings and non-strings in macros is not allowed."));
+
+ return resolved_macro;
+ }
+
+ String resolved_macro_str = resolved_macro;
+
+ result.Replace(pos_first, pos_second - pos_first + 1, resolved_macro_str);
+ offset = pos_first + resolved_macro_str.GetLength();
+ }
+
+ return result;
+}
+
+
+bool MacroProcessor::ValidateMacroString(const String& macro)
+{
+ if (macro.IsEmpty())
+ return true;
+
+ size_t pos_first, pos_second, offset;
+ offset = 0;
+
+ while ((pos_first = macro.FindFirstOf("$", offset)) != String::NPos) {
+ pos_second = macro.FindFirstOf("$", pos_first + 1);
+
+ if (pos_second == String::NPos)
+ return false;
+
+ offset = pos_second + 1;
+ }
+
+ return true;
+}
+
+void MacroProcessor::ValidateCustomVars(const ConfigObject::Ptr& object, const Dictionary::Ptr& value)
+{
+ if (!value)
+ return;
+
+ /* string, array, dictionary */
+ ObjectLock olock(value);
+ for (const Dictionary::Pair& kv : value) {
+ const Value& varval = kv.second;
+
+ if (varval.IsObjectType<Dictionary>()) {
+ /* only one dictonary level */
+ Dictionary::Ptr varval_dict = varval;
+
+ ObjectLock xlock(varval_dict);
+ for (const Dictionary::Pair& kv_var : varval_dict) {
+ if (!kv_var.second.IsString())
+ continue;
+
+ if (!ValidateMacroString(kv_var.second))
+ BOOST_THROW_EXCEPTION(ValidationError(object.get(), { "vars", kv.first, kv_var.first }, "Closing $ not found in macro format string '" + kv_var.second + "'."));
+ }
+ } else if (varval.IsObjectType<Array>()) {
+ /* check all array entries */
+ Array::Ptr varval_arr = varval;
+
+ ObjectLock ylock (varval_arr);
+ for (const Value& arrval : varval_arr) {
+ if (!arrval.IsString())
+ continue;
+
+ if (!ValidateMacroString(arrval)) {
+ BOOST_THROW_EXCEPTION(ValidationError(object.get(), { "vars", kv.first }, "Closing $ not found in macro format string '" + arrval + "'."));
+ }
+ }
+ } else {
+ if (!varval.IsString())
+ continue;
+
+ if (!ValidateMacroString(varval))
+ BOOST_THROW_EXCEPTION(ValidationError(object.get(), { "vars", kv.first }, "Closing $ not found in macro format string '" + varval + "'."));
+ }
+ }
+}
+
+void MacroProcessor::AddArgumentHelper(const Array::Ptr& args, const String& key, const String& value,
+ bool add_key, bool add_value, const Value& separator)
+{
+ if (add_key && separator.GetType() != ValueEmpty && add_value) {
+ args->Add(key + separator + value);
+ } else {
+ if (add_key)
+ args->Add(key);
+
+ if (add_value)
+ args->Add(value);
+ }
+}
+
+Value MacroProcessor::EscapeMacroShellArg(const Value& value)
+{
+ String result;
+
+ if (value.IsObjectType<Array>()) {
+ Array::Ptr arr = value;
+
+ ObjectLock olock(arr);
+ for (const Value& arg : arr) {
+ if (result.GetLength() > 0)
+ result += " ";
+
+ result += Utility::EscapeShellArg(arg);
+ }
+ } else
+ result = Utility::EscapeShellArg(value);
+
+ return result;
+}
+
+struct CommandArgument
+{
+ int Order{0};
+ bool SkipKey{false};
+ bool RepeatKey{true};
+ bool SkipValue{false};
+ String Key;
+ Value Separator;
+ Value AValue;
+
+ bool operator<(const CommandArgument& rhs) const
+ {
+ return Order < rhs.Order;
+ }
+};
+
+Value MacroProcessor::ResolveArguments(const Value& command, const Dictionary::Ptr& arguments,
+ const MacroProcessor::ResolverList& resolvers, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel)
+{
+ if (useResolvedMacros)
+ REQUIRE_NOT_NULL(resolvedMacros);
+
+ Value resolvedCommand;
+ if (!arguments || command.IsObjectType<Array>() || command.IsObjectType<Function>())
+ resolvedCommand = MacroProcessor::ResolveMacros(command, resolvers, cr, nullptr,
+ EscapeMacroShellArg, resolvedMacros, useResolvedMacros, recursionLevel + 1);
+ else {
+ resolvedCommand = new Array({ command });
+ }
+
+ if (arguments) {
+ std::vector<CommandArgument> args;
+
+ ObjectLock olock(arguments);
+ for (const Dictionary::Pair& kv : arguments) {
+ const Value& arginfo = kv.second;
+
+ CommandArgument arg;
+ arg.Key = kv.first;
+
+ bool required = false;
+ Value argval;
+
+ if (arginfo.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr argdict = arginfo;
+ if (argdict->Contains("key"))
+ arg.Key = argdict->Get("key");
+ argval = argdict->Get("value");
+ if (argdict->Contains("required"))
+ required = argdict->Get("required");
+ arg.SkipKey = argdict->Get("skip_key");
+ if (argdict->Contains("repeat_key"))
+ arg.RepeatKey = argdict->Get("repeat_key");
+ arg.Order = argdict->Get("order");
+ arg.Separator = argdict->Get("separator");
+
+ Value set_if = argdict->Get("set_if");
+
+ if (!set_if.IsEmpty()) {
+ String missingMacro;
+ Value set_if_resolved = MacroProcessor::ResolveMacros(set_if, resolvers,
+ cr, &missingMacro, MacroProcessor::EscapeCallback(), resolvedMacros,
+ useResolvedMacros, recursionLevel + 1);
+
+ if (!missingMacro.IsEmpty())
+ continue;
+
+ int value;
+
+ if (set_if_resolved == "true")
+ value = 1;
+ else if (set_if_resolved == "false")
+ value = 0;
+ else {
+ try {
+ value = Convert::ToLong(set_if_resolved);
+ } catch (const std::exception& ex) {
+ /* tried to convert a string */
+ Log(LogWarning, "PluginUtility")
+ << "Error evaluating set_if value '" << set_if_resolved
+ << "' used in argument '" << arg.Key << "': " << ex.what();
+ continue;
+ }
+ }
+
+ if (!value)
+ continue;
+ }
+ }
+ else
+ argval = arginfo;
+
+ if (argval.IsEmpty())
+ arg.SkipValue = true;
+
+ String missingMacro;
+ arg.AValue = MacroProcessor::ResolveMacros(argval, resolvers,
+ cr, &missingMacro, MacroProcessor::EscapeCallback(), resolvedMacros,
+ useResolvedMacros, recursionLevel + 1);
+
+ if (!missingMacro.IsEmpty()) {
+ if (required) {
+ BOOST_THROW_EXCEPTION(ScriptError("Non-optional macro '" + missingMacro + "' used in argument '" +
+ arg.Key + "' is missing."));
+ }
+
+ continue;
+ }
+
+ args.emplace_back(std::move(arg));
+ }
+
+ std::sort(args.begin(), args.end());
+
+ Array::Ptr command_arr = resolvedCommand;
+ for (const CommandArgument& arg : args) {
+
+ if (arg.AValue.IsObjectType<Dictionary>()) {
+ Log(LogWarning, "PluginUtility")
+ << "Tried to use dictionary in argument '" << arg.Key << "'.";
+ continue;
+ } else if (arg.AValue.IsObjectType<Array>()) {
+ bool first = true;
+ Array::Ptr arr = static_cast<Array::Ptr>(arg.AValue);
+
+ ObjectLock olock(arr);
+ for (const Value& value : arr) {
+ bool add_key;
+
+ if (first) {
+ first = false;
+ add_key = !arg.SkipKey;
+ } else
+ add_key = !arg.SkipKey && arg.RepeatKey;
+
+ AddArgumentHelper(command_arr, arg.Key, value, add_key, !arg.SkipValue, arg.Separator);
+ }
+ } else
+ AddArgumentHelper(command_arr, arg.Key, arg.AValue, !arg.SkipKey, !arg.SkipValue, arg.Separator);
+ }
+ }
+
+ return resolvedCommand;
+}
diff --git a/lib/icinga/macroprocessor.hpp b/lib/icinga/macroprocessor.hpp
new file mode 100644
index 0000000..7e74821
--- /dev/null
+++ b/lib/icinga/macroprocessor.hpp
@@ -0,0 +1,75 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MACROPROCESSOR_H
+#define MACROPROCESSOR_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkable.hpp"
+#include "base/value.hpp"
+#include <vector>
+#include <utility>
+
+namespace icinga
+{
+
+/**
+ * Resolves macros.
+ *
+ * @ingroup icinga
+ */
+class MacroProcessor
+{
+public:
+ struct ResolverSpec
+ {
+ String Name;
+ Object::Ptr Obj;
+
+ // Whether to resolve not only e.g. $host.address$, but also just $address$
+ bool ResolveShortMacros;
+
+ inline ResolverSpec(String name, Object::Ptr obj, bool resolveShortMacros = true)
+ : Name(std::move(name)), Obj(std::move(obj)), ResolveShortMacros(resolveShortMacros)
+ {
+ }
+ };
+
+ typedef std::function<Value (const Value&)> EscapeCallback;
+ typedef std::vector<ResolverSpec> ResolverList;
+
+ static Value ResolveMacros(const Value& str, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr = nullptr, String *missingMacro = nullptr,
+ const EscapeCallback& escapeFn = EscapeCallback(),
+ const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false, int recursionLevel = 0);
+
+ static Value ResolveArguments(const Value& command, const Dictionary::Ptr& arguments,
+ const MacroProcessor::ResolverList& resolvers, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel = 0);
+
+ static bool ValidateMacroString(const String& macro);
+ static void ValidateCustomVars(const ConfigObject::Ptr& object, const Dictionary::Ptr& value);
+
+private:
+ MacroProcessor();
+
+ static bool ResolveMacro(const String& macro, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, Value *result, bool *recursive_macro);
+ static Value InternalResolveMacros(const String& str,
+ const ResolverList& resolvers, const CheckResult::Ptr& cr,
+ String *missingMacro, const EscapeCallback& escapeFn,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros,
+ int recursionLevel = 0);
+ static Value EvaluateFunction(const Function::Ptr& func, const ResolverList& resolvers,
+ const CheckResult::Ptr& cr, const MacroProcessor::EscapeCallback& escapeFn,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel);
+
+ static void AddArgumentHelper(const Array::Ptr& args, const String& key, const String& value,
+ bool add_key, bool add_value, const Value& separator);
+ static Value EscapeMacroShellArg(const Value& value);
+
+};
+
+}
+
+#endif /* MACROPROCESSOR_H */
diff --git a/lib/icinga/macroresolver.hpp b/lib/icinga/macroresolver.hpp
new file mode 100644
index 0000000..62cd41d
--- /dev/null
+++ b/lib/icinga/macroresolver.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MACRORESOLVER_H
+#define MACRORESOLVER_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkresult.hpp"
+#include "base/dictionary.hpp"
+#include "base/string.hpp"
+
+namespace icinga
+{
+
+/**
+ * Resolves macros.
+ *
+ * @ingroup icinga
+ */
+class MacroResolver
+{
+public:
+ DECLARE_PTR_TYPEDEFS(MacroResolver);
+
+ static thread_local Dictionary::Ptr OverrideMacros;
+
+ virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const = 0;
+};
+
+}
+
+#endif /* MACRORESOLVER_H */
diff --git a/lib/icinga/notification-apply.cpp b/lib/icinga/notification-apply.cpp
new file mode 100644
index 0000000..f5b3764
--- /dev/null
+++ b/lib/icinga/notification-apply.cpp
@@ -0,0 +1,161 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/notification.hpp"
+#include "icinga/service.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE([]() {
+ ApplyRule::RegisterType("Notification", { "Host", "Service" });
+});
+
+bool Notification::EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter)
+{
+ if (!skipFilter && !rule.EvaluateFilter(frame))
+ return false;
+
+ auto& di (rule.GetDebugInfo());
+
+#ifdef _DEBUG
+ Log(LogDebug, "Notification")
+ << "Applying notification '" << name << "' to object '" << checkable->GetName() << "' for rule " << di;
+#endif /* _DEBUG */
+
+ ConfigItemBuilder builder{di};
+ builder.SetType(Notification::TypeInstance);
+ builder.SetName(name);
+ builder.SetScope(frame.Locals->ShallowClone());
+ builder.SetIgnoreOnError(rule.GetIgnoreOnError());
+
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+
+ if (service)
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "service_name"), OpSetLiteral, MakeLiteral(service->GetShortName()), di));
+
+ String zone = checkable->GetZoneName();
+
+ if (!zone.IsEmpty())
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "zone"), OpSetLiteral, MakeLiteral(zone), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "package"), OpSetLiteral, MakeLiteral(rule.GetPackage()), di));
+
+ builder.AddExpression(new OwnedExpression(rule.GetExpression()));
+
+ ConfigItem::Ptr notificationItem = builder.Compile();
+ notificationItem->Register();
+
+ return true;
+}
+
+bool Notification::EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter)
+{
+ auto& di (rule.GetDebugInfo());
+
+ CONTEXT("Evaluating 'apply' rule (" << di << ")");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ScriptFrame frame(true);
+ if (rule.GetScope())
+ rule.GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+ if (service)
+ frame.Locals->Set("service", service);
+
+ Value vinstances;
+
+ if (rule.GetFTerm()) {
+ try {
+ vinstances = rule.GetFTerm()->Evaluate(frame);
+ } catch (const std::exception&) {
+ /* Silently ignore errors here and assume there are no instances. */
+ return false;
+ }
+ } else {
+ vinstances = new Array({ "" });
+ }
+
+ bool match = false;
+
+ if (vinstances.IsObjectType<Array>()) {
+ if (!rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Dictionary iterator requires value to be a dictionary.", di));
+
+ Array::Ptr arr = vinstances;
+
+ ObjectLock olock(arr);
+ for (const Value& instance : arr) {
+ String name = rule.GetName();
+
+ if (!rule.GetFKVar().IsEmpty()) {
+ frame.Locals->Set(rule.GetFKVar(), instance);
+ name += instance;
+ }
+
+ if (EvaluateApplyRuleInstance(checkable, name, frame, rule, skipFilter))
+ match = true;
+ }
+ } else if (vinstances.IsObjectType<Dictionary>()) {
+ if (rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Array iterator requires value to be an array.", di));
+
+ Dictionary::Ptr dict = vinstances;
+
+ for (const String& key : dict->GetKeys()) {
+ frame.Locals->Set(rule.GetFKVar(), key);
+ frame.Locals->Set(rule.GetFVVar(), dict->Get(key));
+
+ if (EvaluateApplyRuleInstance(checkable, rule.GetName() + key, frame, rule, skipFilter))
+ match = true;
+ }
+ }
+
+ return match;
+}
+
+void Notification::EvaluateApplyRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating 'apply' rules for host '" << host->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Notification::TypeInstance, Host::TypeInstance))
+ {
+ if (EvaluateApplyRule(host, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedHostRules(Notification::TypeInstance, host->GetName())) {
+ if (EvaluateApplyRule(host, *rule, true))
+ rule->AddMatch();
+ }
+}
+
+void Notification::EvaluateApplyRules(const Service::Ptr& service)
+{
+ CONTEXT("Evaluating 'apply' rules for service '" << service->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Notification::TypeInstance, Service::TypeInstance)) {
+ if (EvaluateApplyRule(service, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedServiceRules(Notification::TypeInstance, service->GetHost()->GetName(), service->GetShortName())) {
+ if (EvaluateApplyRule(service, *rule, true))
+ rule->AddMatch();
+ }
+}
diff --git a/lib/icinga/notification.cpp b/lib/icinga/notification.cpp
new file mode 100644
index 0000000..ab8d42b
--- /dev/null
+++ b/lib/icinga/notification.cpp
@@ -0,0 +1,812 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/notification.hpp"
+#include "icinga/notification-ti.cpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/service.hpp"
+#include "remote/apilistener.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include "base/initialize.hpp"
+#include "base/scriptglobal.hpp"
+#include <algorithm>
+
+using namespace icinga;
+
+REGISTER_TYPE(Notification);
+INITIALIZE_ONCE(&Notification::StaticInitialize);
+
+std::map<String, int> Notification::m_StateFilterMap;
+std::map<String, int> Notification::m_TypeFilterMap;
+
+boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> Notification::OnNextNotificationChanged;
+boost::signals2::signal<void (const Notification::Ptr&, const String&, uint_fast8_t, const MessageOrigin::Ptr&)> Notification::OnLastNotifiedStatePerUserUpdated;
+boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> Notification::OnLastNotifiedStatePerUserCleared;
+
+String NotificationNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Notification::Ptr notification = dynamic_pointer_cast<Notification>(context);
+
+ if (!notification)
+ return "";
+
+ String name = notification->GetHostName();
+
+ if (!notification->GetServiceName().IsEmpty())
+ name += "!" + notification->GetServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr NotificationNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Notification name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void Notification::StaticInitialize()
+{
+ ScriptGlobal::Set("Icinga.OK", "OK");
+ ScriptGlobal::Set("Icinga.Warning", "Warning");
+ ScriptGlobal::Set("Icinga.Critical", "Critical");
+ ScriptGlobal::Set("Icinga.Unknown", "Unknown");
+ ScriptGlobal::Set("Icinga.Up", "Up");
+ ScriptGlobal::Set("Icinga.Down", "Down");
+
+ ScriptGlobal::Set("Icinga.DowntimeStart", "DowntimeStart");
+ ScriptGlobal::Set("Icinga.DowntimeEnd", "DowntimeEnd");
+ ScriptGlobal::Set("Icinga.DowntimeRemoved", "DowntimeRemoved");
+ ScriptGlobal::Set("Icinga.Custom", "Custom");
+ ScriptGlobal::Set("Icinga.Acknowledgement", "Acknowledgement");
+ ScriptGlobal::Set("Icinga.Problem", "Problem");
+ ScriptGlobal::Set("Icinga.Recovery", "Recovery");
+ ScriptGlobal::Set("Icinga.FlappingStart", "FlappingStart");
+ ScriptGlobal::Set("Icinga.FlappingEnd", "FlappingEnd");
+
+ m_StateFilterMap["OK"] = StateFilterOK;
+ m_StateFilterMap["Warning"] = StateFilterWarning;
+ m_StateFilterMap["Critical"] = StateFilterCritical;
+ m_StateFilterMap["Unknown"] = StateFilterUnknown;
+ m_StateFilterMap["Up"] = StateFilterUp;
+ m_StateFilterMap["Down"] = StateFilterDown;
+
+ m_TypeFilterMap["DowntimeStart"] = NotificationDowntimeStart;
+ m_TypeFilterMap["DowntimeEnd"] = NotificationDowntimeEnd;
+ m_TypeFilterMap["DowntimeRemoved"] = NotificationDowntimeRemoved;
+ m_TypeFilterMap["Custom"] = NotificationCustom;
+ m_TypeFilterMap["Acknowledgement"] = NotificationAcknowledgement;
+ m_TypeFilterMap["Problem"] = NotificationProblem;
+ m_TypeFilterMap["Recovery"] = NotificationRecovery;
+ m_TypeFilterMap["FlappingStart"] = NotificationFlappingStart;
+ m_TypeFilterMap["FlappingEnd"] = NotificationFlappingEnd;
+}
+
+void Notification::OnConfigLoaded()
+{
+ ObjectImpl<Notification>::OnConfigLoaded();
+
+ SetTypeFilter(FilterArrayToInt(GetTypes(), GetTypeFilterMap(), ~0));
+ SetStateFilter(FilterArrayToInt(GetStates(), GetStateFilterMap(), ~0));
+}
+
+void Notification::OnAllConfigLoaded()
+{
+ ObjectImpl<Notification>::OnAllConfigLoaded();
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+
+ if (GetServiceName().IsEmpty())
+ m_Checkable = host;
+ else
+ m_Checkable = host->GetServiceByShortName(GetServiceName());
+
+ if (!m_Checkable)
+ BOOST_THROW_EXCEPTION(ScriptError("Notification object refers to a host/service which doesn't exist.", GetDebugInfo()));
+
+ GetCheckable()->RegisterNotification(this);
+}
+
+void Notification::Start(bool runtimeCreated)
+{
+ Checkable::Ptr obj = GetCheckable();
+
+ if (obj)
+ obj->RegisterNotification(this);
+
+ if (ApiListener::IsHACluster() && GetNextNotification() < Utility::GetTime() + 60)
+ SetNextNotification(Utility::GetTime() + 60, true);
+
+ for (const UserGroup::Ptr& group : GetUserGroups())
+ group->AddNotification(this);
+
+ ObjectImpl<Notification>::Start(runtimeCreated);
+}
+
+void Notification::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<Notification>::Stop(runtimeRemoved);
+
+ Checkable::Ptr obj = GetCheckable();
+
+ if (obj)
+ obj->UnregisterNotification(this);
+
+ for (const UserGroup::Ptr& group : GetUserGroups())
+ group->RemoveNotification(this);
+}
+
+Checkable::Ptr Notification::GetCheckable() const
+{
+ return static_pointer_cast<Checkable>(m_Checkable);
+}
+
+NotificationCommand::Ptr Notification::GetCommand() const
+{
+ return NotificationCommand::GetByName(GetCommandRaw());
+}
+
+std::set<User::Ptr> Notification::GetUsers() const
+{
+ std::set<User::Ptr> result;
+
+ Array::Ptr users = GetUsersRaw();
+
+ if (users) {
+ ObjectLock olock(users);
+
+ for (const String& name : users) {
+ User::Ptr user = User::GetByName(name);
+
+ if (!user)
+ continue;
+
+ result.insert(user);
+ }
+ }
+
+ return result;
+}
+
+std::set<UserGroup::Ptr> Notification::GetUserGroups() const
+{
+ std::set<UserGroup::Ptr> result;
+
+ Array::Ptr groups = GetUserGroupsRaw();
+
+ if (groups) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ UserGroup::Ptr ug = UserGroup::GetByName(name);
+
+ if (!ug)
+ continue;
+
+ result.insert(ug);
+ }
+ }
+
+ return result;
+}
+
+TimePeriod::Ptr Notification::GetPeriod() const
+{
+ return TimePeriod::GetByName(GetPeriodRaw());
+}
+
+void Notification::UpdateNotificationNumber()
+{
+ SetNotificationNumber(GetNotificationNumber() + 1);
+}
+
+void Notification::ResetNotificationNumber()
+{
+ SetNotificationNumber(0);
+}
+
+void Notification::BeginExecuteNotification(NotificationType type, const CheckResult::Ptr& cr, bool force, bool reminder, const String& author, const String& text)
+{
+ String notificationName = GetName();
+ String notificationTypeName = NotificationTypeToString(type);
+
+ Log(LogNotice, "Notification")
+ << "Attempting to send " << (reminder ? "reminder " : "")
+ << "notifications of type '" << notificationTypeName
+ << "' for notification object '" << notificationName << "'.";
+
+ if (type == NotificationRecovery) {
+ auto states (GetLastNotifiedStatePerUser());
+
+ states->Clear();
+ OnLastNotifiedStatePerUserCleared(this, nullptr);
+ }
+
+ Checkable::Ptr checkable = GetCheckable();
+
+ if (!force) {
+ TimePeriod::Ptr tp = GetPeriod();
+
+ if (tp && !tp->IsInside(Utility::GetTime())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '" << notificationName
+ << "': not in timeperiod '" << tp->GetName() << "'";
+
+ if (!reminder) {
+ switch (type) {
+ case NotificationProblem:
+ case NotificationRecovery:
+ case NotificationFlappingStart:
+ case NotificationFlappingEnd:
+ {
+ /* If a non-reminder notification was suppressed, but just because of its time period,
+ * stash it into a notification types bitmask for maybe re-sending later.
+ */
+
+ ObjectLock olock (this);
+ int suppressedTypesBefore (GetSuppressedNotifications());
+ int suppressedTypesAfter (suppressedTypesBefore | type);
+
+ for (int conflict : {NotificationProblem | NotificationRecovery, NotificationFlappingStart | NotificationFlappingEnd}) {
+ /* E.g. problem and recovery notifications neutralize each other. */
+
+ if ((suppressedTypesAfter & conflict) == conflict) {
+ suppressedTypesAfter &= ~conflict;
+ }
+ }
+
+ if (suppressedTypesAfter != suppressedTypesBefore) {
+ SetSuppressedNotifications(suppressedTypesAfter);
+ }
+ }
+ default:
+ ; // Cheating the compiler on "5 enumeration values not handled in switch"
+ }
+ }
+
+ return;
+ }
+
+ double now = Utility::GetTime();
+ Dictionary::Ptr times = GetTimes();
+
+ if (times && type == NotificationProblem) {
+ Value timesBegin = times->Get("begin");
+ Value timesEnd = times->Get("end");
+
+ if (timesBegin != Empty && timesBegin >= 0 && now < checkable->GetLastHardStateChange() + timesBegin) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << "': before specified begin time (" << Utility::FormatDuration(timesBegin) << ")";
+
+ /* we need to adjust the next notification time
+ * delaying the first notification
+ */
+ SetNextNotification(checkable->GetLastHardStateChange() + timesBegin + 1.0);
+
+ /*
+ * We need to set no more notifications to false, in case
+ * some notifications were sent previously
+ */
+ SetNoMoreNotifications(false);
+
+ return;
+ }
+
+ if (timesEnd != Empty && timesEnd >= 0 && now > checkable->GetLastHardStateChange() + timesEnd) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << "': after specified end time (" << Utility::FormatDuration(timesEnd) << ")";
+ return;
+ }
+ }
+
+ unsigned long ftype = type;
+
+ Log(LogDebug, "Notification")
+ << "Type '" << NotificationTypeToString(type)
+ << "', TypeFilter: " << NotificationFilterToString(GetTypeFilter(), GetTypeFilterMap())
+ << " (FType=" << ftype << ", TypeFilter=" << GetTypeFilter() << ")";
+
+ if (!(ftype & GetTypeFilter())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << "': type '"
+ << NotificationTypeToString(type) << "' does not match type filter: "
+ << NotificationFilterToString(GetTypeFilter(), GetTypeFilterMap()) << ".";
+
+ /* Ensure to reset no_more_notifications on Recovery notifications,
+ * even if the admin did not configure them in the filter.
+ */
+ {
+ ObjectLock olock(this);
+ if (type == NotificationRecovery && GetInterval() <= 0)
+ SetNoMoreNotifications(false);
+ }
+
+ return;
+ }
+
+ /* Check state filters for problem notifications. Recovery notifications will be filtered away later. */
+ if (type == NotificationProblem) {
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ unsigned long fstate;
+ String stateStr;
+
+ if (service) {
+ fstate = ServiceStateToFilter(service->GetState());
+ stateStr = NotificationServiceStateToString(service->GetState());
+ } else {
+ fstate = HostStateToFilter(host->GetState());
+ stateStr = NotificationHostStateToString(host->GetState());
+ }
+
+ Log(LogDebug, "Notification")
+ << "State '" << stateStr << "', StateFilter: " << NotificationFilterToString(GetStateFilter(), GetStateFilterMap())
+ << " (FState=" << fstate << ", StateFilter=" << GetStateFilter() << ")";
+
+ if (!(fstate & GetStateFilter())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << "': state '" << stateStr
+ << "' does not match state filter: " << NotificationFilterToString(GetStateFilter(), GetStateFilterMap()) << ".";
+ return;
+ }
+ }
+ } else {
+ Log(LogNotice, "Notification")
+ << "Not checking " << (reminder ? "reminder " : "") << "notification filters for notification object '"
+ << notificationName << "': Notification was forced.";
+ }
+
+ {
+ ObjectLock olock(this);
+
+ UpdateNotificationNumber();
+ double now = Utility::GetTime();
+ SetLastNotification(now);
+
+ if (type == NotificationProblem && GetInterval() <= 0)
+ SetNoMoreNotifications(true);
+ else
+ SetNoMoreNotifications(false);
+
+ if (type == NotificationProblem && GetInterval() > 0)
+ SetNextNotification(now + GetInterval());
+
+ if (type == NotificationProblem)
+ SetLastProblemNotification(now);
+ }
+
+ std::set<User::Ptr> allUsers;
+
+ std::set<User::Ptr> users = GetUsers();
+ std::copy(users.begin(), users.end(), std::inserter(allUsers, allUsers.begin()));
+
+ for (const UserGroup::Ptr& ug : GetUserGroups()) {
+ std::set<User::Ptr> members = ug->GetMembers();
+ std::copy(members.begin(), members.end(), std::inserter(allUsers, allUsers.begin()));
+ }
+
+ std::set<User::Ptr> allNotifiedUsers;
+ Array::Ptr notifiedProblemUsers = GetNotifiedProblemUsers();
+
+ for (const User::Ptr& user : allUsers) {
+ String userName = user->GetName();
+
+ if (!user->GetEnableNotifications()) {
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': Disabled notifications for user '"
+ << userName << "'. Not sending notification.";
+ continue;
+ }
+
+ if (!CheckNotificationUserFilters(type, user, force, reminder)) {
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': Filters for user '" << userName << "' not matched. Not sending notification.";
+ continue;
+ }
+
+ /* on recovery, check if user was notified before */
+ if (type == NotificationRecovery) {
+ if (!notifiedProblemUsers->Contains(userName) && (NotificationProblem & user->GetTypeFilter())) {
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': We did not notify user '" << userName
+ << "' (Problem types enabled) for a problem before. Not sending Recovery notification.";
+ continue;
+ }
+ }
+
+ /* on acknowledgement, check if user was notified before */
+ if (type == NotificationAcknowledgement) {
+ if (!notifiedProblemUsers->Contains(userName) && (NotificationProblem & user->GetTypeFilter())) {
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': We did not notify user '" << userName
+ << "' (Problem types enabled) for a problem before. Not sending acknowledgement notification.";
+ continue;
+ }
+ }
+
+ if (type == NotificationProblem && !reminder && !checkable->GetVolatile()) {
+ auto [host, service] = GetHostService(checkable);
+ uint_fast8_t state = service ? service->GetState() : host->GetState();
+
+ if (state == (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
+ auto stateStr (service ? NotificationServiceStateToString(service->GetState()) : NotificationHostStateToString(host->GetState()));
+
+ Log(LogNotice, "Notification")
+ << "Notification object '" << notificationName << "': We already notified user '" << userName << "' for a " << stateStr
+ << " problem. Likely after that another state change notification was filtered out by config. Not sending duplicate '"
+ << stateStr << "' notification.";
+
+ continue;
+ }
+ }
+
+ Log(LogInformation, "Notification")
+ << "Sending " << (reminder ? "reminder " : "") << "'" << NotificationTypeToString(type) << "' notification '"
+ << notificationName << "' for user '" << userName << "'";
+
+ // Explicitly use Notification::Ptr to keep the reference counted while the callback is active
+ Notification::Ptr notification (this);
+ Utility::QueueAsyncCallback([notification, type, user, cr, force, author, text]() {
+ notification->ExecuteNotificationHelper(type, user, cr, force, author, text);
+ });
+
+ /* collect all notified users */
+ allNotifiedUsers.insert(user);
+
+ if (type == NotificationProblem) {
+ auto [host, service] = GetHostService(checkable);
+ uint_fast8_t state = service ? service->GetState() : host->GetState();
+
+ if (state != (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
+ GetLastNotifiedStatePerUser()->Set(userName, state);
+ OnLastNotifiedStatePerUserUpdated(this, userName, state, nullptr);
+ }
+ }
+
+ /* store all notified users for later recovery checks */
+ if (type == NotificationProblem && !notifiedProblemUsers->Contains(userName))
+ notifiedProblemUsers->Add(userName);
+ }
+
+ /* if this was a recovery notification, reset all notified users */
+ if (type == NotificationRecovery)
+ notifiedProblemUsers->Clear();
+
+ /* used in db_ido for notification history */
+ Service::OnNotificationSentToAllUsers(this, checkable, allNotifiedUsers, type, cr, author, text, nullptr);
+}
+
+bool Notification::CheckNotificationUserFilters(NotificationType type, const User::Ptr& user, bool force, bool reminder)
+{
+ String notificationName = GetName();
+ String userName = user->GetName();
+
+ if (!force) {
+ TimePeriod::Ptr tp = user->GetPeriod();
+
+ if (tp && !tp->IsInside(Utility::GetTime())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << " and user '" << userName
+ << "': user period not in timeperiod '" << tp->GetName() << "'";
+ return false;
+ }
+
+ unsigned long ftype = type;
+
+ Log(LogDebug, "Notification")
+ << "User '" << userName << "' notification '" << notificationName
+ << "', Type '" << NotificationTypeToString(type)
+ << "', TypeFilter: " << NotificationFilterToString(user->GetTypeFilter(), GetTypeFilterMap())
+ << " (FType=" << ftype << ", TypeFilter=" << GetTypeFilter() << ")";
+
+
+ if (!(ftype & user->GetTypeFilter())) {
+ Log(LogNotice, "Notification")
+ << "Not sending " << (reminder ? "reminder " : "") << "notifications for notification object '"
+ << notificationName << " and user '" << userName << "': type '"
+ << NotificationTypeToString(type) << "' does not match type filter: "
+ << NotificationFilterToString(user->GetTypeFilter(), GetTypeFilterMap()) << ".";
+ return false;
+ }
+
+ /* check state filters it this is not a recovery notification */
+ if (type != NotificationRecovery) {
+ Checkable::Ptr checkable = GetCheckable();
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ unsigned long fstate;
+ String stateStr;
+
+ if (service) {
+ fstate = ServiceStateToFilter(service->GetState());
+ stateStr = NotificationServiceStateToString(service->GetState());
+ } else {
+ fstate = HostStateToFilter(host->GetState());
+ stateStr = NotificationHostStateToString(host->GetState());
+ }
+
+ Log(LogDebug, "Notification")
+ << "User '" << userName << "' notification '" << notificationName
+ << "', State '" << stateStr << "', StateFilter: "
+ << NotificationFilterToString(user->GetStateFilter(), GetStateFilterMap())
+ << " (FState=" << fstate << ", StateFilter=" << user->GetStateFilter() << ")";
+
+ if (!(fstate & user->GetStateFilter())) {
+ Log(LogNotice, "Notification")
+ << "Not " << (reminder ? "reminder " : "") << "sending notifications for notification object '"
+ << notificationName << " and user '" << userName << "': state '" << stateStr
+ << "' does not match state filter: " << NotificationFilterToString(user->GetStateFilter(), GetStateFilterMap()) << ".";
+ return false;
+ }
+ }
+ } else {
+ Log(LogNotice, "Notification")
+ << "Not checking " << (reminder ? "reminder " : "") << "notification filters for notification object '"
+ << notificationName << "' and user '" << userName << "': Notification was forced.";
+ }
+
+ return true;
+}
+
+void Notification::ExecuteNotificationHelper(NotificationType type, const User::Ptr& user, const CheckResult::Ptr& cr, bool force, const String& author, const String& text)
+{
+ String notificationName = GetName();
+ String userName = user->GetName();
+ String checkableName = GetCheckable()->GetName();
+
+ NotificationCommand::Ptr command = GetCommand();
+
+ if (!command) {
+ Log(LogDebug, "Notification")
+ << "No command found for notification '" << notificationName << "'. Skipping execution.";
+ return;
+ }
+
+ String commandName = command->GetName();
+
+ try {
+ command->Execute(this, user, cr, type, author, text);
+
+ /* required by compatlogger */
+ Service::OnNotificationSentToUser(this, GetCheckable(), user, type, cr, author, text, commandName, nullptr);
+
+ Log(LogInformation, "Notification")
+ << "Completed sending '" << NotificationTypeToString(type)
+ << "' notification '" << notificationName
+ << "' for checkable '" << checkableName
+ << "' and user '" << userName << "' using command '" << commandName << "'.";
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "Notification")
+ << "Exception occurred during notification '" << notificationName
+ << "' for checkable '" << checkableName
+ << "' and user '" << userName << "' using command '" << commandName << "': "
+ << DiagnosticInformation(ex, false);
+ }
+}
+
+int icinga::ServiceStateToFilter(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ return StateFilterOK;
+ case ServiceWarning:
+ return StateFilterWarning;
+ case ServiceCritical:
+ return StateFilterCritical;
+ case ServiceUnknown:
+ return StateFilterUnknown;
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
+
+int icinga::HostStateToFilter(HostState state)
+{
+ switch (state) {
+ case HostUp:
+ return StateFilterUp;
+ case HostDown:
+ return StateFilterDown;
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
+
+String Notification::NotificationFilterToString(int filter, const std::map<String, int>& filterMap)
+{
+ std::vector<String> sFilters;
+
+ typedef std::pair<String, int> kv_pair;
+ for (const kv_pair& kv : filterMap) {
+ if (filter & kv.second)
+ sFilters.push_back(kv.first);
+ }
+
+ return Utility::NaturalJoin(sFilters);
+}
+
+/*
+ * Main interface to translate NotificationType values into strings.
+ */
+String Notification::NotificationTypeToString(NotificationType type)
+{
+ auto typeMap = Notification::m_TypeFilterMap;
+
+ auto it = std::find_if(typeMap.begin(), typeMap.end(),
+ [&type](const std::pair<String, int>& p) {
+ return p.second == type;
+ });
+
+ if (it == typeMap.end())
+ return Empty;
+
+ return it->first;
+}
+
+
+/*
+ * Compat interface used in external features.
+ */
+String Notification::NotificationTypeToStringCompat(NotificationType type)
+{
+ switch (type) {
+ case NotificationDowntimeStart:
+ return "DOWNTIMESTART";
+ case NotificationDowntimeEnd:
+ return "DOWNTIMEEND";
+ case NotificationDowntimeRemoved:
+ return "DOWNTIMECANCELLED";
+ case NotificationCustom:
+ return "CUSTOM";
+ case NotificationAcknowledgement:
+ return "ACKNOWLEDGEMENT";
+ case NotificationProblem:
+ return "PROBLEM";
+ case NotificationRecovery:
+ return "RECOVERY";
+ case NotificationFlappingStart:
+ return "FLAPPINGSTART";
+ case NotificationFlappingEnd:
+ return "FLAPPINGEND";
+ default:
+ return "UNKNOWN_NOTIFICATION";
+ }
+}
+
+String Notification::NotificationServiceStateToString(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ return "OK";
+ case ServiceWarning:
+ return "Warning";
+ case ServiceCritical:
+ return "Critical";
+ case ServiceUnknown:
+ return "Unknown";
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
+
+String Notification::NotificationHostStateToString(HostState state)
+{
+ switch (state) {
+ case HostUp:
+ return "Up";
+ case HostDown:
+ return "Down";
+ default:
+ VERIFY(!"Invalid state type.");
+ }
+}
+
+void Notification::Validate(int types, const ValidationUtils& utils)
+{
+ ObjectImpl<Notification>::Validate(types, utils);
+
+ if (!(types & FAConfig))
+ return;
+
+ Array::Ptr users = GetUsersRaw();
+ Array::Ptr groups = GetUserGroupsRaw();
+
+ if ((!users || users->GetLength() == 0) && (!groups || groups->GetLength() == 0))
+ BOOST_THROW_EXCEPTION(ValidationError(this, std::vector<String>(), "Validation failed: No users/user_groups specified."));
+}
+
+void Notification::ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Notification>::ValidateStates(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), GetStateFilterMap(), 0);
+
+ if (GetServiceName().IsEmpty() && (filter == -1 || (filter & ~(StateFilterUp | StateFilterDown)) != 0))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid."));
+
+ if (!GetServiceName().IsEmpty() && (filter == -1 || (filter & ~(StateFilterOK | StateFilterWarning | StateFilterCritical | StateFilterUnknown)) != 0))
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid."));
+}
+
+void Notification::ValidateTypes(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Notification>::ValidateTypes(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), GetTypeFilterMap(), 0);
+
+ if (filter == -1 || (filter & ~(NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved |
+ NotificationCustom | NotificationAcknowledgement | NotificationProblem | NotificationRecovery |
+ NotificationFlappingStart | NotificationFlappingEnd)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "types" }, "Type filter is invalid."));
+}
+
+void Notification::ValidateTimes(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Notification>::ValidateTimes(lvalue, utils);
+
+ Dictionary::Ptr times = lvalue();
+
+ if (!times)
+ return;
+
+ double begin;
+ double end;
+
+ try {
+ begin = Convert::ToDouble(times->Get("begin"));
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "times" }, "'begin' is invalid, must be duration or number." ));
+ }
+
+ try {
+ end = Convert::ToDouble(times->Get("end"));
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "times" }, "'end' is invalid, must be duration or number." ));
+ }
+
+ /* Also solve logical errors where begin > end. */
+ if (begin > 0 && end > 0 && begin > end)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "times" }, "'begin' must be smaller than 'end'."));
+}
+
+Endpoint::Ptr Notification::GetCommandEndpoint() const
+{
+ return Endpoint::GetByName(GetCommandEndpointRaw());
+}
+
+const std::map<String, int>& Notification::GetStateFilterMap()
+{
+ return m_StateFilterMap;
+}
+
+const std::map<String, int>& Notification::GetTypeFilterMap()
+{
+ return m_TypeFilterMap;
+}
diff --git a/lib/icinga/notification.hpp b/lib/icinga/notification.hpp
new file mode 100644
index 0000000..1b6cbed
--- /dev/null
+++ b/lib/icinga/notification.hpp
@@ -0,0 +1,135 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NOTIFICATION_H
+#define NOTIFICATION_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/notification-ti.hpp"
+#include "icinga/checkable-ti.hpp"
+#include "icinga/user.hpp"
+#include "icinga/usergroup.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/checkresult.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/messageorigin.hpp"
+#include "base/array.hpp"
+#include <cstdint>
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+enum NotificationFilter
+{
+ StateFilterOK = 1,
+ StateFilterWarning = 2,
+ StateFilterCritical = 4,
+ StateFilterUnknown = 8,
+
+ StateFilterUp = 16,
+ StateFilterDown = 32
+};
+
+/**
+ * The notification type.
+ *
+ * @ingroup icinga
+ */
+enum NotificationType
+{
+ NotificationDowntimeStart = 1,
+ NotificationDowntimeEnd = 2,
+ NotificationDowntimeRemoved = 4,
+ NotificationCustom = 8,
+ NotificationAcknowledgement = 16,
+ NotificationProblem = 32,
+ NotificationRecovery = 64,
+ NotificationFlappingStart = 128,
+ NotificationFlappingEnd = 256
+};
+
+class NotificationCommand;
+class ApplyRule;
+struct ScriptFrame;
+class Host;
+class Service;
+
+/**
+ * An Icinga notification specification.
+ *
+ * @ingroup icinga
+ */
+class Notification final : public ObjectImpl<Notification>
+{
+public:
+ DECLARE_OBJECT(Notification);
+ DECLARE_OBJECTNAME(Notification);
+
+ static void StaticInitialize();
+
+ intrusive_ptr<Checkable> GetCheckable() const;
+ intrusive_ptr<NotificationCommand> GetCommand() const;
+ TimePeriod::Ptr GetPeriod() const;
+ std::set<User::Ptr> GetUsers() const;
+ std::set<UserGroup::Ptr> GetUserGroups() const;
+
+ void UpdateNotificationNumber();
+ void ResetNotificationNumber();
+
+ void BeginExecuteNotification(NotificationType type, const CheckResult::Ptr& cr, bool force,
+ bool reminder = false, const String& author = "", const String& text = "");
+
+ Endpoint::Ptr GetCommandEndpoint() const;
+
+ // Logging, etc.
+ static String NotificationTypeToString(NotificationType type);
+ // Compat, used for notifications, etc.
+ static String NotificationTypeToStringCompat(NotificationType type);
+ static String NotificationFilterToString(int filter, const std::map<String, int>& filterMap);
+
+ static String NotificationServiceStateToString(ServiceState state);
+ static String NotificationHostStateToString(HostState state);
+
+ static boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> OnNextNotificationChanged;
+ static boost::signals2::signal<void (const Notification::Ptr&, const String&, uint_fast8_t, const MessageOrigin::Ptr&)> OnLastNotifiedStatePerUserUpdated;
+ static boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> OnLastNotifiedStatePerUserCleared;
+
+ void Validate(int types, const ValidationUtils& utils) override;
+
+ void ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateTypes(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateTimes(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+ static void EvaluateApplyRules(const intrusive_ptr<Host>& host);
+ static void EvaluateApplyRules(const intrusive_ptr<Service>& service);
+
+ static const std::map<String, int>& GetStateFilterMap();
+ static const std::map<String, int>& GetTypeFilterMap();
+
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeRemoved) override;
+
+private:
+ ObjectImpl<Checkable>::Ptr m_Checkable;
+
+ bool CheckNotificationUserFilters(NotificationType type, const User::Ptr& user, bool force, bool reminder);
+
+ void ExecuteNotificationHelper(NotificationType type, const User::Ptr& user, const CheckResult::Ptr& cr, bool force, const String& author = "", const String& text = "");
+
+ static bool EvaluateApplyRuleInstance(const intrusive_ptr<Checkable>& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter);
+ static bool EvaluateApplyRule(const intrusive_ptr<Checkable>& checkable, const ApplyRule& rule, bool skipFilter = false);
+
+ static std::map<String, int> m_StateFilterMap;
+ static std::map<String, int> m_TypeFilterMap;
+};
+
+int ServiceStateToFilter(ServiceState state);
+int HostStateToFilter(HostState state);
+
+}
+
+#endif /* NOTIFICATION_H */
diff --git a/lib/icinga/notification.ti b/lib/icinga/notification.ti
new file mode 100644
index 0000000..be07846
--- /dev/null
+++ b/lib/icinga/notification.ti
@@ -0,0 +1,111 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#impl_include "icinga/notificationcommand.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class NotificationNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Notification : CustomVarObject < NotificationNameComposer
+{
+ load_after Host;
+ load_after Service;
+
+ [config, protected, required, navigation] name(NotificationCommand) command (CommandRaw) {
+ navigate {{{
+ return NotificationCommand::GetByName(GetCommandRaw());
+ }}}
+ };
+ [config] double interval {
+ default {{{ return 1800; }}}
+ };
+ [config, navigation] name(TimePeriod) period (PeriodRaw) {
+ navigate {{{
+ return TimePeriod::GetByName(GetPeriodRaw());
+ }}}
+ };
+ [config, signal_with_old_value] array(name(User)) users (UsersRaw);
+ [config, signal_with_old_value] array(name(UserGroup)) user_groups (UserGroupsRaw);
+ [config] Dictionary::Ptr times;
+ [config] array(Value) types;
+ [no_user_view, no_user_modify] int type_filter_real (TypeFilter);
+ [config] array(Value) states;
+ [no_user_view, no_user_modify] int state_filter_real (StateFilter);
+ [config, no_user_modify, protected, required, navigation(host)] name(Host) host_name {
+ navigate {{{
+ return Host::GetByName(GetHostName());
+ }}}
+ };
+ [config, protected, no_user_modify, navigation(service)] String service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+ return host->GetServiceByShortName(GetServiceName());
+ }}}
+ };
+
+ [state, no_user_modify] Array::Ptr notified_problem_users {
+ default {{{ return new Array(); }}}
+ };
+
+ [state, no_user_modify] bool no_more_notifications {
+ default {{{ return false; }}}
+ };
+
+ [state, no_user_view, no_user_modify] Array::Ptr stashed_notifications {
+ default {{{ return new Array(); }}}
+ };
+
+ [state] Timestamp last_notification;
+ [state] Timestamp next_notification;
+ [state] int notification_number;
+ [state] Timestamp last_problem_notification;
+
+ [state, no_user_view, no_user_modify] int suppressed_notifications {
+ default {{{ return 0; }}}
+ };
+
+ [state, no_user_view, no_user_modify] Dictionary::Ptr last_notified_state_per_user {
+ default {{{ return new Dictionary(); }}}
+ };
+
+ [config, navigation] name(Endpoint) command_endpoint (CommandEndpointRaw) {
+ navigate {{{
+ return Endpoint::GetByName(GetCommandEndpointRaw());
+ }}}
+ };
+};
+
+validator Notification {
+ Dictionary times {
+ Number begin;
+ Number end;
+ };
+};
+
+}
diff --git a/lib/icinga/notificationcommand.cpp b/lib/icinga/notificationcommand.cpp
new file mode 100644
index 0000000..d4a5fd6
--- /dev/null
+++ b/lib/icinga/notificationcommand.cpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/notificationcommand.hpp"
+#include "icinga/notificationcommand-ti.cpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(NotificationCommand);
+
+thread_local NotificationCommand::Ptr NotificationCommand::ExecuteOverride;
+
+Dictionary::Ptr NotificationCommand::Execute(const Notification::Ptr& notification,
+ const User::Ptr& user, const CheckResult::Ptr& cr, const NotificationType& type,
+ const String& author, const String& comment, const Dictionary::Ptr& resolvedMacros,
+ bool useResolvedMacros)
+{
+ return GetExecute()->Invoke({
+ notification,
+ user,
+ cr,
+ type,
+ author,
+ comment,
+ resolvedMacros,
+ useResolvedMacros,
+ });
+}
diff --git a/lib/icinga/notificationcommand.hpp b/lib/icinga/notificationcommand.hpp
new file mode 100644
index 0000000..f0f6899
--- /dev/null
+++ b/lib/icinga/notificationcommand.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NOTIFICATIONCOMMAND_H
+#define NOTIFICATIONCOMMAND_H
+
+#include "icinga/notificationcommand-ti.hpp"
+#include "icinga/notification.hpp"
+
+namespace icinga
+{
+
+class Notification;
+
+/**
+ * A notification command.
+ *
+ * @ingroup icinga
+ */
+class NotificationCommand final : public ObjectImpl<NotificationCommand>
+{
+public:
+ DECLARE_OBJECT(NotificationCommand);
+ DECLARE_OBJECTNAME(NotificationCommand);
+
+ static thread_local NotificationCommand::Ptr ExecuteOverride;
+
+ virtual Dictionary::Ptr Execute(const intrusive_ptr<Notification>& notification,
+ const User::Ptr& user, const CheckResult::Ptr& cr, const NotificationType& type,
+ const String& author, const String& comment,
+ const Dictionary::Ptr& resolvedMacros = nullptr,
+ bool useResolvedMacros = false);
+};
+
+}
+
+#endif /* NOTIFICATIONCOMMAND_H */
diff --git a/lib/icinga/notificationcommand.ti b/lib/icinga/notificationcommand.ti
new file mode 100644
index 0000000..51207a3
--- /dev/null
+++ b/lib/icinga/notificationcommand.ti
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/command.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class NotificationCommand : Command
+{
+};
+
+}
diff --git a/lib/icinga/objectutils.cpp b/lib/icinga/objectutils.cpp
new file mode 100644
index 0000000..559ca43
--- /dev/null
+++ b/lib/icinga/objectutils.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/objectutils.hpp"
+#include "icinga/host.hpp"
+#include "icinga/user.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/usergroup.hpp"
+
+using namespace icinga;
+
+REGISTER_FUNCTION(Icinga, get_host, &Host::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_service, &ObjectUtils::GetService, "host:name");
+REGISTER_FUNCTION(Icinga, get_services, &ObjectUtils::GetServices, "host");
+REGISTER_FUNCTION(Icinga, get_user, &User::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_check_command, &CheckCommand::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_event_command, &EventCommand::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_notification_command, &NotificationCommand::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_host_group, &HostGroup::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_service_group, &ServiceGroup::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_user_group, &UserGroup::GetByName, "name");
+REGISTER_FUNCTION(Icinga, get_time_period, &TimePeriod::GetByName, "name");
+
+Service::Ptr ObjectUtils::GetService(const Value& host, const String& name)
+{
+ Host::Ptr hostObj;
+
+ if (host.IsObjectType<Host>())
+ hostObj = host;
+ else
+ hostObj = Host::GetByName(host);
+
+ if (!hostObj)
+ return nullptr;
+
+ return hostObj->GetServiceByShortName(name);
+}
+
+Array::Ptr ObjectUtils::GetServices(const Value& host)
+{
+ Host::Ptr hostObj;
+
+ if (host.IsObjectType<Host>())
+ hostObj = host;
+ else
+ hostObj = Host::GetByName(host);
+
+ if (!hostObj)
+ return nullptr;
+
+ return Array::FromVector(hostObj->GetServices());
+}
diff --git a/lib/icinga/objectutils.hpp b/lib/icinga/objectutils.hpp
new file mode 100644
index 0000000..42e2953
--- /dev/null
+++ b/lib/icinga/objectutils.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTUTILS_H
+#define OBJECTUTILS_H
+
+#include "base/i2-base.hpp"
+#include "base/string.hpp"
+#include "base/array.hpp"
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup icinga
+ */
+class ObjectUtils
+{
+public:
+ static Service::Ptr GetService(const Value& host, const String& name);
+ static Array::Ptr GetServices(const Value& host);
+
+private:
+ ObjectUtils();
+};
+
+}
+
+#endif /* OBJECTUTILS_H */
diff --git a/lib/icinga/pluginutility.cpp b/lib/icinga/pluginutility.cpp
new file mode 100644
index 0000000..4dc46f7
--- /dev/null
+++ b/lib/icinga/pluginutility.cpp
@@ -0,0 +1,218 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/pluginutility.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/convert.hpp"
+#include "base/process.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/trim.hpp>
+
+using namespace icinga;
+
+void PluginUtility::ExecuteCommand(const Command::Ptr& commandObj, const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MacroProcessor::ResolverList& macroResolvers,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int timeout,
+ const std::function<void(const Value& commandLine, const ProcessResult&)>& callback)
+{
+ Value raw_command = commandObj->GetCommandLine();
+ Dictionary::Ptr raw_arguments = commandObj->GetArguments();
+
+ Value command;
+
+ try {
+ command = MacroProcessor::ResolveArguments(raw_command, raw_arguments,
+ macroResolvers, cr, resolvedMacros, useResolvedMacros);
+ } catch (const std::exception& ex) {
+ String message = DiagnosticInformation(ex);
+
+ Log(LogWarning, "PluginUtility", message);
+
+ if (callback) {
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.ExecutionStart = Utility::GetTime();
+ pr.ExecutionEnd = pr.ExecutionStart;
+ pr.ExitStatus = 3; /* Unknown */
+ pr.Output = message;
+ callback(Empty, pr);
+ }
+
+ return;
+ }
+
+ Dictionary::Ptr envMacros = new Dictionary();
+
+ Dictionary::Ptr env = commandObj->GetEnv();
+
+ if (env) {
+ ObjectLock olock(env);
+ for (const Dictionary::Pair& kv : env) {
+ String name = kv.second;
+
+ String missingMacro;
+ Value value = MacroProcessor::ResolveMacros(name, macroResolvers, cr,
+ &missingMacro, MacroProcessor::EscapeCallback(), resolvedMacros,
+ useResolvedMacros);
+
+#ifdef I2_DEBUG
+ if (!missingMacro.IsEmpty())
+ Log(LogDebug, "PluginUtility")
+ << "Macro '" << name << "' is not defined.";
+#endif /* I2_DEBUG */
+
+ if (value.IsObjectType<Array>())
+ value = Utility::Join(value, ';');
+
+ envMacros->Set(kv.first, value);
+ }
+ }
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ Process::Ptr process = new Process(Process::PrepareCommand(command), envMacros);
+
+ process->SetTimeout(timeout);
+ process->SetAdjustPriority(true);
+
+ process->Run([callback, command](const ProcessResult& pr) { callback(command, pr); });
+}
+
+ServiceState PluginUtility::ExitStatusToState(int exitStatus)
+{
+ switch (exitStatus) {
+ case 0:
+ return ServiceOK;
+ case 1:
+ return ServiceWarning;
+ case 2:
+ return ServiceCritical;
+ default:
+ return ServiceUnknown;
+ }
+}
+
+std::pair<String, String> PluginUtility::ParseCheckOutput(const String& output)
+{
+ String text;
+ String perfdata;
+
+ std::vector<String> lines = output.Split("\r\n");
+
+ for (const String& line : lines) {
+ size_t delim = line.FindFirstOf("|");
+
+ if (!text.IsEmpty())
+ text += "\n";
+
+ if (delim != String::NPos && line.FindFirstOf("=", delim) != String::NPos) {
+ text += line.SubStr(0, delim);
+
+ if (!perfdata.IsEmpty())
+ perfdata += " ";
+
+ perfdata += line.SubStr(delim + 1, line.GetLength());
+ } else {
+ text += line;
+ }
+ }
+
+ boost::algorithm::trim(perfdata);
+
+ return std::make_pair(text, perfdata);
+}
+
+Array::Ptr PluginUtility::SplitPerfdata(const String& perfdata)
+{
+ ArrayData result;
+
+ size_t begin = 0;
+ String multi_prefix;
+
+ for (;;) {
+ size_t eqp = perfdata.FindFirstOf('=', begin);
+
+ if (eqp == String::NPos)
+ break;
+
+ String label = perfdata.SubStr(begin, eqp - begin);
+ boost::algorithm::trim_left(label);
+
+ if (label.GetLength() > 2 && label[0] == '\'' && label[label.GetLength() - 1] == '\'')
+ label = label.SubStr(1, label.GetLength() - 2);
+
+ size_t multi_index = label.RFind("::");
+
+ if (multi_index != String::NPos)
+ multi_prefix = "";
+
+ size_t spq = perfdata.FindFirstOf(' ', eqp);
+
+ if (spq == String::NPos)
+ spq = perfdata.GetLength();
+
+ String value = perfdata.SubStr(eqp + 1, spq - eqp - 1);
+
+ if (!multi_prefix.IsEmpty())
+ label = multi_prefix + "::" + label;
+
+ String pdv;
+ if (label.FindFirstOf(" ") != String::NPos)
+ pdv = "'" + label + "'=" + value;
+ else
+ pdv = label + "=" + value;
+
+ result.emplace_back(std::move(pdv));
+
+ if (multi_index != String::NPos)
+ multi_prefix = label.SubStr(0, multi_index);
+
+ begin = spq + 1;
+ }
+
+ return new Array(std::move(result));
+}
+
+String PluginUtility::FormatPerfdata(const Array::Ptr& perfdata, bool normalize)
+{
+ if (!perfdata)
+ return "";
+
+ std::ostringstream result;
+
+ ObjectLock olock(perfdata);
+
+ bool first = true;
+ for (const Value& pdv : perfdata) {
+ if (!first)
+ result << " ";
+ else
+ first = false;
+
+ if (pdv.IsObjectType<PerfdataValue>()) {
+ result << static_cast<PerfdataValue::Ptr>(pdv)->Format();
+ } else if (normalize) {
+ PerfdataValue::Ptr normalized;
+
+ try {
+ normalized = PerfdataValue::Parse(pdv);
+ } catch (const std::invalid_argument& ex) {
+ Log(LogDebug, "PerfdataValue") << ex.what();
+ }
+
+ if (normalized) {
+ result << normalized->Format();
+ } else {
+ result << pdv;
+ }
+ } else {
+ result << pdv;
+ }
+ }
+
+ return result.str();
+}
diff --git a/lib/icinga/pluginutility.hpp b/lib/icinga/pluginutility.hpp
new file mode 100644
index 0000000..3f6a844
--- /dev/null
+++ b/lib/icinga/pluginutility.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PLUGINUTILITY_H
+#define PLUGINUTILITY_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+struct ProcessResult;
+
+/**
+ * Utility functions for plugin-based checks.
+ *
+ * @ingroup icinga
+ */
+class PluginUtility
+{
+public:
+ static void ExecuteCommand(const Command::Ptr& commandObj, const Checkable::Ptr& checkable,
+ const CheckResult::Ptr& cr, const MacroProcessor::ResolverList& macroResolvers,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int timeout,
+ const std::function<void(const Value& commandLine, const ProcessResult&)>& callback = std::function<void(const Value& commandLine, const ProcessResult&)>());
+
+ static ServiceState ExitStatusToState(int exitStatus);
+ static std::pair<String, String> ParseCheckOutput(const String& output);
+
+ static Array::Ptr SplitPerfdata(const String& perfdata);
+ static String FormatPerfdata(const Array::Ptr& perfdata, bool normalize = false);
+
+private:
+ PluginUtility();
+};
+
+}
+
+#endif /* PLUGINUTILITY_H */
diff --git a/lib/icinga/scheduleddowntime-apply.cpp b/lib/icinga/scheduleddowntime-apply.cpp
new file mode 100644
index 0000000..4f8aa47
--- /dev/null
+++ b/lib/icinga/scheduleddowntime-apply.cpp
@@ -0,0 +1,159 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/scheduleddowntime.hpp"
+#include "icinga/service.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE([]() {
+ ApplyRule::RegisterType("ScheduledDowntime", { "Host", "Service" });
+});
+
+bool ScheduledDowntime::EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter)
+{
+ if (!skipFilter && !rule.EvaluateFilter(frame))
+ return false;
+
+ auto& di (rule.GetDebugInfo());
+
+#ifdef _DEBUG
+ Log(LogDebug, "ScheduledDowntime")
+ << "Applying scheduled downtime '" << rule.GetName() << "' to object '" << checkable->GetName() << "' for rule " << di;
+#endif /* _DEBUG */
+
+ ConfigItemBuilder builder{di};
+ builder.SetType(ScheduledDowntime::TypeInstance);
+ builder.SetName(name);
+ builder.SetScope(frame.Locals->ShallowClone());
+ builder.SetIgnoreOnError(rule.GetIgnoreOnError());
+
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+
+ if (service)
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "service_name"), OpSetLiteral, MakeLiteral(service->GetShortName()), di));
+
+ String zone = checkable->GetZoneName();
+
+ if (!zone.IsEmpty())
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "zone"), OpSetLiteral, MakeLiteral(zone), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "package"), OpSetLiteral, MakeLiteral(rule.GetPackage()), di));
+
+ builder.AddExpression(new OwnedExpression(rule.GetExpression()));
+
+ ConfigItem::Ptr downtimeItem = builder.Compile();
+ downtimeItem->Register();
+
+ return true;
+}
+
+bool ScheduledDowntime::EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter)
+{
+ auto& di (rule.GetDebugInfo());
+
+ CONTEXT("Evaluating 'apply' rule (" << di << ")");
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ScriptFrame frame(true);
+ if (rule.GetScope())
+ rule.GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+ if (service)
+ frame.Locals->Set("service", service);
+
+ Value vinstances;
+
+ if (rule.GetFTerm()) {
+ try {
+ vinstances = rule.GetFTerm()->Evaluate(frame);
+ } catch (const std::exception&) {
+ /* Silently ignore errors here and assume there are no instances. */
+ return false;
+ }
+ } else {
+ vinstances = new Array({ "" });
+ }
+
+ bool match = false;
+
+ if (vinstances.IsObjectType<Array>()) {
+ if (!rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Dictionary iterator requires value to be a dictionary.", di));
+
+ Array::Ptr arr = vinstances;
+
+ ObjectLock olock(arr);
+ for (const Value& instance : arr) {
+ String name = rule.GetName();
+
+ if (!rule.GetFKVar().IsEmpty()) {
+ frame.Locals->Set(rule.GetFKVar(), instance);
+ name += instance;
+ }
+
+ if (EvaluateApplyRuleInstance(checkable, name, frame, rule, skipFilter))
+ match = true;
+ }
+ } else if (vinstances.IsObjectType<Dictionary>()) {
+ if (rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Array iterator requires value to be an array.", di));
+
+ Dictionary::Ptr dict = vinstances;
+
+ for (const String& key : dict->GetKeys()) {
+ frame.Locals->Set(rule.GetFKVar(), key);
+ frame.Locals->Set(rule.GetFVVar(), dict->Get(key));
+
+ if (EvaluateApplyRuleInstance(checkable, rule.GetName() + key, frame, rule, skipFilter))
+ match = true;
+ }
+ }
+
+ return match;
+}
+
+void ScheduledDowntime::EvaluateApplyRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating 'apply' rules for host '" << host->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(ScheduledDowntime::TypeInstance, Host::TypeInstance)) {
+ if (EvaluateApplyRule(host, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedHostRules(ScheduledDowntime::TypeInstance, host->GetName())) {
+ if (EvaluateApplyRule(host, *rule, true))
+ rule->AddMatch();
+ }
+}
+
+void ScheduledDowntime::EvaluateApplyRules(const Service::Ptr& service)
+{
+ CONTEXT("Evaluating 'apply' rules for service '" << service->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(ScheduledDowntime::TypeInstance, Service::TypeInstance)) {
+ if (EvaluateApplyRule(service, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedServiceRules(ScheduledDowntime::TypeInstance, service->GetHost()->GetName(), service->GetShortName())) {
+ if (EvaluateApplyRule(service, *rule, true))
+ rule->AddMatch();
+ }
+}
diff --git a/lib/icinga/scheduleddowntime.cpp b/lib/icinga/scheduleddowntime.cpp
new file mode 100644
index 0000000..f23d3e4
--- /dev/null
+++ b/lib/icinga/scheduleddowntime.cpp
@@ -0,0 +1,393 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/scheduleddowntime.hpp"
+#include "icinga/scheduleddowntime-ti.cpp"
+#include "icinga/legacytimeperiod.hpp"
+#include "icinga/downtime.hpp"
+#include "icinga/service.hpp"
+#include "base/timer.hpp"
+#include "base/tlsutility.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/objectlock.hpp"
+#include "base/object-packer.hpp"
+#include "base/serializer.hpp"
+#include "base/convert.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include <boost/thread/once.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_TYPE(ScheduledDowntime);
+
+static Timer::Ptr l_Timer;
+
+String ScheduledDowntimeNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ ScheduledDowntime::Ptr downtime = dynamic_pointer_cast<ScheduledDowntime>(context);
+
+ if (!downtime)
+ return "";
+
+ String name = downtime->GetHostName();
+
+ if (!downtime->GetServiceName().IsEmpty())
+ name += "!" + downtime->GetServiceName();
+
+ name += "!" + shortName;
+
+ return name;
+}
+
+Dictionary::Ptr ScheduledDowntimeNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid ScheduledDowntime name."));
+
+ Dictionary::Ptr result = new Dictionary();
+ result->Set("host_name", tokens[0]);
+
+ if (tokens.size() > 2) {
+ result->Set("service_name", tokens[1]);
+ result->Set("name", tokens[2]);
+ } else {
+ result->Set("name", tokens[1]);
+ }
+
+ return result;
+}
+
+void ScheduledDowntime::OnAllConfigLoaded()
+{
+ ObjectImpl<ScheduledDowntime>::OnAllConfigLoaded();
+
+ if (!GetCheckable())
+ BOOST_THROW_EXCEPTION(ScriptError("ScheduledDowntime '" + GetName() + "' references a host/service which doesn't exist.", GetDebugInfo()));
+
+ m_AllConfigLoaded.store(true);
+}
+
+void ScheduledDowntime::Start(bool runtimeCreated)
+{
+ ObjectImpl<ScheduledDowntime>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, [this]() {
+ l_Timer = Timer::Create();
+ l_Timer->SetInterval(60);
+ l_Timer->OnTimerExpired.connect([](const Timer * const&) { TimerProc(); });
+ l_Timer->Start();
+ });
+
+ if (!IsPaused())
+ Utility::QueueAsyncCallback([this]() { CreateNextDowntime(); });
+}
+
+void ScheduledDowntime::TimerProc()
+{
+ for (const ScheduledDowntime::Ptr& sd : ConfigType::GetObjectsByType<ScheduledDowntime>()) {
+ if (sd->IsActive() && !sd->IsPaused()) {
+ try {
+ sd->CreateNextDowntime();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ScheduledDowntime")
+ << "Exception occurred during creation of next downtime for scheduled downtime '"
+ << sd->GetName() << "': " << DiagnosticInformation(ex, false);
+ continue;
+ }
+
+ try {
+ sd->RemoveObsoleteDowntimes();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ScheduledDowntime")
+ << "Exception occurred during removal of obsolete downtime for scheduled downtime '"
+ << sd->GetName() << "': " << DiagnosticInformation(ex, false);
+ }
+ }
+ }
+}
+
+Checkable::Ptr ScheduledDowntime::GetCheckable() const
+{
+ Host::Ptr host = Host::GetByName(GetHostName());
+
+ if (GetServiceName().IsEmpty())
+ return host;
+ else
+ return host->GetServiceByShortName(GetServiceName());
+}
+
+std::pair<double, double> ScheduledDowntime::FindRunningSegment(double minEnd)
+{
+ time_t refts = Utility::GetTime();
+ tm reference = Utility::LocalTime(refts);
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Finding running scheduled downtime segment for time " << refts
+ << " (minEnd " << (minEnd > 0 ? Utility::FormatDateTime("%c", minEnd) : "-") << ")";
+
+ Dictionary::Ptr ranges = GetRanges();
+
+ if (!ranges)
+ return std::make_pair(0, 0);
+
+ Array::Ptr segments = new Array();
+
+ Dictionary::Ptr bestSegment;
+ double bestBegin = 0.0, bestEnd = 0.0;
+ double now = Utility::GetTime();
+
+ ObjectLock olock(ranges);
+
+ /* Find the longest lasting (and longer than minEnd, if given) segment that's already running */
+ for (const Dictionary::Pair& kv : ranges) {
+ Log(LogDebug, "ScheduledDowntime")
+ << "Evaluating (running?) segment: " << kv.first << ": " << kv.second;
+
+ Dictionary::Ptr segment = LegacyTimePeriod::FindRunningSegment(kv.first, kv.second, &reference);
+
+ if (!segment)
+ continue;
+
+ double begin = segment->Get("begin");
+ double end = segment->Get("end");
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Considering (running?) segment: " << Utility::FormatDateTime("%c", begin) << " -> " << Utility::FormatDateTime("%c", end);
+
+ if (begin >= now || end < now) {
+ Log(LogDebug, "ScheduledDowntime") << "not running.";
+ continue;
+ }
+ if (minEnd && end <= minEnd) {
+ Log(LogDebug, "ScheduledDowntime") << "ending too early.";
+ continue;
+ }
+
+ if (!bestSegment || end > bestEnd) {
+ Log(LogDebug, "ScheduledDowntime") << "(best match yet)";
+ bestSegment = segment;
+ bestBegin = begin;
+ bestEnd = end;
+ }
+ }
+
+ if (bestSegment)
+ return std::make_pair(bestBegin, bestEnd);
+
+ return std::make_pair(0, 0);
+}
+
+std::pair<double, double> ScheduledDowntime::FindNextSegment()
+{
+ time_t refts = Utility::GetTime();
+ tm reference = Utility::LocalTime(refts);
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Finding next scheduled downtime segment for time " << refts;
+
+ Dictionary::Ptr ranges = GetRanges();
+
+ if (!ranges)
+ return std::make_pair(0, 0);
+
+ Array::Ptr segments = new Array();
+
+ Dictionary::Ptr bestSegment;
+ double bestBegin = 0.0, bestEnd = 0.0;
+ double now = Utility::GetTime();
+
+ ObjectLock olock(ranges);
+
+ /* Find the segment starting earliest */
+ for (const Dictionary::Pair& kv : ranges) {
+ Log(LogDebug, "ScheduledDowntime")
+ << "Evaluating segment: " << kv.first << ": " << kv.second;
+
+ Dictionary::Ptr segment = LegacyTimePeriod::FindNextSegment(kv.first, kv.second, &reference);
+
+ if (!segment)
+ continue;
+
+ double begin = segment->Get("begin");
+ double end = segment->Get("end");
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Considering segment: " << Utility::FormatDateTime("%c", begin) << " -> " << Utility::FormatDateTime("%c", end);
+
+ if (begin < now) {
+ Log(LogDebug, "ScheduledDowntime") << "already running.";
+ continue;
+ }
+
+ if (!bestSegment || begin < bestBegin) {
+ Log(LogDebug, "ScheduledDowntime") << "(best match yet)";
+ bestSegment = segment;
+ bestBegin = begin;
+ bestEnd = end;
+ }
+ }
+
+ if (bestSegment)
+ return std::make_pair(bestBegin, bestEnd);
+
+ return std::make_pair(0, 0);
+}
+
+void ScheduledDowntime::CreateNextDowntime()
+{
+ /* HA enabled zones. */
+ if (IsActive() && IsPaused()) {
+ Log(LogNotice, "Checkable")
+ << "Skipping downtime creation for HA-paused Scheduled Downtime object '" << GetName() << "'";
+ return;
+ }
+
+ double minEnd = 0;
+ auto downtimeOptionsHash (HashDowntimeOptions());
+
+ for (const Downtime::Ptr& downtime : GetCheckable()->GetDowntimes()) {
+ if (downtime->GetScheduledBy() != GetName())
+ continue;
+
+ auto configOwnerHash (downtime->GetConfigOwnerHash());
+ if (!configOwnerHash.IsEmpty() && configOwnerHash != downtimeOptionsHash)
+ continue;
+
+ double end = downtime->GetEndTime();
+ if (end > minEnd)
+ minEnd = end;
+
+ if (downtime->GetStartTime() < Utility::GetTime())
+ continue;
+
+ /* We've found a downtime that is owned by us and that hasn't started yet - we're done. */
+ return;
+ }
+
+ Log(LogDebug, "ScheduledDowntime")
+ << "Creating new Downtime for ScheduledDowntime \"" << GetName() << "\"";
+
+ std::pair<double, double> segment = FindRunningSegment(minEnd);
+ if (segment.first == 0 && segment.second == 0) {
+ segment = FindNextSegment();
+ if (segment.first == 0 && segment.second == 0)
+ return;
+ }
+
+ Downtime::Ptr downtime = Downtime::AddDowntime(GetCheckable(), GetAuthor(), GetComment(),
+ segment.first, segment.second,
+ GetFixed(), String(), GetDuration(), GetName(), GetName());
+ String downtimeName = downtime->GetName();
+
+ int childOptions = Downtime::ChildOptionsFromValue(GetChildOptions());
+ if (childOptions > 0) {
+ /* 'DowntimeTriggeredChildren' schedules child downtimes triggered by the parent downtime.
+ * 'DowntimeNonTriggeredChildren' schedules non-triggered downtimes for all children.
+ */
+ String triggerName;
+ if (childOptions == 1)
+ triggerName = downtimeName;
+
+ Log(LogNotice, "ScheduledDowntime")
+ << "Processing child options " << childOptions << " for downtime " << downtimeName;
+
+ for (const Checkable::Ptr& child : GetCheckable()->GetAllChildren()) {
+ Log(LogNotice, "ScheduledDowntime")
+ << "Scheduling downtime for child object " << child->GetName();
+
+ Downtime::Ptr childDowntime = Downtime::AddDowntime(child, GetAuthor(), GetComment(),
+ segment.first, segment.second, GetFixed(), triggerName, GetDuration(), GetName(), GetName());
+
+ Log(LogNotice, "ScheduledDowntime")
+ << "Add child downtime '" << childDowntime->GetName() << "'.";
+ }
+ }
+}
+
+void ScheduledDowntime::RemoveObsoleteDowntimes()
+{
+ auto name (GetName());
+ auto downtimeOptionsHash (HashDowntimeOptions());
+
+ // Just to be sure start and removal don't happen at the same time
+ auto threshold (Utility::GetTime() + 5 * 60);
+
+ for (const Downtime::Ptr& downtime : GetCheckable()->GetDowntimes()) {
+ if (downtime->GetScheduledBy() == name && downtime->GetStartTime() > threshold) {
+ auto configOwnerHash (downtime->GetConfigOwnerHash());
+
+ if (!configOwnerHash.IsEmpty() && configOwnerHash != downtimeOptionsHash)
+ Downtime::RemoveDowntime(downtime->GetName(), false, true);
+ }
+ }
+}
+
+void ScheduledDowntime::ValidateRanges(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<ScheduledDowntime>::ValidateRanges(lvalue, utils);
+
+ if (!lvalue())
+ return;
+
+ /* create a fake time environment to validate the definitions */
+ time_t refts = Utility::GetTime();
+ tm reference = Utility::LocalTime(refts);
+ Array::Ptr segments = new Array();
+
+ ObjectLock olock(lvalue());
+ for (const Dictionary::Pair& kv : lvalue()) {
+ try {
+ tm begin_tm, end_tm;
+ int stride;
+ LegacyTimePeriod::ParseTimeRange(kv.first, &begin_tm, &end_tm, &stride, &reference);
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "ranges" }, "Invalid time specification '" + kv.first + "': " + ex.what()));
+ }
+
+ try {
+ LegacyTimePeriod::ProcessTimeRanges(kv.second, &reference, segments);
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "ranges" }, "Invalid time range definition '" + kv.second + "': " + ex.what()));
+ }
+ }
+}
+
+void ScheduledDowntime::ValidateChildOptions(const Lazy<Value>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<ScheduledDowntime>::ValidateChildOptions(lvalue, utils);
+
+ try {
+ Downtime::ChildOptionsFromValue(lvalue());
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "child_options" }, "Invalid child_options specified"));
+ }
+}
+
+static const std::set<String> l_SDDowntimeOptions ({
+ "author", "child_options", "comment", "duration", "fixed", "ranges", "vars"
+});
+
+String ScheduledDowntime::HashDowntimeOptions()
+{
+ Dictionary::Ptr allOpts = Serialize(this, FAConfig);
+ Dictionary::Ptr opts = new Dictionary();
+
+ for (auto& opt : l_SDDowntimeOptions) {
+ opts->Set(opt, allOpts->Get(opt));
+ }
+
+ return SHA256(PackObject(opts));
+}
+
+bool ScheduledDowntime::AllConfigIsLoaded()
+{
+ return m_AllConfigLoaded.load();
+}
+
+std::atomic<bool> ScheduledDowntime::m_AllConfigLoaded (false);
diff --git a/lib/icinga/scheduleddowntime.hpp b/lib/icinga/scheduleddowntime.hpp
new file mode 100644
index 0000000..e701236
--- /dev/null
+++ b/lib/icinga/scheduleddowntime.hpp
@@ -0,0 +1,60 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SCHEDULEDDOWNTIME_H
+#define SCHEDULEDDOWNTIME_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/scheduleddowntime-ti.hpp"
+#include "icinga/checkable.hpp"
+#include <atomic>
+
+namespace icinga
+{
+
+class ApplyRule;
+struct ScriptFrame;
+class Host;
+class Service;
+
+/**
+ * An Icinga scheduled downtime specification.
+ *
+ * @ingroup icinga
+ */
+class ScheduledDowntime final : public ObjectImpl<ScheduledDowntime>
+{
+public:
+ DECLARE_OBJECT(ScheduledDowntime);
+ DECLARE_OBJECTNAME(ScheduledDowntime);
+
+ Checkable::Ptr GetCheckable() const;
+
+ static void EvaluateApplyRules(const intrusive_ptr<Host>& host);
+ static void EvaluateApplyRules(const intrusive_ptr<Service>& service);
+ static bool AllConfigIsLoaded();
+
+ void ValidateRanges(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateChildOptions(const Lazy<Value>& lvalue, const ValidationUtils& utils) override;
+ String HashDowntimeOptions();
+
+protected:
+ void OnAllConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+
+private:
+ static void TimerProc();
+
+ std::pair<double, double> FindRunningSegment(double minEnd = 0);
+ std::pair<double, double> FindNextSegment();
+ void CreateNextDowntime();
+ void RemoveObsoleteDowntimes();
+
+ static std::atomic<bool> m_AllConfigLoaded;
+
+ static bool EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter);
+ static bool EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter = false);
+};
+
+}
+
+#endif /* SCHEDULEDDOWNTIME_H */
diff --git a/lib/icinga/scheduleddowntime.ti b/lib/icinga/scheduleddowntime.ti
new file mode 100644
index 0000000..1653f27
--- /dev/null
+++ b/lib/icinga/scheduleddowntime.ti
@@ -0,0 +1,76 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#impl_include "icinga/service.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class ScheduledDowntimeNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class ScheduledDowntime : CustomVarObject < ScheduledDowntimeNameComposer
+{
+ // Scheduled Downtimes have a dependency on Downtimes. This is to make sure ScheduledDowntimes are activated after
+ // the Downtimes (and other checkables)
+ activation_priority 20;
+
+ load_after Host;
+ load_after Service;
+
+ [config, protected, no_user_modify, required, navigation(host)] name(Host) host_name {
+ navigate {{{
+ return Host::GetByName(GetHostName());
+ }}}
+ };
+ [config, protected, no_user_modify, navigation(service)] String service_name {
+ track {{{
+ if (!oldValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), oldValue);
+ DependencyGraph::RemoveDependency(this, service.get());
+ }
+
+ if (!newValue.IsEmpty()) {
+ Service::Ptr service = Service::GetByNamePair(GetHostName(), newValue);
+ DependencyGraph::AddDependency(this, service.get());
+ }
+ }}}
+ navigate {{{
+ if (GetServiceName().IsEmpty())
+ return nullptr;
+
+ Host::Ptr host = Host::GetByName(GetHostName());
+ return host->GetServiceByShortName(GetServiceName());
+ }}}
+ };
+
+ [config, required] String author;
+ [config, required] String comment;
+
+ [config] double duration;
+ [config] bool fixed {
+ default {{{ return true; }}}
+ };
+
+ [config] Value child_options {
+ default {{{ return "DowntimeNoChildren"; }}}
+ };
+
+ [config, required] Dictionary::Ptr ranges;
+};
+
+validator ScheduledDowntime {
+ Dictionary ranges {
+ String "*";
+ };
+};
+
+}
diff --git a/lib/icinga/service-apply.cpp b/lib/icinga/service-apply.cpp
new file mode 100644
index 0000000..4419e0b
--- /dev/null
+++ b/lib/icinga/service-apply.cpp
@@ -0,0 +1,133 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "config/configitembuilder.hpp"
+#include "config/applyrule.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+INITIALIZE_ONCE([]() {
+ ApplyRule::RegisterType("Service", { "Host" });
+});
+
+bool Service::EvaluateApplyRuleInstance(const Host::Ptr& host, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter)
+{
+ if (!skipFilter && !rule.EvaluateFilter(frame))
+ return false;
+
+ auto& di (rule.GetDebugInfo());
+
+#ifdef _DEBUG
+ Log(LogDebug, "Service")
+ << "Applying service '" << name << "' to host '" << host->GetName() << "' for rule " << di;
+#endif /* _DEBUG */
+
+ ConfigItemBuilder builder{di};
+ builder.SetType(Service::TypeInstance);
+ builder.SetName(name);
+ builder.SetScope(frame.Locals->ShallowClone());
+ builder.SetIgnoreOnError(rule.GetIgnoreOnError());
+
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "host_name"), OpSetLiteral, MakeLiteral(host->GetName()), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "name"), OpSetLiteral, MakeLiteral(name), di));
+
+ String zone = host->GetZoneName();
+
+ if (!zone.IsEmpty())
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "zone"), OpSetLiteral, MakeLiteral(zone), di));
+
+ builder.AddExpression(new SetExpression(MakeIndexer(ScopeThis, "package"), OpSetLiteral, MakeLiteral(rule.GetPackage()), di));
+
+ builder.AddExpression(new OwnedExpression(rule.GetExpression()));
+
+ ConfigItem::Ptr serviceItem = builder.Compile();
+ serviceItem->Register();
+
+ return true;
+}
+
+bool Service::EvaluateApplyRule(const Host::Ptr& host, const ApplyRule& rule, bool skipFilter)
+{
+ auto& di (rule.GetDebugInfo());
+
+ CONTEXT("Evaluating 'apply' rule (" << di << ")");
+
+ ScriptFrame frame(true);
+ if (rule.GetScope())
+ rule.GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+
+ Value vinstances;
+
+ if (rule.GetFTerm()) {
+ try {
+ vinstances = rule.GetFTerm()->Evaluate(frame);
+ } catch (const std::exception&) {
+ /* Silently ignore errors here and assume there are no instances. */
+ return false;
+ }
+ } else {
+ vinstances = new Array({ "" });
+ }
+
+ bool match = false;
+
+ if (vinstances.IsObjectType<Array>()) {
+ if (!rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Dictionary iterator requires value to be a dictionary.", di));
+
+ Array::Ptr arr = vinstances;
+
+ ObjectLock olock(arr);
+ for (const Value& instance : arr) {
+ String name = rule.GetName();
+
+ if (!rule.GetFKVar().IsEmpty()) {
+ frame.Locals->Set(rule.GetFKVar(), instance);
+ name += instance;
+ }
+
+ if (EvaluateApplyRuleInstance(host, name, frame, rule, skipFilter))
+ match = true;
+ }
+ } else if (vinstances.IsObjectType<Dictionary>()) {
+ if (rule.GetFVVar().IsEmpty())
+ BOOST_THROW_EXCEPTION(ScriptError("Array iterator requires value to be an array.", di));
+
+ Dictionary::Ptr dict = vinstances;
+
+ for (const String& key : dict->GetKeys()) {
+ frame.Locals->Set(rule.GetFKVar(), key);
+ frame.Locals->Set(rule.GetFVVar(), dict->Get(key));
+
+ if (EvaluateApplyRuleInstance(host, rule.GetName() + key, frame, rule, skipFilter))
+ match = true;
+ }
+ }
+
+ return match;
+}
+
+void Service::EvaluateApplyRules(const Host::Ptr& host)
+{
+ CONTEXT("Evaluating 'apply' rules for host '" << host->GetName() << "'");
+
+ for (auto& rule : ApplyRule::GetRules(Service::TypeInstance, Host::TypeInstance)) {
+ if (EvaluateApplyRule(host, *rule))
+ rule->AddMatch();
+ }
+
+ for (auto& rule : ApplyRule::GetTargetedHostRules(Service::TypeInstance, host->GetName())) {
+ if (EvaluateApplyRule(host, *rule, true))
+ rule->AddMatch();
+ }
+}
diff --git a/lib/icinga/service.cpp b/lib/icinga/service.cpp
new file mode 100644
index 0000000..d831136
--- /dev/null
+++ b/lib/icinga/service.cpp
@@ -0,0 +1,287 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/service.hpp"
+#include "icinga/service-ti.cpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/scheduleddowntime.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Service);
+
+boost::signals2::signal<void (const Service::Ptr&, const CheckResult::Ptr&, const MessageOrigin::Ptr&)> Service::OnHostProblemChanged;
+
+String ServiceNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
+{
+ Service::Ptr service = dynamic_pointer_cast<Service>(context);
+
+ if (!service)
+ return "";
+
+ return service->GetHostName() + "!" + shortName;
+}
+
+Dictionary::Ptr ServiceNameComposer::ParseName(const String& name) const
+{
+ std::vector<String> tokens = name.Split("!");
+
+ if (tokens.size() < 2)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid Service name."));
+
+ return new Dictionary({
+ { "host_name", tokens[0] },
+ { "name", tokens[1] }
+ });
+}
+
+void Service::OnAllConfigLoaded()
+{
+ ObjectImpl<Service>::OnAllConfigLoaded();
+
+ String zoneName = GetZoneName();
+
+ if (!zoneName.IsEmpty()) {
+ Zone::Ptr zone = Zone::GetByName(zoneName);
+
+ if (zone && zone->IsGlobal())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Service '" + GetName() + "' cannot be put into global zone '" + zone->GetName() + "'."));
+ }
+
+ m_Host = Host::GetByName(GetHostName());
+
+ if (m_Host)
+ m_Host->AddService(this);
+
+ ServiceGroup::EvaluateObjectRules(this);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ ServiceGroup::Ptr sg = ServiceGroup::GetByName(name);
+
+ if (sg)
+ sg->ResolveGroupMembership(this, true);
+ }
+ }
+}
+
+void Service::CreateChildObjects(const Type::Ptr& childType)
+{
+ if (childType == ScheduledDowntime::TypeInstance)
+ ScheduledDowntime::EvaluateApplyRules(this);
+
+ if (childType == Notification::TypeInstance)
+ Notification::EvaluateApplyRules(this);
+
+ if (childType == Dependency::TypeInstance)
+ Dependency::EvaluateApplyRules(this);
+}
+
+Service::Ptr Service::GetByNamePair(const String& hostName, const String& serviceName)
+{
+ if (!hostName.IsEmpty()) {
+ Host::Ptr host = Host::GetByName(hostName);
+
+ if (!host)
+ return nullptr;
+
+ return host->GetServiceByShortName(serviceName);
+ } else {
+ return Service::GetByName(serviceName);
+ }
+}
+
+Host::Ptr Service::GetHost() const
+{
+ return m_Host;
+}
+
+/* keep in sync with Host::GetSeverity()
+ * One could think it may be smart to use an enum and some bitmask math here.
+ * But the only thing the consuming icingaweb2 cares about is being able to
+ * sort by severity. It is therefore easier to keep them seperated here. */
+int Service::GetSeverity() const
+{
+ int severity;
+
+ ObjectLock olock(this);
+ ServiceState state = GetStateRaw();
+
+ if (!HasBeenChecked()) {
+ severity = 16;
+ } else if (state == ServiceOK) {
+ severity = 0;
+ } else {
+ switch (state) {
+ case ServiceWarning:
+ severity = 32;
+ break;
+ case ServiceUnknown:
+ severity = 64;
+ break;
+ case ServiceCritical:
+ severity = 128;
+ break;
+ default:
+ severity = 256;
+ }
+
+ Host::Ptr host = GetHost();
+ ObjectLock hlock (host);
+ if (host->GetState() != HostUp) {
+ severity += 1024;
+ } else {
+ if (IsAcknowledged())
+ severity += 512;
+ else if (IsInDowntime())
+ severity += 256;
+ else
+ severity += 2048;
+ }
+ hlock.Unlock();
+ }
+
+ olock.Unlock();
+
+ return severity;
+}
+
+bool Service::GetHandled() const
+{
+ return Checkable::GetHandled() || (m_Host && m_Host->GetProblem());
+}
+
+bool Service::IsStateOK(ServiceState state) const
+{
+ return state == ServiceOK;
+}
+
+void Service::SaveLastState(ServiceState state, double timestamp)
+{
+ if (state == ServiceOK)
+ SetLastStateOK(timestamp);
+ else if (state == ServiceWarning)
+ SetLastStateWarning(timestamp);
+ else if (state == ServiceCritical)
+ SetLastStateCritical(timestamp);
+ else if (state == ServiceUnknown)
+ SetLastStateUnknown(timestamp);
+}
+
+ServiceState Service::StateFromString(const String& state)
+{
+ if (state == "OK")
+ return ServiceOK;
+ else if (state == "WARNING")
+ return ServiceWarning;
+ else if (state == "CRITICAL")
+ return ServiceCritical;
+ else
+ return ServiceUnknown;
+}
+
+String Service::StateToString(ServiceState state)
+{
+ switch (state) {
+ case ServiceOK:
+ return "OK";
+ case ServiceWarning:
+ return "WARNING";
+ case ServiceCritical:
+ return "CRITICAL";
+ case ServiceUnknown:
+ default:
+ return "UNKNOWN";
+ }
+}
+
+StateType Service::StateTypeFromString(const String& type)
+{
+ if (type == "SOFT")
+ return StateTypeSoft;
+ else
+ return StateTypeHard;
+}
+
+String Service::StateTypeToString(StateType type)
+{
+ if (type == StateTypeSoft)
+ return "SOFT";
+ else
+ return "HARD";
+}
+
+bool Service::ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const
+{
+ if (macro == "state") {
+ *result = StateToString(GetState());
+ return true;
+ } else if (macro == "state_id") {
+ *result = GetState();
+ return true;
+ } else if (macro == "state_type") {
+ *result = StateTypeToString(GetStateType());
+ return true;
+ } else if (macro == "last_state") {
+ *result = StateToString(GetLastState());
+ return true;
+ } else if (macro == "last_state_id") {
+ *result = GetLastState();
+ return true;
+ } else if (macro == "last_state_type") {
+ *result = StateTypeToString(GetLastStateType());
+ return true;
+ } else if (macro == "last_state_change") {
+ *result = static_cast<long>(GetLastStateChange());
+ return true;
+ } else if (macro == "downtime_depth") {
+ *result = GetDowntimeDepth();
+ return true;
+ } else if (macro == "duration_sec") {
+ *result = Utility::GetTime() - GetLastStateChange();
+ return true;
+ }
+
+ if (cr) {
+ if (macro == "latency") {
+ *result = cr->CalculateLatency();
+ return true;
+ } else if (macro == "execution_time") {
+ *result = cr->CalculateExecutionTime();
+ return true;
+ } else if (macro == "output") {
+ *result = cr->GetOutput();
+ return true;
+ } else if (macro == "perfdata") {
+ *result = PluginUtility::FormatPerfdata(cr->GetPerformanceData());
+ return true;
+ } else if (macro == "check_source") {
+ *result = cr->GetCheckSource();
+ return true;
+ } else if (macro == "scheduling_source") {
+ *result = cr->GetSchedulingSource();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+std::pair<Host::Ptr, Service::Ptr> icinga::GetHostService(const Checkable::Ptr& checkable)
+{
+ Service::Ptr service = dynamic_pointer_cast<Service>(checkable);
+
+ if (service)
+ return std::make_pair(service->GetHost(), service);
+ else
+ return std::make_pair(static_pointer_cast<Host>(checkable), nullptr);
+}
diff --git a/lib/icinga/service.hpp b/lib/icinga/service.hpp
new file mode 100644
index 0000000..ac27c3d
--- /dev/null
+++ b/lib/icinga/service.hpp
@@ -0,0 +1,65 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERVICE_H
+#define SERVICE_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/service-ti.hpp"
+#include "icinga/macroresolver.hpp"
+#include "icinga/host.hpp"
+#include <tuple>
+
+using std::tie;
+
+namespace icinga
+{
+
+/**
+ * An Icinga service.
+ *
+ * @ingroup icinga
+ */
+class Service final : public ObjectImpl<Service>, public MacroResolver
+{
+public:
+ DECLARE_OBJECT(Service);
+ DECLARE_OBJECTNAME(Service);
+
+ static Service::Ptr GetByNamePair(const String& hostName, const String& serviceName);
+
+ Host::Ptr GetHost() const override;
+ int GetSeverity() const override;
+ bool GetHandled() const override;
+
+ bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const override;
+
+ bool IsStateOK(ServiceState state) const override;
+ void SaveLastState(ServiceState state, double timestamp) override;
+
+ static ServiceState StateFromString(const String& state);
+ static String StateToString(ServiceState state);
+
+ static StateType StateTypeFromString(const String& state);
+ static String StateTypeToString(StateType state);
+
+ static void EvaluateApplyRules(const Host::Ptr& host);
+
+ void OnAllConfigLoaded() override;
+
+ static boost::signals2::signal<void (const Service::Ptr&, const CheckResult::Ptr&, const MessageOrigin::Ptr&)> OnHostProblemChanged;
+
+protected:
+ void CreateChildObjects(const Type::Ptr& childType) override;
+
+private:
+ Host::Ptr m_Host;
+
+ static bool EvaluateApplyRuleInstance(const Host::Ptr& host, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter);
+ static bool EvaluateApplyRule(const Host::Ptr& host, const ApplyRule& rule, bool skipFilter = false);
+};
+
+std::pair<Host::Ptr, Service::Ptr> GetHostService(const Checkable::Ptr& checkable);
+
+}
+
+#endif /* SERVICE_H */
diff --git a/lib/icinga/service.ti b/lib/icinga/service.ti
new file mode 100644
index 0000000..12c2d8c
--- /dev/null
+++ b/lib/icinga/service.ti
@@ -0,0 +1,71 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/customvarobject.hpp"
+#impl_include "icinga/servicegroup.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+code {{{
+class ServiceNameComposer : public NameComposer
+{
+public:
+ virtual String MakeName(const String& shortName, const Object::Ptr& context) const;
+ virtual Dictionary::Ptr ParseName(const String& name) const;
+};
+}}}
+
+class Service : Checkable < ServiceNameComposer
+{
+ load_after ApiListener;
+ load_after Endpoint;
+ load_after Host;
+ load_after Zone;
+
+ [config, no_user_modify, required, signal_with_old_value] array(name(ServiceGroup)) groups {
+ default {{{ return new Array(); }}}
+ };
+
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetShortName();
+ else
+ return displayName;
+ }}}
+ };
+ [config, no_user_modify, required] name(Host) host_name;
+ [no_storage, navigation] Host::Ptr host {
+ get;
+ navigate {{{
+ return GetHost();
+ }}}
+ };
+ [enum, no_storage] ServiceState "state" {
+ get {{{
+ return GetStateRaw();
+ }}}
+ };
+ [enum, no_storage] ServiceState last_state {
+ get {{{
+ return GetLastStateRaw();
+ }}}
+ };
+ [enum, no_storage] ServiceState last_hard_state {
+ get {{{
+ return GetLastHardStateRaw();
+ }}}
+ };
+ [state] Timestamp last_state_ok (LastStateOK);
+ [state] Timestamp last_state_warning;
+ [state] Timestamp last_state_critical;
+ [state] Timestamp last_state_unknown;
+};
+
+}
diff --git a/lib/icinga/servicegroup.cpp b/lib/icinga/servicegroup.cpp
new file mode 100644
index 0000000..d21f852
--- /dev/null
+++ b/lib/icinga/servicegroup.cpp
@@ -0,0 +1,111 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/servicegroup.hpp"
+#include "icinga/servicegroup-ti.cpp"
+#include "config/objectrule.hpp"
+#include "config/configitem.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(ServiceGroup);
+
+INITIALIZE_ONCE([]() {
+ ObjectRule::RegisterType("ServiceGroup");
+});
+
+bool ServiceGroup::EvaluateObjectRule(const Service::Ptr& service, const ConfigItem::Ptr& group)
+{
+ String groupName = group->GetName();
+
+ CONTEXT("Evaluating rule for group '" << groupName << "'");
+
+ Host::Ptr host = service->GetHost();
+
+ ScriptFrame frame(true);
+ if (group->GetScope())
+ group->GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("host", host);
+ frame.Locals->Set("service", service);
+
+ if (!group->GetFilter()->Evaluate(frame).GetValue().ToBool())
+ return false;
+
+ Log(LogDebug, "ServiceGroup")
+ << "Assigning membership for group '" << groupName << "' to service '" << service->GetName() << "'";
+
+ Array::Ptr groups = service->GetGroups();
+
+ if (groups && !groups->Contains(groupName))
+ groups->Add(groupName);
+
+ return true;
+}
+
+void ServiceGroup::EvaluateObjectRules(const Service::Ptr& service)
+{
+ CONTEXT("Evaluating group membership for service '" << service->GetName() << "'");
+
+ for (const ConfigItem::Ptr& group : ConfigItem::GetItems(ServiceGroup::TypeInstance))
+ {
+ if (!group->GetFilter())
+ continue;
+
+ EvaluateObjectRule(service, group);
+ }
+}
+
+std::set<Service::Ptr> ServiceGroup::GetMembers() const
+{
+ std::unique_lock<std::mutex> lock(m_ServiceGroupMutex);
+ return m_Members;
+}
+
+void ServiceGroup::AddMember(const Service::Ptr& service)
+{
+ service->AddGroup(GetName());
+
+ std::unique_lock<std::mutex> lock(m_ServiceGroupMutex);
+ m_Members.insert(service);
+}
+
+void ServiceGroup::RemoveMember(const Service::Ptr& service)
+{
+ std::unique_lock<std::mutex> lock(m_ServiceGroupMutex);
+ m_Members.erase(service);
+}
+
+bool ServiceGroup::ResolveGroupMembership(const Service::Ptr& service, bool add, int rstack) {
+
+ if (add && rstack > 20) {
+ Log(LogWarning, "ServiceGroup")
+ << "Too many nested groups for group '" << GetName() << "': Service '"
+ << service->GetName() << "' membership assignment failed.";
+
+ return false;
+ }
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups && groups->GetLength() > 0) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ ServiceGroup::Ptr group = ServiceGroup::GetByName(name);
+
+ if (group && !group->ResolveGroupMembership(service, add, rstack + 1))
+ return false;
+ }
+ }
+
+ if (add)
+ AddMember(service);
+ else
+ RemoveMember(service);
+
+ return true;
+}
diff --git a/lib/icinga/servicegroup.hpp b/lib/icinga/servicegroup.hpp
new file mode 100644
index 0000000..f2d0ab7
--- /dev/null
+++ b/lib/icinga/servicegroup.hpp
@@ -0,0 +1,43 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef SERVICEGROUP_H
+#define SERVICEGROUP_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/servicegroup-ti.hpp"
+#include "icinga/service.hpp"
+
+namespace icinga
+{
+
+class ConfigItem;
+
+/**
+ * An Icinga service group.
+ *
+ * @ingroup icinga
+ */
+class ServiceGroup final : public ObjectImpl<ServiceGroup>
+{
+public:
+ DECLARE_OBJECT(ServiceGroup);
+ DECLARE_OBJECTNAME(ServiceGroup);
+
+ std::set<Service::Ptr> GetMembers() const;
+ void AddMember(const Service::Ptr& service);
+ void RemoveMember(const Service::Ptr& service);
+
+ bool ResolveGroupMembership(const Service::Ptr& service, bool add = true, int rstack = 0);
+
+ static void EvaluateObjectRules(const Service::Ptr& service);
+
+private:
+ mutable std::mutex m_ServiceGroupMutex;
+ std::set<Service::Ptr> m_Members;
+
+ static bool EvaluateObjectRule(const Service::Ptr& service, const intrusive_ptr<ConfigItem>& group);
+};
+
+}
+
+#endif /* SERVICEGROUP_H */
diff --git a/lib/icinga/servicegroup.ti b/lib/icinga/servicegroup.ti
new file mode 100644
index 0000000..7daf9d4
--- /dev/null
+++ b/lib/icinga/servicegroup.ti
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class ServiceGroup : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+
+ [config, no_user_modify] array(name(ServiceGroup)) groups;
+ [config] String notes;
+ [config] String notes_url;
+ [config] String action_url;
+};
+
+}
diff --git a/lib/icinga/timeperiod.cpp b/lib/icinga/timeperiod.cpp
new file mode 100644
index 0000000..db3272e
--- /dev/null
+++ b/lib/icinga/timeperiod.cpp
@@ -0,0 +1,399 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/timeperiod.hpp"
+#include "icinga/timeperiod-ti.cpp"
+#include "icinga/legacytimeperiod.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include "base/timer.hpp"
+#include "base/utility.hpp"
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+REGISTER_TYPE(TimePeriod);
+
+static Timer::Ptr l_UpdateTimer;
+
+void TimePeriod::Start(bool runtimeCreated)
+{
+ ObjectImpl<TimePeriod>::Start(runtimeCreated);
+
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, [this]() {
+ l_UpdateTimer = Timer::Create();
+ l_UpdateTimer->SetInterval(300);
+ l_UpdateTimer->OnTimerExpired.connect([](const Timer * const&) { UpdateTimerHandler(); });
+ l_UpdateTimer->Start();
+ });
+
+ /* Pre-fill the time period for the next 24 hours. */
+ double now = Utility::GetTime();
+ UpdateRegion(now, now + 24 * 3600, true);
+#ifdef _DEBUG
+ Dump();
+#endif /* _DEBUG */
+}
+
+void TimePeriod::AddSegment(double begin, double end)
+{
+ ASSERT(OwnsLock());
+
+ Log(LogDebug, "TimePeriod")
+ << "Adding segment '" << Utility::FormatDateTime("%c", begin) << "' <-> '"
+ << Utility::FormatDateTime("%c", end) << "' to TimePeriod '" << GetName() << "'";
+
+ if (GetValidBegin().IsEmpty() || begin < GetValidBegin())
+ SetValidBegin(begin);
+
+ if (GetValidEnd().IsEmpty() || end > GetValidEnd())
+ SetValidEnd(end);
+
+ Array::Ptr segments = GetSegments();
+
+ if (segments) {
+ /* Try to merge the new segment into an existing segment. */
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ if (segment->Get("begin") <= begin && segment->Get("end") >= end)
+ return; /* New segment is fully contained in this segment. */
+
+ if (segment->Get("begin") >= begin && segment->Get("end") <= end) {
+ segment->Set("begin", begin);
+ segment->Set("end", end); /* Extend an existing segment to both sides */
+ return;
+ }
+
+ if (segment->Get("end") >= begin && segment->Get("end") <= end) {
+ segment->Set("end", end); /* Extend an existing segment to right. */
+ return;
+ }
+
+ if (segment->Get("begin") >= begin && segment->Get("begin") <= end) {
+ segment->Set("begin", begin); /* Extend an existing segment to left. */
+ return;
+ }
+
+ }
+ }
+
+ /* Create new segment if we weren't able to merge this into an existing segment. */
+ Dictionary::Ptr segment = new Dictionary({
+ { "begin", begin },
+ { "end", end }
+ });
+
+ if (!segments) {
+ segments = new Array();
+ SetSegments(segments);
+ }
+
+ segments->Add(segment);
+}
+
+void TimePeriod::AddSegment(const Dictionary::Ptr& segment)
+{
+ AddSegment(segment->Get("begin"), segment->Get("end"));
+}
+
+void TimePeriod::RemoveSegment(double begin, double end)
+{
+ ASSERT(OwnsLock());
+
+ Log(LogDebug, "TimePeriod")
+ << "Removing segment '" << Utility::FormatDateTime("%c", begin) << "' <-> '"
+ << Utility::FormatDateTime("%c", end) << "' from TimePeriod '" << GetName() << "'";
+
+ if (GetValidBegin().IsEmpty() || begin < GetValidBegin())
+ SetValidBegin(begin);
+
+ if (GetValidEnd().IsEmpty() || end > GetValidEnd())
+ SetValidEnd(end);
+
+ Array::Ptr segments = GetSegments();
+
+ if (!segments)
+ return;
+
+ Array::Ptr newSegments = new Array();
+
+ /* Try to split or adjust an existing segment. */
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ /* Fully contained in the specified range? */
+ if (segment->Get("begin") >= begin && segment->Get("end") <= end)
+ // Don't add the old segment, because the segment is fully contained into our range
+ continue;
+
+ /* Not overlapping at all? */
+ if (segment->Get("end") < begin || segment->Get("begin") > end) {
+ newSegments->Add(segment);
+ continue;
+ }
+
+ /* Cut between */
+ if (segment->Get("begin") < begin && segment->Get("end") > end) {
+ newSegments->Add(new Dictionary({
+ { "begin", segment->Get("begin") },
+ { "end", begin }
+ }));
+
+ newSegments->Add(new Dictionary({
+ { "begin", end },
+ { "end", segment->Get("end") }
+ }));
+ // Don't add the old segment, because we have now two new segments and a gap between
+ continue;
+ }
+
+ /* Adjust the begin/end timestamps so as to not overlap with the specified range. */
+ if (segment->Get("begin") > begin && segment->Get("begin") < end)
+ segment->Set("begin", end);
+
+ if (segment->Get("end") > begin && segment->Get("end") < end)
+ segment->Set("end", begin);
+
+ newSegments->Add(segment);
+ }
+
+ SetSegments(newSegments);
+
+#ifdef _DEBUG
+ Dump();
+#endif /* _DEBUG */
+}
+
+void TimePeriod::RemoveSegment(const Dictionary::Ptr& segment)
+{
+ RemoveSegment(segment->Get("begin"), segment->Get("end"));
+}
+
+void TimePeriod::PurgeSegments(double end)
+{
+ ASSERT(OwnsLock());
+
+ Log(LogDebug, "TimePeriod")
+ << "Purging segments older than '" << Utility::FormatDateTime("%c", end)
+ << "' from TimePeriod '" << GetName() << "'";
+
+ if (GetValidBegin().IsEmpty() || end < GetValidBegin())
+ return;
+
+ SetValidBegin(end);
+
+ Array::Ptr segments = GetSegments();
+
+ if (!segments)
+ return;
+
+ Array::Ptr newSegments = new Array();
+
+ /* Remove old segments. */
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ if (segment->Get("end") >= end)
+ newSegments->Add(segment);
+ }
+
+ SetSegments(newSegments);
+}
+
+void TimePeriod::Merge(const TimePeriod::Ptr& timeperiod, bool include)
+{
+ Log(LogDebug, "TimePeriod")
+ << "Merge TimePeriod '" << GetName() << "' with '" << timeperiod->GetName() << "' "
+ << "Method: " << (include ? "include" : "exclude");
+
+ Array::Ptr segments = timeperiod->GetSegments();
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ ObjectLock ilock(this);
+ for (const Dictionary::Ptr& segment : segments) {
+ include ? AddSegment(segment) : RemoveSegment(segment);
+ }
+ }
+}
+
+void TimePeriod::UpdateRegion(double begin, double end, bool clearExisting)
+{
+ if (clearExisting) {
+ ObjectLock olock(this);
+ SetSegments(new Array());
+ } else {
+ if (begin < GetValidEnd())
+ begin = GetValidEnd();
+
+ if (end < GetValidEnd())
+ return;
+ }
+
+ Array::Ptr segments = GetUpdate()->Invoke({ this, begin, end });
+
+ {
+ ObjectLock olock(this);
+ RemoveSegment(begin, end);
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ AddSegment(segment);
+ }
+ }
+ }
+
+ bool preferInclude = GetPreferIncludes();
+
+ /* First handle the non preferred timeranges */
+ Array::Ptr timeranges = preferInclude ? GetExcludes() : GetIncludes();
+
+ if (timeranges) {
+ ObjectLock olock(timeranges);
+ for (const String& name : timeranges) {
+ const TimePeriod::Ptr timeperiod = TimePeriod::GetByName(name);
+
+ if (timeperiod)
+ Merge(timeperiod, !preferInclude);
+ }
+ }
+
+ /* Preferred timeranges must be handled at the end */
+ timeranges = preferInclude ? GetIncludes() : GetExcludes();
+
+ if (timeranges) {
+ ObjectLock olock(timeranges);
+ for (const String& name : timeranges) {
+ const TimePeriod::Ptr timeperiod = TimePeriod::GetByName(name);
+
+ if (timeperiod)
+ Merge(timeperiod, preferInclude);
+ }
+ }
+}
+
+bool TimePeriod::GetIsInside() const
+{
+ return IsInside(Utility::GetTime());
+}
+
+bool TimePeriod::IsInside(double ts) const
+{
+ ObjectLock olock(this);
+
+ if (GetValidBegin().IsEmpty() || ts < GetValidBegin() || GetValidEnd().IsEmpty() || ts > GetValidEnd())
+ return true; /* Assume that all invalid regions are "inside". */
+
+ Array::Ptr segments = GetSegments();
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ if (ts > segment->Get("begin") && ts < segment->Get("end"))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+double TimePeriod::FindNextTransition(double begin)
+{
+ ObjectLock olock(this);
+
+ Array::Ptr segments = GetSegments();
+
+ double closestTransition = -1;
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ if (segment->Get("begin") > begin && (segment->Get("begin") < closestTransition || closestTransition == -1))
+ closestTransition = segment->Get("begin");
+
+ if (segment->Get("end") > begin && (segment->Get("end") < closestTransition || closestTransition == -1))
+ closestTransition = segment->Get("end");
+ }
+ }
+
+ return closestTransition;
+}
+
+void TimePeriod::UpdateTimerHandler()
+{
+ double now = Utility::GetTime();
+
+ for (const TimePeriod::Ptr& tp : ConfigType::GetObjectsByType<TimePeriod>()) {
+ if (!tp->IsActive())
+ continue;
+
+ double valid_end;
+
+ {
+ ObjectLock olock(tp);
+ tp->PurgeSegments(now - 3600);
+
+ valid_end = tp->GetValidEnd();
+ }
+
+ tp->UpdateRegion(valid_end, now + 24 * 3600, false);
+#ifdef _DEBUG
+ tp->Dump();
+#endif /* _DEBUG */
+ }
+}
+
+void TimePeriod::Dump()
+{
+ ObjectLock olock(this);
+
+ Array::Ptr segments = GetSegments();
+
+ Log(LogDebug, "TimePeriod")
+ << "Dumping TimePeriod '" << GetName() << "'";
+
+ Log(LogDebug, "TimePeriod")
+ << "Valid from '" << Utility::FormatDateTime("%c", GetValidBegin())
+ << "' until '" << Utility::FormatDateTime("%c", GetValidEnd());
+
+ if (segments) {
+ ObjectLock dlock(segments);
+ for (const Dictionary::Ptr& segment : segments) {
+ Log(LogDebug, "TimePeriod")
+ << "Segment: " << Utility::FormatDateTime("%c", segment->Get("begin")) << " <-> "
+ << Utility::FormatDateTime("%c", segment->Get("end"));
+ }
+ }
+
+ Log(LogDebug, "TimePeriod", "---");
+}
+
+void TimePeriod::ValidateRanges(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ if (!lvalue())
+ return;
+
+ /* create a fake time environment to validate the definitions */
+ time_t refts = Utility::GetTime();
+ tm reference = Utility::LocalTime(refts);
+ Array::Ptr segments = new Array();
+
+ ObjectLock olock(lvalue());
+ for (const Dictionary::Pair& kv : lvalue()) {
+ try {
+ tm begin_tm, end_tm;
+ int stride;
+ LegacyTimePeriod::ParseTimeRange(kv.first, &begin_tm, &end_tm, &stride, &reference);
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "ranges" }, "Invalid time specification '" + kv.first + "': " + ex.what()));
+ }
+
+ try {
+ LegacyTimePeriod::ProcessTimeRanges(kv.second, &reference, segments);
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "ranges" }, "Invalid time range definition '" + kv.second + "': " + ex.what()));
+ }
+ }
+}
diff --git a/lib/icinga/timeperiod.hpp b/lib/icinga/timeperiod.hpp
new file mode 100644
index 0000000..a5a2f73
--- /dev/null
+++ b/lib/icinga/timeperiod.hpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TIMEPERIOD_H
+#define TIMEPERIOD_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/timeperiod-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * A time period.
+ *
+ * @ingroup icinga
+ */
+class TimePeriod final : public ObjectImpl<TimePeriod>
+{
+public:
+ DECLARE_OBJECT(TimePeriod);
+ DECLARE_OBJECTNAME(TimePeriod);
+
+ void Start(bool runtimeCreated) override;
+
+ void UpdateRegion(double begin, double end, bool clearExisting);
+
+ bool GetIsInside() const override;
+
+ bool IsInside(double ts) const;
+ double FindNextTransition(double begin);
+
+ void ValidateRanges(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ void AddSegment(double s, double end);
+ void AddSegment(const Dictionary::Ptr& segment);
+ void RemoveSegment(double begin, double end);
+ void RemoveSegment(const Dictionary::Ptr& segment);
+ void PurgeSegments(double end);
+
+ void Merge(const TimePeriod::Ptr& timeperiod, bool include = true);
+
+ void Dump();
+
+ static void UpdateTimerHandler();
+};
+
+}
+
+#endif /* TIMEPERIOD_H */
diff --git a/lib/icinga/timeperiod.ti b/lib/icinga/timeperiod.ti
new file mode 100644
index 0000000..bba272e
--- /dev/null
+++ b/lib/icinga/timeperiod.ti
@@ -0,0 +1,47 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "base/function.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class TimePeriod : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+ [config, signal_with_old_value] Dictionary::Ptr ranges;
+ [config, required] Function::Ptr update;
+ [config] bool prefer_includes {
+ default {{{ return true; }}}
+ };
+ [config, required, signal_with_old_value] array(name(TimePeriod)) excludes {
+ default {{{ return new Array(); }}}
+ };
+ [config, required, signal_with_old_value] array(name(TimePeriod)) includes {
+ default {{{ return new Array(); }}}
+ };
+ [state, no_user_modify] Value valid_begin;
+ [state, no_user_modify] Value valid_end;
+ [state, no_user_modify] Array::Ptr segments;
+ [no_storage] bool is_inside {
+ get;
+ };
+};
+
+validator TimePeriod {
+ Dictionary ranges {
+ String "*";
+ };
+};
+
+}
diff --git a/lib/icinga/user.cpp b/lib/icinga/user.cpp
new file mode 100644
index 0000000..4d99db7
--- /dev/null
+++ b/lib/icinga/user.cpp
@@ -0,0 +1,103 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/user.hpp"
+#include "icinga/user-ti.cpp"
+#include "icinga/usergroup.hpp"
+#include "icinga/notification.hpp"
+#include "icinga/usergroup.hpp"
+#include "base/objectlock.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(User);
+
+void User::OnConfigLoaded()
+{
+ ObjectImpl<User>::OnConfigLoaded();
+
+ SetTypeFilter(FilterArrayToInt(GetTypes(), Notification::GetTypeFilterMap(), ~0));
+ SetStateFilter(FilterArrayToInt(GetStates(), Notification::GetStateFilterMap(), ~0));
+}
+
+void User::OnAllConfigLoaded()
+{
+ ObjectImpl<User>::OnAllConfigLoaded();
+
+ UserGroup::EvaluateObjectRules(this);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ groups = groups->ShallowClone();
+
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ UserGroup::Ptr ug = UserGroup::GetByName(name);
+
+ if (ug)
+ ug->ResolveGroupMembership(this, true);
+ }
+ }
+}
+
+void User::Stop(bool runtimeRemoved)
+{
+ ObjectImpl<User>::Stop(runtimeRemoved);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ UserGroup::Ptr ug = UserGroup::GetByName(name);
+
+ if (ug)
+ ug->ResolveGroupMembership(this, false);
+ }
+ }
+}
+
+void User::AddGroup(const String& name)
+{
+ std::unique_lock<std::mutex> lock(m_UserMutex);
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups && groups->Contains(name))
+ return;
+
+ if (!groups)
+ groups = new Array();
+
+ groups->Add(name);
+}
+
+TimePeriod::Ptr User::GetPeriod() const
+{
+ return TimePeriod::GetByName(GetPeriodRaw());
+}
+
+void User::ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<User>::ValidateStates(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), Notification::GetStateFilterMap(), 0);
+
+ if (filter == -1 || (filter & ~(StateFilterUp | StateFilterDown | StateFilterOK | StateFilterWarning | StateFilterCritical | StateFilterUnknown)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid."));
+}
+
+void User::ValidateTypes(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<User>::ValidateTypes(lvalue, utils);
+
+ int filter = FilterArrayToInt(lvalue(), Notification::GetTypeFilterMap(), 0);
+
+ if (filter == -1 || (filter & ~(NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved |
+ NotificationCustom | NotificationAcknowledgement | NotificationProblem | NotificationRecovery |
+ NotificationFlappingStart | NotificationFlappingEnd)) != 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "types" }, "Type filter is invalid."));
+}
diff --git a/lib/icinga/user.hpp b/lib/icinga/user.hpp
new file mode 100644
index 0000000..14e59c2
--- /dev/null
+++ b/lib/icinga/user.hpp
@@ -0,0 +1,44 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef USER_H
+#define USER_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/user-ti.hpp"
+#include "icinga/timeperiod.hpp"
+#include "remote/messageorigin.hpp"
+
+namespace icinga
+{
+
+/**
+ * A User.
+ *
+ * @ingroup icinga
+ */
+class User final : public ObjectImpl<User>
+{
+public:
+ DECLARE_OBJECT(User);
+ DECLARE_OBJECTNAME(User);
+
+ void AddGroup(const String& name);
+
+ /* Notifications */
+ TimePeriod::Ptr GetPeriod() const;
+
+ void ValidateStates(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+ void ValidateTypes(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+protected:
+ void Stop(bool runtimeRemoved) override;
+
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+private:
+ mutable std::mutex m_UserMutex;
+};
+
+}
+
+#endif /* USER_H */
diff --git a/lib/icinga/user.ti b/lib/icinga/user.ti
new file mode 100644
index 0000000..8b8c43a
--- /dev/null
+++ b/lib/icinga/user.ti
@@ -0,0 +1,47 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+#include "base/array.hpp"
+#impl_include "icinga/usergroup.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class User : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+ [config, no_user_modify, required, signal_with_old_value] array(name(UserGroup)) groups {
+ default {{{ return new Array(); }}}
+ };
+ [config, navigation] name(TimePeriod) period (PeriodRaw) {
+ navigate {{{
+ return TimePeriod::GetByName(GetPeriodRaw());
+ }}}
+ };
+
+ [config] array(Value) types;
+ [no_user_view, no_user_modify] int type_filter_real (TypeFilter);
+ [config] array(Value) states;
+ [no_user_view, no_user_modify] int state_filter_real (StateFilter);
+
+ [config] String email;
+ [config] String pager;
+
+ [config] bool enable_notifications {
+ default {{{ return true; }}}
+ };
+
+ [state] Timestamp last_notification;
+};
+
+}
diff --git a/lib/icinga/usergroup.cpp b/lib/icinga/usergroup.cpp
new file mode 100644
index 0000000..27ae45b
--- /dev/null
+++ b/lib/icinga/usergroup.cpp
@@ -0,0 +1,128 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/usergroup.hpp"
+#include "icinga/usergroup-ti.cpp"
+#include "icinga/notification.hpp"
+#include "config/objectrule.hpp"
+#include "config/configitem.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+#include "base/context.hpp"
+#include "base/workqueue.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(UserGroup);
+
+INITIALIZE_ONCE([]() {
+ ObjectRule::RegisterType("UserGroup");
+});
+
+bool UserGroup::EvaluateObjectRule(const User::Ptr& user, const ConfigItem::Ptr& group)
+{
+ String groupName = group->GetName();
+
+ CONTEXT("Evaluating rule for group '" << groupName << "'");
+
+ ScriptFrame frame(true);
+ if (group->GetScope())
+ group->GetScope()->CopyTo(frame.Locals);
+ frame.Locals->Set("user", user);
+
+ if (!group->GetFilter()->Evaluate(frame).GetValue().ToBool())
+ return false;
+
+ Log(LogDebug, "UserGroup")
+ << "Assigning membership for group '" << groupName << "' to user '" << user->GetName() << "'";
+
+ Array::Ptr groups = user->GetGroups();
+
+ if (groups && !groups->Contains(groupName))
+ groups->Add(groupName);
+
+ return true;
+}
+
+void UserGroup::EvaluateObjectRules(const User::Ptr& user)
+{
+ CONTEXT("Evaluating group membership for user '" << user->GetName() << "'");
+
+ for (const ConfigItem::Ptr& group : ConfigItem::GetItems(UserGroup::TypeInstance))
+ {
+ if (!group->GetFilter())
+ continue;
+
+ EvaluateObjectRule(user, group);
+ }
+}
+
+std::set<User::Ptr> UserGroup::GetMembers() const
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ return m_Members;
+}
+
+void UserGroup::AddMember(const User::Ptr& user)
+{
+ user->AddGroup(GetName());
+
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ m_Members.insert(user);
+}
+
+void UserGroup::RemoveMember(const User::Ptr& user)
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ m_Members.erase(user);
+}
+
+std::set<Notification::Ptr> UserGroup::GetNotifications() const
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ return m_Notifications;
+}
+
+void UserGroup::AddNotification(const Notification::Ptr& notification)
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ m_Notifications.insert(notification);
+}
+
+void UserGroup::RemoveNotification(const Notification::Ptr& notification)
+{
+ std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+ m_Notifications.erase(notification);
+}
+
+bool UserGroup::ResolveGroupMembership(const User::Ptr& user, bool add, int rstack) {
+
+ if (add && rstack > 20) {
+ Log(LogWarning, "UserGroup")
+ << "Too many nested groups for group '" << GetName() << "': User '"
+ << user->GetName() << "' membership assignment failed.";
+
+ return false;
+ }
+
+ Array::Ptr groups = GetGroups();
+
+ if (groups && groups->GetLength() > 0) {
+ ObjectLock olock(groups);
+
+ for (const String& name : groups) {
+ UserGroup::Ptr group = UserGroup::GetByName(name);
+
+ if (group && !group->ResolveGroupMembership(user, add, rstack + 1))
+ return false;
+ }
+ }
+
+ if (add)
+ AddMember(user);
+ else
+ RemoveMember(user);
+
+ return true;
+}
+
diff --git a/lib/icinga/usergroup.hpp b/lib/icinga/usergroup.hpp
new file mode 100644
index 0000000..c6f82a1
--- /dev/null
+++ b/lib/icinga/usergroup.hpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef USERGROUP_H
+#define USERGROUP_H
+
+#include "icinga/i2-icinga.hpp"
+#include "icinga/usergroup-ti.hpp"
+#include "icinga/user.hpp"
+
+namespace icinga
+{
+
+class ConfigItem;
+class Notification;
+
+/**
+ * An Icinga user group.
+ *
+ * @ingroup icinga
+ */
+class UserGroup final : public ObjectImpl<UserGroup>
+{
+public:
+ DECLARE_OBJECT(UserGroup);
+ DECLARE_OBJECTNAME(UserGroup);
+
+ std::set<User::Ptr> GetMembers() const;
+ void AddMember(const User::Ptr& user);
+ void RemoveMember(const User::Ptr& user);
+
+ std::set<intrusive_ptr<Notification>> GetNotifications() const;
+ void AddNotification(const intrusive_ptr<Notification>& notification);
+ void RemoveNotification(const intrusive_ptr<Notification>& notification);
+
+ bool ResolveGroupMembership(const User::Ptr& user, bool add = true, int rstack = 0);
+
+ static void EvaluateObjectRules(const User::Ptr& user);
+
+private:
+ mutable std::mutex m_UserGroupMutex;
+ std::set<User::Ptr> m_Members;
+ std::set<intrusive_ptr<Notification>> m_Notifications;
+
+ static bool EvaluateObjectRule(const User::Ptr& user, const intrusive_ptr<ConfigItem>& group);
+};
+
+}
+
+#endif /* USERGROUP_H */
diff --git a/lib/icinga/usergroup.ti b/lib/icinga/usergroup.ti
new file mode 100644
index 0000000..e955c5e
--- /dev/null
+++ b/lib/icinga/usergroup.ti
@@ -0,0 +1,25 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icinga/customvarobject.hpp"
+
+library icinga;
+
+namespace icinga
+{
+
+class UserGroup : CustomVarObject
+{
+ [config] String display_name {
+ get {{{
+ String displayName = m_DisplayName.load();
+ if (displayName.IsEmpty())
+ return GetName();
+ else
+ return displayName;
+ }}}
+ };
+
+ [config, no_user_modify] array(name(UserGroup)) groups;
+};
+
+}
diff --git a/lib/icingadb/CMakeLists.txt b/lib/icingadb/CMakeLists.txt
new file mode 100644
index 0000000..de8e4ad
--- /dev/null
+++ b/lib/icingadb/CMakeLists.txt
@@ -0,0 +1,32 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(icingadb.ti icingadb-ti.cpp icingadb-ti.hpp)
+
+mkembedconfig_target(icingadb-itl.conf icingadb-itl.cpp)
+
+set(icingadb_SOURCES
+ icingadb.cpp icingadb-objects.cpp icingadb-stats.cpp icingadb-utility.cpp redisconnection.cpp icingadb-ti.hpp
+ icingadbchecktask.cpp icingadb-itl.cpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(icingadb icingadb icingadb_SOURCES)
+endif()
+
+add_library(icingadb OBJECT ${icingadb_SOURCES})
+
+include_directories(${icinga2_SOURCE_DIR}/third-party)
+
+add_dependencies(icingadb base config icinga remote)
+
+set_target_properties (
+ icingadb PROPERTIES
+ FOLDER Components
+)
+
+install_if_not_exists(
+ ${PROJECT_SOURCE_DIR}/etc/icinga2/features-available/icingadb.conf
+ ${CMAKE_INSTALL_SYSCONFDIR}/icinga2/features-available
+)
+
+set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE)
diff --git a/lib/icingadb/icingadb-itl.conf b/lib/icingadb/icingadb-itl.conf
new file mode 100644
index 0000000..5f3950e
--- /dev/null
+++ b/lib/icingadb/icingadb-itl.conf
@@ -0,0 +1,24 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+System.assert(Internal.run_with_activation_context(function() {
+ template CheckCommand "icingadb-check-command" use (checkFunc = Internal.IcingadbCheck) {
+ execute = checkFunc
+ }
+
+ object CheckCommand "icingadb" {
+ import "icingadb-check-command"
+
+ vars.icingadb_name = "icingadb"
+
+ vars.icingadb_full_dump_duration_warning = 5m
+ vars.icingadb_full_dump_duration_critical = 10m
+ vars.icingadb_full_sync_duration_warning = 5m
+ vars.icingadb_full_sync_duration_critical = 10m
+ vars.icingadb_redis_backlog_warning = 5m
+ vars.icingadb_redis_backlog_critical = 15m
+ vars.icingadb_database_backlog_warning = 5m
+ vars.icingadb_database_backlog_critical = 15m
+ }
+}))
+
+Internal.remove("IcingadbCheck")
diff --git a/lib/icingadb/icingadb-objects.cpp b/lib/icingadb/icingadb-objects.cpp
new file mode 100644
index 0000000..ff7a833
--- /dev/null
+++ b/lib/icingadb/icingadb-objects.cpp
@@ -0,0 +1,2966 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadb.hpp"
+#include "icingadb/redisconnection.hpp"
+#include "base/configtype.hpp"
+#include "base/configobject.hpp"
+#include "base/defer.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/shared.hpp"
+#include "base/tlsutility.hpp"
+#include "base/initialize.hpp"
+#include "base/convert.hpp"
+#include "base/array.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include "base/object-packer.hpp"
+#include "icinga/command.hpp"
+#include "icinga/compatutility.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/host.hpp"
+#include "icinga/service.hpp"
+#include "icinga/hostgroup.hpp"
+#include "icinga/servicegroup.hpp"
+#include "icinga/usergroup.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/timeperiod.hpp"
+#include "icinga/pluginutility.hpp"
+#include "remote/zone.hpp"
+#include <algorithm>
+#include <chrono>
+#include <cmath>
+#include <cstdint>
+#include <iterator>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <set>
+#include <utility>
+#include <type_traits>
+
+using namespace icinga;
+
+using Prio = RedisConnection::QueryPriority;
+
+std::unordered_set<Type*> IcingaDB::m_IndexedTypes;
+
+INITIALIZE_ONCE(&IcingaDB::ConfigStaticInitialize);
+
+std::vector<Type::Ptr> IcingaDB::GetTypes()
+{
+ // The initial config sync will queue the types in the following order.
+ return {
+ // Sync them first to get their states ASAP.
+ Host::TypeInstance,
+ Service::TypeInstance,
+
+ // Then sync them for similar reasons.
+ Downtime::TypeInstance,
+ Comment::TypeInstance,
+
+ HostGroup::TypeInstance,
+ ServiceGroup::TypeInstance,
+ CheckCommand::TypeInstance,
+ Endpoint::TypeInstance,
+ EventCommand::TypeInstance,
+ Notification::TypeInstance,
+ NotificationCommand::TypeInstance,
+ TimePeriod::TypeInstance,
+ User::TypeInstance,
+ UserGroup::TypeInstance,
+ Zone::TypeInstance
+ };
+}
+
+void IcingaDB::ConfigStaticInitialize()
+{
+ for (auto& type : GetTypes()) {
+ m_IndexedTypes.emplace(type.get());
+ }
+
+ /* triggered in ProcessCheckResult(), requires UpdateNextCheck() to be called before */
+ Checkable::OnStateChange.connect([](const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type, const MessageOrigin::Ptr&) {
+ IcingaDB::StateChangeHandler(checkable, cr, type);
+ });
+
+ Checkable::OnAcknowledgementSet.connect([](const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool, bool persistent, double changeTime, double expiry, const MessageOrigin::Ptr&) {
+ AcknowledgementSetHandler(checkable, author, comment, type, persistent, changeTime, expiry);
+ });
+ Checkable::OnAcknowledgementCleared.connect([](const Checkable::Ptr& checkable, const String& removedBy, double changeTime, const MessageOrigin::Ptr&) {
+ AcknowledgementClearedHandler(checkable, removedBy, changeTime);
+ });
+
+ Checkable::OnReachabilityChanged.connect([](const Checkable::Ptr&, const CheckResult::Ptr&, std::set<Checkable::Ptr> children, const MessageOrigin::Ptr&) {
+ IcingaDB::ReachabilityChangeHandler(children);
+ });
+
+ /* triggered on create, update and delete objects */
+ ConfigObject::OnActiveChanged.connect([](const ConfigObject::Ptr& object, const Value&) {
+ IcingaDB::VersionChangedHandler(object);
+ });
+ ConfigObject::OnVersionChanged.connect([](const ConfigObject::Ptr& object, const Value&) {
+ IcingaDB::VersionChangedHandler(object);
+ });
+
+ /* downtime start */
+ Downtime::OnDowntimeTriggered.connect(&IcingaDB::DowntimeStartedHandler);
+ /* fixed/flexible downtime end or remove */
+ Downtime::OnDowntimeRemoved.connect(&IcingaDB::DowntimeRemovedHandler);
+
+ Checkable::OnNotificationSentToAllUsers.connect([](
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ const NotificationType& type, const CheckResult::Ptr& cr, const String& author, const String& text,
+ const MessageOrigin::Ptr&
+ ) {
+ IcingaDB::NotificationSentToAllUsersHandler(notification, checkable, users, type, cr, author, text);
+ });
+
+ Comment::OnCommentAdded.connect(&IcingaDB::CommentAddedHandler);
+ Comment::OnCommentRemoved.connect(&IcingaDB::CommentRemovedHandler);
+
+ Checkable::OnFlappingChange.connect(&IcingaDB::FlappingChangeHandler);
+
+ Checkable::OnNewCheckResult.connect([](const Checkable::Ptr& checkable, const CheckResult::Ptr&, const MessageOrigin::Ptr&) {
+ IcingaDB::NewCheckResultHandler(checkable);
+ });
+
+ Checkable::OnNextCheckUpdated.connect([](const Checkable::Ptr& checkable) {
+ IcingaDB::NextCheckUpdatedHandler(checkable);
+ });
+
+ Service::OnHostProblemChanged.connect([](const Service::Ptr& service, const CheckResult::Ptr&, const MessageOrigin::Ptr&) {
+ IcingaDB::HostProblemChangedHandler(service);
+ });
+
+ Notification::OnUsersRawChangedWithOldValue.connect([](const Notification::Ptr& notification, const Value& oldValues, const Value& newValues) {
+ IcingaDB::NotificationUsersChangedHandler(notification, oldValues, newValues);
+ });
+ Notification::OnUserGroupsRawChangedWithOldValue.connect([](const Notification::Ptr& notification, const Value& oldValues, const Value& newValues) {
+ IcingaDB::NotificationUserGroupsChangedHandler(notification, oldValues, newValues);
+ });
+ TimePeriod::OnRangesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+ IcingaDB::TimePeriodRangesChangedHandler(timeperiod, oldValues, newValues);
+ });
+ TimePeriod::OnIncludesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+ IcingaDB::TimePeriodIncludesChangedHandler(timeperiod, oldValues, newValues);
+ });
+ TimePeriod::OnExcludesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+ IcingaDB::TimePeriodExcludesChangedHandler(timeperiod, oldValues, newValues);
+ });
+ User::OnGroupsChangedWithOldValue.connect([](const User::Ptr& user, const Value& oldValues, const Value& newValues) {
+ IcingaDB::UserGroupsChangedHandler(user, oldValues, newValues);
+ });
+ Host::OnGroupsChangedWithOldValue.connect([](const Host::Ptr& host, const Value& oldValues, const Value& newValues) {
+ IcingaDB::HostGroupsChangedHandler(host, oldValues, newValues);
+ });
+ Service::OnGroupsChangedWithOldValue.connect([](const Service::Ptr& service, const Value& oldValues, const Value& newValues) {
+ IcingaDB::ServiceGroupsChangedHandler(service, oldValues, newValues);
+ });
+ Command::OnEnvChangedWithOldValue.connect([](const ConfigObject::Ptr& command, const Value& oldValues, const Value& newValues) {
+ IcingaDB::CommandEnvChangedHandler(command, oldValues, newValues);
+ });
+ Command::OnArgumentsChangedWithOldValue.connect([](const ConfigObject::Ptr& command, const Value& oldValues, const Value& newValues) {
+ IcingaDB::CommandArgumentsChangedHandler(command, oldValues, newValues);
+ });
+ CustomVarObject::OnVarsChangedWithOldValue.connect([](const ConfigObject::Ptr& object, const Value& oldValues, const Value& newValues) {
+ IcingaDB::CustomVarsChangedHandler(object, oldValues, newValues);
+ });
+}
+
+void IcingaDB::UpdateAllConfigObjects()
+{
+ m_Rcon->Sync();
+ m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "5"}, Prio::Heartbeat);
+
+ Log(LogInformation, "IcingaDB") << "Starting initial config/status dump";
+ double startTime = Utility::GetTime();
+
+ SetOngoingDumpStart(startTime);
+
+ Defer resetOngoingDumpStart ([this]() {
+ SetOngoingDumpStart(0);
+ });
+
+ // Use a Workqueue to pack objects in parallel
+ WorkQueue upq(25000, Configuration::Concurrency, LogNotice);
+ upq.SetName("IcingaDB:ConfigDump");
+
+ std::vector<Type::Ptr> types = GetTypes();
+
+ m_Rcon->SuppressQueryKind(Prio::CheckResult);
+ m_Rcon->SuppressQueryKind(Prio::RuntimeStateSync);
+
+ Defer unSuppress ([this]() {
+ m_Rcon->UnsuppressQueryKind(Prio::RuntimeStateSync);
+ m_Rcon->UnsuppressQueryKind(Prio::CheckResult);
+ });
+
+ // Add a new type=* state=wip entry to the stream and remove all previous entries (MAXLEN 1).
+ m_Rcon->FireAndForgetQuery({"XADD", "icinga:dump", "MAXLEN", "1", "*", "key", "*", "state", "wip"}, Prio::Config);
+
+ const std::vector<String> globalKeys = {
+ m_PrefixConfigObject + "customvar",
+ m_PrefixConfigObject + "action:url",
+ m_PrefixConfigObject + "notes:url",
+ m_PrefixConfigObject + "icon:image",
+ };
+ DeleteKeys(m_Rcon, globalKeys, Prio::Config);
+ DeleteKeys(m_Rcon, {"icinga:nextupdate:host", "icinga:nextupdate:service"}, Prio::Config);
+ m_Rcon->Sync();
+
+ Defer resetDumpedGlobals ([this]() {
+ m_DumpedGlobals.CustomVar.Reset();
+ m_DumpedGlobals.ActionUrl.Reset();
+ m_DumpedGlobals.NotesUrl.Reset();
+ m_DumpedGlobals.IconImage.Reset();
+ });
+
+ upq.ParallelFor(types, false, [this](const Type::Ptr& type) {
+ String lcType = type->GetName().ToLower();
+ ConfigType *ctype = dynamic_cast<ConfigType *>(type.get());
+ if (!ctype)
+ return;
+
+ auto& rcon (m_Rcons.at(ctype));
+
+ std::vector<String> keys = GetTypeOverwriteKeys(lcType);
+ DeleteKeys(rcon, keys, Prio::Config);
+
+ WorkQueue upqObjectType(25000, Configuration::Concurrency, LogNotice);
+ upqObjectType.SetName("IcingaDB:ConfigDump:" + lcType);
+
+ std::map<String, String> redisCheckSums;
+ String configCheckSum = m_PrefixConfigCheckSum + lcType;
+
+ upqObjectType.Enqueue([&rcon, &configCheckSum, &redisCheckSums]() {
+ String cursor = "0";
+
+ do {
+ Array::Ptr res = rcon->GetResultOfQuery({
+ "HSCAN", configCheckSum, cursor, "COUNT", "1000"
+ }, Prio::Config);
+
+ AddKvsToMap(res->Get(1), redisCheckSums);
+
+ cursor = res->Get(0);
+ } while (cursor != "0");
+ });
+
+ auto objectChunks (ChunkObjects(ctype->GetObjects(), 500));
+ String configObject = m_PrefixConfigObject + lcType;
+
+ // Skimmed away attributes and checksums HMSETs' keys and values by Redis key.
+ std::map<String, std::vector<std::vector<String>>> ourContentRaw {{configCheckSum, {}}, {configObject, {}}};
+ std::mutex ourContentMutex;
+
+ upqObjectType.ParallelFor(objectChunks, [&](decltype(objectChunks)::const_reference chunk) {
+ std::map<String, std::vector<String>> hMSets;
+ // Two values are appended per object: Object ID (Hash encoded) and Object State (IcingaDB::SerializeState() -> JSON encoded)
+ std::vector<String> states = {"HMSET", m_PrefixConfigObject + lcType + ":state"};
+ // Two values are appended per object: Object ID (Hash encoded) and State Checksum ({ "checksum": checksum } -> JSON encoded)
+ std::vector<String> statesChksms = {"HMSET", m_PrefixConfigCheckSum + lcType + ":state"};
+ std::vector<std::vector<String> > transaction = {{"MULTI"}};
+ std::vector<String> hostZAdds = {"ZADD", "icinga:nextupdate:host"}, serviceZAdds = {"ZADD", "icinga:nextupdate:service"};
+
+ auto skimObjects ([&]() {
+ std::lock_guard<std::mutex> l (ourContentMutex);
+
+ for (auto& kv : ourContentRaw) {
+ auto pos (hMSets.find(kv.first));
+
+ if (pos != hMSets.end()) {
+ kv.second.emplace_back(std::move(pos->second));
+ hMSets.erase(pos);
+ }
+ }
+ });
+
+ bool dumpState = (lcType == "host" || lcType == "service");
+
+ size_t bulkCounter = 0;
+ for (const ConfigObject::Ptr& object : chunk) {
+ if (lcType != GetLowerCaseTypeNameDB(object))
+ continue;
+
+ std::vector<Dictionary::Ptr> runtimeUpdates;
+ CreateConfigUpdate(object, lcType, hMSets, runtimeUpdates, false);
+
+ // Write out inital state for checkables
+ if (dumpState) {
+ String objectKey = GetObjectIdentifier(object);
+ Dictionary::Ptr state = SerializeState(dynamic_pointer_cast<Checkable>(object));
+
+ states.emplace_back(objectKey);
+ states.emplace_back(JsonEncode(state));
+
+ statesChksms.emplace_back(objectKey);
+ statesChksms.emplace_back(JsonEncode(new Dictionary({{"checksum", HashValue(state)}})));
+ }
+
+ bulkCounter++;
+ if (!(bulkCounter % 100)) {
+ skimObjects();
+
+ for (auto& kv : hMSets) {
+ if (!kv.second.empty()) {
+ kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
+ transaction.emplace_back(std::move(kv.second));
+ }
+ }
+
+ if (states.size() > 2) {
+ transaction.emplace_back(std::move(states));
+ transaction.emplace_back(std::move(statesChksms));
+ states = {"HMSET", m_PrefixConfigObject + lcType + ":state"};
+ statesChksms = {"HMSET", m_PrefixConfigCheckSum + lcType + ":state"};
+ }
+
+ hMSets = decltype(hMSets)();
+
+ if (transaction.size() > 1) {
+ transaction.push_back({"EXEC"});
+ rcon->FireAndForgetQueries(std::move(transaction), Prio::Config);
+ transaction = {{"MULTI"}};
+ }
+ }
+
+ auto checkable (dynamic_pointer_cast<Checkable>(object));
+
+ if (checkable && checkable->GetEnableActiveChecks()) {
+ auto zAdds (dynamic_pointer_cast<Service>(checkable) ? &serviceZAdds : &hostZAdds);
+
+ zAdds->emplace_back(Convert::ToString(checkable->GetNextUpdate()));
+ zAdds->emplace_back(GetObjectIdentifier(checkable));
+
+ if (zAdds->size() >= 102u) {
+ std::vector<String> header (zAdds->begin(), zAdds->begin() + 2u);
+
+ rcon->FireAndForgetQuery(std::move(*zAdds), Prio::CheckResult);
+
+ *zAdds = std::move(header);
+ }
+ }
+ }
+
+ skimObjects();
+
+ for (auto& kv : hMSets) {
+ if (!kv.second.empty()) {
+ kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
+ transaction.emplace_back(std::move(kv.second));
+ }
+ }
+
+ if (states.size() > 2) {
+ transaction.emplace_back(std::move(states));
+ transaction.emplace_back(std::move(statesChksms));
+ }
+
+ if (transaction.size() > 1) {
+ transaction.push_back({"EXEC"});
+ rcon->FireAndForgetQueries(std::move(transaction), Prio::Config);
+ }
+
+ for (auto zAdds : {&hostZAdds, &serviceZAdds}) {
+ if (zAdds->size() > 2u) {
+ rcon->FireAndForgetQuery(std::move(*zAdds), Prio::CheckResult);
+ }
+ }
+
+ Log(LogNotice, "IcingaDB")
+ << "Dumped " << bulkCounter << " objects of type " << lcType;
+ });
+
+ upqObjectType.Join();
+
+ if (upqObjectType.HasExceptions()) {
+ for (boost::exception_ptr exc : upqObjectType.GetExceptions()) {
+ if (exc) {
+ boost::rethrow_exception(exc);
+ }
+ }
+ }
+
+ std::map<String, std::map<String, String>> ourContent;
+
+ for (auto& source : ourContentRaw) {
+ auto& dest (ourContent[source.first]);
+
+ upqObjectType.Enqueue([&]() {
+ for (auto& hMSet : source.second) {
+ for (decltype(hMSet.size()) i = 0, stop = hMSet.size() - 1u; i < stop; i += 2u) {
+ dest.emplace(std::move(hMSet[i]), std::move(hMSet[i + 1u]));
+ }
+
+ hMSet.clear();
+ }
+
+ source.second.clear();
+ });
+ }
+
+ upqObjectType.Join();
+ ourContentRaw.clear();
+
+ auto& ourCheckSums (ourContent[configCheckSum]);
+ auto& ourObjects (ourContent[configObject]);
+ std::vector<String> setChecksum, setObject, delChecksum, delObject;
+
+ auto redisCurrent (redisCheckSums.begin());
+ auto redisEnd (redisCheckSums.end());
+ auto ourCurrent (ourCheckSums.begin());
+ auto ourEnd (ourCheckSums.end());
+
+ auto flushSets ([&]() {
+ auto affectedConfig (setObject.size() / 2u);
+
+ setChecksum.insert(setChecksum.begin(), {"HMSET", configCheckSum});
+ setObject.insert(setObject.begin(), {"HMSET", configObject});
+
+ std::vector<std::vector<String>> transaction;
+
+ transaction.emplace_back(std::vector<String>{"MULTI"});
+ transaction.emplace_back(std::move(setChecksum));
+ transaction.emplace_back(std::move(setObject));
+ transaction.emplace_back(std::vector<String>{"EXEC"});
+
+ setChecksum.clear();
+ setObject.clear();
+
+ rcon->FireAndForgetQueries(std::move(transaction), Prio::Config, {affectedConfig});
+ });
+
+ auto flushDels ([&]() {
+ auto affectedConfig (delObject.size());
+
+ delChecksum.insert(delChecksum.begin(), {"HDEL", configCheckSum});
+ delObject.insert(delObject.begin(), {"HDEL", configObject});
+
+ std::vector<std::vector<String>> transaction;
+
+ transaction.emplace_back(std::vector<String>{"MULTI"});
+ transaction.emplace_back(std::move(delChecksum));
+ transaction.emplace_back(std::move(delObject));
+ transaction.emplace_back(std::vector<String>{"EXEC"});
+
+ delChecksum.clear();
+ delObject.clear();
+
+ rcon->FireAndForgetQueries(std::move(transaction), Prio::Config, {affectedConfig});
+ });
+
+ auto setOne ([&]() {
+ setChecksum.emplace_back(ourCurrent->first);
+ setChecksum.emplace_back(ourCurrent->second);
+ setObject.emplace_back(ourCurrent->first);
+ setObject.emplace_back(ourObjects[ourCurrent->first]);
+
+ if (setChecksum.size() == 100u) {
+ flushSets();
+ }
+ });
+
+ auto delOne ([&]() {
+ delChecksum.emplace_back(redisCurrent->first);
+ delObject.emplace_back(redisCurrent->first);
+
+ if (delChecksum.size() == 100u) {
+ flushDels();
+ }
+ });
+
+ for (;;) {
+ if (redisCurrent == redisEnd) {
+ for (; ourCurrent != ourEnd; ++ourCurrent) {
+ setOne();
+ }
+
+ break;
+ } else if (ourCurrent == ourEnd) {
+ for (; redisCurrent != redisEnd; ++redisCurrent) {
+ delOne();
+ }
+
+ break;
+ } else if (redisCurrent->first < ourCurrent->first) {
+ delOne();
+ ++redisCurrent;
+ } else if (redisCurrent->first > ourCurrent->first) {
+ setOne();
+ ++ourCurrent;
+ } else {
+ if (redisCurrent->second != ourCurrent->second) {
+ setOne();
+ }
+
+ ++redisCurrent;
+ ++ourCurrent;
+ }
+ }
+
+ if (delChecksum.size()) {
+ flushDels();
+ }
+
+ if (setChecksum.size()) {
+ flushSets();
+ }
+
+ for (auto& key : GetTypeDumpSignalKeys(type)) {
+ rcon->FireAndForgetQuery({"XADD", "icinga:dump", "*", "key", key, "state", "done"}, Prio::Config);
+ }
+ rcon->Sync();
+ });
+
+ upq.Join();
+
+ if (upq.HasExceptions()) {
+ for (boost::exception_ptr exc : upq.GetExceptions()) {
+ try {
+ if (exc) {
+ boost::rethrow_exception(exc);
+ }
+ } catch(const std::exception& e) {
+ Log(LogCritical, "IcingaDB")
+ << "Exception during ConfigDump: " << e.what();
+ }
+ }
+ }
+
+ for (auto& key : globalKeys) {
+ m_Rcon->FireAndForgetQuery({"XADD", "icinga:dump", "*", "key", key, "state", "done"}, Prio::Config);
+ }
+
+ m_Rcon->FireAndForgetQuery({"XADD", "icinga:dump", "*", "key", "*", "state", "done"}, Prio::Config);
+
+ // enqueue a callback that will notify us once all previous queries were executed and wait for this event
+ std::promise<void> p;
+ m_Rcon->EnqueueCallback([&p](boost::asio::yield_context& yc) { p.set_value(); }, Prio::Config);
+ p.get_future().wait();
+
+ auto endTime (Utility::GetTime());
+ auto took (endTime - startTime);
+
+ SetLastdumpTook(took);
+ SetLastdumpEnd(endTime);
+
+ Log(LogInformation, "IcingaDB")
+ << "Initial config/status dump finished in " << took << " seconds.";
+}
+
+std::vector<std::vector<intrusive_ptr<ConfigObject>>> IcingaDB::ChunkObjects(std::vector<intrusive_ptr<ConfigObject>> objects, size_t chunkSize) {
+ std::vector<std::vector<intrusive_ptr<ConfigObject>>> chunks;
+ auto offset (objects.begin());
+ auto end (objects.end());
+
+ chunks.reserve((std::distance(offset, end) + chunkSize - 1) / chunkSize);
+
+ while (std::distance(offset, end) >= chunkSize) {
+ auto until (offset + chunkSize);
+ chunks.emplace_back(offset, until);
+ offset = until;
+ }
+
+ if (offset != end) {
+ chunks.emplace_back(offset, end);
+ }
+
+ return chunks;
+}
+
+void IcingaDB::DeleteKeys(const RedisConnection::Ptr& conn, const std::vector<String>& keys, RedisConnection::QueryPriority priority) {
+ std::vector<String> query = {"DEL"};
+ for (auto& key : keys) {
+ query.emplace_back(key);
+ }
+
+ conn->FireAndForgetQuery(std::move(query), priority);
+}
+
+std::vector<String> IcingaDB::GetTypeOverwriteKeys(const String& type)
+{
+ std::vector<String> keys = {
+ m_PrefixConfigObject + type + ":customvar",
+ };
+
+ if (type == "host" || type == "service" || type == "user") {
+ keys.emplace_back(m_PrefixConfigObject + type + "group:member");
+ keys.emplace_back(m_PrefixConfigObject + type + ":state");
+ keys.emplace_back(m_PrefixConfigCheckSum + type + ":state");
+ } else if (type == "timeperiod") {
+ keys.emplace_back(m_PrefixConfigObject + type + ":override:include");
+ keys.emplace_back(m_PrefixConfigObject + type + ":override:exclude");
+ keys.emplace_back(m_PrefixConfigObject + type + ":range");
+ } else if (type == "notification") {
+ keys.emplace_back(m_PrefixConfigObject + type + ":user");
+ keys.emplace_back(m_PrefixConfigObject + type + ":usergroup");
+ keys.emplace_back(m_PrefixConfigObject + type + ":recipient");
+ } else if (type == "checkcommand" || type == "notificationcommand" || type == "eventcommand") {
+ keys.emplace_back(m_PrefixConfigObject + type + ":envvar");
+ keys.emplace_back(m_PrefixConfigCheckSum + type + ":envvar");
+ keys.emplace_back(m_PrefixConfigObject + type + ":argument");
+ keys.emplace_back(m_PrefixConfigCheckSum + type + ":argument");
+ }
+
+ return keys;
+}
+
+std::vector<String> IcingaDB::GetTypeDumpSignalKeys(const Type::Ptr& type)
+{
+ String lcType = type->GetName().ToLower();
+ std::vector<String> keys = {m_PrefixConfigObject + lcType};
+
+ if (CustomVarObject::TypeInstance->IsAssignableFrom(type)) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":customvar");
+ }
+
+ if (type == Host::TypeInstance || type == Service::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + "group:member");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":state");
+ } else if (type == User::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + "group:member");
+ } else if (type == TimePeriod::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":override:include");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":override:exclude");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":range");
+ } else if (type == Notification::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":user");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":usergroup");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":recipient");
+ } else if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":envvar");
+ keys.emplace_back(m_PrefixConfigObject + lcType + ":argument");
+ }
+
+ return keys;
+}
+
+template<typename ConfigType>
+static ConfigObject::Ptr GetObjectByName(const String& name)
+{
+ return ConfigObject::GetObject<ConfigType>(name);
+}
+
+void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const String typeName, std::map<String, std::vector<String>>& hMSets,
+ std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate)
+{
+ String objectKey = GetObjectIdentifier(object);
+ String objectKeyName = typeName + "_id";
+
+ Type::Ptr type = object->GetReflectionType();
+
+ CustomVarObject::Ptr customVarObject = dynamic_pointer_cast<CustomVarObject>(object);
+
+ if (customVarObject) {
+ auto vars(SerializeVars(customVarObject->GetVars()));
+ if (vars) {
+ auto& typeCvs (hMSets[m_PrefixConfigObject + typeName + ":customvar"]);
+ auto& allCvs (hMSets[m_PrefixConfigObject + "customvar"]);
+
+ ObjectLock varsLock(vars);
+ Array::Ptr varsArray(new Array);
+
+ varsArray->Reserve(vars->GetLength());
+
+ for (auto& kv : vars) {
+ if (runtimeUpdate || m_DumpedGlobals.CustomVar.IsNew(kv.first)) {
+ allCvs.emplace_back(kv.first);
+ allCvs.emplace_back(JsonEncode(kv.second));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, kv.first, m_PrefixConfigObject + "customvar", kv.second);
+ }
+ }
+
+ String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
+ typeCvs.emplace_back(id);
+
+ Dictionary::Ptr data = new Dictionary({{objectKeyName, objectKey}, {"environment_id", m_EnvironmentId}, {"customvar_id", kv.first}});
+ typeCvs.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":customvar", data);
+ }
+ }
+ }
+ }
+
+ if (type == Host::TypeInstance || type == Service::TypeInstance) {
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ String actionUrl = checkable->GetActionUrl();
+ String notesUrl = checkable->GetNotesUrl();
+ String iconImage = checkable->GetIconImage();
+ if (!actionUrl.IsEmpty()) {
+ auto& actionUrls (hMSets[m_PrefixConfigObject + "action:url"]);
+
+ auto id (HashValue(new Array({m_EnvironmentId, actionUrl})));
+
+ if (runtimeUpdate || m_DumpedGlobals.ActionUrl.IsNew(id)) {
+ actionUrls.emplace_back(std::move(id));
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"action_url", actionUrl}});
+ actionUrls.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, actionUrls.at(actionUrls.size() - 2u), m_PrefixConfigObject + "action:url", data);
+ }
+ }
+ }
+ if (!notesUrl.IsEmpty()) {
+ auto& notesUrls (hMSets[m_PrefixConfigObject + "notes:url"]);
+
+ auto id (HashValue(new Array({m_EnvironmentId, notesUrl})));
+
+ if (runtimeUpdate || m_DumpedGlobals.NotesUrl.IsNew(id)) {
+ notesUrls.emplace_back(std::move(id));
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"notes_url", notesUrl}});
+ notesUrls.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, notesUrls.at(notesUrls.size() - 2u), m_PrefixConfigObject + "notes:url", data);
+ }
+ }
+ }
+ if (!iconImage.IsEmpty()) {
+ auto& iconImages (hMSets[m_PrefixConfigObject + "icon:image"]);
+
+ auto id (HashValue(new Array({m_EnvironmentId, iconImage})));
+
+ if (runtimeUpdate || m_DumpedGlobals.IconImage.IsNew(id)) {
+ iconImages.emplace_back(std::move(id));
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"icon_image", iconImage}});
+ iconImages.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, iconImages.at(iconImages.size() - 2u), m_PrefixConfigObject + "icon:image", data);
+ }
+ }
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ ConfigObject::Ptr (*getGroup)(const String& name);
+ Array::Ptr groups;
+ if (service) {
+ groups = service->GetGroups();
+ getGroup = &::GetObjectByName<ServiceGroup>;
+ } else {
+ groups = host->GetGroups();
+ getGroup = &::GetObjectByName<HostGroup>;
+ }
+
+ if (groups) {
+ ObjectLock groupsLock(groups);
+ Array::Ptr groupIds(new Array);
+
+ groupIds->Reserve(groups->GetLength());
+
+ auto& members (hMSets[m_PrefixConfigObject + typeName + "group:member"]);
+
+ for (auto& group : groups) {
+ auto groupObj ((*getGroup)(group));
+ String groupId = GetObjectIdentifier(groupObj);
+ String id = HashValue(new Array({m_EnvironmentId, groupObj->GetName(), object->GetName()}));
+ members.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{objectKeyName, objectKey}, {"environment_id", m_EnvironmentId}, {typeName + "group_id", groupId}});
+ members.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + "group:member", data);
+ }
+
+ groupIds->Add(groupId);
+ }
+ }
+
+ return;
+ }
+
+ if (type == TimePeriod::TypeInstance) {
+ TimePeriod::Ptr timeperiod = static_pointer_cast<TimePeriod>(object);
+
+ Dictionary::Ptr ranges = timeperiod->GetRanges();
+ if (ranges) {
+ ObjectLock rangesLock(ranges);
+ Array::Ptr rangeIds(new Array);
+ auto& typeRanges (hMSets[m_PrefixConfigObject + typeName + ":range"]);
+
+ rangeIds->Reserve(ranges->GetLength());
+
+ for (auto& kv : ranges) {
+ String rangeId = HashValue(new Array({m_EnvironmentId, kv.first, kv.second}));
+ rangeIds->Add(rangeId);
+
+ String id = HashValue(new Array({m_EnvironmentId, kv.first, kv.second, object->GetName()}));
+ typeRanges.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"range_key", kv.first}, {"range_value", kv.second}});
+ typeRanges.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":range", data);
+ }
+ }
+ }
+
+ Array::Ptr includes;
+ ConfigObject::Ptr (*getInclude)(const String& name);
+ includes = timeperiod->GetIncludes();
+ getInclude = &::GetObjectByName<TimePeriod>;
+
+ Array::Ptr includeChecksums = new Array();
+
+ ObjectLock includesLock(includes);
+ ObjectLock includeChecksumsLock(includeChecksums);
+
+ includeChecksums->Reserve(includes->GetLength());
+
+
+ auto& includs (hMSets[m_PrefixConfigObject + typeName + ":override:include"]);
+ for (auto include : includes) {
+ auto includeTp ((*getInclude)(include.Get<String>()));
+ String includeId = GetObjectIdentifier(includeTp);
+ includeChecksums->Add(includeId);
+
+ String id = HashValue(new Array({m_EnvironmentId, includeTp->GetName(), object->GetName()}));
+ includs.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"include_id", includeId}});
+ includs.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":override:include", data);
+ }
+ }
+
+ Array::Ptr excludes;
+ ConfigObject::Ptr (*getExclude)(const String& name);
+
+ excludes = timeperiod->GetExcludes();
+ getExclude = &::GetObjectByName<TimePeriod>;
+
+ Array::Ptr excludeChecksums = new Array();
+
+ ObjectLock excludesLock(excludes);
+ ObjectLock excludeChecksumsLock(excludeChecksums);
+
+ excludeChecksums->Reserve(excludes->GetLength());
+
+ auto& excluds (hMSets[m_PrefixConfigObject + typeName + ":override:exclude"]);
+
+ for (auto exclude : excludes) {
+ auto excludeTp ((*getExclude)(exclude.Get<String>()));
+ String excludeId = GetObjectIdentifier(excludeTp);
+ excludeChecksums->Add(excludeId);
+
+ String id = HashValue(new Array({m_EnvironmentId, excludeTp->GetName(), object->GetName()}));
+ excluds.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"exclude_id", excludeId}});
+ excluds.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":override:exclude", data);
+ }
+ }
+
+ return;
+ }
+
+ if (type == User::TypeInstance) {
+ User::Ptr user = static_pointer_cast<User>(object);
+ Array::Ptr groups = user->GetGroups();
+
+ if (groups) {
+ ObjectLock groupsLock(groups);
+ Array::Ptr groupIds(new Array);
+
+ groupIds->Reserve(groups->GetLength());
+
+ auto& members (hMSets[m_PrefixConfigObject + typeName + "group:member"]);
+ auto& notificationRecipients (hMSets[m_PrefixConfigObject + "notification:recipient"]);
+
+ for (auto& group : groups) {
+ UserGroup::Ptr groupObj = UserGroup::GetByName(group);
+ String groupId = GetObjectIdentifier(groupObj);
+ String id = HashValue(new Array({m_EnvironmentId, groupObj->GetName(), object->GetName()}));
+ members.emplace_back(id);
+ Dictionary::Ptr data = new Dictionary({{"user_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", groupId}});
+ members.emplace_back(JsonEncode(data));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + "group:member", data);
+
+ // Recipients are handled by notifications during initial dumps and only need to be handled here during runtime (e.g. User creation).
+ for (auto& notification : groupObj->GetNotifications()) {
+ String recipientId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), groupObj->GetName(), notification->GetName()}));
+ notificationRecipients.emplace_back(recipientId);
+ Dictionary::Ptr recipientData = new Dictionary({{"notification_id", GetObjectIdentifier(notification)}, {"environment_id", m_EnvironmentId}, {"user_id", objectKey}, {"usergroup_id", groupId}});
+ notificationRecipients.emplace_back(JsonEncode(recipientData));
+
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, recipientId, m_PrefixConfigObject + "notification:recipient", recipientData);
+ }
+ }
+
+ groupIds->Add(groupId);
+ }
+ }
+
+ return;
+ }
+
+ if (type == Notification::TypeInstance) {
+ Notification::Ptr notification = static_pointer_cast<Notification>(object);
+
+ std::set<User::Ptr> users = notification->GetUsers();
+ Array::Ptr userIds = new Array();
+
+ auto usergroups(notification->GetUserGroups());
+ Array::Ptr usergroupIds = new Array();
+
+ userIds->Reserve(users.size());
+
+ auto& usrs (hMSets[m_PrefixConfigObject + typeName + ":user"]);
+ auto& notificationRecipients (hMSets[m_PrefixConfigObject + typeName + ":recipient"]);
+
+ for (auto& user : users) {
+ String userId = GetObjectIdentifier(user);
+ String id = HashValue(new Array({m_EnvironmentId, "user", user->GetName(), object->GetName()}));
+ usrs.emplace_back(id);
+ notificationRecipients.emplace_back(id);
+
+ Dictionary::Ptr data = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}});
+ String dataJson = JsonEncode(data);
+ usrs.emplace_back(dataJson);
+ notificationRecipients.emplace_back(dataJson);
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":user", data);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", data);
+ }
+
+ userIds->Add(userId);
+ }
+
+ usergroupIds->Reserve(usergroups.size());
+
+ auto& groups (hMSets[m_PrefixConfigObject + typeName + ":usergroup"]);
+
+ for (auto& usergroup : usergroups) {
+ String usergroupId = GetObjectIdentifier(usergroup);
+ String id = HashValue(new Array({m_EnvironmentId, "usergroup", usergroup->GetName(), object->GetName()}));
+ groups.emplace_back(id);
+ notificationRecipients.emplace_back(id);
+
+ Dictionary::Ptr groupData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", usergroupId}});
+ String groupDataJson = JsonEncode(groupData);
+ groups.emplace_back(groupDataJson);
+ notificationRecipients.emplace_back(groupDataJson);
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":usergroup", groupData);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", groupData);
+ }
+
+ for (const User::Ptr& user : usergroup->GetMembers()) {
+ String userId = GetObjectIdentifier(user);
+ String recipientId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), usergroup->GetName(), notification->GetName()}));
+ notificationRecipients.emplace_back(recipientId);
+ Dictionary::Ptr userData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}, {"usergroup_id", usergroupId}});
+ notificationRecipients.emplace_back(JsonEncode(userData));
+
+ if (runtimeUpdate) {
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, recipientId, m_PrefixConfigObject + typeName + ":recipient", userData);
+ }
+ }
+
+ usergroupIds->Add(usergroupId);
+ }
+
+ return;
+ }
+
+ if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+ Command::Ptr command = static_pointer_cast<Command>(object);
+
+ Dictionary::Ptr arguments = command->GetArguments();
+ if (arguments) {
+ ObjectLock argumentsLock(arguments);
+ auto& typeArgs (hMSets[m_PrefixConfigObject + typeName + ":argument"]);
+ auto& argChksms (hMSets[m_PrefixConfigCheckSum + typeName + ":argument"]);
+
+ for (auto& kv : arguments) {
+ Dictionary::Ptr values;
+ if (kv.second.IsObjectType<Dictionary>()) {
+ values = kv.second;
+ values = values->ShallowClone();
+ } else if (kv.second.IsObjectType<Array>()) {
+ values = new Dictionary({{"value", JsonEncode(kv.second)}});
+ } else {
+ values = new Dictionary({{"value", kv.second}});
+ }
+
+ for (const char *attr : {"value", "set_if", "separator"}) {
+ Value value;
+
+ // Stringify if set.
+ if (values->Get(attr, &value)) {
+ switch (value.GetType()) {
+ case ValueEmpty:
+ case ValueString:
+ break;
+ case ValueObject:
+ values->Set(attr, value.Get<Object::Ptr>()->ToString());
+ break;
+ default:
+ values->Set(attr, JsonEncode(value));
+ }
+ }
+ }
+
+ for (const char *attr : {"repeat_key", "required", "skip_key"}) {
+ Value value;
+
+ // Boolify if set.
+ if (values->Get(attr, &value)) {
+ values->Set(attr, value.ToBool());
+ }
+ }
+
+ {
+ Value order;
+
+ // Intify if set.
+ if (values->Get("order", &order)) {
+ values->Set("order", (int)order);
+ }
+ }
+
+ values->Set(objectKeyName, objectKey);
+ values->Set("argument_key", kv.first);
+ values->Set("environment_id", m_EnvironmentId);
+
+ String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
+
+ typeArgs.emplace_back(id);
+ typeArgs.emplace_back(JsonEncode(values));
+
+ argChksms.emplace_back(id);
+ String checksum = HashValue(kv.second);
+ argChksms.emplace_back(JsonEncode(new Dictionary({{"checksum", checksum}})));
+
+ if (runtimeUpdate) {
+ values->Set("checksum", checksum);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":argument", values);
+ }
+ }
+ }
+
+ Dictionary::Ptr envvars = command->GetEnv();
+ if (envvars) {
+ ObjectLock envvarsLock(envvars);
+ Array::Ptr envvarIds(new Array);
+ auto& typeVars (hMSets[m_PrefixConfigObject + typeName + ":envvar"]);
+ auto& varChksms (hMSets[m_PrefixConfigCheckSum + typeName + ":envvar"]);
+
+ envvarIds->Reserve(envvars->GetLength());
+
+ for (auto& kv : envvars) {
+ Dictionary::Ptr values;
+ if (kv.second.IsObjectType<Dictionary>()) {
+ values = kv.second;
+ values = values->ShallowClone();
+ } else if (kv.second.IsObjectType<Array>()) {
+ values = new Dictionary({{"value", JsonEncode(kv.second)}});
+ } else {
+ values = new Dictionary({{"value", kv.second}});
+ }
+
+ {
+ Value value;
+
+ // JsonEncode() the value if it's set.
+ if (values->Get("value", &value)) {
+ values->Set("value", JsonEncode(value));
+ }
+ }
+
+ values->Set(objectKeyName, objectKey);
+ values->Set("envvar_key", kv.first);
+ values->Set("environment_id", m_EnvironmentId);
+
+ String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
+
+ typeVars.emplace_back(id);
+ typeVars.emplace_back(JsonEncode(values));
+
+ varChksms.emplace_back(id);
+ String checksum = HashValue(kv.second);
+ varChksms.emplace_back(JsonEncode(new Dictionary({{"checksum", checksum}})));
+
+ if (runtimeUpdate) {
+ values->Set("checksum", checksum);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":envvar", values);
+ }
+ }
+ }
+
+ return;
+ }
+}
+
+/**
+ * Update the state information of a checkable in Redis.
+ *
+ * What is updated exactly depends on the mode parameter:
+ * - Volatile: Update the volatile state information stored in icinga:host:state or icinga:service:state as well as
+ * the corresponding checksum stored in icinga:checksum:host:state or icinga:checksum:service:state.
+ * - RuntimeOnly: Write a runtime update to the icinga:runtime:state stream. It is up to the caller to ensure that
+ * identical volatile state information was already written before to avoid inconsistencies. This mode is only
+ * useful to upgrade a previous Volatile to a Full operation, otherwise Full should be used.
+ * - Full: Perform an update of all state information in Redis, that is updating the volatile information and sending
+ * a corresponding runtime update so that this state update gets written through to the persistent database by a
+ * running icingadb process.
+ *
+ * @param checkable State of this checkable is updated in Redis
+ * @param mode Mode of operation (StateUpdate::Volatile, StateUpdate::RuntimeOnly, or StateUpdate::Full)
+ */
+void IcingaDB::UpdateState(const Checkable::Ptr& checkable, StateUpdate mode)
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ String objectType = GetLowerCaseTypeNameDB(checkable);
+ String objectKey = GetObjectIdentifier(checkable);
+
+ Dictionary::Ptr stateAttrs = SerializeState(checkable);
+
+ String redisStateKey = m_PrefixConfigObject + objectType + ":state";
+ String redisChecksumKey = m_PrefixConfigCheckSum + objectType + ":state";
+ String checksum = HashValue(stateAttrs);
+
+ if (mode & StateUpdate::Volatile) {
+ m_Rcon->FireAndForgetQueries({
+ {"HSET", redisStateKey, objectKey, JsonEncode(stateAttrs)},
+ {"HSET", redisChecksumKey, objectKey, JsonEncode(new Dictionary({{"checksum", checksum}}))},
+ }, Prio::RuntimeStateSync);
+ }
+
+ if (mode & StateUpdate::RuntimeOnly) {
+ ObjectLock olock(stateAttrs);
+
+ std::vector<String> streamadd({
+ "XADD", "icinga:runtime:state", "MAXLEN", "~", "1000000", "*",
+ "runtime_type", "upsert",
+ "redis_key", redisStateKey,
+ "checksum", checksum,
+ });
+
+ for (const Dictionary::Pair& kv : stateAttrs) {
+ streamadd.emplace_back(kv.first);
+ streamadd.emplace_back(IcingaToStreamValue(kv.second));
+ }
+
+ m_Rcon->FireAndForgetQuery(std::move(streamadd), Prio::RuntimeStateStream, {0, 1});
+ }
+}
+
+// Used to update a single object, used for runtime updates
+void IcingaDB::SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpdate)
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ String typeName = GetLowerCaseTypeNameDB(object);
+
+ std::map<String, std::vector<String>> hMSets;
+ std::vector<Dictionary::Ptr> runtimeUpdates;
+
+ CreateConfigUpdate(object, typeName, hMSets, runtimeUpdates, runtimeUpdate);
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+ if (checkable) {
+ UpdateState(checkable, runtimeUpdate ? StateUpdate::Full : StateUpdate::Volatile);
+ }
+
+ std::vector<std::vector<String> > transaction = {{"MULTI"}};
+
+ for (auto& kv : hMSets) {
+ if (!kv.second.empty()) {
+ kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
+ transaction.emplace_back(std::move(kv.second));
+ }
+ }
+
+ for (auto& objectAttributes : runtimeUpdates) {
+ std::vector<String> xAdd({"XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*"});
+ ObjectLock olock(objectAttributes);
+
+ for (const Dictionary::Pair& kv : objectAttributes) {
+ String value = IcingaToStreamValue(kv.second);
+ if (!value.IsEmpty()) {
+ xAdd.emplace_back(kv.first);
+ xAdd.emplace_back(value);
+ }
+ }
+
+ transaction.emplace_back(std::move(xAdd));
+ }
+
+ if (transaction.size() > 1) {
+ transaction.push_back({"EXEC"});
+ m_Rcon->FireAndForgetQueries(std::move(transaction), Prio::Config, {1});
+ }
+
+ if (checkable) {
+ SendNextUpdate(checkable);
+ }
+}
+
+void IcingaDB::AddObjectDataToRuntimeUpdates(std::vector<Dictionary::Ptr>& runtimeUpdates, const String& objectKey,
+ const String& redisKey, const Dictionary::Ptr& data)
+{
+ Dictionary::Ptr dataClone = data->ShallowClone();
+ dataClone->Set("id", objectKey);
+ dataClone->Set("redis_key", redisKey);
+ dataClone->Set("runtime_type", "upsert");
+ runtimeUpdates.emplace_back(dataClone);
+}
+
+// Takes object and collects IcingaDB relevant attributes and computes checksums. Returns whether the object is relevant
+// for IcingaDB.
+bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& attributes, Dictionary::Ptr& checksums)
+{
+ auto originalAttrs (object->GetOriginalAttributes());
+
+ if (originalAttrs) {
+ originalAttrs = originalAttrs->ShallowClone();
+ }
+
+ attributes->Set("name_checksum", SHA1(object->GetName()));
+ attributes->Set("environment_id", m_EnvironmentId);
+ attributes->Set("name", object->GetName());
+ attributes->Set("original_attributes", originalAttrs);
+
+ Zone::Ptr ObjectsZone;
+ Type::Ptr type = object->GetReflectionType();
+
+ if (type == Endpoint::TypeInstance) {
+ ObjectsZone = static_cast<Endpoint*>(object.get())->GetZone();
+ } else {
+ ObjectsZone = static_pointer_cast<Zone>(object->GetZone());
+ }
+
+ if (ObjectsZone) {
+ attributes->Set("zone_id", GetObjectIdentifier(ObjectsZone));
+ attributes->Set("zone_name", ObjectsZone->GetName());
+ }
+
+ if (type == Endpoint::TypeInstance) {
+ return true;
+ }
+
+ if (type == Zone::TypeInstance) {
+ Zone::Ptr zone = static_pointer_cast<Zone>(object);
+
+ attributes->Set("is_global", zone->GetGlobal());
+
+ Zone::Ptr parent = zone->GetParent();
+ if (parent) {
+ attributes->Set("parent_id", GetObjectIdentifier(parent));
+ }
+
+ auto parentsRaw (zone->GetAllParentsRaw());
+ attributes->Set("depth", parentsRaw.size());
+
+ return true;
+ }
+
+ if (type == Host::TypeInstance || type == Service::TypeInstance) {
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+ auto checkTimeout (checkable->GetCheckTimeout());
+
+ attributes->Set("checkcommand_name", checkable->GetCheckCommand()->GetName());
+ attributes->Set("max_check_attempts", checkable->GetMaxCheckAttempts());
+ attributes->Set("check_timeout", checkTimeout.IsEmpty() ? checkable->GetCheckCommand()->GetTimeout() : (double)checkTimeout);
+ attributes->Set("check_interval", checkable->GetCheckInterval());
+ attributes->Set("check_retry_interval", checkable->GetRetryInterval());
+ attributes->Set("active_checks_enabled", checkable->GetEnableActiveChecks());
+ attributes->Set("passive_checks_enabled", checkable->GetEnablePassiveChecks());
+ attributes->Set("event_handler_enabled", checkable->GetEnableEventHandler());
+ attributes->Set("notifications_enabled", checkable->GetEnableNotifications());
+ attributes->Set("flapping_enabled", checkable->GetEnableFlapping());
+ attributes->Set("flapping_threshold_low", checkable->GetFlappingThresholdLow());
+ attributes->Set("flapping_threshold_high", checkable->GetFlappingThresholdHigh());
+ attributes->Set("perfdata_enabled", checkable->GetEnablePerfdata());
+ attributes->Set("is_volatile", checkable->GetVolatile());
+ attributes->Set("notes", checkable->GetNotes());
+ attributes->Set("icon_image_alt", checkable->GetIconImageAlt());
+
+ attributes->Set("checkcommand_id", GetObjectIdentifier(checkable->GetCheckCommand()));
+
+ Endpoint::Ptr commandEndpoint = checkable->GetCommandEndpoint();
+ if (commandEndpoint) {
+ attributes->Set("command_endpoint_id", GetObjectIdentifier(commandEndpoint));
+ attributes->Set("command_endpoint_name", commandEndpoint->GetName());
+ }
+
+ TimePeriod::Ptr timePeriod = checkable->GetCheckPeriod();
+ if (timePeriod) {
+ attributes->Set("check_timeperiod_id", GetObjectIdentifier(timePeriod));
+ attributes->Set("check_timeperiod_name", timePeriod->GetName());
+ }
+
+ EventCommand::Ptr eventCommand = checkable->GetEventCommand();
+ if (eventCommand) {
+ attributes->Set("eventcommand_id", GetObjectIdentifier(eventCommand));
+ attributes->Set("eventcommand_name", eventCommand->GetName());
+ }
+
+ String actionUrl = checkable->GetActionUrl();
+ String notesUrl = checkable->GetNotesUrl();
+ String iconImage = checkable->GetIconImage();
+ if (!actionUrl.IsEmpty())
+ attributes->Set("action_url_id", HashValue(new Array({m_EnvironmentId, actionUrl})));
+ if (!notesUrl.IsEmpty())
+ attributes->Set("notes_url_id", HashValue(new Array({m_EnvironmentId, notesUrl})));
+ if (!iconImage.IsEmpty())
+ attributes->Set("icon_image_id", HashValue(new Array({m_EnvironmentId, iconImage})));
+
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ if (service) {
+ attributes->Set("host_id", GetObjectIdentifier(service->GetHost()));
+ attributes->Set("display_name", service->GetDisplayName());
+
+ // Overwrite name here, `object->name` is 'HostName!ServiceName' but we only want the name of the Service
+ attributes->Set("name", service->GetShortName());
+ } else {
+ attributes->Set("display_name", host->GetDisplayName());
+ attributes->Set("address", host->GetAddress());
+ attributes->Set("address6", host->GetAddress6());
+ }
+
+ return true;
+ }
+
+ if (type == User::TypeInstance) {
+ User::Ptr user = static_pointer_cast<User>(object);
+
+ attributes->Set("display_name", user->GetDisplayName());
+ attributes->Set("email", user->GetEmail());
+ attributes->Set("pager", user->GetPager());
+ attributes->Set("notifications_enabled", user->GetEnableNotifications());
+ attributes->Set("states", user->GetStates());
+ attributes->Set("types", user->GetTypes());
+
+ if (user->GetPeriod())
+ attributes->Set("timeperiod_id", GetObjectIdentifier(user->GetPeriod()));
+
+ return true;
+ }
+
+ if (type == TimePeriod::TypeInstance) {
+ TimePeriod::Ptr timeperiod = static_pointer_cast<TimePeriod>(object);
+
+ attributes->Set("display_name", timeperiod->GetDisplayName());
+ attributes->Set("prefer_includes", timeperiod->GetPreferIncludes());
+ return true;
+ }
+
+ if (type == Notification::TypeInstance) {
+ Notification::Ptr notification = static_pointer_cast<Notification>(object);
+
+ Host::Ptr host;
+ Service::Ptr service;
+
+ tie(host, service) = GetHostService(notification->GetCheckable());
+
+ attributes->Set("notificationcommand_id", GetObjectIdentifier(notification->GetCommand()));
+
+ attributes->Set("host_id", GetObjectIdentifier(host));
+ if (service)
+ attributes->Set("service_id", GetObjectIdentifier(service));
+
+ TimePeriod::Ptr timeperiod = notification->GetPeriod();
+ if (timeperiod)
+ attributes->Set("timeperiod_id", GetObjectIdentifier(timeperiod));
+
+ if (notification->GetTimes()) {
+ auto begin (notification->GetTimes()->Get("begin"));
+ auto end (notification->GetTimes()->Get("end"));
+
+ if (begin != Empty && (double)begin >= 0) {
+ attributes->Set("times_begin", std::round((double)begin));
+ }
+
+ if (end != Empty && (double)end >= 0) {
+ attributes->Set("times_end", std::round((double)end));
+ }
+ }
+
+ attributes->Set("notification_interval", std::max(0.0, std::round(notification->GetInterval())));
+ attributes->Set("states", notification->GetStates());
+ attributes->Set("types", notification->GetTypes());
+
+ return true;
+ }
+
+ if (type == Comment::TypeInstance) {
+ Comment::Ptr comment = static_pointer_cast<Comment>(object);
+
+ attributes->Set("author", comment->GetAuthor());
+ attributes->Set("text", comment->GetText());
+ attributes->Set("entry_type", comment->GetEntryType());
+ attributes->Set("entry_time", TimestampToMilliseconds(comment->GetEntryTime()));
+ attributes->Set("is_persistent", comment->GetPersistent());
+ attributes->Set("is_sticky", comment->GetSticky());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(comment->GetCheckable());
+
+ attributes->Set("host_id", GetObjectIdentifier(host));
+ if (service) {
+ attributes->Set("object_type", "service");
+ attributes->Set("service_id", GetObjectIdentifier(service));
+ } else
+ attributes->Set("object_type", "host");
+
+ auto expireTime (comment->GetExpireTime());
+
+ if (expireTime > 0) {
+ attributes->Set("expire_time", TimestampToMilliseconds(expireTime));
+ }
+
+ return true;
+ }
+
+ if (type == Downtime::TypeInstance) {
+ Downtime::Ptr downtime = static_pointer_cast<Downtime>(object);
+
+ attributes->Set("author", downtime->GetAuthor());
+ attributes->Set("comment", downtime->GetComment());
+ attributes->Set("entry_time", TimestampToMilliseconds(downtime->GetEntryTime()));
+ attributes->Set("scheduled_start_time", TimestampToMilliseconds(downtime->GetStartTime()));
+ attributes->Set("scheduled_end_time", TimestampToMilliseconds(downtime->GetEndTime()));
+ attributes->Set("scheduled_duration", TimestampToMilliseconds(std::max(0.0, downtime->GetEndTime() - downtime->GetStartTime())));
+ attributes->Set("flexible_duration", TimestampToMilliseconds(std::max(0.0, downtime->GetDuration())));
+ attributes->Set("is_flexible", !downtime->GetFixed());
+ attributes->Set("is_in_effect", downtime->IsInEffect());
+ if (downtime->IsInEffect()) {
+ attributes->Set("start_time", TimestampToMilliseconds(downtime->GetTriggerTime()));
+
+ attributes->Set("end_time", TimestampToMilliseconds(
+ downtime->GetFixed() ? downtime->GetEndTime() : (downtime->GetTriggerTime() + std::max(0.0, downtime->GetDuration()))
+ ));
+ }
+
+ auto duration = downtime->GetDuration();
+ if (downtime->GetFixed()) {
+ duration = downtime->GetEndTime() - downtime->GetStartTime();
+ }
+ attributes->Set("duration", TimestampToMilliseconds(std::max(0.0, duration)));
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(downtime->GetCheckable());
+
+ attributes->Set("host_id", GetObjectIdentifier(host));
+ if (service) {
+ attributes->Set("object_type", "service");
+ attributes->Set("service_id", GetObjectIdentifier(service));
+ } else
+ attributes->Set("object_type", "host");
+
+ auto triggeredBy (Downtime::GetByName(downtime->GetTriggeredBy()));
+ if (triggeredBy) {
+ attributes->Set("triggered_by_id", GetObjectIdentifier(triggeredBy));
+ }
+
+ auto scheduledBy (downtime->GetScheduledBy());
+ if (!scheduledBy.IsEmpty()) {
+ attributes->Set("scheduled_by", scheduledBy);
+ }
+
+ auto parent (Downtime::GetByName(downtime->GetParent()));
+ if (parent) {
+ attributes->Set("parent_id", GetObjectIdentifier(parent));
+ }
+
+ return true;
+ }
+
+ if (type == UserGroup::TypeInstance) {
+ UserGroup::Ptr userGroup = static_pointer_cast<UserGroup>(object);
+
+ attributes->Set("display_name", userGroup->GetDisplayName());
+
+ return true;
+ }
+
+ if (type == HostGroup::TypeInstance) {
+ HostGroup::Ptr hostGroup = static_pointer_cast<HostGroup>(object);
+
+ attributes->Set("display_name", hostGroup->GetDisplayName());
+
+ return true;
+ }
+
+ if (type == ServiceGroup::TypeInstance) {
+ ServiceGroup::Ptr serviceGroup = static_pointer_cast<ServiceGroup>(object);
+
+ attributes->Set("display_name", serviceGroup->GetDisplayName());
+
+ return true;
+ }
+
+ if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+ Command::Ptr command = static_pointer_cast<Command>(object);
+
+ attributes->Set("command", JsonEncode(command->GetCommandLine()));
+ attributes->Set("timeout", std::max(0, command->GetTimeout()));
+
+ return true;
+ }
+
+ return false;
+}
+
+/* Creates a config update with computed checksums etc.
+ * Writes attributes, customVars and checksums into the respective supplied vectors. Adds two values to each vector
+ * (if applicable), first the key then the value. To use in a Redis command the command (e.g. HSET) and the key (e.g.
+ * icinga:config:object:downtime) need to be prepended. There is nothing to indicate success or failure.
+ */
+void
+IcingaDB::CreateConfigUpdate(const ConfigObject::Ptr& object, const String typeName, std::map<String, std::vector<String>>& hMSets,
+ std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate)
+{
+ /* TODO: This isn't essentially correct as we don't keep track of config objects ourselves. This would avoid duplicated config updates at startup.
+ if (!runtimeUpdate && m_ConfigDumpInProgress)
+ return;
+ */
+
+ if (m_Rcon == nullptr)
+ return;
+
+ Dictionary::Ptr attr = new Dictionary;
+ Dictionary::Ptr chksm = new Dictionary;
+
+ if (!PrepareObject(object, attr, chksm))
+ return;
+
+ InsertObjectDependencies(object, typeName, hMSets, runtimeUpdates, runtimeUpdate);
+
+ String objectKey = GetObjectIdentifier(object);
+ auto& attrs (hMSets[m_PrefixConfigObject + typeName]);
+ auto& chksms (hMSets[m_PrefixConfigCheckSum + typeName]);
+
+ attrs.emplace_back(objectKey);
+ attrs.emplace_back(JsonEncode(attr));
+
+ String checksum = HashValue(attr);
+ chksms.emplace_back(objectKey);
+ chksms.emplace_back(JsonEncode(new Dictionary({{"checksum", checksum}})));
+
+ /* Send an update event to subscribers. */
+ if (runtimeUpdate) {
+ attr->Set("checksum", checksum);
+ AddObjectDataToRuntimeUpdates(runtimeUpdates, objectKey, m_PrefixConfigObject + typeName, attr);
+ }
+}
+
+void IcingaDB::SendConfigDelete(const ConfigObject::Ptr& object)
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ Type::Ptr type = object->GetReflectionType();
+ String typeName = type->GetName().ToLower();
+ String objectKey = GetObjectIdentifier(object);
+
+ m_Rcon->FireAndForgetQueries({
+ {"HDEL", m_PrefixConfigObject + typeName, objectKey},
+ {"HDEL", m_PrefixConfigCheckSum + typeName, objectKey},
+ {
+ "XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*",
+ "redis_key", m_PrefixConfigObject + typeName, "id", objectKey, "runtime_type", "delete"
+ }
+ }, Prio::Config);
+
+ CustomVarObject::Ptr customVarObject = dynamic_pointer_cast<CustomVarObject>(object);
+
+ if (customVarObject) {
+ Dictionary::Ptr vars = customVarObject->GetVars();
+ SendCustomVarsChanged(object, vars, nullptr);
+ }
+
+ if (type == Host::TypeInstance || type == Service::TypeInstance) {
+ Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ m_Rcon->FireAndForgetQuery({
+ "ZREM",
+ service ? "icinga:nextupdate:service" : "icinga:nextupdate:host",
+ GetObjectIdentifier(checkable)
+ }, Prio::CheckResult);
+
+ m_Rcon->FireAndForgetQueries({
+ {"HDEL", m_PrefixConfigObject + typeName + ":state", objectKey},
+ {"HDEL", m_PrefixConfigCheckSum + typeName + ":state", objectKey}
+ }, Prio::RuntimeStateSync);
+
+ if (service) {
+ SendGroupsChanged<ServiceGroup>(checkable, service->GetGroups(), nullptr);
+ } else {
+ SendGroupsChanged<HostGroup>(checkable, host->GetGroups(), nullptr);
+ }
+
+ return;
+ }
+
+ if (type == TimePeriod::TypeInstance) {
+ TimePeriod::Ptr timeperiod = static_pointer_cast<TimePeriod>(object);
+ SendTimePeriodRangesChanged(timeperiod, timeperiod->GetRanges(), nullptr);
+ SendTimePeriodIncludesChanged(timeperiod, timeperiod->GetIncludes(), nullptr);
+ SendTimePeriodExcludesChanged(timeperiod, timeperiod->GetExcludes(), nullptr);
+ return;
+ }
+
+ if (type == User::TypeInstance) {
+ User::Ptr user = static_pointer_cast<User>(object);
+ SendGroupsChanged<UserGroup>(user, user->GetGroups(), nullptr);
+ return;
+ }
+
+ if (type == Notification::TypeInstance) {
+ Notification::Ptr notification = static_pointer_cast<Notification>(object);
+ SendNotificationUsersChanged(notification, notification->GetUsersRaw(), nullptr);
+ SendNotificationUserGroupsChanged(notification, notification->GetUserGroupsRaw(), nullptr);
+ return;
+ }
+
+ if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+ Command::Ptr command = static_pointer_cast<Command>(object);
+ SendCommandArgumentsChanged(command, command->GetArguments(), nullptr);
+ SendCommandEnvChanged(command, command->GetEnv(), nullptr);
+ return;
+ }
+}
+
+static inline
+unsigned short GetPreviousState(const Checkable::Ptr& checkable, const Service::Ptr& service, StateType type)
+{
+ auto phs ((type == StateTypeHard ? checkable->GetLastHardStatesRaw() : checkable->GetLastSoftStatesRaw()) % 100u);
+
+ if (service) {
+ return phs;
+ } else {
+ return phs == 99 ? phs : Host::CalculateState(ServiceState(phs));
+ }
+}
+
+void IcingaDB::SendStateChange(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Checkable::Ptr checkable = dynamic_pointer_cast<Checkable>(object);
+ if (!checkable)
+ return;
+
+ if (!cr)
+ return;
+
+ Host::Ptr host;
+ Service::Ptr service;
+
+ tie(host, service) = GetHostService(checkable);
+
+ UpdateState(checkable, StateUpdate::RuntimeOnly);
+
+ int hard_state;
+ if (!cr) {
+ hard_state = 99;
+ } else {
+ hard_state = service ? Convert::ToLong(service->GetLastHardState()) : Convert::ToLong(host->GetLastHardState());
+ }
+
+ auto eventTime (cr->GetExecutionEnd());
+ auto eventTs (TimestampToMilliseconds(eventTime));
+
+ Array::Ptr rawId = new Array({m_EnvironmentId, object->GetName()});
+ rawId->Add(eventTs);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:state", "*",
+ "id", HashValue(rawId),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "state_type", Convert::ToString(type),
+ "soft_state", Convert::ToString(cr ? service ? Convert::ToLong(cr->GetState()) : Convert::ToLong(Host::CalculateState(cr->GetState())) : 99),
+ "hard_state", Convert::ToString(hard_state),
+ "check_attempt", Convert::ToString(checkable->GetCheckAttempt()),
+ "previous_soft_state", Convert::ToString(GetPreviousState(checkable, service, StateTypeSoft)),
+ "previous_hard_state", Convert::ToString(GetPreviousState(checkable, service, StateTypeHard)),
+ "max_check_attempts", Convert::ToString(checkable->GetMaxCheckAttempts()),
+ "event_time", Convert::ToString(eventTs),
+ "event_id", CalcEventID("state_change", object, eventTime),
+ "event_type", "state_change"
+ });
+
+ if (cr) {
+ auto output (cr->GetOutput());
+ auto pos (output.Find("\n"));
+
+ if (pos != String::NPos) {
+ auto longOutput (output.SubStr(pos + 1u));
+ output.erase(output.Begin() + pos, output.End());
+
+ xAdd.emplace_back("long_output");
+ xAdd.emplace_back(Utility::ValidateUTF8(std::move(longOutput)));
+ }
+
+ xAdd.emplace_back("output");
+ xAdd.emplace_back(Utility::ValidateUTF8(std::move(output)));
+ xAdd.emplace_back("check_source");
+ xAdd.emplace_back(cr->GetCheckSource());
+ xAdd.emplace_back("scheduling_source");
+ xAdd.emplace_back(cr->GetSchedulingSource());
+ }
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendSentNotification(
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text, double sendTime
+)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ auto finalText = text;
+ if (finalText == "" && cr) {
+ finalText = cr->GetOutput();
+ }
+
+ auto usersAmount (users.size());
+ auto sendTs (TimestampToMilliseconds(sendTime));
+
+ Array::Ptr rawId = new Array({m_EnvironmentId, notification->GetName()});
+ rawId->Add(GetNotificationTypeByEnum(type));
+ rawId->Add(sendTs);
+
+ auto notificationHistoryId (HashValue(rawId));
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:notification", "*",
+ "id", notificationHistoryId,
+ "environment_id", m_EnvironmentId,
+ "notification_id", GetObjectIdentifier(notification),
+ "host_id", GetObjectIdentifier(host),
+ "type", Convert::ToString(type),
+ "state", Convert::ToString(cr ? service ? Convert::ToLong(cr->GetState()) : Convert::ToLong(Host::CalculateState(cr->GetState())) : 99),
+ "previous_hard_state", Convert::ToString(cr ? Convert::ToLong(service ? cr->GetPreviousHardState() : Host::CalculateState(cr->GetPreviousHardState())) : 99),
+ "author", Utility::ValidateUTF8(author),
+ "text", Utility::ValidateUTF8(finalText),
+ "users_notified", Convert::ToString(usersAmount),
+ "send_time", Convert::ToString(sendTs),
+ "event_id", CalcEventID("notification", notification, sendTime, type),
+ "event_type", "notification"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ if (!users.empty()) {
+ Array::Ptr users_notified = new Array();
+ for (const User::Ptr& user : users) {
+ users_notified->Add(GetObjectIdentifier(user));
+ }
+ xAdd.emplace_back("users_notified_ids");
+ xAdd.emplace_back(JsonEncode(users_notified));
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendStartedDowntime(const Downtime::Ptr& downtime)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ SendConfigUpdate(downtime, true);
+
+ auto checkable (downtime->GetCheckable());
+ auto triggeredBy (Downtime::GetByName(downtime->GetTriggeredBy()));
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ /* Update checkable state as in_downtime may have changed. */
+ UpdateState(checkable, StateUpdate::Full);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:downtime", "*",
+ "downtime_id", GetObjectIdentifier(downtime),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "entry_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEntryTime())),
+ "author", Utility::ValidateUTF8(downtime->GetAuthor()),
+ "comment", Utility::ValidateUTF8(downtime->GetComment()),
+ "is_flexible", Convert::ToString((unsigned short)!downtime->GetFixed()),
+ "flexible_duration", Convert::ToString(TimestampToMilliseconds(std::max(0.0, downtime->GetDuration()))),
+ "scheduled_start_time", Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())),
+ "scheduled_end_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())),
+ "has_been_cancelled", Convert::ToString((unsigned short)downtime->GetWasCancelled()),
+ "trigger_time", Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())),
+ "cancel_time", Convert::ToString(TimestampToMilliseconds(downtime->GetRemoveTime())),
+ "event_id", CalcEventID("downtime_start", downtime),
+ "event_type", "downtime_start"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ if (triggeredBy) {
+ xAdd.emplace_back("triggered_by_id");
+ xAdd.emplace_back(GetObjectIdentifier(triggeredBy));
+ }
+
+ if (downtime->GetFixed()) {
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())));
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())));
+ } else {
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())));
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime() + std::max(0.0, downtime->GetDuration()))));
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ auto parent (Downtime::GetByName(downtime->GetParent()));
+
+ if (parent) {
+ xAdd.emplace_back("parent_id");
+ xAdd.emplace_back(GetObjectIdentifier(parent));
+ }
+
+ auto scheduledBy (downtime->GetScheduledBy());
+
+ if (!scheduledBy.IsEmpty()) {
+ xAdd.emplace_back("scheduled_by");
+ xAdd.emplace_back(scheduledBy);
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendRemovedDowntime(const Downtime::Ptr& downtime)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ auto checkable (downtime->GetCheckable());
+ auto triggeredBy (Downtime::GetByName(downtime->GetTriggeredBy()));
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ // Downtime never got triggered (didn't send "downtime_start") so we don't want to send "downtime_end"
+ if (downtime->GetTriggerTime() == 0)
+ return;
+
+ /* Update checkable state as in_downtime may have changed. */
+ UpdateState(checkable, StateUpdate::Full);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:downtime", "*",
+ "downtime_id", GetObjectIdentifier(downtime),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "entry_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEntryTime())),
+ "author", Utility::ValidateUTF8(downtime->GetAuthor()),
+ "cancelled_by", Utility::ValidateUTF8(downtime->GetRemovedBy()),
+ "comment", Utility::ValidateUTF8(downtime->GetComment()),
+ "is_flexible", Convert::ToString((unsigned short)!downtime->GetFixed()),
+ "flexible_duration", Convert::ToString(TimestampToMilliseconds(std::max(0.0, downtime->GetDuration()))),
+ "scheduled_start_time", Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())),
+ "scheduled_end_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())),
+ "has_been_cancelled", Convert::ToString((unsigned short)downtime->GetWasCancelled()),
+ "trigger_time", Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())),
+ "cancel_time", Convert::ToString(TimestampToMilliseconds(downtime->GetRemoveTime())),
+ "event_id", CalcEventID("downtime_end", downtime),
+ "event_type", "downtime_end"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ if (triggeredBy) {
+ xAdd.emplace_back("triggered_by_id");
+ xAdd.emplace_back(GetObjectIdentifier(triggeredBy));
+ }
+
+ if (downtime->GetFixed()) {
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())));
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())));
+ } else {
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())));
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime() + std::max(0.0, downtime->GetDuration()))));
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ auto parent (Downtime::GetByName(downtime->GetParent()));
+
+ if (parent) {
+ xAdd.emplace_back("parent_id");
+ xAdd.emplace_back(GetObjectIdentifier(parent));
+ }
+
+ auto scheduledBy (downtime->GetScheduledBy());
+
+ if (!scheduledBy.IsEmpty()) {
+ xAdd.emplace_back("scheduled_by");
+ xAdd.emplace_back(scheduledBy);
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendAddedComment(const Comment::Ptr& comment)
+{
+ if (comment->GetEntryType() != CommentUser || !GetActive())
+ return;
+
+ auto checkable (comment->GetCheckable());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:comment", "*",
+ "comment_id", GetObjectIdentifier(comment),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "entry_time", Convert::ToString(TimestampToMilliseconds(comment->GetEntryTime())),
+ "author", Utility::ValidateUTF8(comment->GetAuthor()),
+ "comment", Utility::ValidateUTF8(comment->GetText()),
+ "entry_type", Convert::ToString(comment->GetEntryType()),
+ "is_persistent", Convert::ToString((unsigned short)comment->GetPersistent()),
+ "is_sticky", Convert::ToString((unsigned short)comment->GetSticky()),
+ "event_id", CalcEventID("comment_add", comment),
+ "event_type", "comment_add"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ {
+ auto expireTime (comment->GetExpireTime());
+
+ if (expireTime > 0) {
+ xAdd.emplace_back("expire_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(expireTime)));
+ }
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+ UpdateState(checkable, StateUpdate::Full);
+}
+
+void IcingaDB::SendRemovedComment(const Comment::Ptr& comment)
+{
+ if (comment->GetEntryType() != CommentUser || !GetActive()) {
+ return;
+ }
+
+ double removeTime = comment->GetRemoveTime();
+ bool wasRemoved = removeTime > 0;
+
+ double expireTime = comment->GetExpireTime();
+ bool hasExpireTime = expireTime > 0;
+ bool isExpired = hasExpireTime && expireTime <= Utility::GetTime();
+
+ if (!wasRemoved && !isExpired) {
+ /* The comment object disappeared for no apparent reason, most likely because it simply was deleted instead
+ * of using the proper remove-comment API action. In this case, information that should normally be set is
+ * missing and a proper history event cannot be generated.
+ */
+ return;
+ }
+
+ auto checkable (comment->GetCheckable());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:comment", "*",
+ "comment_id", GetObjectIdentifier(comment),
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "entry_time", Convert::ToString(TimestampToMilliseconds(comment->GetEntryTime())),
+ "author", Utility::ValidateUTF8(comment->GetAuthor()),
+ "comment", Utility::ValidateUTF8(comment->GetText()),
+ "entry_type", Convert::ToString(comment->GetEntryType()),
+ "is_persistent", Convert::ToString((unsigned short)comment->GetPersistent()),
+ "is_sticky", Convert::ToString((unsigned short)comment->GetSticky()),
+ "event_id", CalcEventID("comment_remove", comment),
+ "event_type", "comment_remove"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ if (wasRemoved) {
+ xAdd.emplace_back("remove_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(removeTime)));
+ xAdd.emplace_back("has_been_removed");
+ xAdd.emplace_back("1");
+ xAdd.emplace_back("removed_by");
+ xAdd.emplace_back(Utility::ValidateUTF8(comment->GetRemovedBy()));
+ } else {
+ xAdd.emplace_back("has_been_removed");
+ xAdd.emplace_back("0");
+ }
+
+ if (hasExpireTime) {
+ xAdd.emplace_back("expire_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(expireTime)));
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+ UpdateState(checkable, StateUpdate::Full);
+}
+
+void IcingaDB::SendFlappingChange(const Checkable::Ptr& checkable, double changeTime, double flappingLastChange)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:flapping", "*",
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "flapping_threshold_low", Convert::ToString(checkable->GetFlappingThresholdLow()),
+ "flapping_threshold_high", Convert::ToString(checkable->GetFlappingThresholdHigh())
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ long long startTime;
+
+ if (checkable->IsFlapping()) {
+ startTime = TimestampToMilliseconds(changeTime);
+
+ xAdd.emplace_back("event_type");
+ xAdd.emplace_back("flapping_start");
+ xAdd.emplace_back("percent_state_change_start");
+ xAdd.emplace_back(Convert::ToString(checkable->GetFlappingCurrent()));
+ } else {
+ startTime = TimestampToMilliseconds(flappingLastChange);
+
+ xAdd.emplace_back("event_type");
+ xAdd.emplace_back("flapping_end");
+ xAdd.emplace_back("end_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(changeTime)));
+ xAdd.emplace_back("percent_state_change_end");
+ xAdd.emplace_back(Convert::ToString(checkable->GetFlappingCurrent()));
+ }
+
+ xAdd.emplace_back("start_time");
+ xAdd.emplace_back(Convert::ToString(startTime));
+ xAdd.emplace_back("event_id");
+ xAdd.emplace_back(CalcEventID(checkable->IsFlapping() ? "flapping_start" : "flapping_end", checkable, startTime));
+ xAdd.emplace_back("id");
+ xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), startTime})));
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendNextUpdate(const Checkable::Ptr& checkable)
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ if (checkable->GetEnableActiveChecks()) {
+ m_Rcon->FireAndForgetQuery(
+ {
+ "ZADD",
+ dynamic_pointer_cast<Service>(checkable) ? "icinga:nextupdate:service" : "icinga:nextupdate:host",
+ Convert::ToString(checkable->GetNextUpdate()),
+ GetObjectIdentifier(checkable)
+ },
+ Prio::CheckResult
+ );
+ } else {
+ m_Rcon->FireAndForgetQuery(
+ {
+ "ZREM",
+ dynamic_pointer_cast<Service>(checkable) ? "icinga:nextupdate:service" : "icinga:nextupdate:host",
+ GetObjectIdentifier(checkable)
+ },
+ Prio::CheckResult
+ );
+ }
+}
+
+void IcingaDB::SendAcknowledgementSet(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ /* Update checkable state as is_acknowledged may have changed. */
+ UpdateState(checkable, StateUpdate::Full);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:acknowledgement", "*",
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "event_type", "ack_set",
+ "author", author,
+ "comment", comment,
+ "is_sticky", Convert::ToString((unsigned short)(type == AcknowledgementSticky)),
+ "is_persistent", Convert::ToString((unsigned short)persistent)
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ if (expiry > 0) {
+ xAdd.emplace_back("expire_time");
+ xAdd.emplace_back(Convert::ToString(TimestampToMilliseconds(expiry)));
+ }
+
+ long long setTime = TimestampToMilliseconds(changeTime);
+
+ xAdd.emplace_back("set_time");
+ xAdd.emplace_back(Convert::ToString(setTime));
+ xAdd.emplace_back("event_id");
+ xAdd.emplace_back(CalcEventID("ack_set", checkable, setTime));
+ xAdd.emplace_back("id");
+ xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), setTime})));
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::SendAcknowledgementCleared(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, double ackLastChange)
+{
+ if (!GetActive()) {
+ return;
+ }
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ /* Update checkable state as is_acknowledged may have changed. */
+ UpdateState(checkable, StateUpdate::Full);
+
+ std::vector<String> xAdd ({
+ "XADD", "icinga:history:stream:acknowledgement", "*",
+ "environment_id", m_EnvironmentId,
+ "host_id", GetObjectIdentifier(host),
+ "clear_time", Convert::ToString(TimestampToMilliseconds(changeTime)),
+ "event_type", "ack_clear"
+ });
+
+ if (service) {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("service");
+ xAdd.emplace_back("service_id");
+ xAdd.emplace_back(GetObjectIdentifier(checkable));
+ } else {
+ xAdd.emplace_back("object_type");
+ xAdd.emplace_back("host");
+ }
+
+ auto endpoint (Endpoint::GetLocalEndpoint());
+
+ if (endpoint) {
+ xAdd.emplace_back("endpoint_id");
+ xAdd.emplace_back(GetObjectIdentifier(endpoint));
+ }
+
+ long long setTime = TimestampToMilliseconds(ackLastChange);
+
+ xAdd.emplace_back("set_time");
+ xAdd.emplace_back(Convert::ToString(setTime));
+ xAdd.emplace_back("event_id");
+ xAdd.emplace_back(CalcEventID("ack_clear", checkable, setTime));
+ xAdd.emplace_back("id");
+ xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), setTime})));
+
+ if (!removedBy.IsEmpty()) {
+ xAdd.emplace_back("cleared_by");
+ xAdd.emplace_back(removedBy);
+ }
+
+ m_HistoryBulker.ProduceOne(std::move(xAdd));
+}
+
+void IcingaDB::ForwardHistoryEntries()
+{
+ using clock = std::chrono::steady_clock;
+
+ const std::chrono::seconds logInterval (10);
+ auto nextLog (clock::now() + logInterval);
+
+ auto logPeriodically ([this, logInterval, &nextLog]() {
+ if (clock::now() > nextLog) {
+ nextLog += logInterval;
+
+ auto size (m_HistoryBulker.Size());
+
+ Log(size > m_HistoryBulker.GetBulkSize() ? LogInformation : LogNotice, "IcingaDB")
+ << "Pending history queries: " << size;
+ }
+ });
+
+ for (;;) {
+ logPeriodically();
+
+ auto haystack (m_HistoryBulker.ConsumeMany());
+
+ if (haystack.empty()) {
+ if (!GetActive()) {
+ break;
+ }
+
+ continue;
+ }
+
+ uintmax_t attempts = 0;
+
+ auto logFailure ([&haystack, &attempts](const char* err = nullptr) {
+ Log msg (LogNotice, "IcingaDB");
+
+ msg << "history: " << haystack.size() << " queries failed temporarily (attempt #" << ++attempts << ")";
+
+ if (err) {
+ msg << ": " << err;
+ }
+ });
+
+ for (;;) {
+ logPeriodically();
+
+ if (m_Rcon && m_Rcon->IsConnected()) {
+ try {
+ m_Rcon->GetResultsOfQueries(haystack, Prio::History, {0, 0, haystack.size()});
+ break;
+ } catch (const std::exception& ex) {
+ logFailure(ex.what());
+ } catch (...) {
+ logFailure();
+ }
+ } else {
+ logFailure("not connected to Redis");
+ }
+
+ if (!GetActive()) {
+ Log(LogCritical, "IcingaDB") << "history: " << haystack.size() << " queries failed (attempt #" << attempts
+ << ") while we're about to shut down. Giving up and discarding additional "
+ << m_HistoryBulker.Size() << " queued history queries.";
+
+ return;
+ }
+
+ Utility::Sleep(2);
+ }
+ }
+}
+
+void IcingaDB::SendNotificationUsersChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedUsers = GetArrayDeletedValues(oldValues, newValues);
+
+ for (const auto& userName : deletedUsers) {
+ String id = HashValue(new Array({m_EnvironmentId, "user", userName, notification->GetName()}));
+ DeleteRelationship(id, "notification:user");
+ DeleteRelationship(id, "notification:recipient");
+ }
+}
+
+void IcingaDB::SendNotificationUserGroupsChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedUserGroups = GetArrayDeletedValues(oldValues, newValues);
+
+ for (const auto& userGroupName : deletedUserGroups) {
+ UserGroup::Ptr userGroup = UserGroup::GetByName(userGroupName);
+ String id = HashValue(new Array({m_EnvironmentId, "usergroup", userGroupName, notification->GetName()}));
+ DeleteRelationship(id, "notification:usergroup");
+ DeleteRelationship(id, "notification:recipient");
+
+ for (const User::Ptr& user : userGroup->GetMembers()) {
+ String userId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), userGroupName, notification->GetName()}));
+ DeleteRelationship(userId, "notification:recipient");
+ }
+ }
+}
+
+void IcingaDB::SendTimePeriodRangesChanged(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+ String typeName = GetLowerCaseTypeNameDB(timeperiod);
+
+ for (const auto& rangeKey : deletedKeys) {
+ String id = HashValue(new Array({m_EnvironmentId, rangeKey, oldValues->Get(rangeKey), timeperiod->GetName()}));
+ DeleteRelationship(id, "timeperiod:range");
+ }
+}
+
+void IcingaDB::SendTimePeriodIncludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedIncludes = GetArrayDeletedValues(oldValues, newValues);
+
+ for (const auto& includeName : deletedIncludes) {
+ String id = HashValue(new Array({m_EnvironmentId, includeName, timeperiod->GetName()}));
+ DeleteRelationship(id, "timeperiod:override:include");
+ }
+}
+
+void IcingaDB::SendTimePeriodExcludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedExcludes = GetArrayDeletedValues(oldValues, newValues);
+
+ for (const auto& excludeName : deletedExcludes) {
+ String id = HashValue(new Array({m_EnvironmentId, excludeName, timeperiod->GetName()}));
+ DeleteRelationship(id, "timeperiod:override:exclude");
+ }
+}
+
+template<typename T>
+void IcingaDB::SendGroupsChanged(const ConfigObject::Ptr& object, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<Value> deletedGroups = GetArrayDeletedValues(oldValues, newValues);
+ String typeName = GetLowerCaseTypeNameDB(object);
+
+ for (const auto& groupName : deletedGroups) {
+ typename T::Ptr group = ConfigObject::GetObject<T>(groupName);
+ String id = HashValue(new Array({m_EnvironmentId, group->GetName(), object->GetName()}));
+ DeleteRelationship(id, typeName + "group:member");
+
+ if (std::is_same<T, UserGroup>::value) {
+ UserGroup::Ptr userGroup = dynamic_pointer_cast<UserGroup>(group);
+
+ for (const auto& notification : userGroup->GetNotifications()) {
+ String userId = HashValue(new Array({m_EnvironmentId, "usergroupuser", object->GetName(), groupName, notification->GetName()}));
+ DeleteRelationship(userId, "notification:recipient");
+ }
+ }
+ }
+}
+
+void IcingaDB::SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+ String typeName = GetLowerCaseTypeNameDB(command);
+
+ for (const auto& envvarKey : deletedKeys) {
+ String id = HashValue(new Array({m_EnvironmentId, envvarKey, command->GetName()}));
+ DeleteRelationship(id, typeName + ":envvar", true);
+ }
+}
+
+void IcingaDB::SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+ String typeName = GetLowerCaseTypeNameDB(command);
+
+ for (const auto& argumentKey : deletedKeys) {
+ String id = HashValue(new Array({m_EnvironmentId, argumentKey, command->GetName()}));
+ DeleteRelationship(id, typeName + ":argument", true);
+ }
+}
+
+void IcingaDB::SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ if (m_IndexedTypes.find(object->GetReflectionType().get()) == m_IndexedTypes.end()) {
+ return;
+ }
+
+ if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+ return;
+ }
+
+ Dictionary::Ptr oldVars = SerializeVars(oldValues);
+ Dictionary::Ptr newVars = SerializeVars(newValues);
+
+ std::vector<String> deletedVars = GetDictionaryDeletedKeys(oldVars, newVars);
+ String typeName = GetLowerCaseTypeNameDB(object);
+
+ for (const auto& varId : deletedVars) {
+ String id = HashValue(new Array({m_EnvironmentId, varId, object->GetName()}));
+ DeleteRelationship(id, typeName + ":customvar");
+ }
+}
+
+Dictionary::Ptr IcingaDB::SerializeState(const Checkable::Ptr& checkable)
+{
+ Dictionary::Ptr attrs = new Dictionary();
+
+ Host::Ptr host;
+ Service::Ptr service;
+
+ tie(host, service) = GetHostService(checkable);
+
+ String id = GetObjectIdentifier(checkable);
+
+ /*
+ * As there is a 1:1 relationship between host and host state, the host ID ('host_id')
+ * is also used as the host state ID ('id'). These are duplicated to 1) avoid having
+ * special handling for this in Icinga DB and 2) to have both a primary key and a foreign key
+ * in the SQL database in the end. In the database 'host_id' ends up as foreign key 'host_state.host_id'
+ * referring to 'host.id' while 'id' ends up as the primary key 'host_state.id'. This also applies for service.
+ */
+ attrs->Set("id", id);
+ attrs->Set("environment_id", m_EnvironmentId);
+ attrs->Set("state_type", checkable->HasBeenChecked() ? checkable->GetStateType() : StateTypeHard);
+
+ // TODO: last_hard/soft_state should be "previous".
+ if (service) {
+ attrs->Set("service_id", id);
+ auto state = service->HasBeenChecked() ? service->GetState() : 99;
+ attrs->Set("soft_state", state);
+ attrs->Set("hard_state", service->HasBeenChecked() ? service->GetLastHardState() : 99);
+ attrs->Set("severity", service->GetSeverity());
+ attrs->Set("host_id", GetObjectIdentifier(host));
+ } else {
+ attrs->Set("host_id", id);
+ auto state = host->HasBeenChecked() ? host->GetState() : 99;
+ attrs->Set("soft_state", state);
+ attrs->Set("hard_state", host->HasBeenChecked() ? host->GetLastHardState() : 99);
+ attrs->Set("severity", host->GetSeverity());
+ }
+
+ attrs->Set("previous_soft_state", GetPreviousState(checkable, service, StateTypeSoft));
+ attrs->Set("previous_hard_state", GetPreviousState(checkable, service, StateTypeHard));
+ attrs->Set("check_attempt", checkable->GetCheckAttempt());
+
+ attrs->Set("is_active", checkable->IsActive());
+
+ CheckResult::Ptr cr = checkable->GetLastCheckResult();
+
+ if (cr) {
+ String rawOutput = cr->GetOutput();
+ if (!rawOutput.IsEmpty()) {
+ size_t lineBreak = rawOutput.Find("\n");
+ String output = rawOutput.SubStr(0, lineBreak);
+ if (!output.IsEmpty())
+ attrs->Set("output", rawOutput.SubStr(0, lineBreak));
+
+ if (lineBreak > 0 && lineBreak != String::NPos) {
+ String longOutput = rawOutput.SubStr(lineBreak+1, rawOutput.GetLength());
+ if (!longOutput.IsEmpty())
+ attrs->Set("long_output", longOutput);
+ }
+ }
+
+ String perfData = PluginUtility::FormatPerfdata(cr->GetPerformanceData());
+ if (!perfData.IsEmpty())
+ attrs->Set("performance_data", perfData);
+
+ String normedPerfData = PluginUtility::FormatPerfdata(cr->GetPerformanceData(), true);
+ if (!normedPerfData.IsEmpty())
+ attrs->Set("normalized_performance_data", normedPerfData);
+
+ if (!cr->GetCommand().IsEmpty())
+ attrs->Set("check_commandline", FormatCommandLine(cr->GetCommand()));
+ attrs->Set("execution_time", TimestampToMilliseconds(fmax(0.0, cr->CalculateExecutionTime())));
+ attrs->Set("latency", TimestampToMilliseconds(cr->CalculateLatency()));
+ attrs->Set("check_source", cr->GetCheckSource());
+ attrs->Set("scheduling_source", cr->GetSchedulingSource());
+ }
+
+ attrs->Set("is_problem", checkable->GetProblem());
+ attrs->Set("is_handled", checkable->GetHandled());
+ attrs->Set("is_reachable", checkable->IsReachable());
+ attrs->Set("is_flapping", checkable->IsFlapping());
+
+ attrs->Set("is_acknowledged", checkable->GetAcknowledgement());
+ if (checkable->IsAcknowledged()) {
+ Timestamp entry = 0;
+ Comment::Ptr AckComment;
+ for (const Comment::Ptr& c : checkable->GetComments()) {
+ if (c->GetEntryType() == CommentAcknowledgement) {
+ if (c->GetEntryTime() > entry) {
+ entry = c->GetEntryTime();
+ AckComment = c;
+ }
+ }
+ }
+ if (AckComment != nullptr) {
+ attrs->Set("acknowledgement_comment_id", GetObjectIdentifier(AckComment));
+ }
+ }
+
+ {
+ auto lastComment (checkable->GetLastComment());
+
+ if (lastComment) {
+ attrs->Set("last_comment_id", GetObjectIdentifier(lastComment));
+ }
+ }
+
+ attrs->Set("in_downtime", checkable->IsInDowntime());
+
+ if (checkable->GetCheckTimeout().IsEmpty())
+ attrs->Set("check_timeout", TimestampToMilliseconds(checkable->GetCheckCommand()->GetTimeout()));
+ else
+ attrs->Set("check_timeout", TimestampToMilliseconds(checkable->GetCheckTimeout()));
+
+ long long lastCheck = TimestampToMilliseconds(checkable->GetLastCheck());
+ if (lastCheck > 0)
+ attrs->Set("last_update", lastCheck);
+
+ attrs->Set("last_state_change", TimestampToMilliseconds(checkable->GetLastStateChange()));
+ attrs->Set("next_check", TimestampToMilliseconds(checkable->GetNextCheck()));
+ attrs->Set("next_update", TimestampToMilliseconds(checkable->GetNextUpdate()));
+
+ return attrs;
+}
+
+std::vector<String>
+IcingaDB::UpdateObjectAttrs(const ConfigObject::Ptr& object, int fieldType,
+ const String& typeNameOverride)
+{
+ Type::Ptr type = object->GetReflectionType();
+ Dictionary::Ptr attrs(new Dictionary);
+
+ for (int fid = 0; fid < type->GetFieldCount(); fid++) {
+ Field field = type->GetFieldInfo(fid);
+
+ if ((field.Attributes & fieldType) == 0)
+ continue;
+
+ Value val = object->GetField(fid);
+
+ /* hide attributes which shouldn't be user-visible */
+ if (field.Attributes & FANoUserView)
+ continue;
+
+ /* hide internal navigation fields */
+ if (field.Attributes & FANavigation && !(field.Attributes & (FAConfig | FAState)))
+ continue;
+
+ attrs->Set(field.Name, Serialize(val));
+ }
+
+ /* Downtimes require in_effect, which is not an attribute */
+ Downtime::Ptr downtime = dynamic_pointer_cast<Downtime>(object);
+ if (downtime) {
+ attrs->Set("in_effect", Serialize(downtime->IsInEffect()));
+ attrs->Set("trigger_time", Serialize(TimestampToMilliseconds(downtime->GetTriggerTime())));
+ }
+
+
+ /* Use the name checksum as unique key. */
+ String typeName = type->GetName().ToLower();
+ if (!typeNameOverride.IsEmpty())
+ typeName = typeNameOverride.ToLower();
+
+ return {GetObjectIdentifier(object), JsonEncode(attrs)};
+ //m_Rcon->FireAndForgetQuery({"HSET", keyPrefix + typeName, GetObjectIdentifier(object), JsonEncode(attrs)});
+}
+
+void IcingaDB::StateChangeHandler(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type)
+{
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendStateChange(object, cr, type);
+ }
+}
+
+void IcingaDB::ReachabilityChangeHandler(const std::set<Checkable::Ptr>& children)
+{
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ for (auto& checkable : children) {
+ rw->UpdateState(checkable, StateUpdate::Full);
+ }
+ }
+}
+
+void IcingaDB::VersionChangedHandler(const ConfigObject::Ptr& object)
+{
+ Type::Ptr type = object->GetReflectionType();
+
+ if (m_IndexedTypes.find(type.get()) == m_IndexedTypes.end()) {
+ return;
+ }
+
+ if (object->IsActive()) {
+ // Create or update the object config
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ if (rw)
+ rw->SendConfigUpdate(object, true);
+ }
+ } else if (!object->IsActive() &&
+ object->GetExtension("ConfigObjectDeleted")) { // same as in apilistener-configsync.cpp
+ // Delete object config
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ if (rw)
+ rw->SendConfigDelete(object);
+ }
+ }
+}
+
+void IcingaDB::DowntimeStartedHandler(const Downtime::Ptr& downtime)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendStartedDowntime(downtime);
+ }
+}
+
+void IcingaDB::DowntimeRemovedHandler(const Downtime::Ptr& downtime)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendRemovedDowntime(downtime);
+ }
+}
+
+void IcingaDB::NotificationSentToAllUsersHandler(
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text
+)
+{
+ auto rws (ConfigType::GetObjectsByType<IcingaDB>());
+ auto sendTime (notification->GetLastNotification());
+
+ if (!rws.empty()) {
+ for (auto& rw : rws) {
+ rw->SendSentNotification(notification, checkable, users, type, cr, author, text, sendTime);
+ }
+ }
+}
+
+void IcingaDB::CommentAddedHandler(const Comment::Ptr& comment)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendAddedComment(comment);
+ }
+}
+
+void IcingaDB::CommentRemovedHandler(const Comment::Ptr& comment)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendRemovedComment(comment);
+ }
+}
+
+void IcingaDB::FlappingChangeHandler(const Checkable::Ptr& checkable, double changeTime)
+{
+ auto flappingLastChange (checkable->GetFlappingLastChange());
+
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendFlappingChange(checkable, changeTime, flappingLastChange);
+ }
+}
+
+void IcingaDB::NewCheckResultHandler(const Checkable::Ptr& checkable)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->UpdateState(checkable, StateUpdate::Volatile);
+ rw->SendNextUpdate(checkable);
+ }
+}
+
+void IcingaDB::NextCheckUpdatedHandler(const Checkable::Ptr& checkable)
+{
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->UpdateState(checkable, StateUpdate::Volatile);
+ rw->SendNextUpdate(checkable);
+ }
+}
+
+void IcingaDB::HostProblemChangedHandler(const Service::Ptr& service) {
+ for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ /* Host state changes affect is_handled and severity of services. */
+ rw->UpdateState(service, StateUpdate::Full);
+ }
+}
+
+void IcingaDB::AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry)
+{
+ auto rws (ConfigType::GetObjectsByType<IcingaDB>());
+
+ if (!rws.empty()) {
+ for (auto& rw : rws) {
+ rw->SendAcknowledgementSet(checkable, author, comment, type, persistent, changeTime, expiry);
+ }
+ }
+}
+
+void IcingaDB::AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime)
+{
+ auto rws (ConfigType::GetObjectsByType<IcingaDB>());
+
+ if (!rws.empty()) {
+ auto rb (Shared<String>::Make(removedBy));
+ auto ackLastChange (checkable->GetAcknowledgementLastChange());
+
+ for (auto& rw : rws) {
+ rw->SendAcknowledgementCleared(checkable, *rb, changeTime, ackLastChange);
+ }
+ }
+}
+
+void IcingaDB::NotificationUsersChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendNotificationUsersChanged(notification, oldValues, newValues);
+ }
+}
+
+void IcingaDB::NotificationUserGroupsChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendNotificationUserGroupsChanged(notification, oldValues, newValues);
+ }
+}
+
+void IcingaDB::TimePeriodRangesChangedHandler(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendTimePeriodRangesChanged(timeperiod, oldValues, newValues);
+ }
+}
+
+void IcingaDB::TimePeriodIncludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendTimePeriodIncludesChanged(timeperiod, oldValues, newValues);
+ }
+}
+
+void IcingaDB::TimePeriodExcludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendTimePeriodExcludesChanged(timeperiod, oldValues, newValues);
+ }
+}
+
+void IcingaDB::UserGroupsChangedHandler(const User::Ptr& user, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendGroupsChanged<UserGroup>(user, oldValues, newValues);
+ }
+}
+
+void IcingaDB::HostGroupsChangedHandler(const Host::Ptr& host, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendGroupsChanged<HostGroup>(host, oldValues, newValues);
+ }
+}
+
+void IcingaDB::ServiceGroupsChangedHandler(const Service::Ptr& service, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendGroupsChanged<ServiceGroup>(service, oldValues, newValues);
+ }
+}
+
+void IcingaDB::CommandEnvChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendCommandEnvChanged(command, oldValues, newValues);
+ }
+}
+
+void IcingaDB::CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendCommandArgumentsChanged(command, oldValues, newValues);
+ }
+}
+
+void IcingaDB::CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+ for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+ rw->SendCustomVarsChanged(object, oldValues, newValues);
+ }
+}
+
+void IcingaDB::DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum) {
+ Log(LogNotice, "IcingaDB") << "Deleting relationship '" << redisKeyWithoutPrefix << " -> '" << id << "'";
+
+ String redisKey = m_PrefixConfigObject + redisKeyWithoutPrefix;
+
+ std::vector<std::vector<String>> queries;
+
+ if (hasChecksum) {
+ queries.push_back({"HDEL", m_PrefixConfigCheckSum + redisKeyWithoutPrefix, id});
+ }
+
+ queries.push_back({"HDEL", redisKey, id});
+ queries.push_back({
+ "XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*",
+ "redis_key", redisKey, "id", id, "runtime_type", "delete"
+ });
+
+ m_Rcon->FireAndForgetQueries(queries, Prio::Config);
+}
diff --git a/lib/icingadb/icingadb-stats.cpp b/lib/icingadb/icingadb-stats.cpp
new file mode 100644
index 0000000..476756b
--- /dev/null
+++ b/lib/icingadb/icingadb-stats.cpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadb.hpp"
+#include "base/application.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/statsfunction.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+Dictionary::Ptr IcingaDB::GetStats()
+{
+ Dictionary::Ptr stats = new Dictionary();
+
+ //TODO: Figure out if more stats can be useful here.
+ Namespace::Ptr statsFunctions = ScriptGlobal::Get("StatsFunctions", &Empty);
+
+ if (!statsFunctions)
+ Dictionary::Ptr();
+
+ ObjectLock olock(statsFunctions);
+
+ for (auto& kv : statsFunctions)
+ {
+ Function::Ptr func = kv.second.Val;
+
+ if (!func)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid status function name."));
+
+ Dictionary::Ptr status = new Dictionary();
+ Array::Ptr perfdata = new Array();
+ func->Invoke({ status, perfdata });
+
+ stats->Set(kv.first, new Dictionary({
+ { "status", status },
+ { "perfdata", Serialize(perfdata, FAState) }
+ }));
+ }
+
+ typedef Dictionary::Ptr DP;
+ DP app = DP(DP(DP(stats->Get("IcingaApplication"))->Get("status"))->Get("icingaapplication"))->Get("app");
+
+ app->Set("program_start", TimestampToMilliseconds(Application::GetStartTime()));
+
+ auto localEndpoint (Endpoint::GetLocalEndpoint());
+ if (localEndpoint) {
+ app->Set("endpoint_id", GetObjectIdentifier(localEndpoint));
+ }
+
+ return stats;
+}
+
diff --git a/lib/icingadb/icingadb-utility.cpp b/lib/icingadb/icingadb-utility.cpp
new file mode 100644
index 0000000..b247ed8
--- /dev/null
+++ b/lib/icingadb/icingadb-utility.cpp
@@ -0,0 +1,319 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadb.hpp"
+#include "base/configtype.hpp"
+#include "base/object-packer.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/tlsutility.hpp"
+#include "base/initialize.hpp"
+#include "base/objectlock.hpp"
+#include "base/array.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/convert.hpp"
+#include "base/json.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/notificationcommand.hpp"
+#include "icinga/eventcommand.hpp"
+#include "icinga/host.hpp"
+#include <boost/algorithm/string.hpp>
+#include <map>
+#include <utility>
+#include <vector>
+
+using namespace icinga;
+
+String IcingaDB::FormatCheckSumBinary(const String& str)
+{
+ char output[20*2+1];
+ for (int i = 0; i < 20; i++)
+ sprintf(output + 2 * i, "%02x", str[i]);
+
+ return output;
+}
+
+String IcingaDB::FormatCommandLine(const Value& commandLine)
+{
+ String result;
+ if (commandLine.IsObjectType<Array>()) {
+ Array::Ptr args = commandLine;
+ bool first = true;
+
+ ObjectLock olock(args);
+ for (const Value& arg : args) {
+ String token = "'" + Convert::ToString(arg) + "'";
+
+ if (first)
+ first = false;
+ else
+ result += String(1, ' ');
+
+ result += token;
+ }
+ } else if (!commandLine.IsEmpty()) {
+ result = commandLine;
+ boost::algorithm::replace_all(result, "\'", "\\'");
+ result = "'" + result + "'";
+ }
+
+ return result;
+}
+
+String IcingaDB::GetObjectIdentifier(const ConfigObject::Ptr& object)
+{
+ String identifier = object->GetIcingadbIdentifier();
+ if (identifier.IsEmpty()) {
+ identifier = HashValue(new Array({m_EnvironmentId, object->GetName()}));
+ object->SetIcingadbIdentifier(identifier);
+ }
+
+ return identifier;
+}
+
+/**
+ * Calculates a deterministic history event ID like SHA1(env, eventType, x...[, nt][, eventTime])
+ *
+ * Where SHA1(env, x...) = GetObjectIdentifier(object)
+ */
+String IcingaDB::CalcEventID(const char* eventType, const ConfigObject::Ptr& object, double eventTime, NotificationType nt)
+{
+ Array::Ptr rawId = new Array({object->GetName()});
+ rawId->Insert(0, m_EnvironmentId);
+ rawId->Insert(1, eventType);
+
+ if (nt) {
+ rawId->Add(GetNotificationTypeByEnum(nt));
+ }
+
+ if (eventTime) {
+ rawId->Add(TimestampToMilliseconds(eventTime));
+ }
+
+ return HashValue(std::move(rawId));
+}
+
+static const std::set<String> metadataWhitelist ({"package", "source_location", "templates"});
+
+/**
+ * Prepare custom vars for being written to Redis
+ *
+ * object.vars = {
+ * "disks": {
+ * "disk": {},
+ * "disk /": {
+ * "disk_partitions": "/"
+ * }
+ * }
+ * }
+ *
+ * return {
+ * SHA1(PackObject([
+ * EnvironmentId,
+ * "disks",
+ * {
+ * "disk": {},
+ * "disk /": {
+ * "disk_partitions": "/"
+ * }
+ * }
+ * ])): {
+ * "environment_id": EnvironmentId,
+ * "name_checksum": SHA1("disks"),
+ * "name": "disks",
+ * "value": {
+ * "disk": {},
+ * "disk /": {
+ * "disk_partitions": "/"
+ * }
+ * }
+ * }
+ * }
+ *
+ * @param Dictionary Config object with custom vars
+ *
+ * @return JSON-like data structure for Redis
+ */
+Dictionary::Ptr IcingaDB::SerializeVars(const Dictionary::Ptr& vars)
+{
+ if (!vars)
+ return nullptr;
+
+ Dictionary::Ptr res = new Dictionary();
+
+ ObjectLock olock(vars);
+
+ for (auto& kv : vars) {
+ res->Set(
+ SHA1(PackObject((Array::Ptr)new Array({m_EnvironmentId, kv.first, kv.second}))),
+ (Dictionary::Ptr)new Dictionary({
+ {"environment_id", m_EnvironmentId},
+ {"name_checksum", SHA1(kv.first)},
+ {"name", kv.first},
+ {"value", JsonEncode(kv.second)},
+ })
+ );
+ }
+
+ return res;
+}
+
+const char* IcingaDB::GetNotificationTypeByEnum(NotificationType type)
+{
+ switch (type) {
+ case NotificationDowntimeStart:
+ return "downtime_start";
+ case NotificationDowntimeEnd:
+ return "downtime_end";
+ case NotificationDowntimeRemoved:
+ return "downtime_removed";
+ case NotificationCustom:
+ return "custom";
+ case NotificationAcknowledgement:
+ return "acknowledgement";
+ case NotificationProblem:
+ return "problem";
+ case NotificationRecovery:
+ return "recovery";
+ case NotificationFlappingStart:
+ return "flapping_start";
+ case NotificationFlappingEnd:
+ return "flapping_end";
+ }
+
+ VERIFY(!"Invalid notification type.");
+}
+
+static const std::set<String> propertiesBlacklistEmpty;
+
+String IcingaDB::HashValue(const Value& value)
+{
+ return HashValue(value, propertiesBlacklistEmpty);
+}
+
+String IcingaDB::HashValue(const Value& value, const std::set<String>& propertiesBlacklist, bool propertiesWhitelist)
+{
+ Value temp;
+ bool mutabl;
+
+ Type::Ptr type = value.GetReflectionType();
+
+ if (ConfigObject::TypeInstance->IsAssignableFrom(type)) {
+ temp = Serialize(value, FAConfig);
+ mutabl = true;
+ } else {
+ temp = value;
+ mutabl = false;
+ }
+
+ if (propertiesBlacklist.size() && temp.IsObject()) {
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>((Object::Ptr)temp);
+
+ if (dict) {
+ if (!mutabl)
+ dict = dict->ShallowClone();
+
+ ObjectLock olock(dict);
+
+ if (propertiesWhitelist) {
+ auto current = dict->Begin();
+ auto propertiesBlacklistEnd = propertiesBlacklist.end();
+
+ while (current != dict->End()) {
+ if (propertiesBlacklist.find(current->first) == propertiesBlacklistEnd) {
+ dict->Remove(current++);
+ } else {
+ ++current;
+ }
+ }
+ } else {
+ for (auto& property : propertiesBlacklist)
+ dict->Remove(property);
+ }
+
+ if (!mutabl)
+ temp = dict;
+ }
+ }
+
+ return SHA1(PackObject(temp));
+}
+
+String IcingaDB::GetLowerCaseTypeNameDB(const ConfigObject::Ptr& obj)
+{
+ return obj->GetReflectionType()->GetName().ToLower();
+}
+
+long long IcingaDB::TimestampToMilliseconds(double timestamp) {
+ return static_cast<long long>(timestamp * 1000);
+}
+
+String IcingaDB::IcingaToStreamValue(const Value& value)
+{
+ switch (value.GetType()) {
+ case ValueBoolean:
+ return Convert::ToString(int(value));
+ case ValueString:
+ return Utility::ValidateUTF8(value);
+ case ValueNumber:
+ case ValueEmpty:
+ return Convert::ToString(value);
+ default:
+ return JsonEncode(value);
+ }
+}
+
+// Returns the items that exist in "arrayOld" but not in "arrayNew"
+std::vector<Value> IcingaDB::GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew) {
+ std::vector<Value> deletedValues;
+
+ if (!arrayOld) {
+ return deletedValues;
+ }
+
+ if (!arrayNew) {
+ ObjectLock olock (arrayOld);
+ return std::vector<Value>(arrayOld->Begin(), arrayOld->End());
+ }
+
+ std::vector<Value> vectorOld;
+ {
+ ObjectLock olock (arrayOld);
+ vectorOld.assign(arrayOld->Begin(), arrayOld->End());
+ }
+ std::sort(vectorOld.begin(), vectorOld.end());
+ vectorOld.erase(std::unique(vectorOld.begin(), vectorOld.end()), vectorOld.end());
+
+ std::vector<Value> vectorNew;
+ {
+ ObjectLock olock (arrayNew);
+ vectorNew.assign(arrayNew->Begin(), arrayNew->End());
+ }
+ std::sort(vectorNew.begin(), vectorNew.end());
+ vectorNew.erase(std::unique(vectorNew.begin(), vectorNew.end()), vectorNew.end());
+
+ std::set_difference(vectorOld.begin(), vectorOld.end(), vectorNew.begin(), vectorNew.end(), std::back_inserter(deletedValues));
+
+ return deletedValues;
+}
+
+// Returns the keys that exist in "dictOld" but not in "dictNew"
+std::vector<String> IcingaDB::GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew) {
+ std::vector<String> deletedKeys;
+
+ if (!dictOld) {
+ return deletedKeys;
+ }
+
+ std::vector<String> oldKeys = dictOld->GetKeys();
+
+ if (!dictNew) {
+ return oldKeys;
+ }
+
+ std::vector<String> newKeys = dictNew->GetKeys();
+
+ std::set_difference(oldKeys.begin(), oldKeys.end(), newKeys.begin(), newKeys.end(), std::back_inserter(deletedKeys));
+
+ return deletedKeys;
+}
diff --git a/lib/icingadb/icingadb.cpp b/lib/icingadb/icingadb.cpp
new file mode 100644
index 0000000..6d5ded9
--- /dev/null
+++ b/lib/icingadb/icingadb.cpp
@@ -0,0 +1,311 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadb.hpp"
+#include "icingadb/icingadb-ti.cpp"
+#include "icingadb/redisconnection.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/eventqueue.hpp"
+#include "base/configuration.hpp"
+#include "base/json.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/statsfunction.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/host.hpp"
+#include <boost/algorithm/string.hpp>
+#include <fstream>
+#include <memory>
+#include <utility>
+
+using namespace icinga;
+
+#define MAX_EVENTS_DEFAULT 5000
+
+using Prio = RedisConnection::QueryPriority;
+
+String IcingaDB::m_EnvironmentId;
+std::mutex IcingaDB::m_EnvironmentIdInitMutex;
+
+REGISTER_TYPE(IcingaDB);
+
+IcingaDB::IcingaDB()
+ : m_Rcon(nullptr)
+{
+ m_RconLocked.store(nullptr);
+
+ m_WorkQueue.SetName("IcingaDB");
+
+ m_PrefixConfigObject = "icinga:";
+ m_PrefixConfigCheckSum = "icinga:checksum:";
+}
+
+void IcingaDB::Validate(int types, const ValidationUtils& utils)
+{
+ ObjectImpl<IcingaDB>::Validate(types, utils);
+
+ if (!(types & FAConfig))
+ return;
+
+ if (GetEnableTls() && GetCertPath().IsEmpty() != GetKeyPath().IsEmpty()) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, std::vector<String>(), "Validation failed: Either both a client certificate (cert_path) and its private key (key_path) or none of them must be given."));
+ }
+
+ try {
+ InitEnvironmentId();
+ } catch (const std::exception& e) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, std::vector<String>(),
+ String("Validation failed: ") + e.what()));
+ }
+}
+
+/**
+ * Starts the component.
+ */
+void IcingaDB::Start(bool runtimeCreated)
+{
+ ObjectImpl<IcingaDB>::Start(runtimeCreated);
+
+ VERIFY(!m_EnvironmentId.IsEmpty());
+ PersistEnvironmentId();
+
+ Log(LogInformation, "IcingaDB")
+ << "'" << GetName() << "' started.";
+
+ m_ConfigDumpInProgress = false;
+ m_ConfigDumpDone = false;
+
+ m_WorkQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); });
+
+ m_Rcon = new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(),
+ GetEnableTls(), GetInsecureNoverify(), GetCertPath(), GetKeyPath(), GetCaPath(), GetCrlPath(),
+ GetTlsProtocolmin(), GetCipherList(), GetConnectTimeout(), GetDebugInfo());
+ m_RconLocked.store(m_Rcon);
+
+ for (const Type::Ptr& type : GetTypes()) {
+ auto ctype (dynamic_cast<ConfigType*>(type.get()));
+ if (!ctype)
+ continue;
+
+ RedisConnection::Ptr con = new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(),
+ GetEnableTls(), GetInsecureNoverify(), GetCertPath(), GetKeyPath(), GetCaPath(), GetCrlPath(),
+ GetTlsProtocolmin(), GetCipherList(), GetConnectTimeout(), GetDebugInfo(), m_Rcon);
+
+ con->SetConnectedCallback([this, con](boost::asio::yield_context& yc) {
+ con->SetConnectedCallback(nullptr);
+
+ size_t pending = --m_PendingRcons;
+ Log(LogDebug, "IcingaDB") << pending << " pending child connections remaining";
+ if (pending == 0) {
+ m_WorkQueue.Enqueue([this]() { OnConnectedHandler(); });
+ }
+ });
+
+ m_Rcons[ctype] = std::move(con);
+ }
+
+ m_PendingRcons = m_Rcons.size();
+
+ m_Rcon->SetConnectedCallback([this](boost::asio::yield_context& yc) {
+ m_Rcon->SetConnectedCallback(nullptr);
+
+ for (auto& kv : m_Rcons) {
+ kv.second->Start();
+ }
+ });
+ m_Rcon->Start();
+
+ m_StatsTimer = Timer::Create();
+ m_StatsTimer->SetInterval(1);
+ m_StatsTimer->OnTimerExpired.connect([this](const Timer * const&) { PublishStatsTimerHandler(); });
+ m_StatsTimer->Start();
+
+ m_WorkQueue.SetName("IcingaDB");
+
+ m_Rcon->SuppressQueryKind(Prio::CheckResult);
+ m_Rcon->SuppressQueryKind(Prio::RuntimeStateSync);
+
+ Ptr keepAlive (this);
+
+ m_HistoryThread = std::async(std::launch::async, [this, keepAlive]() { ForwardHistoryEntries(); });
+}
+
+void IcingaDB::ExceptionHandler(boost::exception_ptr exp)
+{
+ Log(LogCritical, "IcingaDB", "Exception during redis query. Verify that Redis is operational.");
+
+ Log(LogDebug, "IcingaDB")
+ << "Exception during redis operation: " << DiagnosticInformation(exp);
+}
+
+void IcingaDB::OnConnectedHandler()
+{
+ AssertOnWorkQueue();
+
+ if (m_ConfigDumpInProgress || m_ConfigDumpDone)
+ return;
+
+ /* Config dump */
+ m_ConfigDumpInProgress = true;
+ PublishStats();
+
+ UpdateAllConfigObjects();
+
+ m_ConfigDumpDone = true;
+
+ m_ConfigDumpInProgress = false;
+}
+
+void IcingaDB::PublishStatsTimerHandler(void)
+{
+ PublishStats();
+}
+
+void IcingaDB::PublishStats()
+{
+ if (!m_Rcon || !m_Rcon->IsConnected())
+ return;
+
+ Dictionary::Ptr status = GetStats();
+ status->Set("config_dump_in_progress", m_ConfigDumpInProgress);
+ status->Set("timestamp", TimestampToMilliseconds(Utility::GetTime()));
+ status->Set("icingadb_environment", m_EnvironmentId);
+
+ std::vector<String> query {"XADD", "icinga:stats", "MAXLEN", "1", "*"};
+
+ {
+ ObjectLock statusLock (status);
+ for (auto& kv : status) {
+ query.emplace_back(kv.first);
+ query.emplace_back(JsonEncode(kv.second));
+ }
+ }
+
+ m_Rcon->FireAndForgetQuery(std::move(query), Prio::Heartbeat);
+}
+
+void IcingaDB::Stop(bool runtimeRemoved)
+{
+ Log(LogInformation, "IcingaDB")
+ << "Flushing history data buffer to Redis.";
+
+ if (m_HistoryThread.wait_for(std::chrono::minutes(1)) == std::future_status::timeout) {
+ Log(LogCritical, "IcingaDB")
+ << "Flushing takes more than one minute (while we're about to shut down). Giving up and discarding "
+ << m_HistoryBulker.Size() << " queued history queries.";
+ }
+
+ m_StatsTimer->Stop(true);
+
+ Log(LogInformation, "IcingaDB")
+ << "'" << GetName() << "' stopped.";
+
+ ObjectImpl<IcingaDB>::Stop(runtimeRemoved);
+}
+
+void IcingaDB::ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<IcingaDB>::ValidateTlsProtocolmin(lvalue, utils);
+
+ try {
+ ResolveTlsProtocolVersion(lvalue());
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "tls_protocolmin" }, ex.what()));
+ }
+}
+
+void IcingaDB::ValidateConnectTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<IcingaDB>::ValidateConnectTimeout(lvalue, utils);
+
+ if (lvalue() <= 0) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "connect_timeout" }, "Value must be greater than 0."));
+ }
+}
+
+void IcingaDB::AssertOnWorkQueue()
+{
+ ASSERT(m_WorkQueue.IsWorkerThread());
+}
+
+void IcingaDB::DumpedGlobals::Reset()
+{
+ std::lock_guard<std::mutex> l (m_Mutex);
+ m_Ids.clear();
+}
+
+String IcingaDB::GetEnvironmentId() const {
+ return m_EnvironmentId;
+}
+
+bool IcingaDB::DumpedGlobals::IsNew(const String& id)
+{
+ std::lock_guard<std::mutex> l (m_Mutex);
+ return m_Ids.emplace(id).second;
+}
+
+/**
+ * Initializes the m_EnvironmentId attribute or throws an exception on failure to do so. Can be called concurrently.
+ */
+void IcingaDB::InitEnvironmentId()
+{
+ // Initialize m_EnvironmentId once across all IcingaDB objects. In theory, this could be done using
+ // std::call_once, however, due to a bug in libstdc++ (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66146),
+ // this can result in a deadlock when an exception is thrown (which is explicitly allowed by the standard).
+ std::unique_lock<std::mutex> lock (m_EnvironmentIdInitMutex);
+
+ if (m_EnvironmentId.IsEmpty()) {
+ String path = Configuration::DataDir + "/icingadb.env";
+ String envId;
+
+ if (Utility::PathExists(path)) {
+ envId = Utility::LoadJsonFile(path);
+
+ if (envId.GetLength() != 2*SHA_DIGEST_LENGTH) {
+ throw std::runtime_error("environment ID stored at " + path + " is corrupt: wrong length.");
+ }
+
+ for (unsigned char c : envId) {
+ if (!std::isxdigit(c)) {
+ throw std::runtime_error("environment ID stored at " + path + " is corrupt: invalid hex string.");
+ }
+ }
+ } else {
+ String caPath = ApiListener::GetDefaultCaPath();
+
+ if (!Utility::PathExists(caPath)) {
+ throw std::runtime_error("Cannot find the CA certificate at '" + caPath + "'. "
+ "Please ensure the ApiListener is enabled first using 'icinga2 api setup'.");
+ }
+
+ std::shared_ptr<X509> cert = GetX509Certificate(caPath);
+
+ unsigned int n;
+ unsigned char digest[EVP_MAX_MD_SIZE];
+ if (X509_pubkey_digest(cert.get(), EVP_sha1(), digest, &n) != 1) {
+ BOOST_THROW_EXCEPTION(openssl_error()
+ << boost::errinfo_api_function("X509_pubkey_digest")
+ << errinfo_openssl_error(ERR_peek_error()));
+ }
+
+ envId = BinaryToHex(digest, n);
+ }
+
+ m_EnvironmentId = envId.ToLower();
+ }
+}
+
+/**
+ * Ensures that the environment ID is persisted on disk or throws an exception on failure to do so.
+ * Can be called concurrently.
+ */
+void IcingaDB::PersistEnvironmentId()
+{
+ String path = Configuration::DataDir + "/icingadb.env";
+
+ std::unique_lock<std::mutex> lock (m_EnvironmentIdInitMutex);
+
+ if (!Utility::PathExists(path)) {
+ Utility::SaveJsonFile(path, 0600, m_EnvironmentId);
+ }
+}
diff --git a/lib/icingadb/icingadb.hpp b/lib/icingadb/icingadb.hpp
new file mode 100644
index 0000000..6652d9c
--- /dev/null
+++ b/lib/icingadb/icingadb.hpp
@@ -0,0 +1,241 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ICINGADB_H
+#define ICINGADB_H
+
+#include "icingadb/icingadb-ti.hpp"
+#include "icingadb/redisconnection.hpp"
+#include "base/atomic.hpp"
+#include "base/bulker.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include "icinga/customvarobject.hpp"
+#include "icinga/checkable.hpp"
+#include "icinga/service.hpp"
+#include "icinga/downtime.hpp"
+#include "remote/messageorigin.hpp"
+#include <atomic>
+#include <chrono>
+#include <future>
+#include <memory>
+#include <mutex>
+#include <set>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+
+namespace icinga
+{
+
+/**
+ * @ingroup icingadb
+ */
+class IcingaDB : public ObjectImpl<IcingaDB>
+{
+public:
+ DECLARE_OBJECT(IcingaDB);
+ DECLARE_OBJECTNAME(IcingaDB);
+
+ IcingaDB();
+
+ static void ConfigStaticInitialize();
+
+ void Validate(int types, const ValidationUtils& utils) override;
+ virtual void Start(bool runtimeCreated) override;
+ virtual void Stop(bool runtimeRemoved) override;
+
+ String GetEnvironmentId() const override;
+
+ inline RedisConnection::Ptr GetConnection()
+ {
+ return m_RconLocked.load();
+ }
+
+ template<class T>
+ static void AddKvsToMap(const Array::Ptr& kvs, T& map)
+ {
+ Value* key = nullptr;
+ ObjectLock oLock (kvs);
+
+ for (auto& kv : kvs) {
+ if (key) {
+ map.emplace(std::move(*key), std::move(kv));
+ key = nullptr;
+ } else {
+ key = &kv;
+ }
+ }
+ }
+
+protected:
+ void ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+ void ValidateConnectTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ class DumpedGlobals
+ {
+ public:
+ void Reset();
+ bool IsNew(const String& id);
+
+ private:
+ std::set<String> m_Ids;
+ std::mutex m_Mutex;
+ };
+
+ enum StateUpdate
+ {
+ Volatile = 1ull << 0,
+ RuntimeOnly = 1ull << 1,
+ Full = Volatile | RuntimeOnly,
+ };
+
+ void OnConnectedHandler();
+
+ void PublishStatsTimerHandler();
+ void PublishStats();
+
+ /* config & status dump */
+ void UpdateAllConfigObjects();
+ std::vector<std::vector<intrusive_ptr<ConfigObject>>> ChunkObjects(std::vector<intrusive_ptr<ConfigObject>> objects, size_t chunkSize);
+ void DeleteKeys(const RedisConnection::Ptr& conn, const std::vector<String>& keys, RedisConnection::QueryPriority priority);
+ std::vector<String> GetTypeOverwriteKeys(const String& type);
+ std::vector<String> GetTypeDumpSignalKeys(const Type::Ptr& type);
+ void InsertObjectDependencies(const ConfigObject::Ptr& object, const String typeName, std::map<String, std::vector<String>>& hMSets,
+ std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate);
+ void UpdateState(const Checkable::Ptr& checkable, StateUpdate mode);
+ void SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpdate);
+ void CreateConfigUpdate(const ConfigObject::Ptr& object, const String type, std::map<String, std::vector<String>>& hMSets,
+ std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate);
+ void SendConfigDelete(const ConfigObject::Ptr& object);
+ void SendStateChange(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type);
+ void AddObjectDataToRuntimeUpdates(std::vector<Dictionary::Ptr>& runtimeUpdates, const String& objectKey,
+ const String& redisKey, const Dictionary::Ptr& data);
+ void DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum = false);
+
+ void SendSentNotification(
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text, double sendTime
+ );
+
+ void SendStartedDowntime(const Downtime::Ptr& downtime);
+ void SendRemovedDowntime(const Downtime::Ptr& downtime);
+ void SendAddedComment(const Comment::Ptr& comment);
+ void SendRemovedComment(const Comment::Ptr& comment);
+ void SendFlappingChange(const Checkable::Ptr& checkable, double changeTime, double flappingLastChange);
+ void SendNextUpdate(const Checkable::Ptr& checkable);
+ void SendAcknowledgementSet(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry);
+ void SendAcknowledgementCleared(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, double ackLastChange);
+ void SendNotificationUsersChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ void SendNotificationUserGroupsChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ void SendTimePeriodRangesChanged(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ void SendTimePeriodIncludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ void SendTimePeriodExcludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ template<class T>
+ void SendGroupsChanged(const ConfigObject::Ptr& command, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ void SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ void SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ void SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+
+ void ForwardHistoryEntries();
+
+ std::vector<String> UpdateObjectAttrs(const ConfigObject::Ptr& object, int fieldType, const String& typeNameOverride);
+ Dictionary::Ptr SerializeState(const Checkable::Ptr& checkable);
+
+ /* Stats */
+ Dictionary::Ptr GetStats();
+
+ /* utilities */
+ static String FormatCheckSumBinary(const String& str);
+ static String FormatCommandLine(const Value& commandLine);
+ static long long TimestampToMilliseconds(double timestamp);
+ static String IcingaToStreamValue(const Value& value);
+ static std::vector<Value> GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew);
+ static std::vector<String> GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew);
+
+ static String GetObjectIdentifier(const ConfigObject::Ptr& object);
+ static String CalcEventID(const char* eventType, const ConfigObject::Ptr& object, double eventTime = 0, NotificationType nt = NotificationType(0));
+ static const char* GetNotificationTypeByEnum(NotificationType type);
+ static Dictionary::Ptr SerializeVars(const Dictionary::Ptr& vars);
+
+ static String HashValue(const Value& value);
+ static String HashValue(const Value& value, const std::set<String>& propertiesBlacklist, bool propertiesWhitelist = false);
+
+ static String GetLowerCaseTypeNameDB(const ConfigObject::Ptr& obj);
+ static bool PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& attributes, Dictionary::Ptr& checkSums);
+
+ static void ReachabilityChangeHandler(const std::set<Checkable::Ptr>& children);
+ static void StateChangeHandler(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type);
+ static void VersionChangedHandler(const ConfigObject::Ptr& object);
+ static void DowntimeStartedHandler(const Downtime::Ptr& downtime);
+ static void DowntimeRemovedHandler(const Downtime::Ptr& downtime);
+
+ static void NotificationSentToAllUsersHandler(
+ const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
+ NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text
+ );
+
+ static void CommentAddedHandler(const Comment::Ptr& comment);
+ static void CommentRemovedHandler(const Comment::Ptr& comment);
+ static void FlappingChangeHandler(const Checkable::Ptr& checkable, double changeTime);
+ static void NewCheckResultHandler(const Checkable::Ptr& checkable);
+ static void NextCheckUpdatedHandler(const Checkable::Ptr& checkable);
+ static void HostProblemChangedHandler(const Service::Ptr& service);
+ static void AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry);
+ static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime);
+ static void NotificationUsersChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void NotificationUserGroupsChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void TimePeriodRangesChangedHandler(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ static void TimePeriodIncludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void TimePeriodExcludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void UserGroupsChangedHandler(const User::Ptr& user, const Array::Ptr&, const Array::Ptr& newValues);
+ static void HostGroupsChangedHandler(const Host::Ptr& host, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void ServiceGroupsChangedHandler(const Service::Ptr& service, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+ static void CommandEnvChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ static void CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+ static void CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+
+ void AssertOnWorkQueue();
+
+ void ExceptionHandler(boost::exception_ptr exp);
+
+ static std::vector<Type::Ptr> GetTypes();
+
+ static void InitEnvironmentId();
+ static void PersistEnvironmentId();
+
+ Timer::Ptr m_StatsTimer;
+ WorkQueue m_WorkQueue{0, 1, LogNotice};
+
+ std::future<void> m_HistoryThread;
+ Bulker<RedisConnection::Query> m_HistoryBulker {4096, std::chrono::milliseconds(250)};
+
+ String m_PrefixConfigObject;
+ String m_PrefixConfigCheckSum;
+
+ bool m_ConfigDumpInProgress;
+ bool m_ConfigDumpDone;
+
+ RedisConnection::Ptr m_Rcon;
+ // m_RconLocked containes a copy of the value in m_Rcon where all accesses are guarded by a mutex to allow safe
+ // concurrent access like from the icingadb check command. It's a copy to still allow fast access without additional
+ // syncronization to m_Rcon within the IcingaDB feature itself.
+ Locked<RedisConnection::Ptr> m_RconLocked;
+ std::unordered_map<ConfigType*, RedisConnection::Ptr> m_Rcons;
+ std::atomic_size_t m_PendingRcons;
+
+ struct {
+ DumpedGlobals CustomVar, ActionUrl, NotesUrl, IconImage;
+ } m_DumpedGlobals;
+
+ // m_EnvironmentId is shared across all IcingaDB objects (typically there is at most one, but it is perfectly fine
+ // to have multiple ones). It is initialized once (synchronized using m_EnvironmentIdInitMutex). After successful
+ // initialization, the value is read-only and can be accessed without further synchronization.
+ static String m_EnvironmentId;
+ static std::mutex m_EnvironmentIdInitMutex;
+
+ static std::unordered_set<Type*> m_IndexedTypes;
+};
+}
+
+#endif /* ICINGADB_H */
diff --git a/lib/icingadb/icingadb.ti b/lib/icingadb/icingadb.ti
new file mode 100644
index 0000000..1c649c8
--- /dev/null
+++ b/lib/icingadb/icingadb.ti
@@ -0,0 +1,63 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/tlsutility.hpp"
+
+library icingadb;
+
+namespace icinga
+{
+
+class IcingaDB : ConfigObject
+{
+ activation_priority 100;
+
+ [config] String host {
+ default {{{ return "127.0.0.1"; }}}
+ };
+ [config] int port {
+ default {{{ return 6380; }}}
+ };
+ [config] String path;
+ [config, no_user_view, no_user_modify] String password;
+ [config] int db_index;
+
+ [config] bool enable_tls {
+ default {{{ return false; }}}
+ };
+
+ [config] bool insecure_noverify {
+ default {{{ return false; }}}
+ };
+
+ [config] String cert_path;
+ [config] String key_path;
+ [config] String ca_path;
+ [config] String crl_path;
+ [config] String cipher_list {
+ default {{{ return DEFAULT_TLS_CIPHERS; }}}
+ };
+ [config] String tls_protocolmin {
+ default {{{ return DEFAULT_TLS_PROTOCOLMIN; }}}
+ };
+
+ [config] double connect_timeout {
+ default {{{ return DEFAULT_CONNECT_TIMEOUT; }}}
+ };
+
+ [no_storage] String environment_id {
+ get;
+ };
+
+ [set_protected] double ongoing_dump_start {
+ default {{{ return 0; }}}
+ };
+ [state, set_protected] double lastdump_end {
+ default {{{ return 0; }}}
+ };
+ [state, set_protected] double lastdump_took {
+ default {{{ return 0; }}}
+ };
+};
+
+}
diff --git a/lib/icingadb/icingadbchecktask.cpp b/lib/icingadb/icingadbchecktask.cpp
new file mode 100644
index 0000000..f7c5964
--- /dev/null
+++ b/lib/icingadb/icingadbchecktask.cpp
@@ -0,0 +1,513 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/icingadbchecktask.hpp"
+#include "icinga/host.hpp"
+#include "icinga/checkcommand.hpp"
+#include "icinga/macroprocessor.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/function.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/convert.hpp"
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, IcingadbCheck, &IcingadbCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+static void ReportIcingadbCheck(
+ const Checkable::Ptr& checkable, const CheckCommand::Ptr& commandObj,
+ const CheckResult::Ptr& cr, String output, ServiceState state)
+{
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ double now = Utility::GetTime();
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = std::move(output);
+ pr.ExecutionStart = now;
+ pr.ExecutionEnd = now;
+ pr.ExitStatus = state;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(commandObj->GetName(), pr);
+ } else {
+ cr->SetState(state);
+ cr->SetOutput(output);
+ checkable->ProcessCheckResult(cr);
+ }
+}
+
+static inline
+double GetXMessageTs(const Array::Ptr& xMessage)
+{
+ return Convert::ToLong(String(xMessage->Get(0)).Split("-")[0]) / 1000.0;
+}
+
+void IcingadbCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ CheckCommand::Ptr commandObj = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+ String silenceMissingMacroWarning;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", commandObj);
+
+ auto resolve ([&](const String& macro) {
+ return MacroProcessor::ResolveMacros(macro, resolvers, checkable->GetLastCheckResult(),
+ &silenceMissingMacroWarning, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros);
+ });
+
+ struct Thresholds
+ {
+ Value Warning, Critical;
+ };
+
+ auto resolveThresholds ([&resolve](const String& wmacro, const String& cmacro) {
+ return Thresholds{resolve(wmacro), resolve(cmacro)};
+ });
+
+ String icingadbName = resolve("$icingadb_name$");
+
+ auto dumpTakesThresholds (resolveThresholds("$icingadb_full_dump_duration_warning$", "$icingadb_full_dump_duration_critical$"));
+ auto syncTakesThresholds (resolveThresholds("$icingadb_full_sync_duration_warning$", "$icingadb_full_sync_duration_critical$"));
+ auto icingaBacklogThresholds (resolveThresholds("$icingadb_redis_backlog_warning$", "$icingadb_redis_backlog_critical$"));
+ auto icingadbBacklogThresholds (resolveThresholds("$icingadb_database_backlog_warning$", "$icingadb_database_backlog_critical$"));
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ if (icingadbName.IsEmpty()) {
+ ReportIcingadbCheck(checkable, commandObj, cr, "Icinga DB UNKNOWN: Attribute 'icingadb_name' must be set.", ServiceUnknown);
+ return;
+ }
+
+ auto conn (IcingaDB::GetByName(icingadbName));
+
+ if (!conn) {
+ ReportIcingadbCheck(checkable, commandObj, cr, "Icinga DB UNKNOWN: Icinga DB connection '" + icingadbName + "' does not exist.", ServiceUnknown);
+ return;
+ }
+
+ auto redis (conn->GetConnection());
+
+ if (!redis || !redis->GetConnected()) {
+ ReportIcingadbCheck(checkable, commandObj, cr, "Icinga DB CRITICAL: Not connected to Redis.", ServiceCritical);
+ return;
+ }
+
+ auto now (Utility::GetTime());
+ Array::Ptr redisTime, xReadHeartbeat, xReadStats, xReadRuntimeBacklog, xReadHistoryBacklog;
+
+ try {
+ auto replies (redis->GetResultsOfQueries(
+ {
+ {"TIME"},
+ {"XREAD", "STREAMS", "icingadb:telemetry:heartbeat", "0-0"},
+ {"XREAD", "STREAMS", "icingadb:telemetry:stats", "0-0"},
+ {"XREAD", "COUNT", "1", "STREAMS", "icinga:runtime", "icinga:runtime:state", "0-0", "0-0"},
+ {
+ "XREAD", "COUNT", "1", "STREAMS",
+ "icinga:history:stream:acknowledgement",
+ "icinga:history:stream:comment",
+ "icinga:history:stream:downtime",
+ "icinga:history:stream:flapping",
+ "icinga:history:stream:notification",
+ "icinga:history:stream:state",
+ "0-0", "0-0", "0-0", "0-0", "0-0", "0-0",
+ }
+ },
+ RedisConnection::QueryPriority::Heartbeat
+ ));
+
+ redisTime = std::move(replies.at(0));
+ xReadHeartbeat = std::move(replies.at(1));
+ xReadStats = std::move(replies.at(2));
+ xReadRuntimeBacklog = std::move(replies.at(3));
+ xReadHistoryBacklog = std::move(replies.at(4));
+ } catch (const std::exception& ex) {
+ ReportIcingadbCheck(
+ checkable, commandObj, cr,
+ String("Icinga DB CRITICAL: Could not query Redis: ") + ex.what(), ServiceCritical
+ );
+ return;
+ }
+
+ if (!xReadHeartbeat) {
+ ReportIcingadbCheck(
+ checkable, commandObj, cr,
+ "Icinga DB CRITICAL: The Icinga DB daemon seems to have never run. (Missing heartbeat)",
+ ServiceCritical
+ );
+
+ return;
+ }
+
+ auto redisOldestPending (redis->GetOldestPendingQueryTs());
+ auto ongoingDumpStart (conn->GetOngoingDumpStart());
+ auto dumpWhen (conn->GetLastdumpEnd());
+ auto dumpTook (conn->GetLastdumpTook());
+
+ auto redisNow (Convert::ToLong(redisTime->Get(0)) + Convert::ToLong(redisTime->Get(1)) / 1000000.0);
+ Array::Ptr heartbeatMessage = Array::Ptr(Array::Ptr(xReadHeartbeat->Get(0))->Get(1))->Get(0);
+ auto heartbeatTime (GetXMessageTs(heartbeatMessage));
+ std::map<String, String> heartbeatData;
+
+ IcingaDB::AddKvsToMap(heartbeatMessage->Get(1), heartbeatData);
+
+ String version = heartbeatData.at("version");
+ auto icingadbNow (Convert::ToLong(heartbeatData.at("time")) / 1000.0 + (redisNow - heartbeatTime));
+ auto icingadbStartTime (Convert::ToLong(heartbeatData.at("start-time")) / 1000.0);
+ String errMsg (heartbeatData.at("error"));
+ auto errSince (Convert::ToLong(heartbeatData.at("error-since")) / 1000.0);
+ String perfdataFromRedis = heartbeatData.at("performance-data");
+ auto heartbeatLastReceived (Convert::ToLong(heartbeatData.at("last-heartbeat-received")) / 1000.0);
+ bool weResponsible = Convert::ToLong(heartbeatData.at("ha-responsible"));
+ auto weResponsibleTs (Convert::ToLong(heartbeatData.at("ha-responsible-ts")) / 1000.0);
+ bool otherResponsible = Convert::ToLong(heartbeatData.at("ha-other-responsible"));
+ auto syncOngoingSince (Convert::ToLong(heartbeatData.at("sync-ongoing-since")) / 1000.0);
+ auto syncSuccessWhen (Convert::ToLong(heartbeatData.at("sync-success-finish")) / 1000.0);
+ auto syncSuccessTook (Convert::ToLong(heartbeatData.at("sync-success-duration")) / 1000.0);
+
+ std::ostringstream i2okmsgs, idbokmsgs, warnmsgs, critmsgs;
+ Array::Ptr perfdata = new Array();
+
+ i2okmsgs << std::fixed << std::setprecision(3);
+ idbokmsgs << std::fixed << std::setprecision(3);
+ warnmsgs << std::fixed << std::setprecision(3);
+ critmsgs << std::fixed << std::setprecision(3);
+
+ const auto downForCritical (10);
+ auto downFor (redisNow - heartbeatTime);
+ bool down = false;
+
+ if (downFor > downForCritical) {
+ down = true;
+
+ critmsgs << " Last seen " << Utility::FormatDuration(downFor)
+ << " ago, greater than CRITICAL threshold (" << Utility::FormatDuration(downForCritical) << ")!";
+ } else {
+ idbokmsgs << "\n* Last seen: " << Utility::FormatDuration(downFor) << " ago";
+ }
+
+ perfdata->Add(new PerfdataValue("icingadb_heartbeat_age", downFor, false, "seconds", Empty, downForCritical, 0));
+
+ const auto errForCritical (10);
+ auto err (!errMsg.IsEmpty());
+ auto errFor (icingadbNow - errSince);
+
+ if (err) {
+ if (errFor > errForCritical) {
+ critmsgs << " ERROR: " << errMsg << "!";
+ }
+
+ perfdata->Add(new PerfdataValue("error_for", errFor * (err ? 1 : -1), false, "seconds", Empty, errForCritical, 0));
+ }
+
+ if (!down) {
+ const auto heartbeatLagWarning (3/* Icinga DB read freq. */ + 1/* Icinga DB write freq. */ + 2/* threshold */);
+ auto heartbeatLag (fmin(icingadbNow - heartbeatLastReceived, 10 * 60));
+
+ if (!heartbeatLastReceived) {
+ critmsgs << " Lost Icinga 2 heartbeat!";
+ } else if (heartbeatLag > heartbeatLagWarning) {
+ warnmsgs << " Icinga 2 heartbeat lag: " << Utility::FormatDuration(heartbeatLag)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(heartbeatLagWarning) << ").";
+ }
+
+ perfdata->Add(new PerfdataValue("icinga2_heartbeat_age", heartbeatLag, false, "seconds", heartbeatLagWarning, Empty, 0));
+ }
+
+ if (weResponsible) {
+ idbokmsgs << "\n* Responsible";
+ } else if (otherResponsible) {
+ idbokmsgs << "\n* Not responsible, but another instance is";
+ } else {
+ critmsgs << " No instance is responsible!";
+ }
+
+ perfdata->Add(new PerfdataValue("icingadb_responsible_instances", int(weResponsible || otherResponsible), false, "", Empty, Empty, 0, 1));
+
+ const auto clockDriftWarning (5);
+ const auto clockDriftCritical (30);
+ auto clockDrift (std::max({
+ fabs(now - redisNow),
+ fabs(redisNow - icingadbNow),
+ fabs(icingadbNow - now),
+ }));
+
+ if (clockDrift > clockDriftCritical) {
+ critmsgs << " Icinga 2/Redis/Icinga DB clock drift: " << Utility::FormatDuration(clockDrift)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(clockDriftCritical) << ")!";
+ } else if (clockDrift > clockDriftWarning) {
+ warnmsgs << " Icinga 2/Redis/Icinga DB clock drift: " << Utility::FormatDuration(clockDrift)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(clockDriftWarning) << ").";
+ }
+
+ perfdata->Add(new PerfdataValue("clock_drift", clockDrift, false, "seconds", clockDriftWarning, clockDriftCritical, 0));
+
+ if (ongoingDumpStart) {
+ auto ongoingDumpTakes (now - ongoingDumpStart);
+
+ if (!dumpTakesThresholds.Critical.IsEmpty() && ongoingDumpTakes > dumpTakesThresholds.Critical) {
+ critmsgs << " Current Icinga 2 full dump already takes " << Utility::FormatDuration(ongoingDumpTakes)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(dumpTakesThresholds.Critical) << ")!";
+ } else if (!dumpTakesThresholds.Warning.IsEmpty() && ongoingDumpTakes > dumpTakesThresholds.Warning) {
+ warnmsgs << " Current Icinga 2 full dump already takes " << Utility::FormatDuration(ongoingDumpTakes)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(dumpTakesThresholds.Warning) << ").";
+ } else {
+ i2okmsgs << "\n* Current full dump running for " << Utility::FormatDuration(ongoingDumpTakes);
+ }
+
+ perfdata->Add(new PerfdataValue("icinga2_current_full_dump_duration", ongoingDumpTakes, false, "seconds",
+ dumpTakesThresholds.Warning, dumpTakesThresholds.Critical, 0));
+ }
+
+ if (!down && syncOngoingSince) {
+ auto ongoingSyncTakes (icingadbNow - syncOngoingSince);
+
+ if (!syncTakesThresholds.Critical.IsEmpty() && ongoingSyncTakes > syncTakesThresholds.Critical) {
+ critmsgs << " Current full sync already takes " << Utility::FormatDuration(ongoingSyncTakes)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(syncTakesThresholds.Critical) << ")!";
+ } else if (!syncTakesThresholds.Warning.IsEmpty() && ongoingSyncTakes > syncTakesThresholds.Warning) {
+ warnmsgs << " Current full sync already takes " << Utility::FormatDuration(ongoingSyncTakes)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(syncTakesThresholds.Warning) << ").";
+ } else {
+ idbokmsgs << "\n* Current full sync running for " << Utility::FormatDuration(ongoingSyncTakes);
+ }
+
+ perfdata->Add(new PerfdataValue("icingadb_current_full_sync_duration", ongoingSyncTakes, false, "seconds",
+ syncTakesThresholds.Warning, syncTakesThresholds.Critical, 0));
+ }
+
+ auto redisBacklog (now - redisOldestPending);
+
+ if (!redisOldestPending) {
+ redisBacklog = 0;
+ }
+
+ if (!icingaBacklogThresholds.Critical.IsEmpty() && redisBacklog > icingaBacklogThresholds.Critical) {
+ critmsgs << " Icinga 2 Redis query backlog: " << Utility::FormatDuration(redisBacklog)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(icingaBacklogThresholds.Critical) << ")!";
+ } else if (!icingaBacklogThresholds.Warning.IsEmpty() && redisBacklog > icingaBacklogThresholds.Warning) {
+ warnmsgs << " Icinga 2 Redis query backlog: " << Utility::FormatDuration(redisBacklog)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(icingaBacklogThresholds.Warning) << ").";
+ }
+
+ perfdata->Add(new PerfdataValue("icinga2_redis_query_backlog", redisBacklog, false, "seconds",
+ icingaBacklogThresholds.Warning, icingaBacklogThresholds.Critical, 0));
+
+ if (!down) {
+ auto getBacklog = [redisNow](const Array::Ptr& streams) -> double {
+ if (!streams) {
+ return 0;
+ }
+
+ double minTs = 0;
+ ObjectLock lock (streams);
+
+ for (Array::Ptr stream : streams) {
+ auto ts (GetXMessageTs(Array::Ptr(stream->Get(1))->Get(0)));
+
+ if (minTs == 0 || ts < minTs) {
+ minTs = ts;
+ }
+ }
+
+ if (minTs > 0) {
+ return redisNow - minTs;
+ } else {
+ return 0;
+ }
+ };
+
+ double historyBacklog = getBacklog(xReadHistoryBacklog);
+
+ if (!icingadbBacklogThresholds.Critical.IsEmpty() && historyBacklog > icingadbBacklogThresholds.Critical) {
+ critmsgs << " History backlog: " << Utility::FormatDuration(historyBacklog)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(icingadbBacklogThresholds.Critical) << ")!";
+ } else if (!icingadbBacklogThresholds.Warning.IsEmpty() && historyBacklog > icingadbBacklogThresholds.Warning) {
+ warnmsgs << " History backlog: " << Utility::FormatDuration(historyBacklog)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(icingadbBacklogThresholds.Warning) << ").";
+ }
+
+ perfdata->Add(new PerfdataValue("icingadb_history_backlog", historyBacklog, false, "seconds",
+ icingadbBacklogThresholds.Warning, icingadbBacklogThresholds.Critical, 0));
+
+ double runtimeBacklog = 0;
+
+ if (weResponsible && !syncOngoingSince) {
+ // These streams are only processed by the responsible instance after the full sync finished,
+ // it's fine for some backlog to exist otherwise.
+ runtimeBacklog = getBacklog(xReadRuntimeBacklog);
+
+ if (!icingadbBacklogThresholds.Critical.IsEmpty() && runtimeBacklog > icingadbBacklogThresholds.Critical) {
+ critmsgs << " Runtime update backlog: " << Utility::FormatDuration(runtimeBacklog)
+ << ", greater than CRITICAL threshold (" << Utility::FormatDuration(icingadbBacklogThresholds.Critical) << ")!";
+ } else if (!icingadbBacklogThresholds.Warning.IsEmpty() && runtimeBacklog > icingadbBacklogThresholds.Warning) {
+ warnmsgs << " Runtime update backlog: " << Utility::FormatDuration(runtimeBacklog)
+ << ", greater than WARNING threshold (" << Utility::FormatDuration(icingadbBacklogThresholds.Warning) << ").";
+ }
+ }
+
+ // Also report the perfdata value on the standby instance or during a full sync (as 0 in this case).
+ perfdata->Add(new PerfdataValue("icingadb_runtime_update_backlog", runtimeBacklog, false, "seconds",
+ icingadbBacklogThresholds.Warning, icingadbBacklogThresholds.Critical, 0));
+ }
+
+ auto dumpAgo (now - dumpWhen);
+
+ if (dumpWhen) {
+ perfdata->Add(new PerfdataValue("icinga2_last_full_dump_ago", dumpAgo, false, "seconds", Empty, Empty, 0));
+ }
+
+ if (dumpTook) {
+ perfdata->Add(new PerfdataValue("icinga2_last_full_dump_duration", dumpTook, false, "seconds", Empty, Empty, 0));
+ }
+
+ if (dumpWhen && dumpTook) {
+ i2okmsgs << "\n* Last full dump: " << Utility::FormatDuration(dumpAgo)
+ << " ago, took " << Utility::FormatDuration(dumpTook);
+ }
+
+ auto icingadbUptime (icingadbNow - icingadbStartTime);
+
+ if (!down) {
+ perfdata->Add(new PerfdataValue("icingadb_uptime", icingadbUptime, false, "seconds", Empty, Empty, 0));
+ }
+
+ {
+ Array::Ptr values = PluginUtility::SplitPerfdata(perfdataFromRedis);
+ ObjectLock lock (values);
+
+ for (auto& v : values) {
+ perfdata->Add(PerfdataValue::Parse(v));
+ }
+ }
+
+ if (weResponsibleTs) {
+ perfdata->Add(new PerfdataValue("icingadb_responsible_for",
+ (weResponsible ? 1 : -1) * (icingadbNow - weResponsibleTs), false, "seconds"));
+ }
+
+ auto syncAgo (icingadbNow - syncSuccessWhen);
+
+ if (syncSuccessWhen) {
+ perfdata->Add(new PerfdataValue("icingadb_last_full_sync_ago", syncAgo, false, "seconds", Empty, Empty, 0));
+ }
+
+ if (syncSuccessTook) {
+ perfdata->Add(new PerfdataValue("icingadb_last_full_sync_duration", syncSuccessTook, false, "seconds", Empty, Empty, 0));
+ }
+
+ if (syncSuccessWhen && syncSuccessTook) {
+ idbokmsgs << "\n* Last full sync: " << Utility::FormatDuration(syncAgo)
+ << " ago, took " << Utility::FormatDuration(syncSuccessTook);
+ }
+
+ std::map<String, RingBuffer> statsPerOp;
+
+ const char * const icingadbKnownStats[] = {
+ "config_sync", "state_sync", "history_sync", "overdue_sync", "history_cleanup"
+ };
+
+ for (auto metric : icingadbKnownStats) {
+ statsPerOp.emplace(std::piecewise_construct, std::forward_as_tuple(metric), std::forward_as_tuple(15 * 60));
+ }
+
+ if (xReadStats) {
+ Array::Ptr messages = Array::Ptr(xReadStats->Get(0))->Get(1);
+ ObjectLock lock (messages);
+
+ for (Array::Ptr message : messages) {
+ auto ts (GetXMessageTs(message));
+ std::map<String, String> opsPerSec;
+
+ IcingaDB::AddKvsToMap(message->Get(1), opsPerSec);
+
+ for (auto& kv : opsPerSec) {
+ auto buf (statsPerOp.find(kv.first));
+
+ if (buf == statsPerOp.end()) {
+ buf = statsPerOp.emplace(
+ std::piecewise_construct,
+ std::forward_as_tuple(kv.first), std::forward_as_tuple(15 * 60)
+ ).first;
+ }
+
+ buf->second.InsertValue(ts, Convert::ToLong(kv.second));
+ }
+ }
+ }
+
+ for (auto& kv : statsPerOp) {
+ perfdata->Add(new PerfdataValue("icingadb_" + kv.first + "_items_1min", kv.second.UpdateAndGetValues(now, 60), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue("icingadb_" + kv.first + "_items_5mins", kv.second.UpdateAndGetValues(now, 5 * 60), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue("icingadb_" + kv.first + "_items_15mins", kv.second.UpdateAndGetValues(now, 15 * 60), false, "", Empty, Empty, 0));
+ }
+
+ perfdata->Add(new PerfdataValue("icinga2_redis_queries_1min", redis->GetQueryCount(60), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue("icinga2_redis_queries_5mins", redis->GetQueryCount(5 * 60), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue("icinga2_redis_queries_15mins", redis->GetQueryCount(15 * 60), false, "", Empty, Empty, 0));
+
+ perfdata->Add(new PerfdataValue("icinga2_redis_pending_queries", redis->GetPendingQueryCount(), false, "", Empty, Empty, 0));
+
+ struct {
+ const char * Name;
+ int (RedisConnection::* Getter)(RingBuffer::SizeType span, RingBuffer::SizeType tv);
+ } const icingaWriteSubjects[] = {
+ {"config_dump", &RedisConnection::GetWrittenConfigFor},
+ {"state_dump", &RedisConnection::GetWrittenStateFor},
+ {"history_dump", &RedisConnection::GetWrittenHistoryFor}
+ };
+
+ for (auto subject : icingaWriteSubjects) {
+ perfdata->Add(new PerfdataValue(String("icinga2_") + subject.Name + "_items_1min", (redis.get()->*subject.Getter)(60, now), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue(String("icinga2_") + subject.Name + "_items_5mins", (redis.get()->*subject.Getter)(5 * 60, now), false, "", Empty, Empty, 0));
+ perfdata->Add(new PerfdataValue(String("icinga2_") + subject.Name + "_items_15mins", (redis.get()->*subject.Getter)(15 * 60, now), false, "", Empty, Empty, 0));
+ }
+
+ ServiceState state;
+ std::ostringstream msgbuf;
+ auto i2okmsg (i2okmsgs.str());
+ auto idbokmsg (idbokmsgs.str());
+ auto warnmsg (warnmsgs.str());
+ auto critmsg (critmsgs.str());
+
+ msgbuf << "Icinga DB ";
+
+ if (!critmsg.empty()) {
+ state = ServiceCritical;
+ msgbuf << "CRITICAL:" << critmsg;
+
+ if (!warnmsg.empty()) {
+ msgbuf << "\n\nWARNING:" << warnmsg;
+ }
+ } else if (!warnmsg.empty()) {
+ state = ServiceWarning;
+ msgbuf << "WARNING:" << warnmsg;
+ } else {
+ state = ServiceOK;
+ msgbuf << "OK: Uptime: " << Utility::FormatDuration(icingadbUptime) << ". Version: " << version << ".";
+ }
+
+ if (!i2okmsg.empty()) {
+ msgbuf << "\n\nIcinga 2:\n" << i2okmsg;
+ }
+
+ if (!idbokmsg.empty()) {
+ msgbuf << "\n\nIcinga DB:\n" << idbokmsg;
+ }
+
+ cr->SetPerformanceData(perfdata);
+ ReportIcingadbCheck(checkable, commandObj, cr, msgbuf.str(), state);
+}
diff --git a/lib/icingadb/icingadbchecktask.hpp b/lib/icingadb/icingadbchecktask.hpp
new file mode 100644
index 0000000..ba7d61b
--- /dev/null
+++ b/lib/icingadb/icingadbchecktask.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#ifndef ICINGADBCHECKTASK_H
+#define ICINGADBCHECKTASK_H
+
+#include "icingadb/icingadb.hpp"
+#include "icinga/checkable.hpp"
+
+namespace icinga
+{
+
+/**
+ * Icinga DB check.
+ *
+ * @ingroup icingadb
+ */
+class IcingadbCheckTask
+{
+public:
+ static void ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros);
+
+private:
+ IcingadbCheckTask();
+};
+
+}
+
+#endif /* ICINGADBCHECKTASK_H */
diff --git a/lib/icingadb/redisconnection.cpp b/lib/icingadb/redisconnection.cpp
new file mode 100644
index 0000000..798a827
--- /dev/null
+++ b/lib/icingadb/redisconnection.cpp
@@ -0,0 +1,773 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "icingadb/redisconnection.hpp"
+#include "base/array.hpp"
+#include "base/convert.hpp"
+#include "base/defer.hpp"
+#include "base/exception.hpp"
+#include "base/io-engine.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/string.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
+#include <boost/asio.hpp>
+#include <boost/coroutine/exceptions.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/utility/string_view.hpp>
+#include <boost/variant/get.hpp>
+#include <exception>
+#include <future>
+#include <iterator>
+#include <memory>
+#include <openssl/ssl.h>
+#include <openssl/x509_vfy.h>
+#include <utility>
+
+using namespace icinga;
+namespace asio = boost::asio;
+
+boost::regex RedisConnection::m_ErrAuth ("\\AERR AUTH ");
+
+RedisConnection::RedisConnection(const String& host, int port, const String& path, const String& password, int db,
+ bool useTls, bool insecure, const String& certPath, const String& keyPath, const String& caPath, const String& crlPath,
+ const String& tlsProtocolmin, const String& cipherList, double connectTimeout, DebugInfo di, const RedisConnection::Ptr& parent)
+ : RedisConnection(IoEngine::Get().GetIoContext(), host, port, path, password, db,
+ useTls, insecure, certPath, keyPath, caPath, crlPath, tlsProtocolmin, cipherList, connectTimeout, std::move(di), parent)
+{
+}
+
+RedisConnection::RedisConnection(boost::asio::io_context& io, String host, int port, String path, String password,
+ int db, bool useTls, bool insecure, String certPath, String keyPath, String caPath, String crlPath,
+ String tlsProtocolmin, String cipherList, double connectTimeout, DebugInfo di, const RedisConnection::Ptr& parent)
+ : m_Host(std::move(host)), m_Port(port), m_Path(std::move(path)), m_Password(std::move(password)),
+ m_DbIndex(db), m_CertPath(std::move(certPath)), m_KeyPath(std::move(keyPath)), m_Insecure(insecure),
+ m_CaPath(std::move(caPath)), m_CrlPath(std::move(crlPath)), m_TlsProtocolmin(std::move(tlsProtocolmin)),
+ m_CipherList(std::move(cipherList)), m_ConnectTimeout(connectTimeout), m_DebugInfo(std::move(di)), m_Connecting(false), m_Connected(false),
+ m_Started(false), m_Strand(io), m_QueuedWrites(io), m_QueuedReads(io), m_LogStatsTimer(io), m_Parent(parent)
+{
+ if (useTls && m_Path.IsEmpty()) {
+ UpdateTLSContext();
+ }
+}
+
+void RedisConnection::UpdateTLSContext()
+{
+ m_TLSContext = SetupSslContext(m_CertPath, m_KeyPath, m_CaPath,
+ m_CrlPath, m_CipherList, m_TlsProtocolmin, m_DebugInfo);
+}
+
+void RedisConnection::Start()
+{
+ if (!m_Started.exchange(true)) {
+ Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { ReadLoop(yc); });
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { WriteLoop(yc); });
+
+ if (!m_Parent) {
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { LogStats(yc); });
+ }
+ }
+
+ if (!m_Connecting.exchange(true)) {
+ Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { Connect(yc); });
+ }
+}
+
+bool RedisConnection::IsConnected() {
+ return m_Connected.load();
+}
+
+/**
+ * Append a Redis query to a log message
+ *
+ * @param query Redis query
+ * @param msg Log message
+ */
+static inline
+void LogQuery(RedisConnection::Query& query, Log& msg)
+{
+ int i = 0;
+
+ for (auto& arg : query) {
+ if (++i == 8) {
+ msg << " ...";
+ break;
+ }
+
+ if (arg.GetLength() > 64) {
+ msg << " '" << arg.SubStr(0, 61) << "...'";
+ } else {
+ msg << " '" << arg << '\'';
+ }
+ }
+}
+
+/**
+ * Queue a Redis query for sending
+ *
+ * @param query Redis query
+ * @param priority The query's priority
+ */
+void RedisConnection::FireAndForgetQuery(RedisConnection::Query query, RedisConnection::QueryPriority priority, QueryAffects affects)
+{
+ if (LogDebug >= Logger::GetMinLogSeverity()) {
+ Log msg (LogDebug, "IcingaDB", "Firing and forgetting query:");
+ LogQuery(query, msg);
+ }
+
+ auto item (Shared<Query>::Make(std::move(query)));
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, item, priority, ctime, affects]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{item, nullptr, nullptr, nullptr, nullptr, ctime, affects});
+ m_QueuedWrites.Set();
+ IncreasePendingQueries(1);
+ });
+}
+
+/**
+ * Queue Redis queries for sending
+ *
+ * @param queries Redis queries
+ * @param priority The queries' priority
+ */
+void RedisConnection::FireAndForgetQueries(RedisConnection::Queries queries, RedisConnection::QueryPriority priority, QueryAffects affects)
+{
+ if (LogDebug >= Logger::GetMinLogSeverity()) {
+ for (auto& query : queries) {
+ Log msg(LogDebug, "IcingaDB", "Firing and forgetting query:");
+ LogQuery(query, msg);
+ }
+ }
+
+ auto item (Shared<Queries>::Make(std::move(queries)));
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, item, priority, ctime, affects]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{nullptr, item, nullptr, nullptr, nullptr, ctime, affects});
+ m_QueuedWrites.Set();
+ IncreasePendingQueries(item->size());
+ });
+}
+
+/**
+ * Queue a Redis query for sending, wait for the response and return (or throw) it
+ *
+ * @param query Redis query
+ * @param priority The query's priority
+ *
+ * @return The response
+ */
+RedisConnection::Reply RedisConnection::GetResultOfQuery(RedisConnection::Query query, RedisConnection::QueryPriority priority, QueryAffects affects)
+{
+ if (LogDebug >= Logger::GetMinLogSeverity()) {
+ Log msg (LogDebug, "IcingaDB", "Executing query:");
+ LogQuery(query, msg);
+ }
+
+ std::promise<Reply> promise;
+ auto future (promise.get_future());
+ auto item (Shared<std::pair<Query, std::promise<Reply>>>::Make(std::move(query), std::move(promise)));
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, item, priority, ctime, affects]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{nullptr, nullptr, item, nullptr, nullptr, ctime, affects});
+ m_QueuedWrites.Set();
+ IncreasePendingQueries(1);
+ });
+
+ item = nullptr;
+ future.wait();
+ return future.get();
+}
+
+/**
+ * Queue Redis queries for sending, wait for the responses and return (or throw) them
+ *
+ * @param queries Redis queries
+ * @param priority The queries' priority
+ *
+ * @return The responses
+ */
+RedisConnection::Replies RedisConnection::GetResultsOfQueries(RedisConnection::Queries queries, RedisConnection::QueryPriority priority, QueryAffects affects)
+{
+ if (LogDebug >= Logger::GetMinLogSeverity()) {
+ for (auto& query : queries) {
+ Log msg(LogDebug, "IcingaDB", "Executing query:");
+ LogQuery(query, msg);
+ }
+ }
+
+ std::promise<Replies> promise;
+ auto future (promise.get_future());
+ auto item (Shared<std::pair<Queries, std::promise<Replies>>>::Make(std::move(queries), std::move(promise)));
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, item, priority, ctime, affects]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{nullptr, nullptr, nullptr, item, nullptr, ctime, affects});
+ m_QueuedWrites.Set();
+ IncreasePendingQueries(item->first.size());
+ });
+
+ item = nullptr;
+ future.wait();
+ return future.get();
+}
+
+void RedisConnection::EnqueueCallback(const std::function<void(boost::asio::yield_context&)>& callback, RedisConnection::QueryPriority priority)
+{
+ auto ctime (Utility::GetTime());
+
+ asio::post(m_Strand, [this, callback, priority, ctime]() {
+ m_Queues.Writes[priority].emplace(WriteQueueItem{nullptr, nullptr, nullptr, nullptr, callback, ctime});
+ m_QueuedWrites.Set();
+ });
+}
+
+/**
+ * Puts a no-op command with a result at the end of the queue and wait for the result,
+ * i.e. for everything enqueued to be processed by the server.
+ *
+ * @ingroup icingadb
+ */
+void RedisConnection::Sync()
+{
+ GetResultOfQuery({"PING"}, RedisConnection::QueryPriority::SyncConnection);
+}
+
+/**
+ * Get the enqueue time of the oldest still queued Redis query
+ *
+ * @return *nix timestamp or 0
+ */
+double RedisConnection::GetOldestPendingQueryTs()
+{
+ auto promise (Shared<std::promise<double>>::Make());
+ auto future (promise->get_future());
+
+ asio::post(m_Strand, [this, promise]() {
+ double oldest = 0;
+
+ for (auto& queue : m_Queues.Writes) {
+ if (m_SuppressedQueryKinds.find(queue.first) == m_SuppressedQueryKinds.end() && !queue.second.empty()) {
+ auto ctime (queue.second.front().CTime);
+
+ if (ctime < oldest || oldest == 0) {
+ oldest = ctime;
+ }
+ }
+ }
+
+ promise->set_value(oldest);
+ });
+
+ future.wait();
+ return future.get();
+}
+
+/**
+ * Mark kind as kind of queries not to actually send yet
+ *
+ * @param kind Query kind
+ */
+void RedisConnection::SuppressQueryKind(RedisConnection::QueryPriority kind)
+{
+ asio::post(m_Strand, [this, kind]() { m_SuppressedQueryKinds.emplace(kind); });
+}
+
+/**
+ * Unmark kind as kind of queries not to actually send yet
+ *
+ * @param kind Query kind
+ */
+void RedisConnection::UnsuppressQueryKind(RedisConnection::QueryPriority kind)
+{
+ asio::post(m_Strand, [this, kind]() {
+ m_SuppressedQueryKinds.erase(kind);
+ m_QueuedWrites.Set();
+ });
+}
+
+/**
+ * Try to connect to Redis
+ */
+void RedisConnection::Connect(asio::yield_context& yc)
+{
+ Defer notConnecting ([this]() { m_Connecting.store(m_Connected.load()); });
+
+ boost::asio::deadline_timer timer (m_Strand.context());
+
+ auto waitForReadLoop ([this, &yc]() {
+ while (!m_Queues.FutureResponseActions.empty()) {
+ IoEngine::YieldCurrentCoroutine(yc);
+ }
+ });
+
+ for (;;) {
+ try {
+ if (m_Path.IsEmpty()) {
+ if (m_TLSContext) {
+ Log(m_Parent ? LogNotice : LogInformation, "IcingaDB")
+ << "Trying to connect to Redis server (async, TLS) on host '" << m_Host << ":" << m_Port << "'";
+
+ auto conn (Shared<AsioTlsStream>::Make(m_Strand.context(), *m_TLSContext, m_Host));
+ auto& tlsConn (conn->next_layer());
+ auto connectTimeout (MakeTimeout(conn));
+ Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); });
+
+ icinga::Connect(conn->lowest_layer(), m_Host, Convert::ToString(m_Port), yc);
+ tlsConn.async_handshake(tlsConn.client, yc);
+
+ if (!m_Insecure) {
+ std::shared_ptr<X509> cert (tlsConn.GetPeerCertificate());
+
+ if (!cert) {
+ BOOST_THROW_EXCEPTION(std::runtime_error(
+ "Redis didn't present any TLS certificate."
+ ));
+ }
+
+ if (!tlsConn.IsVerifyOK()) {
+ BOOST_THROW_EXCEPTION(std::runtime_error(
+ "TLS certificate validation failed: " + std::string(tlsConn.GetVerifyError())
+ ));
+ }
+ }
+
+ Handshake(conn, yc);
+ waitForReadLoop();
+ m_TlsConn = std::move(conn);
+ } else {
+ Log(m_Parent ? LogNotice : LogInformation, "IcingaDB")
+ << "Trying to connect to Redis server (async) on host '" << m_Host << ":" << m_Port << "'";
+
+ auto conn (Shared<TcpConn>::Make(m_Strand.context()));
+ auto connectTimeout (MakeTimeout(conn));
+ Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); });
+
+ icinga::Connect(conn->next_layer(), m_Host, Convert::ToString(m_Port), yc);
+ Handshake(conn, yc);
+ waitForReadLoop();
+ m_TcpConn = std::move(conn);
+ }
+ } else {
+ Log(LogInformation, "IcingaDB")
+ << "Trying to connect to Redis server (async) on unix socket path '" << m_Path << "'";
+
+ auto conn (Shared<UnixConn>::Make(m_Strand.context()));
+ auto connectTimeout (MakeTimeout(conn));
+ Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); });
+
+ conn->next_layer().async_connect(Unix::endpoint(m_Path.CStr()), yc);
+ Handshake(conn, yc);
+ waitForReadLoop();
+ m_UnixConn = std::move(conn);
+ }
+
+ m_Connected.store(true);
+
+ Log(m_Parent ? LogNotice : LogInformation, "IcingaDB", "Connected to Redis server");
+
+ // Operate on a copy so that the callback can set a new callback without destroying itself while running.
+ auto callback (m_ConnectedCallback);
+ if (callback) {
+ callback(yc);
+ }
+
+ break;
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "IcingaDB")
+ << "Cannot connect to " << m_Host << ":" << m_Port << ": " << ex.what();
+ }
+
+ timer.expires_from_now(boost::posix_time::seconds(5));
+ timer.async_wait(yc);
+ }
+
+}
+
+/**
+ * Actually receive the responses to the Redis queries send by WriteItem() and handle them
+ */
+void RedisConnection::ReadLoop(asio::yield_context& yc)
+{
+ for (;;) {
+ m_QueuedReads.Wait(yc);
+
+ while (!m_Queues.FutureResponseActions.empty()) {
+ auto item (std::move(m_Queues.FutureResponseActions.front()));
+ m_Queues.FutureResponseActions.pop();
+
+ switch (item.Action) {
+ case ResponseAction::Ignore:
+ try {
+ for (auto i (item.Amount); i; --i) {
+ ReadOne(yc);
+ }
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "IcingaDB")
+ << "Error during receiving the response to a query which has been fired and forgotten: " << ex.what();
+
+ continue;
+ } catch (...) {
+ Log(LogCritical, "IcingaDB")
+ << "Error during receiving the response to a query which has been fired and forgotten";
+
+ continue;
+ }
+
+ break;
+ case ResponseAction::Deliver:
+ for (auto i (item.Amount); i; --i) {
+ auto promise (std::move(m_Queues.ReplyPromises.front()));
+ m_Queues.ReplyPromises.pop();
+
+ Reply reply;
+
+ try {
+ reply = ReadOne(yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ promise.set_exception(std::current_exception());
+
+ continue;
+ }
+
+ promise.set_value(std::move(reply));
+ }
+
+ break;
+ case ResponseAction::DeliverBulk:
+ {
+ auto promise (std::move(m_Queues.RepliesPromises.front()));
+ m_Queues.RepliesPromises.pop();
+
+ Replies replies;
+ replies.reserve(item.Amount);
+
+ for (auto i (item.Amount); i; --i) {
+ try {
+ replies.emplace_back(ReadOne(yc));
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ promise.set_exception(std::current_exception());
+ break;
+ }
+ }
+
+ try {
+ promise.set_value(std::move(replies));
+ } catch (const std::future_error&) {
+ // Complaint about the above op is not allowed
+ // due to promise.set_exception() was already called
+ }
+ }
+ }
+ }
+
+ m_QueuedReads.Clear();
+ }
+}
+
+/**
+ * Actually send the Redis queries queued by {FireAndForget,GetResultsOf}{Query,Queries}()
+ */
+void RedisConnection::WriteLoop(asio::yield_context& yc)
+{
+ for (;;) {
+ m_QueuedWrites.Wait(yc);
+
+ WriteFirstOfHighestPrio:
+ for (auto& queue : m_Queues.Writes) {
+ if (m_SuppressedQueryKinds.find(queue.first) != m_SuppressedQueryKinds.end() || queue.second.empty()) {
+ continue;
+ }
+
+ auto next (std::move(queue.second.front()));
+ queue.second.pop();
+
+ WriteItem(yc, std::move(next));
+
+ goto WriteFirstOfHighestPrio;
+ }
+
+ m_QueuedWrites.Clear();
+ }
+}
+
+/**
+ * Periodically log current query performance
+ */
+void RedisConnection::LogStats(asio::yield_context& yc)
+{
+ double lastMessage = 0;
+
+ m_LogStatsTimer.expires_from_now(boost::posix_time::seconds(10));
+
+ for (;;) {
+ m_LogStatsTimer.async_wait(yc);
+ m_LogStatsTimer.expires_from_now(boost::posix_time::seconds(10));
+
+ if (!IsConnected())
+ continue;
+
+ auto now (Utility::GetTime());
+ bool timeoutReached = now - lastMessage >= 5 * 60;
+
+ if (m_PendingQueries < 1 && !timeoutReached)
+ continue;
+
+ auto output (round(m_OutputQueries.CalculateRate(now, 10)));
+
+ if (m_PendingQueries < output * 5 && !timeoutReached)
+ continue;
+
+ Log(LogInformation, "IcingaDB")
+ << "Pending queries: " << m_PendingQueries << " (Input: "
+ << round(m_InputQueries.CalculateRate(now, 10)) << "/s; Output: " << output << "/s)";
+
+ lastMessage = now;
+ }
+}
+
+/**
+ * Send next and schedule receiving the response
+ *
+ * @param next Redis queries
+ */
+void RedisConnection::WriteItem(boost::asio::yield_context& yc, RedisConnection::WriteQueueItem next)
+{
+ if (next.FireAndForgetQuery) {
+ auto& item (*next.FireAndForgetQuery);
+ DecreasePendingQueries(1);
+
+ try {
+ WriteOne(item, yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (const std::exception& ex) {
+ Log msg (LogCritical, "IcingaDB", "Error during sending query");
+ LogQuery(item, msg);
+ msg << " which has been fired and forgotten: " << ex.what();
+
+ return;
+ } catch (...) {
+ Log msg (LogCritical, "IcingaDB", "Error during sending query");
+ LogQuery(item, msg);
+ msg << " which has been fired and forgotten";
+
+ return;
+ }
+
+ if (m_Queues.FutureResponseActions.empty() || m_Queues.FutureResponseActions.back().Action != ResponseAction::Ignore) {
+ m_Queues.FutureResponseActions.emplace(FutureResponseAction{1, ResponseAction::Ignore});
+ } else {
+ ++m_Queues.FutureResponseActions.back().Amount;
+ }
+
+ m_QueuedReads.Set();
+ }
+
+ if (next.FireAndForgetQueries) {
+ auto& item (*next.FireAndForgetQueries);
+ size_t i = 0;
+
+ DecreasePendingQueries(item.size());
+
+ try {
+ for (auto& query : item) {
+ WriteOne(query, yc);
+ ++i;
+ }
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (const std::exception& ex) {
+ Log msg (LogCritical, "IcingaDB", "Error during sending query");
+ LogQuery(item[i], msg);
+ msg << " which has been fired and forgotten: " << ex.what();
+
+ return;
+ } catch (...) {
+ Log msg (LogCritical, "IcingaDB", "Error during sending query");
+ LogQuery(item[i], msg);
+ msg << " which has been fired and forgotten";
+
+ return;
+ }
+
+ if (m_Queues.FutureResponseActions.empty() || m_Queues.FutureResponseActions.back().Action != ResponseAction::Ignore) {
+ m_Queues.FutureResponseActions.emplace(FutureResponseAction{item.size(), ResponseAction::Ignore});
+ } else {
+ m_Queues.FutureResponseActions.back().Amount += item.size();
+ }
+
+ m_QueuedReads.Set();
+ }
+
+ if (next.GetResultOfQuery) {
+ auto& item (*next.GetResultOfQuery);
+ DecreasePendingQueries(1);
+
+ try {
+ WriteOne(item.first, yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ item.second.set_exception(std::current_exception());
+
+ return;
+ }
+
+ m_Queues.ReplyPromises.emplace(std::move(item.second));
+
+ if (m_Queues.FutureResponseActions.empty() || m_Queues.FutureResponseActions.back().Action != ResponseAction::Deliver) {
+ m_Queues.FutureResponseActions.emplace(FutureResponseAction{1, ResponseAction::Deliver});
+ } else {
+ ++m_Queues.FutureResponseActions.back().Amount;
+ }
+
+ m_QueuedReads.Set();
+ }
+
+ if (next.GetResultsOfQueries) {
+ auto& item (*next.GetResultsOfQueries);
+ DecreasePendingQueries(item.first.size());
+
+ try {
+ for (auto& query : item.first) {
+ WriteOne(query, yc);
+ }
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ item.second.set_exception(std::current_exception());
+
+ return;
+ }
+
+ m_Queues.RepliesPromises.emplace(std::move(item.second));
+ m_Queues.FutureResponseActions.emplace(FutureResponseAction{item.first.size(), ResponseAction::DeliverBulk});
+
+ m_QueuedReads.Set();
+ }
+
+ if (next.Callback) {
+ next.Callback(yc);
+ }
+
+ RecordAffected(next.Affects, Utility::GetTime());
+}
+
+/**
+ * Receive the response to a Redis query
+ *
+ * @return The response
+ */
+RedisConnection::Reply RedisConnection::ReadOne(boost::asio::yield_context& yc)
+{
+ if (m_Path.IsEmpty()) {
+ if (m_TLSContext) {
+ return ReadOne(m_TlsConn, yc);
+ } else {
+ return ReadOne(m_TcpConn, yc);
+ }
+ } else {
+ return ReadOne(m_UnixConn, yc);
+ }
+}
+
+/**
+ * Send query
+ *
+ * @param query Redis query
+ */
+void RedisConnection::WriteOne(RedisConnection::Query& query, asio::yield_context& yc)
+{
+ if (m_Path.IsEmpty()) {
+ if (m_TLSContext) {
+ WriteOne(m_TlsConn, query, yc);
+ } else {
+ WriteOne(m_TcpConn, query, yc);
+ }
+ } else {
+ WriteOne(m_UnixConn, query, yc);
+ }
+}
+
+/**
+ * Specify a callback that is run each time a connection is successfully established
+ *
+ * The callback is executed from a Boost.Asio coroutine and should therefore not perform blocking operations.
+ *
+ * @param callback Callback to execute
+ */
+void RedisConnection::SetConnectedCallback(std::function<void(asio::yield_context& yc)> callback) {
+ m_ConnectedCallback = std::move(callback);
+}
+
+int RedisConnection::GetQueryCount(RingBuffer::SizeType span)
+{
+ return m_OutputQueries.UpdateAndGetValues(Utility::GetTime(), span);
+}
+
+void RedisConnection::IncreasePendingQueries(int count)
+{
+ if (m_Parent) {
+ auto parent (m_Parent);
+
+ asio::post(parent->m_Strand, [parent, count]() {
+ parent->IncreasePendingQueries(count);
+ });
+ } else {
+ m_PendingQueries += count;
+ m_InputQueries.InsertValue(Utility::GetTime(), count);
+ }
+}
+
+void RedisConnection::DecreasePendingQueries(int count)
+{
+ if (m_Parent) {
+ auto parent (m_Parent);
+
+ asio::post(parent->m_Strand, [parent, count]() {
+ parent->DecreasePendingQueries(count);
+ });
+ } else {
+ m_PendingQueries -= count;
+ m_OutputQueries.InsertValue(Utility::GetTime(), count);
+ }
+}
+
+void RedisConnection::RecordAffected(RedisConnection::QueryAffects affected, double when)
+{
+ if (m_Parent) {
+ auto parent (m_Parent);
+
+ asio::post(parent->m_Strand, [parent, affected, when]() {
+ parent->RecordAffected(affected, when);
+ });
+ } else {
+ if (affected.Config) {
+ m_WrittenConfig.InsertValue(when, affected.Config);
+ }
+
+ if (affected.State) {
+ m_WrittenState.InsertValue(when, affected.State);
+ }
+
+ if (affected.History) {
+ m_WrittenHistory.InsertValue(when, affected.History);
+ }
+ }
+}
diff --git a/lib/icingadb/redisconnection.hpp b/lib/icingadb/redisconnection.hpp
new file mode 100644
index 0000000..f346ba2
--- /dev/null
+++ b/lib/icingadb/redisconnection.hpp
@@ -0,0 +1,678 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef REDISCONNECTION_H
+#define REDISCONNECTION_H
+
+#include "base/array.hpp"
+#include "base/atomic.hpp"
+#include "base/convert.hpp"
+#include "base/io-engine.hpp"
+#include "base/object.hpp"
+#include "base/ringbuffer.hpp"
+#include "base/shared.hpp"
+#include "base/string.hpp"
+#include "base/tlsstream.hpp"
+#include "base/value.hpp"
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/buffered_stream.hpp>
+#include <boost/asio/deadline_timer.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/io_context_strand.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/local/stream_protocol.hpp>
+#include <boost/asio/read.hpp>
+#include <boost/asio/read_until.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/asio/streambuf.hpp>
+#include <boost/asio/write.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/regex.hpp>
+#include <boost/utility/string_view.hpp>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <future>
+#include <map>
+#include <memory>
+#include <queue>
+#include <set>
+#include <stdexcept>
+#include <utility>
+#include <vector>
+
+namespace icinga
+{
+/**
+ * An Async Redis connection.
+ *
+ * @ingroup icingadb
+ */
+ class RedisConnection final : public Object
+ {
+ public:
+ DECLARE_PTR_TYPEDEFS(RedisConnection);
+
+ typedef std::vector<String> Query;
+ typedef std::vector<Query> Queries;
+ typedef Value Reply;
+ typedef std::vector<Reply> Replies;
+
+ /**
+ * Redis query priorities, highest first.
+ *
+ * @ingroup icingadb
+ */
+ enum class QueryPriority : unsigned char
+ {
+ Heartbeat,
+ RuntimeStateStream, // runtime state updates, doesn't affect initially synced states
+ Config, // includes initially synced states
+ RuntimeStateSync, // updates initially synced states at runtime, in parallel to config dump, therefore must be < Config
+ History,
+ CheckResult,
+ SyncConnection = 255
+ };
+
+ struct QueryAffects
+ {
+ size_t Config;
+ size_t State;
+ size_t History;
+
+ QueryAffects(size_t config = 0, size_t state = 0, size_t history = 0)
+ : Config(config), State(state), History(history) { }
+ };
+
+ RedisConnection(const String& host, int port, const String& path, const String& password, int db,
+ bool useTls, bool insecure, const String& certPath, const String& keyPath, const String& caPath, const String& crlPath,
+ const String& tlsProtocolmin, const String& cipherList, double connectTimeout, DebugInfo di, const Ptr& parent = nullptr);
+
+ void UpdateTLSContext();
+
+ void Start();
+
+ bool IsConnected();
+
+ void FireAndForgetQuery(Query query, QueryPriority priority, QueryAffects affects = {});
+ void FireAndForgetQueries(Queries queries, QueryPriority priority, QueryAffects affects = {});
+
+ Reply GetResultOfQuery(Query query, QueryPriority priority, QueryAffects affects = {});
+ Replies GetResultsOfQueries(Queries queries, QueryPriority priority, QueryAffects affects = {});
+
+ void EnqueueCallback(const std::function<void(boost::asio::yield_context&)>& callback, QueryPriority priority);
+ void Sync();
+ double GetOldestPendingQueryTs();
+
+ void SuppressQueryKind(QueryPriority kind);
+ void UnsuppressQueryKind(QueryPriority kind);
+
+ void SetConnectedCallback(std::function<void(boost::asio::yield_context& yc)> callback);
+
+ inline bool GetConnected()
+ {
+ return m_Connected.load();
+ }
+
+ int GetQueryCount(RingBuffer::SizeType span);
+
+ inline int GetPendingQueryCount()
+ {
+ return m_PendingQueries;
+ }
+
+ inline int GetWrittenConfigFor(RingBuffer::SizeType span, RingBuffer::SizeType tv = Utility::GetTime())
+ {
+ return m_WrittenConfig.UpdateAndGetValues(tv, span);
+ }
+
+ inline int GetWrittenStateFor(RingBuffer::SizeType span, RingBuffer::SizeType tv = Utility::GetTime())
+ {
+ return m_WrittenState.UpdateAndGetValues(tv, span);
+ }
+
+ inline int GetWrittenHistoryFor(RingBuffer::SizeType span, RingBuffer::SizeType tv = Utility::GetTime())
+ {
+ return m_WrittenHistory.UpdateAndGetValues(tv, span);
+ }
+
+ private:
+ /**
+ * What to do with the responses to Redis queries.
+ *
+ * @ingroup icingadb
+ */
+ enum class ResponseAction : unsigned char
+ {
+ Ignore, // discard
+ Deliver, // submit to the requestor
+ DeliverBulk // submit multiple responses to the requestor at once
+ };
+
+ /**
+ * What to do with how many responses to Redis queries.
+ *
+ * @ingroup icingadb
+ */
+ struct FutureResponseAction
+ {
+ size_t Amount;
+ ResponseAction Action;
+ };
+
+ /**
+ * Something to be send to Redis.
+ *
+ * @ingroup icingadb
+ */
+ struct WriteQueueItem
+ {
+ Shared<Query>::Ptr FireAndForgetQuery;
+ Shared<Queries>::Ptr FireAndForgetQueries;
+ Shared<std::pair<Query, std::promise<Reply>>>::Ptr GetResultOfQuery;
+ Shared<std::pair<Queries, std::promise<Replies>>>::Ptr GetResultsOfQueries;
+ std::function<void(boost::asio::yield_context&)> Callback;
+
+ double CTime;
+ QueryAffects Affects;
+ };
+
+ typedef boost::asio::ip::tcp Tcp;
+ typedef boost::asio::local::stream_protocol Unix;
+
+ typedef boost::asio::buffered_stream<Tcp::socket> TcpConn;
+ typedef boost::asio::buffered_stream<Unix::socket> UnixConn;
+
+ Shared<boost::asio::ssl::context>::Ptr m_TLSContext;
+
+ template<class AsyncReadStream>
+ static Value ReadRESP(AsyncReadStream& stream, boost::asio::yield_context& yc);
+
+ template<class AsyncReadStream>
+ static std::vector<char> ReadLine(AsyncReadStream& stream, boost::asio::yield_context& yc, size_t hint = 0);
+
+ template<class AsyncWriteStream>
+ static void WriteRESP(AsyncWriteStream& stream, const Query& query, boost::asio::yield_context& yc);
+
+ static boost::regex m_ErrAuth;
+
+ RedisConnection(boost::asio::io_context& io, String host, int port, String path, String password,
+ int db, bool useTls, bool insecure, String certPath, String keyPath, String caPath, String crlPath,
+ String tlsProtocolmin, String cipherList, double connectTimeout, DebugInfo di, const Ptr& parent);
+
+ void Connect(boost::asio::yield_context& yc);
+ void ReadLoop(boost::asio::yield_context& yc);
+ void WriteLoop(boost::asio::yield_context& yc);
+ void LogStats(boost::asio::yield_context& yc);
+ void WriteItem(boost::asio::yield_context& yc, WriteQueueItem item);
+ Reply ReadOne(boost::asio::yield_context& yc);
+ void WriteOne(Query& query, boost::asio::yield_context& yc);
+
+ template<class StreamPtr>
+ Reply ReadOne(StreamPtr& stream, boost::asio::yield_context& yc);
+
+ template<class StreamPtr>
+ void WriteOne(StreamPtr& stream, Query& query, boost::asio::yield_context& yc);
+
+ void IncreasePendingQueries(int count);
+ void DecreasePendingQueries(int count);
+ void RecordAffected(QueryAffects affected, double when);
+
+ template<class StreamPtr>
+ void Handshake(StreamPtr& stream, boost::asio::yield_context& yc);
+
+ template<class StreamPtr>
+ Timeout::Ptr MakeTimeout(StreamPtr& stream);
+
+ String m_Path;
+ String m_Host;
+ int m_Port;
+ String m_Password;
+ int m_DbIndex;
+
+ String m_CertPath;
+ String m_KeyPath;
+ bool m_Insecure;
+ String m_CaPath;
+ String m_CrlPath;
+ String m_TlsProtocolmin;
+ String m_CipherList;
+ double m_ConnectTimeout;
+ DebugInfo m_DebugInfo;
+
+ boost::asio::io_context::strand m_Strand;
+ Shared<TcpConn>::Ptr m_TcpConn;
+ Shared<UnixConn>::Ptr m_UnixConn;
+ Shared<AsioTlsStream>::Ptr m_TlsConn;
+ Atomic<bool> m_Connecting, m_Connected, m_Started;
+
+ struct {
+ // Items to be send to Redis
+ std::map<QueryPriority, std::queue<WriteQueueItem>> Writes;
+ // Requestors, each waiting for a single response
+ std::queue<std::promise<Reply>> ReplyPromises;
+ // Requestors, each waiting for multiple responses at once
+ std::queue<std::promise<Replies>> RepliesPromises;
+ // Metadata about all of the above
+ std::queue<FutureResponseAction> FutureResponseActions;
+ } m_Queues;
+
+ // Kinds of queries not to actually send yet
+ std::set<QueryPriority> m_SuppressedQueryKinds;
+
+ // Indicate that there's something to send/receive
+ AsioConditionVariable m_QueuedWrites, m_QueuedReads;
+
+ std::function<void(boost::asio::yield_context& yc)> m_ConnectedCallback;
+
+ // Stats
+ RingBuffer m_InputQueries{10};
+ RingBuffer m_OutputQueries{15 * 60};
+ RingBuffer m_WrittenConfig{15 * 60};
+ RingBuffer m_WrittenState{15 * 60};
+ RingBuffer m_WrittenHistory{15 * 60};
+ int m_PendingQueries{0};
+ boost::asio::deadline_timer m_LogStatsTimer;
+ Ptr m_Parent;
+ };
+
+/**
+ * An error response from the Redis server.
+ *
+ * @ingroup icingadb
+ */
+class RedisError final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(RedisError);
+
+ inline RedisError(String message) : m_Message(std::move(message))
+ {
+ }
+
+ inline const String& GetMessage()
+ {
+ return m_Message;
+ }
+
+private:
+ String m_Message;
+};
+
+/**
+ * Thrown if the connection to the Redis server has already been lost.
+ *
+ * @ingroup icingadb
+ */
+class RedisDisconnected : public std::runtime_error
+{
+public:
+ inline RedisDisconnected() : runtime_error("")
+ {
+ }
+};
+
+/**
+ * Thrown on malformed Redis server responses.
+ *
+ * @ingroup icingadb
+ */
+class RedisProtocolError : public std::runtime_error
+{
+protected:
+ inline RedisProtocolError() : runtime_error("")
+ {
+ }
+};
+
+/**
+ * Thrown on malformed types in Redis server responses.
+ *
+ * @ingroup icingadb
+ */
+class BadRedisType : public RedisProtocolError
+{
+public:
+ inline BadRedisType(char type) : m_What{type, 0}
+ {
+ }
+
+ virtual const char * what() const noexcept override
+ {
+ return m_What;
+ }
+
+private:
+ char m_What[2];
+};
+
+/**
+ * Thrown on malformed ints in Redis server responses.
+ *
+ * @ingroup icingadb
+ */
+class BadRedisInt : public RedisProtocolError
+{
+public:
+ inline BadRedisInt(std::vector<char> intStr) : m_What(std::move(intStr))
+ {
+ m_What.emplace_back(0);
+ }
+
+ virtual const char * what() const noexcept override
+ {
+ return m_What.data();
+ }
+
+private:
+ std::vector<char> m_What;
+};
+
+/**
+ * Read a Redis server response from stream
+ *
+ * @param stream Redis server connection
+ *
+ * @return The response
+ */
+template<class StreamPtr>
+RedisConnection::Reply RedisConnection::ReadOne(StreamPtr& stream, boost::asio::yield_context& yc)
+{
+ namespace asio = boost::asio;
+
+ if (!stream) {
+ throw RedisDisconnected();
+ }
+
+ auto strm (stream);
+
+ try {
+ return ReadRESP(*strm, yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ if (m_Connecting.exchange(false)) {
+ m_Connected.store(false);
+ stream = nullptr;
+
+ if (!m_Connecting.exchange(true)) {
+ Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { Connect(yc); });
+ }
+ }
+
+ throw;
+ }
+}
+
+/**
+ * Write a Redis query to stream
+ *
+ * @param stream Redis server connection
+ * @param query Redis query
+ */
+template<class StreamPtr>
+void RedisConnection::WriteOne(StreamPtr& stream, RedisConnection::Query& query, boost::asio::yield_context& yc)
+{
+ namespace asio = boost::asio;
+
+ if (!stream) {
+ throw RedisDisconnected();
+ }
+
+ auto strm (stream);
+
+ try {
+ WriteRESP(*strm, query, yc);
+ strm->async_flush(yc);
+ } catch (const boost::coroutines::detail::forced_unwind&) {
+ throw;
+ } catch (...) {
+ if (m_Connecting.exchange(false)) {
+ m_Connected.store(false);
+ stream = nullptr;
+
+ if (!m_Connecting.exchange(true)) {
+ Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_Strand, [this, keepAlive](asio::yield_context yc) { Connect(yc); });
+ }
+ }
+
+ throw;
+ }
+}
+
+/**
+ * Initialize a Redis stream
+ *
+ * @param stream Redis server connection
+ * @param query Redis query
+ */
+template<class StreamPtr>
+void RedisConnection::Handshake(StreamPtr& strm, boost::asio::yield_context& yc)
+{
+ if (m_Password.IsEmpty() && !m_DbIndex) {
+ // Trigger NOAUTH
+ WriteRESP(*strm, {"PING"}, yc);
+ } else {
+ if (!m_Password.IsEmpty()) {
+ WriteRESP(*strm, {"AUTH", m_Password}, yc);
+ }
+
+ if (m_DbIndex) {
+ WriteRESP(*strm, {"SELECT", Convert::ToString(m_DbIndex)}, yc);
+ }
+ }
+
+ strm->async_flush(yc);
+
+ if (m_Password.IsEmpty() && !m_DbIndex) {
+ Reply pong (ReadRESP(*strm, yc));
+
+ if (pong.IsObjectType<RedisError>()) {
+ // Likely NOAUTH
+ BOOST_THROW_EXCEPTION(std::runtime_error(RedisError::Ptr(pong)->GetMessage()));
+ }
+ } else {
+ if (!m_Password.IsEmpty()) {
+ Reply auth (ReadRESP(*strm, yc));
+
+ if (auth.IsObjectType<RedisError>()) {
+ auto& authErr (RedisError::Ptr(auth)->GetMessage().GetData());
+ boost::smatch what;
+
+ if (boost::regex_search(authErr, what, m_ErrAuth)) {
+ Log(LogWarning, "IcingaDB") << authErr;
+ } else {
+ // Likely WRONGPASS
+ BOOST_THROW_EXCEPTION(std::runtime_error(authErr));
+ }
+ }
+ }
+
+ if (m_DbIndex) {
+ Reply select (ReadRESP(*strm, yc));
+
+ if (select.IsObjectType<RedisError>()) {
+ // Likely NOAUTH or ERR DB
+ BOOST_THROW_EXCEPTION(std::runtime_error(RedisError::Ptr(select)->GetMessage()));
+ }
+ }
+ }
+}
+
+/**
+ * Creates a Timeout which cancels stream's I/O after m_ConnectTimeout
+ *
+ * @param stream Redis server connection
+ */
+template<class StreamPtr>
+Timeout::Ptr RedisConnection::MakeTimeout(StreamPtr& stream)
+{
+ Ptr keepAlive (this);
+
+ return new Timeout(
+ m_Strand.context(),
+ m_Strand,
+ boost::posix_time::microseconds(intmax_t(m_ConnectTimeout * 1000000)),
+ [keepAlive, stream](boost::asio::yield_context yc) {
+ boost::system::error_code ec;
+ stream->lowest_layer().cancel(ec);
+ }
+ );
+}
+
+/**
+ * Read a Redis protocol value from stream
+ *
+ * @param stream Redis server connection
+ *
+ * @return The value
+ */
+template<class AsyncReadStream>
+Value RedisConnection::ReadRESP(AsyncReadStream& stream, boost::asio::yield_context& yc)
+{
+ namespace asio = boost::asio;
+
+ char type = 0;
+ asio::async_read(stream, asio::mutable_buffer(&type, 1), yc);
+
+ switch (type) {
+ case '+':
+ {
+ auto buf (ReadLine(stream, yc));
+ return String(buf.begin(), buf.end());
+ }
+ case '-':
+ {
+ auto buf (ReadLine(stream, yc));
+ return new RedisError(String(buf.begin(), buf.end()));
+ }
+ case ':':
+ {
+ auto buf (ReadLine(stream, yc, 21));
+ intmax_t i = 0;
+
+ try {
+ i = boost::lexical_cast<intmax_t>(boost::string_view(buf.data(), buf.size()));
+ } catch (...) {
+ throw BadRedisInt(std::move(buf));
+ }
+
+ return (double)i;
+ }
+ case '$':
+ {
+ auto buf (ReadLine(stream, yc, 21));
+ intmax_t i = 0;
+
+ try {
+ i = boost::lexical_cast<intmax_t>(boost::string_view(buf.data(), buf.size()));
+ } catch (...) {
+ throw BadRedisInt(std::move(buf));
+ }
+
+ if (i < 0) {
+ return Value();
+ }
+
+ buf.clear();
+ buf.insert(buf.end(), i, 0);
+ asio::async_read(stream, asio::mutable_buffer(buf.data(), buf.size()), yc);
+
+ {
+ char crlf[2];
+ asio::async_read(stream, asio::mutable_buffer(crlf, 2), yc);
+ }
+
+ return String(buf.begin(), buf.end());
+ }
+ case '*':
+ {
+ auto buf (ReadLine(stream, yc, 21));
+ intmax_t i = 0;
+
+ try {
+ i = boost::lexical_cast<intmax_t>(boost::string_view(buf.data(), buf.size()));
+ } catch (...) {
+ throw BadRedisInt(std::move(buf));
+ }
+
+ if (i < 0) {
+ return Empty;
+ }
+
+ Array::Ptr arr = new Array();
+
+ arr->Reserve(i);
+
+ for (; i; --i) {
+ arr->Add(ReadRESP(stream, yc));
+ }
+
+ return arr;
+ }
+ default:
+ throw BadRedisType(type);
+ }
+}
+
+/**
+ * Read from stream until \r\n
+ *
+ * @param stream Redis server connection
+ * @param hint Expected amount of data
+ *
+ * @return Read data ex. \r\n
+ */
+template<class AsyncReadStream>
+std::vector<char> RedisConnection::ReadLine(AsyncReadStream& stream, boost::asio::yield_context& yc, size_t hint)
+{
+ namespace asio = boost::asio;
+
+ std::vector<char> line;
+ line.reserve(hint);
+
+ char next = 0;
+ asio::mutable_buffer buf (&next, 1);
+
+ for (;;) {
+ asio::async_read(stream, buf, yc);
+
+ if (next == '\r') {
+ asio::async_read(stream, buf, yc);
+ return line;
+ }
+
+ line.emplace_back(next);
+ }
+}
+
+/**
+ * Write a Redis protocol value to stream
+ *
+ * @param stream Redis server connection
+ * @param query Redis protocol value
+ */
+template<class AsyncWriteStream>
+void RedisConnection::WriteRESP(AsyncWriteStream& stream, const Query& query, boost::asio::yield_context& yc)
+{
+ namespace asio = boost::asio;
+
+ asio::streambuf writeBuffer;
+ std::ostream msg(&writeBuffer);
+
+ msg << "*" << query.size() << "\r\n";
+
+ for (auto& arg : query) {
+ msg << "$" << arg.GetLength() << "\r\n" << arg << "\r\n";
+ }
+
+ asio::async_write(stream, writeBuffer, yc);
+}
+
+}
+
+#endif //REDISCONNECTION_H