diff options
Diffstat (limited to '')
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(©); +} + +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 |