From 56ae875861ab260b80a030f50c4aff9f9dc8fff0 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 13:32:39 +0200 Subject: Adding upstream version 2.14.2. Signed-off-by: Daniel Baumann --- lib/icinga/apiactions.cpp | 962 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 962 insertions(+) create mode 100644 lib/icinga/apiactions.cpp (limited to 'lib/icinga/apiactions.cpp') 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 + +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(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(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(object); + + if (!checkable) + return ApiActions::CreateResult(404, "Cannot reschedule check for non-existent object."); + + if (Convert::ToBool(HttpUtility::GetLastParameter(params, "force"))) + checkable->SetForceNextCheck(true); + + double nextCheck; + if (params->Contains("next_check")) + nextCheck = HttpUtility::GetLastParameter(params, "next_check"); + else + nextCheck = Utility::GetTime(); + + checkable->SetNextCheck(nextCheck); + + /* trigger update event for DB IDO */ + Checkable::OnNextCheckUpdated(checkable); + + return ApiActions::CreateResult(200, "Successfully rescheduled check for object '" + checkable->GetName() + "'."); +} + +Dictionary::Ptr ApiActions::SendCustomNotification(const ConfigObject::Ptr& object, + const Dictionary::Ptr& params) +{ + Checkable::Ptr checkable = static_pointer_cast(object); + + if (!checkable) + return ApiActions::CreateResult(404, "Cannot send notification for non-existent object."); + + if (!params->Contains("author")) + return ApiActions::CreateResult(400, "Parameter 'author' is required."); + + if (!params->Contains("comment")) + return ApiActions::CreateResult(400, "Parameter 'comment' is required."); + + if (Convert::ToBool(HttpUtility::GetLastParameter(params, "force"))) + checkable->SetForceNextNotification(true); + + Checkable::OnNotificationsRequested(checkable, NotificationCustom, checkable->GetLastCheckResult(), + HttpUtility::GetLastParameter(params, "author"), HttpUtility::GetLastParameter(params, "comment"), nullptr); + + return ApiActions::CreateResult(200, "Successfully sent custom notification for object '" + checkable->GetName() + "'."); +} + +Dictionary::Ptr ApiActions::DelayNotification(const ConfigObject::Ptr& object, + const Dictionary::Ptr& params) +{ + Checkable::Ptr checkable = static_pointer_cast(object); + + if (!checkable) + return ApiActions::CreateResult(404, "Cannot delay notifications for non-existent object"); + + if (!params->Contains("timestamp")) + return ApiActions::CreateResult(400, "A timestamp is required to delay notifications"); + + for (const Notification::Ptr& notification : checkable->GetNotifications()) { + notification->SetNextNotification(HttpUtility::GetLastParameter(params, "timestamp")); + } + + return ApiActions::CreateResult(200, "Successfully delayed notifications for object '" + checkable->GetName() + "'."); +} + +Dictionary::Ptr ApiActions::AcknowledgeProblem(const ConfigObject::Ptr& object, + const Dictionary::Ptr& params) +{ + Checkable::Ptr checkable = static_pointer_cast(object); + + if (!checkable) + return ApiActions::CreateResult(404, "Cannot acknowledge problem for non-existent object."); + + if (!params->Contains("author") || !params->Contains("comment")) + return ApiActions::CreateResult(400, "Acknowledgements require author and comment."); + + AcknowledgementType sticky = AcknowledgementNormal; + bool notify = false; + bool persistent = false; + double timestamp = 0.0; + + if (params->Contains("sticky") && HttpUtility::GetLastParameter(params, "sticky")) + sticky = AcknowledgementSticky; + if (params->Contains("notify")) + notify = HttpUtility::GetLastParameter(params, "notify"); + if (params->Contains("persistent")) + persistent = HttpUtility::GetLastParameter(params, "persistent"); + if (params->Contains("expiry")) { + timestamp = HttpUtility::GetLastParameter(params, "expiry"); + + if (timestamp <= Utility::GetTime()) + return ApiActions::CreateResult(409, "Acknowledgement 'expiry' timestamp must be in the future for object " + checkable->GetName()); + } else + timestamp = 0; + + ObjectLock oLock (checkable); + + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); + + if (!service) { + if (host->GetState() == HostUp) + return ApiActions::CreateResult(409, "Host " + checkable->GetName() + " is UP."); + } else { + if (service->GetState() == ServiceOK) + return ApiActions::CreateResult(409, "Service " + checkable->GetName() + " is OK."); + } + + if (checkable->IsAcknowledged()) { + return ApiActions::CreateResult(409, (service ? "Service " : "Host ") + checkable->GetName() + " is already acknowledged."); + } + + 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(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(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(object); + + if (checkable) { + std::set comments = checkable->GetComments(); + + for (const Comment::Ptr& comment : comments) { + Comment::RemoveComment(comment->GetName(), true, author); + } + + return ApiActions::CreateResult(200, "Successfully removed all comments for object '" + checkable->GetName() + "'."); + } + + Comment::Ptr comment = static_pointer_cast(object); + + if (!comment) + return ApiActions::CreateResult(404, "Cannot remove non-existent comment object."); + + String commentName = comment->GetName(); + + Comment::RemoveComment(commentName, true, author); + + return ApiActions::CreateResult(200, "Successfully removed comment '" + commentName + "'."); +} + +Dictionary::Ptr ApiActions::ScheduleDowntime(const ConfigObject::Ptr& object, + const Dictionary::Ptr& params) +{ + Checkable::Ptr checkable = static_pointer_cast(object); + + if (!checkable) + return ApiActions::CreateResult(404, "Can't schedule downtime for non-existent object."); + + if (!params->Contains("start_time") || !params->Contains("end_time") || + !params->Contains("author") || !params->Contains("comment")) { + + return ApiActions::CreateResult(400, "Options 'start_time', 'end_time', 'author' and 'comment' are required"); + } + + bool fixed = true; + if (params->Contains("fixed")) + fixed = HttpUtility::GetLastParameter(params, "fixed"); + + if (!fixed && !params->Contains("duration")) + return ApiActions::CreateResult(400, "Option 'duration' is required for flexible downtime"); + + double duration = 0.0; + if (params->Contains("duration")) + duration = HttpUtility::GetLastParameter(params, "duration"); + + String triggerName; + if (params->Contains("trigger_name")) + triggerName = HttpUtility::GetLastParameter(params, "trigger_name"); + + String author = HttpUtility::GetLastParameter(params, "author"); + String comment = HttpUtility::GetLastParameter(params, "comment"); + double startTime = HttpUtility::GetLastParameter(params, "start_time"); + double endTime = HttpUtility::GetLastParameter(params, "end_time"); + + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); + + DowntimeChildOptions childOptions = DowntimeNoChildren; + if (params->Contains("child_options")) { + try { + childOptions = Downtime::ChildOptionsFromValue(HttpUtility::GetLastParameter(params, "child_options")); + } catch (const std::exception&) { + return ApiActions::CreateResult(400, "Option 'child_options' provided an invalid value."); + } + } + + 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 allChildren = checkable->GetAllChildren(); + for (const Checkable::Ptr& child : allChildren) { + Host::Ptr childHost; + Service::Ptr childService; + tie(childHost, childService) = GetHostService(child); + + if (allServices && childService && + allChildren.find(static_pointer_cast(childHost)) != allChildren.end()) { + /* When scheduling downtimes for all service and all children, the current child is a service, and its + * host is also a child, skip it here. The downtime for this service will be scheduled below together + * with the downtimes of all services for that host. Scheduling it below ensures that the relation + * from the child service downtime to the child host downtime is set properly. */ + continue; + } + + Log(LogNotice, "ApiActions") + << "Scheduling downtime for child object " << child->GetName(); + + Downtime::Ptr childDowntime = Downtime::AddDowntime(child, author, comment, startTime, endTime, + fixed, triggerName, duration); + String childDowntimeName = childDowntime->GetName(); + + Log(LogNotice, "ApiActions") + << "Add child downtime '" << childDowntimeName << "'."; + + Dictionary::Ptr childAdditional = new Dictionary({ + { "name", childDowntimeName }, + { "legacy_id", childDowntime->GetLegacyId() } + }); + + /* For a host, also schedule all service downtimes if requested. */ + if (allServices && !childService) { + ArrayData childServiceDowntimes; + + for (const Service::Ptr& childService : childHost->GetServices()) { + Log(LogNotice, "ApiActions") + << "Creating downtime for service " << childService->GetName() << " on child host " << childHost->GetName(); + + Downtime::Ptr serviceDowntime = Downtime::AddDowntime(childService, author, comment, startTime, endTime, + fixed, triggerName, duration, String(), String(), childDowntimeName); + String serviceDowntimeName = serviceDowntime->GetName(); + + childServiceDowntimes.push_back(new Dictionary({ + { "name", serviceDowntimeName }, + { "legacy_id", serviceDowntime->GetLegacyId() } + })); + } + + childAdditional->Set("service_downtimes", new Array(std::move(childServiceDowntimes))); + } + + childDowntimes.push_back(childAdditional); + } + + additional->Set("child_downtimes", new Array(std::move(childDowntimes))); + } + + return ApiActions::CreateResult(200, "Successfully scheduled downtime '" + + downtimeName + "' for object '" + checkable->GetName() + "'.", additional); +} + +Dictionary::Ptr ApiActions::RemoveDowntime(const ConfigObject::Ptr& object, + const Dictionary::Ptr& params) +{ + 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(object); + + size_t childCount = 0; + + if (checkable) { + std::set downtimes = checkable->GetDowntimes(); + + for (const Downtime::Ptr& downtime : downtimes) { + childCount += downtime->GetChildren().size(); + + try { + Downtime::RemoveDowntime(downtime->GetName(), true, true, false, author); + } catch (const invalid_downtime_removal_error& error) { + Log(LogWarning, "ApiActions") << error.what(); + + return ApiActions::CreateResult(400, error.what()); + } + } + + return ApiActions::CreateResult(200, "Successfully removed all downtimes for object '" + + checkable->GetName() + "' and " + std::to_string(childCount) + " child downtimes."); + } + + Downtime::Ptr downtime = static_pointer_cast(object); + + if (!downtime) + return ApiActions::CreateResult(404, "Cannot remove non-existent downtime object."); + + childCount += downtime->GetChildren().size(); + + try { + String downtimeName = downtime->GetName(); + Downtime::RemoveDowntime(downtimeName, true, true, false, author); + + return ApiActions::CreateResult(200, "Successfully removed downtime '" + downtimeName + + "' and " + std::to_string(childCount) + " child downtimes."); + } catch (const invalid_downtime_removal_error& error) { + Log(LogWarning, "ApiActions") << error.what(); + + return ApiActions::CreateResult(400, error.what()); + } +} + +Dictionary::Ptr ApiActions::ShutdownProcess(const ConfigObject::Ptr& object, + const Dictionary::Ptr& params) +{ + Application::RequestShutdown(); + + return ApiActions::CreateResult(200, "Shutting down Icinga 2."); +} + +Dictionary::Ptr ApiActions::RestartProcess(const ConfigObject::Ptr& object, + const Dictionary::Ptr& params) +{ + Application::RequestRestart(); + + return ApiActions::CreateResult(200, "Restarting Icinga 2."); +} + +Dictionary::Ptr ApiActions::GenerateTicket(const ConfigObject::Ptr&, + const Dictionary::Ptr& params) +{ + if (!params->Contains("cn")) + return ApiActions::CreateResult(400, "Option 'cn' is required"); + + String cn = HttpUtility::GetLastParameter(params, "cn"); + + ApiListener::Ptr listener = ApiListener::GetInstance(); + String salt = listener->GetTicketSalt(); + + if (salt.IsEmpty()) + return ApiActions::CreateResult(500, "Ticket salt is not configured in ApiListener object"); + + String ticket = PBKDF2_SHA1(cn, salt, 50000); + + Dictionary::Ptr additional = new Dictionary({ + { "ticket", ticket } + }); + + return ApiActions::CreateResult(200, "Generated PKI ticket '" + ticket + "' for common name '" + + cn + "'.", additional); +} + +Value ApiActions::GetSingleObjectByNameUsingPermissions(const String& type, const String& objectName, const ApiUser::Ptr& user) +{ + Dictionary::Ptr queryParams = new Dictionary(); + queryParams->Set("type", type); + queryParams->Set(type.ToLower(), objectName); + + QueryDescription qd; + qd.Types.insert(type); + qd.Permission = "objects/query/" + type; + + std::vector objs; + + try { + objs = FilterUtility::GetFilterTargets(qd, queryParams, user); + } catch (const std::exception& ex) { + Log(LogWarning, "ApiActions") << DiagnosticInformation(ex); + return nullptr; + } + + if (objs.empty()) + return nullptr; + + return objs.at(0); +}; + +Dictionary::Ptr ApiActions::ExecuteCommand(const ConfigObject::Ptr& object, const Dictionary::Ptr& params) +{ + ApiListener::Ptr listener = ApiListener::GetInstance(); + + if (!listener) + BOOST_THROW_EXCEPTION(std::invalid_argument("No ApiListener instance configured.")); + + /* Get command_type */ + String command_type = "EventCommand"; + + if (params->Contains("command_type")) + command_type = HttpUtility::GetLastParameter(params, "command_type"); + + /* Validate command_type */ + if (command_type != "EventCommand" && command_type != "CheckCommand" && command_type != "NotificationCommand") + return ApiActions::CreateResult(400, "Invalid command_type '" + command_type + "'."); + + Checkable::Ptr checkable = dynamic_pointer_cast(object); + + if (!checkable) + return ApiActions::CreateResult(404, "Can't start a command execution for a non-existent object."); + + /* Get TTL param */ + if (!params->Contains("ttl")) + return ApiActions::CreateResult(400, "Parameter ttl is required."); + + double ttl = HttpUtility::GetLastParameter(params, "ttl"); + + if (ttl <= 0) + return ApiActions::CreateResult(400, "Parameter ttl must be greater than 0."); + + ObjectLock oLock (checkable); + + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); + + String endpoint = "$command_endpoint$"; + + if (params->Contains("endpoint")) + endpoint = HttpUtility::GetLastParameter(params, "endpoint"); + + MacroProcessor::ResolverList resolvers; + Value macros; + + if (params->Contains("macros")) { + macros = HttpUtility::GetLastParameter(params, "macros"); + if (macros.IsObjectType()) { + resolvers.emplace_back("override", macros); + } else { + return ApiActions::CreateResult(400, "Parameter macros must be a dictionary."); + } + } + + if (service) + resolvers.emplace_back("service", service); + + resolvers.emplace_back("host", host); + + 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()) { + /* Fetch immediate child zone members */ + if (zone->GetParent() == localZone && zone->CanAccessObject(endpointPtr->GetZone())) { + std::set endpoints = zone->GetEndpoints(); + + for (const Endpoint::Ptr& childEndpoint : endpoints) { + if (!(childEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand)) { + /* Update execution */ + double now = Utility::GetTime(); + pending_execution->Set("exit", 126); + pending_execution->Set("output", "Endpoint '" + childEndpoint->GetName() + "' doesn't support executing arbitrary commands."); + pending_execution->Set("start", now); + pending_execution->Set("end", now); + pending_execution->Remove("pending"); + + listener->RelayMessage(origin, checkable, updateMessage, true); + + Dictionary::Ptr result = new Dictionary(); + result->Set("checkable", checkable->GetName()); + result->Set("execution", uuid); + return ApiActions::CreateResult(202, "Accepted", result); + } + } + } + } + + Dictionary::Ptr execMessage = new Dictionary(); + execMessage->Set("jsonrpc", "2.0"); + execMessage->Set("method", "event::ExecuteCommand"); + execMessage->Set("params", execParams); + + listener->RelayMessage(origin, endpointPtr->GetZone(), execMessage, true); + } + + Dictionary::Ptr result = new Dictionary(); + result->Set("checkable", checkable->GetName()); + result->Set("execution", uuid); + return ApiActions::CreateResult(202, "Accepted", result); +} -- cgit v1.2.3