diff --git a/doc/12-icinga2-api.md b/doc/12-icinga2-api.md index 81aab559d..0202eafc5 100644 --- a/doc/12-icinga2-api.md +++ b/doc/12-icinga2-api.md @@ -1595,6 +1595,50 @@ $ curl -k -s -S -i -u root:icinga -H 'Accept: application/json' \ } ``` +### execute-command + +Executes a particular check/notification/event-command on a particular +endpoint in the context of a particular checkable. Example use cases: + +* Test a check command without actually triggering notifications +* Reboot a node via an event command +* Test a notification command without actually reproducing the notification reason + +Send a `POST` request to the URL endpoint `/v1/actions/execute-command`. + + Parameter | Type | Description + --------------|------------|-------------- + ttl | Number | **Required.** The time to live of the execution expressed in seconds. + command_type | String | **Optional.** The command type: `CheckCommand` or `EventCommand` or `NotificationCommand`. Default: `EventCommand` + command | String | **Optional.** The command to execute. Its type must the same as `command_type`. It can be a macro string. Default: depending on the `command_type` it's either `$check_command$`, `$event_command$` or `$notification_command$` + endpoint | String | **Optional.** The endpoint to execute the command on. It can be a macro string. Default: `$command_endpoint$`. + macros | Dictionary | **Optional.** Macro overrides. Default: `{}` + user | String | **Optional.** The user used for the notification command. + notification | String | **Optional.** The notification used for the notification command. + +Example: + +``` +$ curl -k -s -S -i -u root:icinga -H 'Accept: application/json' \ + -X POST 'https://localhost:5665/v1/actions/execute-command' \ + -d '{"type": "Service", "service": "agent!custom_service", "ttl": 15, "macros": { "command_endpoint": "master", "ls_dir": "/tmp/foo" }, "command": "custom_command", "command_type": "CheckCommand" }' +``` + +``` +{ + "results": [ + { + "checkable": "agent!custom_service", + "code": 202.0, + "execution": "3541d906-9afe-4c0e-ae6d-f549ee9bb3e7", + "status": "Accepted" + } + ] +} +``` + +You may poll the state of the execution by [querying](#icinga2-api-config-objects-query) the checkable's attribute `executions`. + ## Event Streams Event streams can be used to receive check results, downtimes, comments, diff --git a/doc/19-technical-concepts.md b/doc/19-technical-concepts.md index 5e46985c8..18f88e839 100644 --- a/doc/19-technical-concepts.md +++ b/doc/19-technical-concepts.md @@ -1286,7 +1286,10 @@ params | Dictionary ##### Params -Currently empty. +Key | Type | Description +---------------------|-------------|------------------ +capabilities | Number | Bitmask, see `lib/remote/apilistener.hpp`. +version | Number | Icinga 2 version, e.g. 21300 for v2.13.0. ##### Functions @@ -1817,11 +1820,14 @@ command\_type | String | `check_command` or `event_command`. command | String | CheckCommand or EventCommand name. check\_timeout | Number | Check timeout of the checkable object, if specified as `check_timeout` attribute. macros | Dictionary | Command arguments as key/value pairs for remote execution. +endpoint | String | The endpoint to execute the command on. +deadline | Number | A Unix timestamp indicating the execution deadline +source | String | The execution UUID ##### Functions -**Event Sender:** This gets constructed directly in `Checkable::ExecuteCheck()` or `Checkable::ExecuteEventHandler()` when a remote command endpoint is configured. +**Event Sender:** This gets constructed directly in `Checkable::ExecuteCheck()`, `Checkable::ExecuteEventHandler()` or `ApiActions::ExecuteCommand()` when a remote command endpoint is configured. * `Get{CheckCommand,EventCommand}()->Execute()` simulates an execution and extracts all command arguments into the `macro` dictionary (inside lib/methods tasks). * When the endpoint is connected, the message is constructed and sent directly. @@ -1831,6 +1837,7 @@ macros | Dictionary | Command arguments as key/value pairs for remote Special handling, calls `ClusterEvents::EnqueueCheck()` for command endpoint checks. This function enqueues check tasks into a queue which is controlled in `RemoteCheckThreadProc()`. +If the `endpoint` parameter is specified and is not equal to the local endpoint then the message is forwarded to the correct endpoint zone. ##### Permissions @@ -1852,6 +1859,78 @@ The returned messages are synced directly to the sender's endpoint, no cluster b > **Note**: EventCommand errors are just logged on the remote endpoint. +### event::UpdateExecutions + +> Location: `clusterevents.cpp` + +##### Message Body + +Key | Value +----------|--------- +jsonrpc | 2.0 +method | event::UpdateExecutions +params | Dictionary + +##### Params + +Key | Type | Description +---------------|---------------|------------------ +host | String | Host name. +service | String | Service name. +executions | Dictionary | Executions to be updated + +##### Functions + +**Event Sender:** `ClusterEvents::ExecutedCommandAPIHandler`, `ClusterEvents::UpdateExecutionsAPIHandler`, `ApiActions::ExecuteCommand` +**Event Receiver:** `ClusterEvents::UpdateExecutionsAPIHandler` + +##### Permissions + +The receiver will not process messages from not configured endpoints. + +Message updates will be dropped when: + +* Checkable does not exist. +* Origin endpoint's zone is not allowed to access this checkable. + +### event::ExecutedCommand + +> Location: `clusterevents.cpp` + +##### Message Body + +Key | Value +----------|--------- +jsonrpc | 2.0 +method | event::ExecutedCommand +params | Dictionary + +##### Params + +Key | Type | Description +---------------|---------------|------------------ +host | String | Host name. +service | String | Service name. +execution | String | The execution ID executed. +exitStatus | Number | The command exit status. +output | String | The command output. +start | Number | The unix timestamp at the start of the command execution +end | Number | The unix timestamp at the end of the command execution + +##### Functions + +**Event Sender:** `ClusterEvents::ExecuteCheckFromQueue`, `ClusterEvents::ExecuteCommandAPIHandler` +**Event Receiver:** `ClusterEvents::ExecutedCommandAPIHandler` + +##### Permissions + +The receiver will not process messages from not configured endpoints. + +Message updates will be dropped when: + +* Checkable does not exist. +* Origin endpoint's zone is not allowed to access this checkable. + #### config::Update > Location: `apilistener-filesync.cpp` diff --git a/lib/base/atomic.hpp b/lib/base/atomic.hpp index 0ebcddefb..62fa37638 100644 --- a/lib/base/atomic.hpp +++ b/lib/base/atomic.hpp @@ -4,6 +4,8 @@ #define ATOMIC_H #include +#include +#include namespace icinga { @@ -38,6 +40,80 @@ public: } }; +/** + * Wraps T into a std::atomic-like interface. + * + * @ingroup base + */ +template +class NotAtomic +{ +public: + inline T load() const + { + return m_Value; + } + + inline void store(T desired) + { + m_Value = std::move(desired); + } + + T m_Value; +}; + +/** + * Tells whether to use std::atomic or NotAtomic. + * + * @ingroup base + */ +template +struct Atomicable +{ + // Doesn't work with too old compilers. + //static constexpr bool value = std::is_trivially_copyable::value && sizeof(T) <= sizeof(void*); + static constexpr bool value = (std::is_fundamental::value || std::is_pointer::value) && sizeof(T) <= sizeof(void*); +}; + +/** + * Uses either std::atomic or NotAtomic depending on atomicable. + * + * @ingroup base + */ +template +struct AtomicTemplate; + +template<> +struct AtomicTemplate +{ + template + struct tmplt + { + typedef NotAtomic type; + }; +}; + +template<> +struct AtomicTemplate +{ + template + struct tmplt + { + typedef std::atomic type; + }; +}; + +/** + * Uses either std::atomic or NotAtomic depending on T. + * + * @ingroup base + */ +template +struct EventuallyAtomic +{ + typedef typename AtomicTemplate::value>::template tmplt::type type; +}; + } #endif /* ATOMIC_H */ diff --git a/lib/base/configobject.ti b/lib/base/configobject.ti index fcfcb0223..3e5859c81 100644 --- a/lib/base/configobject.ti +++ b/lib/base/configobject.ti @@ -59,10 +59,10 @@ abstract class ConfigObject : ConfigObjectBase < ConfigType [config, no_user_modify] String __name (Name); [config, no_user_modify] String "name" (ShortName) { get {{{ - if (m_ShortName.IsEmpty()) + if (m_ShortName.m_Value.IsEmpty()) return GetName(); else - return m_ShortName; + return m_ShortName.m_Value; }}} }; [config, no_user_modify] name(Zone) zone (ZoneName); diff --git a/lib/db_ido/idochecktask.cpp b/lib/db_ido/idochecktask.cpp index 8a9536dc3..d16c91b0c 100644 --- a/lib/db_ido/idochecktask.cpp +++ b/lib/db_ido/idochecktask.cpp @@ -12,15 +12,39 @@ #include "base/perfdatavalue.hpp" #include "base/configtype.hpp" #include "base/convert.hpp" +#include using namespace icinga; REGISTER_FUNCTION_NONCONST(Internal, IdoCheck, &IdoCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros"); +static void ReportIdoCheck( + const Checkable::Ptr& checkable, const CheckCommand::Ptr& commandObj, + const CheckResult::Ptr& cr, String output, ServiceState state = ServiceUnknown +) +{ + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = std::move(output); + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; + + Checkable::ExecuteCommandProcessFinishedHandler(commandObj->GetName(), pr); + } else { + cr->SetState(state); + cr->SetOutput(output); + checkable->ProcessCheckResult(cr); + } +} + void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros) { - CheckCommand::Ptr commandObj = checkable->GetCheckCommand(); + ServiceState state; + CheckCommand::Ptr commandObj = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand(); Value raw_command = commandObj->GetCommandLine(); Host::Ptr host; @@ -28,6 +52,10 @@ void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult tie(host, service) = GetHostService(checkable); MacroProcessor::ResolverList resolvers; + + if (MacroResolver::OverrideMacros) + resolvers.emplace_back("override", MacroResolver::OverrideMacros); + if (service) resolvers.emplace_back("service", service); resolvers.emplace_back("host", host); @@ -61,25 +89,19 @@ void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult return; if (idoType.IsEmpty()) { - cr->SetOutput("Attribute 'ido_type' must be set."); - cr->SetState(ServiceUnknown); - checkable->ProcessCheckResult(cr); + ReportIdoCheck(checkable, commandObj, cr, "Attribute 'ido_type' must be set."); return; } if (idoName.IsEmpty()) { - cr->SetOutput("Attribute 'ido_name' must be set."); - cr->SetState(ServiceUnknown); - checkable->ProcessCheckResult(cr); + ReportIdoCheck(checkable, commandObj, cr, "Attribute 'ido_name' must be set."); return; } Type::Ptr type = Type::GetByName(idoType); if (!type || !DbConnection::TypeInstance->IsAssignableFrom(type)) { - cr->SetOutput("DB IDO type '" + idoType + "' is invalid."); - cr->SetState(ServiceUnknown); - checkable->ProcessCheckResult(cr); + ReportIdoCheck(checkable, commandObj, cr, "DB IDO type '" + idoType + "' is invalid."); return; } @@ -89,18 +111,14 @@ void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult DbConnection::Ptr conn = static_pointer_cast(dtype->GetObject(idoName)); if (!conn) { - cr->SetOutput("DB IDO connection '" + idoName + "' does not exist."); - cr->SetState(ServiceUnknown); - checkable->ProcessCheckResult(cr); + ReportIdoCheck(checkable, commandObj, cr, "DB IDO connection '" + idoName + "' does not exist."); return; } double qps = conn->GetQueryCount(60) / 60.0; if (conn->IsPaused()) { - cr->SetOutput("DB IDO connection is temporarily disabled on this cluster instance."); - cr->SetState(ServiceOK); - checkable->ProcessCheckResult(cr); + ReportIdoCheck(checkable, commandObj, cr, "DB IDO connection is temporarily disabled on this cluster instance.", ServiceOK); return; } @@ -108,15 +126,13 @@ void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult if (!conn->GetConnected()) { if (conn->GetShouldConnect()) { - cr->SetOutput("Could not connect to the database server."); - cr->SetState(ServiceCritical); + ReportIdoCheck(checkable, commandObj, cr, "Could not connect to the database server.", ServiceCritical); } else { - cr->SetOutput("Not currently enabled: Another cluster instance is responsible for the IDO database."); - cr->SetState(ServiceOK); + ReportIdoCheck( + checkable, commandObj, cr, + "Not currently enabled: Another cluster instance is responsible for the IDO database.", ServiceOK + ); } - - checkable->ProcessCheckResult(cr); - return; } @@ -130,13 +146,13 @@ void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult << " Queries per second: " << std::fixed << std::setprecision(3) << qps << " Pending queries: " << std::fixed << std::setprecision(3) << pendingQueries << "."; - cr->SetState(ServiceWarning); + state = ServiceWarning; } else { msgbuf << "Connected to the database server (Schema version: '" << schema_version << "')." << " Queries per second: " << std::fixed << std::setprecision(3) << qps << " Pending queries: " << std::fixed << std::setprecision(3) << pendingQueries << "."; - cr->SetState(ServiceOK); + state = ServiceOK; } if (conn->GetEnableHa()) { @@ -149,27 +165,25 @@ void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult if (missingQueriesCritical.IsEmpty() && qps < queriesCritical) { msgbuf << " " << qps << " queries/s lower than critical threshold (" << queriesCritical << " queries/s)."; - cr->SetState(ServiceCritical); + state= ServiceCritical; } else if (missingQueriesWarning.IsEmpty() && qps < queriesWarning) { msgbuf << " " << qps << " queries/s lower than warning threshold (" << queriesWarning << " queries/s)."; - cr->SetState(ServiceWarning); + state = ServiceWarning; } if (missingPendingQueriesCritical.IsEmpty() && pendingQueries > pendingQueriesCritical) { msgbuf << " " << pendingQueries << " pending queries greater than critical threshold (" << pendingQueriesCritical << " queries)."; - cr->SetState(ServiceCritical); + state = ServiceCritical; } else if (missingPendingQueriesWarning.IsEmpty() && pendingQueries > pendingQueriesWarning) { msgbuf << " " << pendingQueries << " pending queries greater than warning threshold (" << pendingQueriesWarning << " queries)."; - cr->SetState(ServiceWarning); + state = ServiceWarning; } - cr->SetOutput(msgbuf.str()); - cr->SetPerformanceData(new Array({ { new PerfdataValue("queries", qps, false, "", queriesWarning, queriesCritical) }, { new PerfdataValue("queries_1min", conn->GetQueryCount(60)) }, @@ -178,5 +192,5 @@ void IdoCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult { new PerfdataValue("pending_queries", pendingQueries, false, "", pendingQueriesWarning, pendingQueriesCritical) } })); - checkable->ProcessCheckResult(cr); + ReportIdoCheck(checkable, commandObj, cr, msgbuf.str(), state); } diff --git a/lib/icinga/apiactions.cpp b/lib/icinga/apiactions.cpp index 8a7e8a9a8..afd53e981 100644 --- a/lib/icinga/apiactions.cpp +++ b/lib/icinga/apiactions.cpp @@ -8,12 +8,16 @@ #include "icinga/checkcommand.hpp" #include "icinga/eventcommand.hpp" #include "icinga/notificationcommand.hpp" +#include "icinga/clusterevents.hpp" #include "remote/apiaction.hpp" #include "remote/apilistener.hpp" +#include "remote/filterutility.hpp" #include "remote/pkiutility.hpp" #include "remote/httputility.hpp" #include "base/utility.hpp" #include "base/convert.hpp" +#include "base/defer.hpp" +#include "remote/actionshandler.hpp" #include using namespace icinga; @@ -31,6 +35,7 @@ REGISTER_APIACTION(remove_downtime, "Service;Host;Downtime", &ApiActions::Remove 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) @@ -550,3 +555,328 @@ Dictionary::Ptr ApiActions::GenerateTicket(const ConfigObject::Ptr&, return ApiActions::CreateResult(200, "Generated PKI ticket '" + ticket + "' for common name '" + cn + "'.", additional); } + +Value ApiActions::GetSingleObjectByNameUsingPermissions(const String& type, const String& objectName, const ApiUser::Ptr& user) +{ + Dictionary::Ptr queryParams = new Dictionary(); + queryParams->Set("type", type); + queryParams->Set(type.ToLower(), objectName); + + QueryDescription qd; + qd.Types.insert(type); + qd.Permission = "objects/query/" + type; + + std::vector objs; + + try { + objs = FilterUtility::GetFilterTargets(qd, queryParams, user); + } catch (const std::exception& ex) { + Log(LogWarning, "ApiActions") << DiagnosticInformation(ex); + return nullptr; + } + + if (objs.empty()) + return nullptr; + + return objs.at(0); +}; + +Dictionary::Ptr ApiActions::ExecuteCommand(const ConfigObject::Ptr& object, const Dictionary::Ptr& params) +{ + ApiListener::Ptr listener = ApiListener::GetInstance(); + + if (!listener) + BOOST_THROW_EXCEPTION(std::invalid_argument("No ApiListener instance configured.")); + + /* Get command_type */ + String command_type = "EventCommand"; + + if (params->Contains("command_type")) + command_type = HttpUtility::GetLastParameter(params, "command_type"); + + /* Validate command_type */ + if (command_type != "EventCommand" && command_type != "CheckCommand" && command_type != "NotificationCommand") + return ApiActions::CreateResult(400, "Invalid command_type '" + command_type + "'."); + + Checkable::Ptr checkable = dynamic_pointer_cast(object); + + if (!checkable) + return ApiActions::CreateResult(404, "Can't start a command execution for a non-existent object."); + + /* Get TTL param */ + if (!params->Contains("ttl")) + return ApiActions::CreateResult(400, "Parameter ttl is required."); + + double ttl = HttpUtility::GetLastParameter(params, "ttl"); + + if (ttl <= 0) + return ApiActions::CreateResult(400, "Parameter ttl must be greater than 0."); + + ObjectLock oLock (checkable); + + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); + + String endpoint = "$command_endpoint$"; + + if (params->Contains("endpoint")) + endpoint = HttpUtility::GetLastParameter(params, "endpoint"); + + MacroProcessor::ResolverList resolvers; + Value macros; + + if (params->Contains("macros")) { + macros = HttpUtility::GetLastParameter(params, "macros"); + if (macros.IsObjectType()) { + resolvers.emplace_back("override", macros); + } else { + return ApiActions::CreateResult(400, "Parameter macros must be a dictionary."); + } + } + + if (service) + resolvers.emplace_back("service", service); + + resolvers.emplace_back("host", host); + resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); + + String resolved_endpoint = MacroProcessor::ResolveMacros( + endpoint, resolvers, checkable->GetLastCheckResult(), + nullptr, MacroProcessor::EscapeCallback(), nullptr, false + ); + + if (!ActionsHandler::AuthenticatedApiUser) + BOOST_THROW_EXCEPTION(std::invalid_argument("Can't find API user.")); + + /* Get endpoint */ + Endpoint::Ptr endpointPtr = GetSingleObjectByNameUsingPermissions(Endpoint::GetTypeName(), resolved_endpoint, ActionsHandler::AuthenticatedApiUser); + + if (!endpointPtr) + return ApiActions::CreateResult(404, "Can't find a valid endpoint for '" + resolved_endpoint + "'."); + + /* Get command */ + String command; + + if (!params->Contains("command")) { + if (command_type == "CheckCommand" ) { + command = "$check_command$"; + } else if (command_type == "EventCommand") { + command = "$event_command$"; + } else if (command_type == "NotificationCommand") { + command = "$notification_command$"; + } + } else { + command = HttpUtility::GetLastParameter(params, "command"); + } + + /* Resolve command macro */ + String resolved_command = MacroProcessor::ResolveMacros( + command, resolvers, checkable->GetLastCheckResult(), nullptr, + MacroProcessor::EscapeCallback(), nullptr, false + ); + + CheckResult::Ptr cr = checkable->GetLastCheckResult(); + + if (!cr) + cr = new CheckResult(); + + /* Check if resolved_command exists and it is of type command_type */ + Dictionary::Ptr execMacros = new Dictionary(); + + MacroResolver::OverrideMacros = macros; + Defer o ([]() { + MacroResolver::OverrideMacros = nullptr; + }); + + /* Create execution parameters */ + Dictionary::Ptr execParams = new Dictionary(); + + if (command_type == "CheckCommand") { + CheckCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(CheckCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser); + + if (!cmd) + return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'."); + else { + CheckCommand::ExecuteOverride = cmd; + Defer resetCheckCommandOverride([]() { + CheckCommand::ExecuteOverride = nullptr; + }); + cmd->Execute(checkable, cr, execMacros, false); + } + } else if (command_type == "EventCommand") { + EventCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(EventCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser); + + if (!cmd) + return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'."); + else { + EventCommand::ExecuteOverride = cmd; + Defer resetEventCommandOverride ([]() { + EventCommand::ExecuteOverride = nullptr; + }); + cmd->Execute(checkable, execMacros, false); + } + } else if (command_type == "NotificationCommand") { + NotificationCommand::Ptr cmd = GetSingleObjectByNameUsingPermissions(NotificationCommand::GetTypeName(), resolved_command, ActionsHandler::AuthenticatedApiUser); + + if (!cmd) + return ApiActions::CreateResult(404, "Can't find a valid " + command_type + " for '" + resolved_command + "'."); + else { + /* Get user */ + String user_string = ""; + + if (params->Contains("user")) + user_string = HttpUtility::GetLastParameter(params, "user"); + + /* Resolve user macro */ + String resolved_user = MacroProcessor::ResolveMacros( + user_string, resolvers, checkable->GetLastCheckResult(), nullptr, + MacroProcessor::EscapeCallback(), nullptr, false + ); + + User::Ptr user = GetSingleObjectByNameUsingPermissions(User::GetTypeName(), resolved_user, ActionsHandler::AuthenticatedApiUser); + + if (!user) + return ApiActions::CreateResult(404, "Can't find a valid user for '" + resolved_user + "'."); + + execParams->Set("user", user->GetName()); + + /* Get notification */ + String notification_string = ""; + + if (params->Contains("notification")) + notification_string = HttpUtility::GetLastParameter(params, "notification"); + + /* Resolve notification macro */ + String resolved_notification = MacroProcessor::ResolveMacros( + notification_string, resolvers, checkable->GetLastCheckResult(), nullptr, + MacroProcessor::EscapeCallback(), nullptr, false + ); + + Notification::Ptr notification = GetSingleObjectByNameUsingPermissions(Notification::GetTypeName(), resolved_notification, ActionsHandler::AuthenticatedApiUser); + + if (!notification) + return ApiActions::CreateResult(404, "Can't find a valid notification for '" + resolved_notification + "'."); + + execParams->Set("notification", notification->GetName()); + + NotificationCommand::ExecuteOverride = cmd; + Defer resetNotificationCommandOverride ([]() { + NotificationCommand::ExecuteOverride = nullptr; + }); + + cmd->Execute(notification, user, cr, NotificationType::NotificationCustom, + ActionsHandler::AuthenticatedApiUser->GetName(), "", execMacros, false); + } + } + + /* This generates a UUID */ + String uuid = Utility::NewUniqueID(); + + /* Create the deadline */ + double deadline = Utility::GetTime() + ttl; + + /* Update executions */ + Dictionary::Ptr pending_execution = new Dictionary(); + pending_execution->Set("pending", true); + pending_execution->Set("deadline", deadline); + Dictionary::Ptr executions = checkable->GetExecutions(); + + if (!executions) + executions = new Dictionary(); + + executions->Set(uuid, pending_execution); + checkable->SetExecutions(executions); + + /* Broadcast the update */ + Dictionary::Ptr executionsToBroadcast = new Dictionary(); + executionsToBroadcast->Set(uuid, pending_execution); + Dictionary::Ptr updateParams = new Dictionary(); + updateParams->Set("host", host->GetName()); + + if (service) + updateParams->Set("service", service->GetShortName()); + + updateParams->Set("executions", executionsToBroadcast); + + Dictionary::Ptr updateMessage = new Dictionary(); + updateMessage->Set("jsonrpc", "2.0"); + updateMessage->Set("method", "event::UpdateExecutions"); + updateMessage->Set("params", updateParams); + + MessageOrigin::Ptr origin = new MessageOrigin(); + listener->RelayMessage(origin, checkable, updateMessage, true); + + /* Populate execution parameters */ + if (command_type == "CheckCommand") + execParams->Set("command_type", "check_command"); + else if (command_type == "EventCommand") + execParams->Set("command_type", "event_command"); + else if (command_type == "NotificationCommand") + execParams->Set("command_type", "notification_command"); + + execParams->Set("command", resolved_command); + execParams->Set("host", host->GetName()); + + if (service) + execParams->Set("service", service->GetShortName()); + + /* + * If the host/service object specifies the 'check_timeout' attribute, + * forward this to the remote endpoint to limit the command execution time. + */ + if (!checkable->GetCheckTimeout().IsEmpty()) + execParams->Set("check_timeout", checkable->GetCheckTimeout()); + + execParams->Set("source", uuid); + execParams->Set("deadline", deadline); + execParams->Set("macros", execMacros); + execParams->Set("endpoint", resolved_endpoint); + + /* Execute command */ + bool local = endpointPtr == Endpoint::GetLocalEndpoint(); + + if (local) { + ClusterEvents::ExecuteCommandAPIHandler(origin, execParams); + } else { + /* Check if the child endpoints have Icinga version >= 2.13 */ + Zone::Ptr localZone = Zone::GetLocalZone(); + for (const Zone::Ptr& zone : ConfigType::GetObjectsByType()) { + /* Fetch immediate child zone members */ + if (zone->GetParent() == localZone && zone->CanAccessObject(endpointPtr->GetZone())) { + std::set endpoints = zone->GetEndpoints(); + + for (const Endpoint::Ptr& childEndpoint : endpoints) { + if (!(childEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand)) { + /* Update execution */ + double now = Utility::GetTime(); + pending_execution->Set("exit", 126); + pending_execution->Set("output", "Endpoint '" + childEndpoint->GetName() + "' doesn't support executing arbitrary commands."); + pending_execution->Set("start", now); + pending_execution->Set("end", now); + pending_execution->Remove("pending"); + + listener->RelayMessage(origin, checkable, updateMessage, true); + + Dictionary::Ptr result = new Dictionary(); + result->Set("checkable", checkable->GetName()); + result->Set("execution", uuid); + return ApiActions::CreateResult(202, "Accepted", result); + } + } + } + } + + Dictionary::Ptr execMessage = new Dictionary(); + execMessage->Set("jsonrpc", "2.0"); + execMessage->Set("method", "event::ExecuteCommand"); + execMessage->Set("params", execParams); + + listener->RelayMessage(origin, endpointPtr->GetZone(), execMessage, true); + } + + Dictionary::Ptr result = new Dictionary(); + result->Set("checkable", checkable->GetName()); + result->Set("execution", uuid); + return ApiActions::CreateResult(202, "Accepted", result); +} diff --git a/lib/icinga/apiactions.hpp b/lib/icinga/apiactions.hpp index c2c8b6188..b6ba83500 100644 --- a/lib/icinga/apiactions.hpp +++ b/lib/icinga/apiactions.hpp @@ -6,6 +6,7 @@ #include "icinga/i2-icinga.hpp" #include "base/configobject.hpp" #include "base/dictionary.hpp" +#include "remote/apiuser.hpp" namespace icinga { @@ -29,9 +30,11 @@ public: 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); }; } diff --git a/lib/icinga/checkable.cpp b/lib/icinga/checkable.cpp index 8f21e71f1..93aa7bb52 100644 --- a/lib/icinga/checkable.cpp +++ b/lib/icinga/checkable.cpp @@ -20,6 +20,9 @@ boost::signals2::signal Checkable::OnFlappingChange; static Timer::Ptr l_CheckablesFireSuppressedNotifications; +static Timer::Ptr l_CleanDeadlinedExecutions; + +thread_local std::function Checkable::ExecuteCommandProcessFinishedHandler; void Checkable::StaticInitialize() { @@ -86,6 +89,11 @@ void Checkable::Start(bool runtimeCreated) l_CheckablesFireSuppressedNotifications->SetInterval(5); l_CheckablesFireSuppressedNotifications->OnTimerExpired.connect(&Checkable::FireSuppressedNotifications); l_CheckablesFireSuppressedNotifications->Start(); + + l_CleanDeadlinedExecutions = new Timer(); + l_CleanDeadlinedExecutions->SetInterval(300); + l_CleanDeadlinedExecutions->OnTimerExpired.connect(&Checkable::CleanDeadlinedExecutions); + l_CleanDeadlinedExecutions->Start(); }); } @@ -265,3 +273,34 @@ void Checkable::ValidateMaxCheckAttempts(const Lazy& lvalue, const Validati if (lvalue() <= 0) BOOST_THROW_EXCEPTION(ValidationError(this, { "max_check_attempts" }, "Value must be greater than 0.")); } + +void Checkable::CleanDeadlinedExecutions(const Timer * const&) +{ + double now = Utility::GetTime(); + Dictionary::Ptr executions; + Dictionary::Ptr execution; + + for (auto& host : ConfigType::GetObjectsByType()) { + executions = host->GetExecutions(); + if (executions) { + for (const String& key : executions->GetKeys()) { + execution = executions->Get(key); + if (execution->Contains("deadline") && now > execution->Get("deadline")) { + executions->Remove(key); + } + } + } + } + + for (auto& service : ConfigType::GetObjectsByType()) { + executions = service->GetExecutions(); + if (executions) { + for (const String& key : executions->GetKeys()) { + execution = executions->Get(key); + if (execution->Contains("deadline") && now > execution->Get("deadline")) { + executions->Remove(key); + } + } + } + } +} diff --git a/lib/icinga/checkable.hpp b/lib/icinga/checkable.hpp index dc782aca5..c9799f07a 100644 --- a/lib/icinga/checkable.hpp +++ b/lib/icinga/checkable.hpp @@ -5,6 +5,7 @@ #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" @@ -14,6 +15,7 @@ #include "remote/endpoint.hpp" #include "remote/messageorigin.hpp" #include +#include namespace icinga { @@ -55,6 +57,7 @@ public: DECLARE_OBJECTNAME(Checkable); static void StaticInitialize(); + static thread_local std::function ExecuteCommandProcessFinishedHandler; Checkable(); @@ -205,6 +208,7 @@ private: static void NotifyDowntimeEnd(const Downtime::Ptr& downtime); static void FireSuppressedNotifications(const Timer * const&); + static void CleanDeadlinedExecutions(const Timer * const&); /* Comments */ std::set m_Comments; diff --git a/lib/icinga/checkable.ti b/lib/icinga/checkable.ti index ad34f6d42..1715899c0 100644 --- a/lib/icinga/checkable.ti +++ b/lib/icinga/checkable.ti @@ -175,6 +175,9 @@ abstract class Checkable : CustomVarObject 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 index e0da41547..fb8032a19 100644 --- a/lib/icinga/checkcommand.cpp +++ b/lib/icinga/checkcommand.cpp @@ -8,6 +8,8 @@ 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) { diff --git a/lib/icinga/checkcommand.hpp b/lib/icinga/checkcommand.hpp index 6eb6119a3..eb8f5a012 100644 --- a/lib/icinga/checkcommand.hpp +++ b/lib/icinga/checkcommand.hpp @@ -20,6 +20,8 @@ public: DECLARE_OBJECT(CheckCommand); DECLARE_OBJECTNAME(CheckCommand); + static thread_local CheckCommand::Ptr ExecuteOverride; + virtual void Execute(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const Dictionary::Ptr& resolvedMacros = nullptr, bool useResolvedMacros = false); diff --git a/lib/icinga/clusterevents-check.cpp b/lib/icinga/clusterevents-check.cpp index 530234c6b..30745dbd7 100644 --- a/lib/icinga/clusterevents-check.cpp +++ b/lib/icinga/clusterevents-check.cpp @@ -4,6 +4,7 @@ #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 @@ -75,44 +76,127 @@ void ClusterEvents::EnqueueCheck(const MessageOrigin::Ptr& origin, const Diction } } +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 = origin->FromClient->GetEndpoint(); + 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)."; + << "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."); + Log(LogCritical, "ApiListener") << "No instance available."; return; } - if (!listener->GetAcceptCommands()) { - Log(LogWarning, "ApiListener") - << "Ignoring command. '" << listener->GetName() << "' does not accept commands."; + Defer resetExecuteCommandProcessFinishedHandler ([]() { + Checkable::ExecuteCommandProcessFinishedHandler = nullptr; + }); - Host::Ptr host = new Host(); - Dictionary::Ptr attrs = new Dictionary(); + if (params->Contains("source")) { + String uuid = params->Get("source"); - attrs->Set("__name", params->Get("host")); - attrs->Set("type", "Host"); - attrs->Set("enable_active_checks", false); - - Deserialize(host, attrs, false, FAConfig); + String checkableName = params->Get("host"); if (params->Contains("service")) - host->SetExtension("agent_service_name", params->Get("service")); + checkableName += "!" + params->Get("service"); - CheckResult::Ptr cr = new CheckResult(); - cr->SetState(ServiceUnknown); - cr->SetOutput("Endpoint '" + Endpoint::GetLocalEndpoint()->GetName() + "' does not accept commands."); - Dictionary::Ptr message = MakeCheckResultMessage(host, cr); - listener->SyncSendMessage(sourceEndpoint, message); + /* 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; } @@ -142,21 +226,47 @@ void ClusterEvents::ExecuteCheckFromQueue(const MessageOrigin::Ptr& origin, cons if (command_type == "check_command") { if (!CheckCommand::GetByName(command)) { - CheckResult::Ptr cr = new CheckResult(); - cr->SetState(ServiceUnknown); - cr->SetOutput("Check command '" + command + "' does not exist."); - Dictionary::Ptr message = MakeCheckResultMessage(host, cr); - listener->SyncSendMessage(sourceEndpoint, message); + 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)) { - Log(LogWarning, "ClusterEvents") - << "Event command '" << command << "' does not exist."; + 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 - 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()); @@ -171,25 +281,74 @@ void ClusterEvents::ExecuteCheckFromQueue(const MessageOrigin::Ptr& origin, cons try { host->ExecuteRemoteCheck(macros); } catch (const std::exception& ex) { - CheckResult::Ptr cr = new CheckResult(); - cr->SetState(ServiceUnknown); - String output = "Exception occurred while checking '" + host->GetName() + "': " + DiagnosticInformation(ex); - cr->SetOutput(output); - + ServiceState state = ServiceUnknown; double now = Utility::GetTime(); - cr->SetScheduleStart(now); - cr->SetScheduleEnd(now); - cr->SetExecutionStart(now); - cr->SetExecutionEnd(now); - Dictionary::Ptr message = MakeCheckResultMessage(host, cr); - listener->SyncSendMessage(sourceEndpoint, message); + 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") { - host->ExecuteEventHandler(macros, true); + 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); + } } } diff --git a/lib/icinga/clusterevents.cpp b/lib/icinga/clusterevents.cpp index c9112eb0d..e8f9bde2a 100644 --- a/lib/icinga/clusterevents.cpp +++ b/lib/icinga/clusterevents.cpp @@ -36,6 +36,8 @@ REGISTER_APIFUNCTION(ExecuteCommand, event, &ClusterEvents::ExecuteCommandAPIHan 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); void ClusterEvents::StaticInitialize() { @@ -729,6 +731,66 @@ Value ClusterEvents::AcknowledgementClearedAPIHandler(const MessageOrigin::Ptr& Value ClusterEvents::ExecuteCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params) { + ApiListener::Ptr listener = ApiListener::GetInstance(); + + if (!listener) + return Empty; + + if (params->Contains("endpoint")) { + Endpoint::Ptr execEndpoint = Endpoint::GetByName(params->Get("endpoint")); + + if (execEndpoint != Endpoint::GetLocalEndpoint()) { + Zone::Ptr endpointZone = execEndpoint->GetZone(); + Zone::Ptr localZone = Zone::GetLocalZone(); + + if (!endpointZone->IsChildOf(localZone)) { + return Empty; + } + + /* Check if the child endpoints have Icinga version >= 2.13 */ + for (const Zone::Ptr &zone : ConfigType::GetObjectsByType()) { + /* Fetch immediate child zone members */ + if (zone->GetParent() == localZone && zone->CanAccessObject(endpointZone)) { + std::set endpoints = zone->GetEndpoints(); + + for (const Endpoint::Ptr &childEndpoint : endpoints) { + if (!(childEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand)) { + double now = Utility::GetTime(); + Dictionary::Ptr executedParams = new Dictionary(); + executedParams->Set("execution", params->Get("source")); + executedParams->Set("host", params->Get("host")); + + if (params->Contains("service")) + executedParams->Set("service", params->Get("service")); + + executedParams->Set("exit", 126); + executedParams->Set("output", + "Endpoint '" + childEndpoint->GetName() + "' doesn't support executing arbitrary commands."); + executedParams->Set("start", now); + executedParams->Set("end", now); + + Dictionary::Ptr executedMessage = new Dictionary(); + executedMessage->Set("jsonrpc", "2.0"); + executedMessage->Set("method", "event::ExecutedCommand"); + executedMessage->Set("params", executedParams); + + listener->RelayMessage(nullptr, nullptr, executedMessage, true); + return Empty; + } + } + } + } + + Dictionary::Ptr execMessage = new Dictionary(); + execMessage->Set("jsonrpc", "2.0"); + execMessage->Set("method", "event::ExecuteCommand"); + execMessage->Set("params", params); + + listener->RelayMessage(origin, endpointZone, execMessage, true); + return Empty; + } + } + EnqueueCheck(origin, params); return Empty; @@ -1051,3 +1113,172 @@ Value ClusterEvents::NotificationSentToAllUsersAPIHandler(const MessageOrigin::P return Empty; } + +Value ClusterEvents::ExecutedCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params) +{ + ApiListener::Ptr listener = ApiListener::GetInstance(); + + if (!listener) + return Empty; + + Endpoint::Ptr endpoint; + + if (origin->FromClient) { + endpoint = origin->FromClient->GetEndpoint(); + } else if (origin->IsLocal()) { + endpoint = Endpoint::GetLocalEndpoint(); + } + + if (!endpoint) { + Log(LogNotice, "ClusterEvents") + << "Discarding 'update executions API handler' message from '" << origin->FromClient->GetIdentity() + << "': Invalid endpoint origin (client not allowed)."; + + return Empty; + } + + Host::Ptr host = Host::GetByName(params->Get("host")); + + if (!host) + return Empty; + + Checkable::Ptr checkable; + + if (params->Contains("service")) + checkable = host->GetServiceByShortName(params->Get("service")); + else + checkable = host; + + if (!checkable) + return Empty; + + ObjectLock oLock (checkable); + + if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) { + Log(LogNotice, "ClusterEvents") + << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName() + << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access."; + return Empty; + } + + if (!params->Contains("execution")) { + Log(LogNotice, "ClusterEvents") + << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName() + << "' from '" << origin->FromClient->GetIdentity() << "': Execution UUID missing."; + return Empty; + } + + String uuid = params->Get("execution"); + + Dictionary::Ptr executions = checkable->GetExecutions(); + + if (!executions) { + Log(LogNotice, "ClusterEvents") + << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName() + << "' from '" << origin->FromClient->GetIdentity() << "': Execution '" << uuid << "' missing."; + return Empty; + } + + Dictionary::Ptr execution = executions->Get(uuid); + + if (!execution) { + Log(LogNotice, "ClusterEvents") + << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName() + << "' from '" << origin->FromClient->GetIdentity() << "': Execution '" << uuid << "' missing."; + return Empty; + } + + if (params->Contains("exit")) + execution->Set("exit", params->Get("exit")); + + if (params->Contains("output")) + execution->Set("output", params->Get("output")); + + if (params->Contains("start")) + execution->Set("start", params->Get("start")); + + if (params->Contains("end")) + execution->Set("end", params->Get("end")); + + execution->Remove("pending"); + + /* Broadcast the update */ + Dictionary::Ptr executionsToBroadcast = new Dictionary(); + executionsToBroadcast->Set(uuid, execution); + Dictionary::Ptr updateParams = new Dictionary(); + updateParams->Set("host", host->GetName()); + + if (params->Contains("service")) + updateParams->Set("service", params->Get("service")); + + updateParams->Set("executions", executionsToBroadcast); + + Dictionary::Ptr updateMessage = new Dictionary(); + updateMessage->Set("jsonrpc", "2.0"); + updateMessage->Set("method", "event::UpdateExecutions"); + updateMessage->Set("params", updateParams); + + listener->RelayMessage(nullptr, checkable, updateMessage, true); + + return Empty; +} + +Value ClusterEvents::UpdateExecutionsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params) +{ + Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint(); + + if (!endpoint) { + Log(LogNotice, "ClusterEvents") + << "Discarding 'update executions API handler' message from '" << origin->FromClient->GetIdentity() + << "': Invalid endpoint origin (client not allowed)."; + + return Empty; + } + + Host::Ptr host = Host::GetByName(params->Get("host")); + + if (!host) + return Empty; + + Checkable::Ptr checkable; + + if (params->Contains("service")) + checkable = host->GetServiceByShortName(params->Get("service")); + else + checkable = host; + + if (!checkable) + return Empty; + + ObjectLock oLock (checkable); + + if (origin->FromZone && !origin->FromZone->CanAccessObject(checkable)) { + Log(LogNotice, "ClusterEvents") + << "Discarding 'update executions API handler' message for checkable '" << checkable->GetName() + << "' from '" << origin->FromClient->GetIdentity() << "': Unauthorized access."; + return Empty; + } + + Dictionary::Ptr executions = checkable->GetExecutions(); + + if (!executions) + executions = new Dictionary(); + + Dictionary::Ptr newExecutions = params->Get("executions"); + newExecutions->CopyTo(executions); + checkable->SetExecutions(executions); + + ApiListener::Ptr listener = ApiListener::GetInstance(); + + if (!listener) + return Empty; + + Dictionary::Ptr updateMessage = new Dictionary(); + updateMessage->Set("jsonrpc", "2.0"); + updateMessage->Set("method", "event::UpdateExecutions"); + updateMessage->Set("params", params); + + listener->RelayMessage(origin, Zone::GetLocalZone(), updateMessage, true); + + return Empty; +} diff --git a/lib/icinga/clusterevents.hpp b/lib/icinga/clusterevents.hpp index 174a03d44..1620b26b2 100644 --- a/lib/icinga/clusterevents.hpp +++ b/lib/icinga/clusterevents.hpp @@ -66,6 +66,8 @@ public: static void NotificationSentToAllUsersHandler(const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set& users, NotificationType notificationType, const CheckResult::Ptr& cr, const String& author, const String& commentText, const MessageOrigin::Ptr& origin); static Value NotificationSentToAllUsersAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params); + static Value ExecutedCommandAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params); + static Value UpdateExecutionsAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params); static int GetCheckRequestQueueSize(); static void LogRemoteCheckQueueInformation(); diff --git a/lib/icinga/eventcommand.cpp b/lib/icinga/eventcommand.cpp index f9ab3be19..39f2d3126 100644 --- a/lib/icinga/eventcommand.cpp +++ b/lib/icinga/eventcommand.cpp @@ -7,6 +7,8 @@ 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) { diff --git a/lib/icinga/eventcommand.hpp b/lib/icinga/eventcommand.hpp index 95bd1095a..064cb5ad0 100644 --- a/lib/icinga/eventcommand.hpp +++ b/lib/icinga/eventcommand.hpp @@ -20,6 +20,8 @@ public: DECLARE_OBJECT(EventCommand); DECLARE_OBJECTNAME(EventCommand); + static thread_local EventCommand::Ptr ExecuteOverride; + virtual void Execute(const Checkable::Ptr& checkable, const Dictionary::Ptr& resolvedMacros = nullptr, bool useResolvedMacros = false); diff --git a/lib/icinga/host.ti b/lib/icinga/host.ti index 03c606228..1c8a6c9fe 100644 --- a/lib/icinga/host.ti +++ b/lib/icinga/host.ti @@ -21,10 +21,10 @@ class Host : Checkable [config] String display_name { get {{{ - if (m_DisplayName.IsEmpty()) + if (m_DisplayName.m_Value.IsEmpty()) return GetName(); else - return m_DisplayName; + return m_DisplayName.m_Value; }}} }; diff --git a/lib/icinga/hostgroup.ti b/lib/icinga/hostgroup.ti index a4404eafe..efcb45e54 100644 --- a/lib/icinga/hostgroup.ti +++ b/lib/icinga/hostgroup.ti @@ -11,10 +11,10 @@ class HostGroup : CustomVarObject { [config] String display_name { get {{{ - if (m_DisplayName.IsEmpty()) + if (m_DisplayName.m_Value.IsEmpty()) return GetName(); else - return m_DisplayName; + return m_DisplayName.m_Value; }}} }; diff --git a/lib/icinga/macroprocessor.cpp b/lib/icinga/macroprocessor.cpp index df1e41d17..b72e2f0b3 100644 --- a/lib/icinga/macroprocessor.cpp +++ b/lib/icinga/macroprocessor.cpp @@ -15,6 +15,8 @@ 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, diff --git a/lib/icinga/macroresolver.hpp b/lib/icinga/macroresolver.hpp index ea29ce1e4..62cd41d3c 100644 --- a/lib/icinga/macroresolver.hpp +++ b/lib/icinga/macroresolver.hpp @@ -21,6 +21,8 @@ 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; }; diff --git a/lib/icinga/notificationcommand.cpp b/lib/icinga/notificationcommand.cpp index 8ae3e82a5..d4a5fd6ab 100644 --- a/lib/icinga/notificationcommand.cpp +++ b/lib/icinga/notificationcommand.cpp @@ -7,6 +7,8 @@ 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, diff --git a/lib/icinga/notificationcommand.hpp b/lib/icinga/notificationcommand.hpp index 210c91e86..f0f6899e3 100644 --- a/lib/icinga/notificationcommand.hpp +++ b/lib/icinga/notificationcommand.hpp @@ -22,6 +22,8 @@ public: DECLARE_OBJECT(NotificationCommand); DECLARE_OBJECTNAME(NotificationCommand); + static thread_local NotificationCommand::Ptr ExecuteOverride; + virtual Dictionary::Ptr Execute(const intrusive_ptr& notification, const User::Ptr& user, const CheckResult::Ptr& cr, const NotificationType& type, const String& author, const String& comment, diff --git a/lib/icinga/service.ti b/lib/icinga/service.ti index bab1ebc8f..f76750ef4 100644 --- a/lib/icinga/service.ti +++ b/lib/icinga/service.ti @@ -33,10 +33,10 @@ class Service : Checkable < ServiceNameComposer [config] String display_name { get {{{ - if (m_DisplayName.IsEmpty()) + if (m_DisplayName.m_Value.IsEmpty()) return GetShortName(); else - return m_DisplayName; + return m_DisplayName.m_Value; }}} }; [config, required] name(Host) host_name; diff --git a/lib/icinga/servicegroup.ti b/lib/icinga/servicegroup.ti index 69f300508..69a0887b9 100644 --- a/lib/icinga/servicegroup.ti +++ b/lib/icinga/servicegroup.ti @@ -11,10 +11,10 @@ class ServiceGroup : CustomVarObject { [config] String display_name { get {{{ - if (m_DisplayName.IsEmpty()) + if (m_DisplayName.m_Value.IsEmpty()) return GetName(); else - return m_DisplayName; + return m_DisplayName.m_Value; }}} }; diff --git a/lib/icinga/timeperiod.ti b/lib/icinga/timeperiod.ti index 27365988e..ccfbcc3d4 100644 --- a/lib/icinga/timeperiod.ti +++ b/lib/icinga/timeperiod.ti @@ -12,10 +12,10 @@ class TimePeriod : CustomVarObject { [config] String display_name { get {{{ - if (m_DisplayName.IsEmpty()) + if (m_DisplayName.m_Value.IsEmpty()) return GetName(); else - return m_DisplayName; + return m_DisplayName.m_Value; }}} }; [config] Dictionary::Ptr ranges; diff --git a/lib/icinga/user.ti b/lib/icinga/user.ti index 450d95358..5de35b523 100644 --- a/lib/icinga/user.ti +++ b/lib/icinga/user.ti @@ -13,10 +13,10 @@ class User : CustomVarObject { [config] String display_name { get {{{ - if (m_DisplayName.IsEmpty()) + if (m_DisplayName.m_Value.IsEmpty()) return GetName(); else - return m_DisplayName; + return m_DisplayName.m_Value; }}} }; [config, no_user_modify, required] array(name(UserGroup)) groups { diff --git a/lib/icinga/usergroup.ti b/lib/icinga/usergroup.ti index 311932171..35c7ccff4 100644 --- a/lib/icinga/usergroup.ti +++ b/lib/icinga/usergroup.ti @@ -11,10 +11,10 @@ class UserGroup : CustomVarObject { [config] String display_name { get {{{ - if (m_DisplayName.IsEmpty()) + if (m_DisplayName.m_Value.IsEmpty()) return GetName(); else - return m_DisplayName; + return m_DisplayName.m_Value; }}} }; diff --git a/lib/methods/clusterchecktask.cpp b/lib/methods/clusterchecktask.cpp index a9be0414d..6ce28cac4 100644 --- a/lib/methods/clusterchecktask.cpp +++ b/lib/methods/clusterchecktask.cpp @@ -28,46 +28,72 @@ void ClusterCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRe if (resolvedMacros && !useResolvedMacros) return; - CheckCommand::Ptr command = checkable->GetCheckCommand(); - cr->SetCommand(command->GetName()); + CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand(); + String commandName = command->GetName(); ApiListener::Ptr listener = ApiListener::GetInstance(); - if (!listener) { - cr->SetOutput("No API listener is configured for this instance."); - cr->SetState(ServiceUnknown); - checkable->ProcessCheckResult(cr); + String output = "No API listener is configured for this instance."; + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = 126; + pr.Output = output; + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + cr->SetOutput(output); + cr->SetState(ServiceUnknown); + checkable->ProcessCheckResult(cr); + } + return; } std::pair stats = listener->GetStatus(); - Dictionary::Ptr status = stats.first; - - /* use feature stats perfdata */ - std::pair feature_stats = CIB::GetFeatureStats(); - cr->SetPerformanceData(feature_stats.second); - int numConnEndpoints = status->Get("num_conn_endpoints"); int numNotConnEndpoints = status->Get("num_not_conn_endpoints"); + ServiceState state; String output = "Icinga 2 Cluster"; if (numNotConnEndpoints > 0) { output += " Problem: " + Convert::ToString(numNotConnEndpoints) + " endpoints are not connected."; output += "\n(" + FormatArray(status->Get("not_conn_endpoints")) + ")"; - cr->SetState(ServiceCritical); + state = ServiceCritical; } else { output += " OK: " + Convert::ToString(numConnEndpoints) + " endpoints are connected."; output += "\n(" + FormatArray(status->Get("conn_endpoints")) + ")"; - cr->SetState(ServiceOK); + state = ServiceOK; } - cr->SetOutput(output); + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; - checkable->ProcessCheckResult(cr); + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + /* use feature stats perfdata */ + std::pair feature_stats = CIB::GetFeatureStats(); + cr->SetPerformanceData(feature_stats.second); + + cr->SetCommand(commandName); + cr->SetState(state); + cr->SetOutput(output); + + checkable->ProcessCheckResult(cr); + } } String ClusterCheckTask::FormatArray(const Array::Ptr& arr) diff --git a/lib/methods/clusterzonechecktask.cpp b/lib/methods/clusterzonechecktask.cpp index c955b7dd9..c5b6bbbf3 100644 --- a/lib/methods/clusterzonechecktask.cpp +++ b/lib/methods/clusterzonechecktask.cpp @@ -21,15 +21,33 @@ void ClusterZoneCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const Che REQUIRE_NOT_NULL(cr); ApiListener::Ptr listener = ApiListener::GetInstance(); + CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand(); + String commandName = command->GetName(); if (!listener) { - cr->SetOutput("No API listener is configured for this instance."); - cr->SetState(ServiceUnknown); - checkable->ProcessCheckResult(cr); + String output = "No API listener is configured for this instance."; + ServiceState state = ServiceUnknown; + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; + + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + cr->SetCommand(commandName); + cr->SetOutput(output); + cr->SetState(state); + checkable->ProcessCheckResult(cr); + } + return; } - CheckCommand::Ptr command = checkable->GetCheckCommand(); Value raw_command = command->GetCommandLine(); Host::Ptr host; @@ -37,6 +55,10 @@ void ClusterZoneCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const Che tie(host, service) = GetHostService(checkable); MacroProcessor::ResolverList resolvers; + + if (MacroResolver::OverrideMacros) + resolvers.emplace_back("override", MacroResolver::OverrideMacros); + if (service) resolvers.emplace_back("service", service); resolvers.emplace_back("host", host); @@ -58,21 +80,52 @@ void ClusterZoneCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const Che if (resolvedMacros && !useResolvedMacros) return; - cr->SetCommand(command->GetName()); - if (zoneName.IsEmpty()) { - cr->SetOutput("Macro 'cluster_zone' must be set."); - cr->SetState(ServiceUnknown); - checkable->ProcessCheckResult(cr); + String output = "Macro 'cluster_zone' must be set."; + ServiceState state = ServiceUnknown; + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; + + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + cr->SetCommand(commandName); + cr->SetOutput(output); + cr->SetState(state); + checkable->ProcessCheckResult(cr); + } + return; } Zone::Ptr zone = Zone::GetByName(zoneName); if (!zone) { - cr->SetOutput("Zone '" + zoneName + "' does not exist."); - cr->SetState(ServiceUnknown); - checkable->ProcessCheckResult(cr); + String output = "Zone '" + zoneName + "' does not exist."; + ServiceState state = ServiceUnknown; + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; + + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + cr->SetCommand(commandName); + cr->SetOutput(output); + cr->SetState(state); + checkable->ProcessCheckResult(cr); + } return; } @@ -107,34 +160,52 @@ void ClusterZoneCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const Che bytesReceivedPerSecond += endpoint->GetBytesReceivedPerSecond(); } + ServiceState state; + String output; + if (connected) { - cr->SetState(ServiceOK); - cr->SetOutput("Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag)); + state = ServiceOK; + output = "Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag); /* Check whether the thresholds have been resolved and compare them */ if (missingLagCritical.IsEmpty() && zoneLag > lagCritical) { - cr->SetState(ServiceCritical); - cr->SetOutput("Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag) - + " greater than critical threshold: " + Utility::FormatDuration(lagCritical)); + state = ServiceCritical; + output = "Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag) + + " greater than critical threshold: " + Utility::FormatDuration(lagCritical); } else if (missingLagWarning.IsEmpty() && zoneLag > lagWarning) { - cr->SetState(ServiceWarning); - cr->SetOutput("Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag) - + " greater than warning threshold: " + Utility::FormatDuration(lagWarning)); + state = ServiceWarning; + output = "Zone '" + zoneName + "' is connected. Log lag: " + Utility::FormatDuration(zoneLag) + + " greater than warning threshold: " + Utility::FormatDuration(lagWarning); } } else { - cr->SetState(ServiceCritical); - cr->SetOutput("Zone '" + zoneName + "' is not connected. Log lag: " + Utility::FormatDuration(zoneLag)); + state = ServiceCritical; + output = "Zone '" + zoneName + "' is not connected. Log lag: " + Utility::FormatDuration(zoneLag); } - cr->SetPerformanceData(new Array({ - new PerfdataValue("slave_lag", zoneLag, false, "s", lagWarning, lagCritical), - new PerfdataValue("last_messages_sent", lastMessageSent), - new PerfdataValue("last_messages_received", lastMessageReceived), - new PerfdataValue("sum_messages_sent_per_second", messagesSentPerSecond), - new PerfdataValue("sum_messages_received_per_second", messagesReceivedPerSecond), - new PerfdataValue("sum_bytes_sent_per_second", bytesSentPerSecond), - new PerfdataValue("sum_bytes_received_per_second", bytesReceivedPerSecond) - })); + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; - checkable->ProcessCheckResult(cr); + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + cr->SetCommand(commandName); + cr->SetState(state); + cr->SetOutput(output); + cr->SetPerformanceData(new Array({ + new PerfdataValue("slave_lag", zoneLag, false, "s", lagWarning, lagCritical), + new PerfdataValue("last_messages_sent", lastMessageSent), + new PerfdataValue("last_messages_received", lastMessageReceived), + new PerfdataValue("sum_messages_sent_per_second", messagesSentPerSecond), + new PerfdataValue("sum_messages_received_per_second", messagesReceivedPerSecond), + new PerfdataValue("sum_bytes_sent_per_second", bytesSentPerSecond), + new PerfdataValue("sum_bytes_received_per_second", bytesReceivedPerSecond) + })); + + checkable->ProcessCheckResult(cr); + } } diff --git a/lib/methods/dummychecktask.cpp b/lib/methods/dummychecktask.cpp index e42754f31..561415c64 100644 --- a/lib/methods/dummychecktask.cpp +++ b/lib/methods/dummychecktask.cpp @@ -22,13 +22,17 @@ void DummyCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResu REQUIRE_NOT_NULL(checkable); REQUIRE_NOT_NULL(cr); - CheckCommand::Ptr command = checkable->GetCheckCommand(); + CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand(); Host::Ptr host; Service::Ptr service; tie(host, service) = GetHostService(checkable); MacroProcessor::ResolverList resolvers; + + if (MacroResolver::OverrideMacros) + resolvers.emplace_back("override", MacroResolver::OverrideMacros); + if (service) resolvers.emplace_back("service", service); resolvers.emplace_back("host", host); @@ -48,14 +52,26 @@ void DummyCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResu std::pair co = PluginUtility::ParseCheckOutput(dummyText); double now = Utility::GetTime(); + String commandName = command->GetName(); - cr->SetOutput(co.first); - cr->SetPerformanceData(PluginUtility::SplitPerfdata(co.second)); - cr->SetState(PluginUtility::ExitStatusToState(dummyState)); - cr->SetExitStatus(dummyState); - cr->SetExecutionStart(now); - cr->SetExecutionEnd(now); - cr->SetCommand(command->GetName()); + if (Checkable::ExecuteCommandProcessFinishedHandler) { + ProcessResult pr; + pr.PID = -1; + pr.Output = dummyText; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = dummyState; - checkable->ProcessCheckResult(cr); + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + cr->SetOutput(co.first); + cr->SetPerformanceData(PluginUtility::SplitPerfdata(co.second)); + cr->SetState(PluginUtility::ExitStatusToState(dummyState)); + cr->SetExitStatus(dummyState); + cr->SetExecutionStart(now); + cr->SetExecutionEnd(now); + cr->SetCommand(commandName); + + checkable->ProcessCheckResult(cr); + } } diff --git a/lib/methods/exceptionchecktask.cpp b/lib/methods/exceptionchecktask.cpp index 5ce3a9262..47707f26f 100644 --- a/lib/methods/exceptionchecktask.cpp +++ b/lib/methods/exceptionchecktask.cpp @@ -23,5 +23,19 @@ void ExceptionCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const Check if (resolvedMacros && !useResolvedMacros) return; - BOOST_THROW_EXCEPTION(ScriptError("Test") << boost::errinfo_api_function("Test")); + ScriptError scriptError = ScriptError("Test") << boost::errinfo_api_function("Test"); + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = scriptError.what(); + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = 3; + + Checkable::ExecuteCommandProcessFinishedHandler("", pr); + } else { + BOOST_THROW_EXCEPTION(ScriptError("Test") << boost::errinfo_api_function("Test")); + } } diff --git a/lib/methods/icingachecktask.cpp b/lib/methods/icingachecktask.cpp index 2c2c5c592..40795495d 100644 --- a/lib/methods/icingachecktask.cpp +++ b/lib/methods/icingachecktask.cpp @@ -26,13 +26,17 @@ void IcingaCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes REQUIRE_NOT_NULL(checkable); REQUIRE_NOT_NULL(cr); - CheckCommand::Ptr command = checkable->GetCheckCommand(); + CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand(); Host::Ptr host; Service::Ptr service; tie(host, service) = GetHostService(checkable); MacroProcessor::ResolverList resolvers; + + if (MacroResolver::OverrideMacros) + resolvers.emplace_back("override", MacroResolver::OverrideMacros); + if (service) resolvers.emplace_back("service", service); resolvers.emplace_back("host", host); @@ -148,7 +152,7 @@ void IcingaCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes perfdata->Add(new PerfdataValue("sum_bytes_received_per_second", bytesReceivedPerSecond)); cr->SetPerformanceData(perfdata); - cr->SetState(ServiceOK); + ServiceState state = ServiceOK; String appVersion = Application::GetAppVersion(); @@ -160,7 +164,7 @@ void IcingaCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes if (lastReloadFailed > 0) { output += "; Last reload attempt failed at " + Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", lastReloadFailed); - cr->SetState(ServiceWarning); + state =ServiceWarning; } /* Indicate a warning when the last synced config caused a stage validation error. */ @@ -173,7 +177,7 @@ void IcingaCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes output += "; Last zone sync stage validation failed at " + Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", validationResult->Get("ts")); - cr->SetState(ServiceWarning); + state = ServiceWarning; } } @@ -182,11 +186,26 @@ void IcingaCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes /* Return an error if the version is less than specified (optional). */ if (missingIcingaMinVersion.IsEmpty() && !icingaMinVersion.IsEmpty() && Utility::CompareVersion(icingaMinVersion, parsedAppVersion) < 0) { output += "; Minimum version " + icingaMinVersion + " is not installed."; - cr->SetState(ServiceCritical); + state = ServiceCritical; } - cr->SetOutput(output); - cr->SetCommand(command->GetName()); + String commandName = command->GetName(); - checkable->ProcessCheckResult(cr); + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; + + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + cr->SetState(state); + cr->SetOutput(output); + cr->SetCommand(commandName); + + checkable->ProcessCheckResult(cr); + } } diff --git a/lib/methods/nullchecktask.cpp b/lib/methods/nullchecktask.cpp index b56087b2e..ee660294e 100644 --- a/lib/methods/nullchecktask.cpp +++ b/lib/methods/nullchecktask.cpp @@ -26,12 +26,25 @@ void NullCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResul String output = "Hello from "; output += IcingaApplication::GetInstance()->GetNodeName(); + ServiceState state = ServiceOK; - cr->SetOutput(output); - cr->SetPerformanceData(new Array({ - new PerfdataValue("time", Convert::ToDouble(Utility::GetTime())) - })); - cr->SetState(ServiceOK); + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; - checkable->ProcessCheckResult(cr); + Checkable::ExecuteCommandProcessFinishedHandler("", pr); + } else { + cr->SetOutput(output); + cr->SetPerformanceData(new Array({ + new PerfdataValue("time", Convert::ToDouble(Utility::GetTime())) + })); + cr->SetState(state); + + checkable->ProcessCheckResult(cr); + } } diff --git a/lib/methods/nulleventtask.cpp b/lib/methods/nulleventtask.cpp index 0755a4d01..3c02f234f 100644 --- a/lib/methods/nulleventtask.cpp +++ b/lib/methods/nulleventtask.cpp @@ -11,4 +11,16 @@ REGISTER_FUNCTION_NONCONST(Internal, NullEvent, &NullEventTask::ScriptFunc, "che void NullEventTask::ScriptFunc(const Checkable::Ptr& checkable, const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros) { REQUIRE_NOT_NULL(checkable); + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = ""; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = 0; + + Checkable::ExecuteCommandProcessFinishedHandler("", pr); + } } diff --git a/lib/methods/pluginchecktask.cpp b/lib/methods/pluginchecktask.cpp index 212d7447c..488c2185e 100644 --- a/lib/methods/pluginchecktask.cpp +++ b/lib/methods/pluginchecktask.cpp @@ -22,13 +22,17 @@ void PluginCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes REQUIRE_NOT_NULL(checkable); REQUIRE_NOT_NULL(cr); - CheckCommand::Ptr commandObj = checkable->GetCheckCommand(); + CheckCommand::Ptr commandObj = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand(); Host::Ptr host; Service::Ptr service; tie(host, service) = GetHostService(checkable); MacroProcessor::ResolverList resolvers; + + if (MacroResolver::OverrideMacros) + resolvers.emplace_back("override", MacroResolver::OverrideMacros); + if (service) resolvers.emplace_back("service", service); resolvers.emplace_back("host", host); @@ -40,9 +44,16 @@ void PluginCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes if (!checkable->GetCheckTimeout().IsEmpty()) timeout = checkable->GetCheckTimeout(); + std::function callback; + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + callback = Checkable::ExecuteCommandProcessFinishedHandler; + } else { + callback = std::bind(&PluginCheckTask::ProcessFinishedHandler, checkable, cr, _1, _2); + } + PluginUtility::ExecuteCommand(commandObj, checkable, checkable->GetLastCheckResult(), - resolvers, resolvedMacros, useResolvedMacros, timeout, - std::bind(&PluginCheckTask::ProcessFinishedHandler, checkable, cr, _1, _2)); + resolvers, resolvedMacros, useResolvedMacros, timeout, callback); if (!resolvedMacros || useResolvedMacros) { Checkable::CurrentConcurrentChecks.fetch_add(1); diff --git a/lib/methods/plugineventtask.cpp b/lib/methods/plugineventtask.cpp index b2bb466b8..7a96901ff 100644 --- a/lib/methods/plugineventtask.cpp +++ b/lib/methods/plugineventtask.cpp @@ -21,13 +21,17 @@ void PluginEventTask::ScriptFunc(const Checkable::Ptr& checkable, { REQUIRE_NOT_NULL(checkable); - EventCommand::Ptr commandObj = checkable->GetEventCommand(); + EventCommand::Ptr commandObj = EventCommand::ExecuteOverride ? EventCommand::ExecuteOverride : checkable->GetEventCommand(); Host::Ptr host; Service::Ptr service; tie(host, service) = GetHostService(checkable); MacroProcessor::ResolverList resolvers; + + if (MacroResolver::OverrideMacros) + resolvers.emplace_back("override", MacroResolver::OverrideMacros); + if (service) resolvers.emplace_back("service", service); resolvers.emplace_back("host", host); @@ -35,10 +39,16 @@ void PluginEventTask::ScriptFunc(const Checkable::Ptr& checkable, resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); int timeout = commandObj->GetTimeout(); + std::function callback; + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + callback = Checkable::ExecuteCommandProcessFinishedHandler; + } else { + callback = std::bind(&PluginEventTask::ProcessFinishedHandler, checkable, _1, _2); + } PluginUtility::ExecuteCommand(commandObj, checkable, checkable->GetLastCheckResult(), - resolvers, resolvedMacros, useResolvedMacros, timeout, - std::bind(&PluginEventTask::ProcessFinishedHandler, checkable, _1, _2)); + resolvers, resolvedMacros, useResolvedMacros, timeout, callback); } void PluginEventTask::ProcessFinishedHandler(const Checkable::Ptr& checkable, const Value& commandLine, const ProcessResult& pr) diff --git a/lib/methods/pluginnotificationtask.cpp b/lib/methods/pluginnotificationtask.cpp index 0fcc95dc2..8dde4ff37 100644 --- a/lib/methods/pluginnotificationtask.cpp +++ b/lib/methods/pluginnotificationtask.cpp @@ -25,7 +25,7 @@ void PluginNotificationTask::ScriptFunc(const Notification::Ptr& notification, REQUIRE_NOT_NULL(notification); REQUIRE_NOT_NULL(user); - NotificationCommand::Ptr commandObj = notification->GetCommand(); + NotificationCommand::Ptr commandObj = NotificationCommand::ExecuteOverride ? NotificationCommand::ExecuteOverride : notification->GetCommand(); auto type = static_cast(itype); @@ -42,6 +42,10 @@ void PluginNotificationTask::ScriptFunc(const Notification::Ptr& notification, tie(host, service) = GetHostService(checkable); MacroProcessor::ResolverList resolvers; + + if (MacroResolver::OverrideMacros) + resolvers.emplace_back("override", MacroResolver::OverrideMacros); + resolvers.emplace_back("user", user); resolvers.emplace_back("notification", notificationExtra); resolvers.emplace_back("notification", notification); @@ -52,10 +56,16 @@ void PluginNotificationTask::ScriptFunc(const Notification::Ptr& notification, resolvers.emplace_back("icinga", IcingaApplication::GetInstance()); int timeout = commandObj->GetTimeout(); + std::function callback; + + if (Checkable::ExecuteCommandProcessFinishedHandler) { + callback = Checkable::ExecuteCommandProcessFinishedHandler; + } else { + callback = std::bind(&PluginNotificationTask::ProcessFinishedHandler, checkable, _1, _2); + } PluginUtility::ExecuteCommand(commandObj, checkable, cr, resolvers, - resolvedMacros, useResolvedMacros, timeout, - std::bind(&PluginNotificationTask::ProcessFinishedHandler, checkable, _1, _2)); + resolvedMacros, useResolvedMacros, timeout, callback); } void PluginNotificationTask::ProcessFinishedHandler(const Checkable::Ptr& checkable, const Value& commandLine, const ProcessResult& pr) diff --git a/lib/methods/randomchecktask.cpp b/lib/methods/randomchecktask.cpp index 35e10d2a6..9b133ef0e 100644 --- a/lib/methods/randomchecktask.cpp +++ b/lib/methods/randomchecktask.cpp @@ -31,22 +31,35 @@ void RandomCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes + ". Icinga 2 has been running for " + Utility::FormatDuration(uptime) + ". Version: " + Application::GetAppVersion(); - cr->SetOutput(output); + CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand(); + String commandName = command->GetName(); + ServiceState state = static_cast(Utility::Random() % 4); - double random = Utility::Random() % 1000; + if (Checkable::ExecuteCommandProcessFinishedHandler) { + double now = Utility::GetTime(); + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now; + pr.ExecutionEnd = now; + pr.ExitStatus = state; - cr->SetPerformanceData(new Array({ - new PerfdataValue("time", now), - new PerfdataValue("value", random), - new PerfdataValue("value_1m", random * 0.9), - new PerfdataValue("value_5m", random * 0.8), - new PerfdataValue("uptime", uptime), - })); + Checkable::ExecuteCommandProcessFinishedHandler(commandName, pr); + } else { + cr->SetOutput(output); - cr->SetState(static_cast(Utility::Random() % 4)); + double random = Utility::Random() % 1000; + cr->SetPerformanceData(new Array({ + new PerfdataValue("time", now), + new PerfdataValue("value", random), + new PerfdataValue("value_1m", random * 0.9), + new PerfdataValue("value_5m", random * 0.8), + new PerfdataValue("uptime", uptime), + })); - CheckCommand::Ptr command = checkable->GetCheckCommand(); - cr->SetCommand(command->GetName()); + cr->SetState(state); + cr->SetCommand(commandName); - checkable->ProcessCheckResult(cr); + checkable->ProcessCheckResult(cr); + } } diff --git a/lib/methods/sleepchecktask.cpp b/lib/methods/sleepchecktask.cpp index 47f735c36..a018de46b 100644 --- a/lib/methods/sleepchecktask.cpp +++ b/lib/methods/sleepchecktask.cpp @@ -18,13 +18,17 @@ void SleepCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResu REQUIRE_NOT_NULL(checkable); REQUIRE_NOT_NULL(cr); - CheckCommand::Ptr commandObj = checkable->GetCheckCommand(); + CheckCommand::Ptr commandObj = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand(); Host::Ptr host; Service::Ptr service; tie(host, service) = GetHostService(checkable); - MacroProcessor::ResolverList resolvers; + MacroProcessor::ResolverList resolvers; + + if (MacroResolver::OverrideMacros) + resolvers.emplace_back("override", MacroResolver::OverrideMacros); + if (service) resolvers.emplace_back("service", service); resolvers.emplace_back("host", host); @@ -42,13 +46,24 @@ void SleepCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResu String output = "Slept for " + Convert::ToString(sleepTime) + " seconds."; double now = Utility::GetTime(); + CheckCommand::Ptr command = checkable->GetCheckCommand(); + String commandName = command->GetName(); - cr->SetOutput(output); - cr->SetExecutionStart(now); - cr->SetExecutionEnd(now); + if (Checkable::ExecuteCommandProcessFinishedHandler) { + ProcessResult pr; + pr.PID = -1; + pr.Output = output; + pr.ExecutionStart = now - sleepTime; + pr.ExecutionEnd = now; + pr.ExitStatus = 0; - CheckCommand::Ptr command = checkable->GetCheckCommand(); - cr->SetCommand(command->GetName()); + Checkable::ExecuteCommandProcessFinishedHandler("", pr); + } else { + cr->SetOutput(output); + cr->SetExecutionStart(now); + cr->SetExecutionEnd(now); + cr->SetCommand(commandName); - checkable->ProcessCheckResult(cr); + checkable->ProcessCheckResult(cr); + } } \ No newline at end of file diff --git a/lib/remote/actionshandler.cpp b/lib/remote/actionshandler.cpp index 7b61cd5b9..f8a823632 100644 --- a/lib/remote/actionshandler.cpp +++ b/lib/remote/actionshandler.cpp @@ -4,12 +4,15 @@ #include "remote/httputility.hpp" #include "remote/filterutility.hpp" #include "remote/apiaction.hpp" +#include "base/defer.hpp" #include "base/exception.hpp" #include "base/logger.hpp" #include using namespace icinga; +thread_local ApiUser::Ptr ActionsHandler::AuthenticatedApiUser; + REGISTER_URLHANDLER("/v1/actions", ActionsHandler); bool ActionsHandler::HandleRequest( @@ -71,6 +74,11 @@ bool ActionsHandler::HandleRequest( bool verbose = false; + ActionsHandler::AuthenticatedApiUser = user; + Defer a ([]() { + ActionsHandler::AuthenticatedApiUser = nullptr; + }); + if (params) verbose = HttpUtility::GetLastParameter(params, "verbose"); @@ -94,7 +102,7 @@ bool ActionsHandler::HandleRequest( int statusCode = 500; for (const Dictionary::Ptr& res : results) { - if (res->Contains("code") && res->Get("code") == 200) { + if (res->Contains("code") && res->Get("code") >= 200 && res->Get("code") <= 299) { statusCode = 200; break; } diff --git a/lib/remote/actionshandler.hpp b/lib/remote/actionshandler.hpp index c2465cf7e..ca662caba 100644 --- a/lib/remote/actionshandler.hpp +++ b/lib/remote/actionshandler.hpp @@ -13,6 +13,8 @@ class ActionsHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(ActionsHandler); + static thread_local ApiUser::Ptr AuthenticatedApiUser; + bool HandleRequest( AsioTlsStream& stream, const ApiUser::Ptr& user, diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index 0462d728f..066986405 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include #include #include @@ -506,6 +508,22 @@ void ApiListener::NewClientHandler( } } +static const auto l_AppVersionInt (([]() -> unsigned long { + auto appVersion (Application::GetAppVersion()); + boost::regex rgx (R"EOF(^v?(\d+)\.(\d+)\.(\d+))EOF"); + boost::smatch match; + + if (!boost::regex_search(appVersion.GetData(), match, rgx)) { + return 0; + } + + return 100u * 100u * boost::lexical_cast(match[1].str()) + + 100u * boost::lexical_cast(match[2].str()) + + boost::lexical_cast(match[3].str()); +})()); + +static const auto l_MyCapabilities (ApiCapabilities::ExecuteArbitraryCommand); + /** * Processes a new client connection. * @@ -637,7 +655,10 @@ void ApiListener::NewClientHandlerInternal( JsonRpc::SendMessage(client, new Dictionary({ { "jsonrpc", "2.0" }, { "method", "icinga::Hello" }, - { "params", new Dictionary() } + { "params", new Dictionary({ + { "version", (double)l_AppVersionInt }, + { "capabilities", (double)l_MyCapabilities } + }) } }), yc); client->async_flush(yc); @@ -670,6 +691,17 @@ void ApiListener::NewClientHandlerInternal( } if (firstByte >= '0' && firstByte <= '9') { + JsonRpc::SendMessage(client, new Dictionary({ + { "jsonrpc", "2.0" }, + { "method", "icinga::Hello" }, + { "params", new Dictionary({ + { "version", (double)l_AppVersionInt }, + { "capabilities", (double)l_MyCapabilities } + }) } + }), yc); + + client->async_flush(yc); + ctype = ClientJsonRpc; } else { ctype = ClientHttp; @@ -1613,6 +1645,19 @@ std::set ApiListener::GetHttpClients() const Value ApiListener::HelloAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params) { + if (origin) { + auto client (origin->FromClient); + + if (client) { + auto endpoint (client->GetEndpoint()); + + if (endpoint) { + endpoint->SetIcingaVersion((double)params->Get("version")); + endpoint->SetCapabilities((double)params->Get("capabilities")); + } + } + } + return Empty; } diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp index c808581e3..166414073 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -39,6 +40,35 @@ struct ConfigDirInformation Dictionary::Ptr Checksums; }; +/** + * If the version reported by icinga::Hello is not enough to tell whether + * the peer has a specific capability, add the latter to this bitmask. + * + * Note that due to the capability exchange via JSON-RPC and the state storage via JSON + * the bitmask numbers are stored in IEEE 754 64-bit floats. + * The latter have 53 digit bits which limit the bitmask. + * Not to run out of bits: + * + * Once all Icinga versions which don't have a specific capability are completely EOL, + * remove the respective capability checks and assume the peer has the capability. + * Once all Icinga versions which still check for the capability are completely EOL, + * remove the respective bit from icinga::Hello. + * Once all Icinga versions which still have the respective bit in icinga::Hello + * are completely EOL, remove the bit here. + * Once all Icinga versions which still have the respective bit here + * are completely EOL, feel free to re-use the bit. + * + * completely EOL = not supported, even if an important customer of us used it and + * not expected to appear in a multi-level cluster, e.g. a 4 level cluster with + * v2.11 -> v2.10 -> v2.9 -> v2.8 - v2.7 isn't here + * + * @ingroup remote + */ +enum class ApiCapabilities : uint_fast64_t +{ + ExecuteArbitraryCommand = 1u +}; + /** * @ingroup remote */ diff --git a/lib/remote/endpoint.ti b/lib/remote/endpoint.ti index 1c3b654d1..78551ecf0 100644 --- a/lib/remote/endpoint.ti +++ b/lib/remote/endpoint.ti @@ -1,6 +1,7 @@ /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ #include "base/configobject.hpp" +#include library remote; @@ -21,6 +22,12 @@ class Endpoint : ConfigObject [state] Timestamp local_log_position; [state] Timestamp remote_log_position; + [state] "unsigned long" icinga_version { + default {{{ return 0; }}} + }; + [state] uint_fast64_t capabilities { + default {{{ return 0; }}} + }; [no_user_modify] bool connecting; [no_user_modify] bool syncing; diff --git a/tools/mkclass/classcompiler.cpp b/tools/mkclass/classcompiler.cpp index fd30df8e1..f6793da50 100644 --- a/tools/mkclass/classcompiler.cpp +++ b/tools/mkclass/classcompiler.cpp @@ -797,7 +797,7 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&) << "{" << std::endl; if (field.GetAccessor.empty() && !(field.Attributes & FANoStorage)) - m_Impl << "\t" << "return m_" << field.GetFriendlyName() << ";" << std::endl; + m_Impl << "\t" << "return m_" << field.GetFriendlyName() << ".load();" << std::endl; else m_Impl << field.GetAccessor << std::endl; @@ -835,7 +835,7 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&) if (field.SetAccessor.empty() && !(field.Attributes & FANoStorage)) - m_Impl << "\t" << "m_" << field.GetFriendlyName() << " = value;" << std::endl; + m_Impl << "\t" << "m_" << field.GetFriendlyName() << ".store(value);" << std::endl; else m_Impl << field.SetAccessor << std::endl << std::endl; @@ -1044,7 +1044,7 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&) if (field.Attributes & FANoStorage) continue; - m_Header << "\t" << field.Type.GetRealType() << " m_" << field.GetFriendlyName() << ";" << std::endl; + m_Header << "\tEventuallyAtomic<" << field.Type.GetRealType() << ">::type m_" << field.GetFriendlyName() << ";" << std::endl; } /* signal */ @@ -1431,6 +1431,7 @@ void ClassCompiler::CompileStream(const std::string& path, std::istream& input, << "#include \"base/type.hpp\"" << std::endl << "#include \"base/value.hpp\"" << std::endl << "#include \"base/array.hpp\"" << std::endl + << "#include \"base/atomic.hpp\"" << std::endl << "#include \"base/dictionary.hpp\"" << std::endl << "#include " << std::endl << std::endl;