summaryrefslogtreecommitdiffstats
path: root/lib/methods/ifwapichecktask.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:32:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:32:39 +0000
commit56ae875861ab260b80a030f50c4aff9f9dc8fff0 (patch)
tree531412110fc901a5918c7f7442202804a83cada9 /lib/methods/ifwapichecktask.cpp
parentInitial commit. (diff)
downloadicinga2-56ae875861ab260b80a030f50c4aff9f9dc8fff0.tar.xz
icinga2-56ae875861ab260b80a030f50c4aff9f9dc8fff0.zip
Adding upstream version 2.14.2.upstream/2.14.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--lib/methods/ifwapichecktask.cpp531
1 files changed, 531 insertions, 0 deletions
diff --git a/lib/methods/ifwapichecktask.cpp b/lib/methods/ifwapichecktask.cpp
new file mode 100644
index 0000000..8516d70
--- /dev/null
+++ b/lib/methods/ifwapichecktask.cpp
@@ -0,0 +1,531 @@
+/* Icinga 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+# include <stdlib.h>
+#endif /* _WIN32 */
+#include "methods/ifwapichecktask.hpp"
+#include "methods/pluginchecktask.hpp"
+#include "icinga/checkresult-ti.hpp"
+#include "icinga/icingaapplication.hpp"
+#include "icinga/pluginutility.hpp"
+#include "base/base64.hpp"
+#include "base/defer.hpp"
+#include "base/utility.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/convert.hpp"
+#include "base/function.hpp"
+#include "base/io-engine.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include "base/shared.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/tlsstream.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/url.hpp"
+#include <boost/asio.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/system/system_error.hpp>
+#include <exception>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_FUNCTION_NONCONST(Internal, IfwApiCheck, &IfwApiCheckTask::ScriptFunc, "checkable:cr:resolvedMacros:useResolvedMacros");
+
+static void ReportIfwCheckResult(
+ const Checkable::Ptr& checkable, const Value& cmdLine, const CheckResult::Ptr& cr,
+ const String& output, double start, double end, int exitcode = 3, const Array::Ptr& perfdata = nullptr
+)
+{
+ if (Checkable::ExecuteCommandProcessFinishedHandler) {
+ ProcessResult pr;
+ pr.PID = -1;
+ pr.Output = perfdata ? output + " |" + String(perfdata->Join(" ")) : output;
+ pr.ExecutionStart = start;
+ pr.ExecutionEnd = end;
+ pr.ExitStatus = exitcode;
+
+ Checkable::ExecuteCommandProcessFinishedHandler(cmdLine, pr);
+ } else {
+ auto splittedPerfdata (perfdata);
+
+ if (perfdata) {
+ splittedPerfdata = new Array();
+ ObjectLock oLock (perfdata);
+
+ for (String pv : perfdata) {
+ PluginUtility::SplitPerfdata(pv)->CopyTo(splittedPerfdata);
+ }
+ }
+
+ cr->SetOutput(output);
+ cr->SetPerformanceData(splittedPerfdata);
+ cr->SetState((ServiceState)exitcode);
+ cr->SetExitStatus(exitcode);
+ cr->SetExecutionStart(start);
+ cr->SetExecutionEnd(end);
+ cr->SetCommand(cmdLine);
+
+ checkable->ProcessCheckResult(cr);
+ }
+}
+
+static void ReportIfwCheckResult(
+ boost::asio::yield_context yc, const Checkable::Ptr& checkable, const Value& cmdLine,
+ const CheckResult::Ptr& cr, const String& output, double start
+)
+{
+ double end = Utility::GetTime();
+ CpuBoundWork cbw (yc);
+
+ ReportIfwCheckResult(checkable, cmdLine, cr, output, start, end);
+}
+
+static const char* GetUnderstandableError(const std::exception& ex)
+{
+ auto se (dynamic_cast<const boost::system::system_error*>(&ex));
+
+ if (se && se->code() == boost::asio::error::operation_aborted) {
+ return "Timeout exceeded";
+ }
+
+ return ex.what();
+}
+
+static void DoIfwNetIo(
+ boost::asio::yield_context yc, const Checkable::Ptr& checkable, const Array::Ptr& cmdLine,
+ const CheckResult::Ptr& cr, const String& psCommand, const String& psHost, const String& san, const String& psPort,
+ AsioTlsStream& conn, boost::beast::http::request<boost::beast::http::string_body>& req, double start
+)
+{
+ namespace http = boost::beast::http;
+
+ boost::beast::flat_buffer buf;
+ http::response<http::string_body> resp;
+
+ try {
+ Connect(conn.lowest_layer(), psHost, psPort, yc);
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "Can't connect to IfW API on host '" + psHost + "' port '" + psPort + "': " + GetUnderstandableError(ex),
+ start
+ );
+ return;
+ }
+
+ auto& sslConn (conn.next_layer());
+
+ try {
+ sslConn.async_handshake(conn.next_layer().client, yc);
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "TLS handshake with IfW API on host '" + psHost + "' (SNI: '" + san
+ + "') port '" + psPort + "' failed: " + GetUnderstandableError(ex),
+ start
+ );
+ return;
+ }
+
+ if (!sslConn.IsVerifyOK()) {
+ auto cert (sslConn.GetPeerCertificate());
+ Value cn;
+
+ try {
+ cn = GetCertificateCN(cert);
+ } catch (const std::exception&) {
+ }
+
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "Certificate validation failed for IfW API on host '" + psHost + "' (SNI: '" + san + "'; CN: "
+ + (cn.IsString() ? "'" + cn + "'" : "N/A") + ") port '" + psPort + "': " + sslConn.GetVerifyError(),
+ start
+ );
+ return;
+ }
+
+ try {
+ http::async_write(conn, req, yc);
+ conn.async_flush(yc);
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "Can't send HTTP request to IfW API on host '" + psHost + "' port '" + psPort + "': " + GetUnderstandableError(ex),
+ start
+ );
+ return;
+ }
+
+ try {
+ http::async_read(conn, buf, resp, yc);
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ yc, checkable, cmdLine, cr,
+ "Can't read HTTP response from IfW API on host '" + psHost + "' port '" + psPort + "': " + GetUnderstandableError(ex),
+ start
+ );
+ return;
+ }
+
+ double end = Utility::GetTime();
+
+ {
+ boost::system::error_code ec;
+ sslConn.async_shutdown(yc[ec]);
+ }
+
+ CpuBoundWork cbw (yc);
+ Value jsonRoot;
+
+ try {
+ jsonRoot = JsonDecode(resp.body());
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got bad JSON from IfW API on host '" + psHost + "' port '" + psPort + "': " + ex.what(), start, end
+ );
+ return;
+ }
+
+ if (!jsonRoot.IsObjectType<Dictionary>()) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got JSON, but not an object, from IfW API on host '"
+ + psHost + "' port '" + psPort + "': " + JsonEncode(jsonRoot),
+ start, end
+ );
+ return;
+ }
+
+ Value jsonBranch;
+
+ if (!Dictionary::Ptr(jsonRoot)->Get(psCommand, &jsonBranch)) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Missing ." + psCommand + " in JSON object from IfW API on host '"
+ + psHost + "' port '" + psPort + "': " + JsonEncode(jsonRoot),
+ start, end
+ );
+ return;
+ }
+
+ if (!jsonBranch.IsObjectType<Dictionary>()) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "." + psCommand + " in JSON from IfW API on host '"
+ + psHost + "' port '" + psPort + "' is not an object: " + JsonEncode(jsonBranch),
+ start, end
+ );
+ return;
+ }
+
+ Dictionary::Ptr result = jsonBranch;
+
+ Value exitcode;
+
+ if (!result->Get("exitcode", &exitcode)) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Missing ." + psCommand + ".exitcode in JSON object from IfW API on host '"
+ + psHost + "' port '" + psPort + "': " + JsonEncode(result),
+ start, end
+ );
+ return;
+ }
+
+ static const std::set<double> exitcodes {ServiceOK, ServiceWarning, ServiceCritical, ServiceUnknown};
+ static const auto exitcodeList (Array::FromSet(exitcodes)->Join(", "));
+
+ if (!exitcode.IsNumber() || exitcodes.find(exitcode) == exitcodes.end()) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got bad exitcode " + JsonEncode(exitcode) + " from IfW API on host '" + psHost + "' port '" + psPort
+ + "', expected one of: " + exitcodeList,
+ start, end
+ );
+ return;
+ }
+
+ auto perfdataVal (result->Get("perfdata"));
+ Array::Ptr perfdata;
+
+ try {
+ perfdata = perfdataVal;
+ } catch (const std::exception&) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got bad perfdata " + JsonEncode(perfdataVal) + " from IfW API on host '"
+ + psHost + "' port '" + psPort + "', expected an array",
+ start, end
+ );
+ return;
+ }
+
+ if (perfdata) {
+ ObjectLock oLock (perfdata);
+
+ for (auto& pv : perfdata) {
+ if (!pv.IsString()) {
+ ReportIfwCheckResult(
+ checkable, cmdLine, cr,
+ "Got bad perfdata value " + JsonEncode(perfdata) + " from IfW API on host '"
+ + psHost + "' port '" + psPort + "', expected an array of strings",
+ start, end
+ );
+ return;
+ }
+ }
+ }
+
+ ReportIfwCheckResult(checkable, cmdLine, cr, result->Get("checkresult"), start, end, exitcode, perfdata);
+}
+
+void IfwApiCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr,
+ const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros)
+{
+ namespace asio = boost::asio;
+ namespace http = boost::beast::http;
+ using http::field;
+
+ REQUIRE_NOT_NULL(checkable);
+ REQUIRE_NOT_NULL(cr);
+
+ // We're going to just resolve macros for the actual check execution happening elsewhere
+ if (resolvedMacros && !useResolvedMacros) {
+ auto commandEndpoint (checkable->GetCommandEndpoint());
+
+ // There's indeed a command endpoint, obviously for the actual check execution
+ if (commandEndpoint) {
+ // But it doesn't have this function, yet ("ifw-api-check-command")
+ if (!(commandEndpoint->GetCapabilities() & (uint_fast64_t)ApiCapabilities::IfwApiCheckCommand)) {
+ // Assume "ifw-api-check-command" has been imported into a check command which can also work
+ // based on "plugin-check-command", delegate respectively and hope for the best
+ PluginCheckTask::ScriptFunc(checkable, cr, resolvedMacros, useResolvedMacros);
+ return;
+ }
+ }
+ }
+
+ CheckCommand::Ptr command = CheckCommand::ExecuteOverride ? CheckCommand::ExecuteOverride : checkable->GetCheckCommand();
+ auto lcr (checkable->GetLastCheckResult());
+
+ Host::Ptr host;
+ Service::Ptr service;
+ tie(host, service) = GetHostService(checkable);
+
+ MacroProcessor::ResolverList resolvers;
+
+ if (MacroResolver::OverrideMacros)
+ resolvers.emplace_back("override", MacroResolver::OverrideMacros);
+
+ if (service)
+ resolvers.emplace_back("service", service);
+ resolvers.emplace_back("host", host);
+ resolvers.emplace_back("command", command);
+
+ auto resolveMacros ([&resolvers, &lcr, &resolvedMacros, useResolvedMacros](const char* macros) -> Value {
+ return MacroProcessor::ResolveMacros(
+ macros, resolvers, lcr, nullptr, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros
+ );
+ });
+
+ String psCommand = resolveMacros("$ifw_api_command$");
+ Dictionary::Ptr arguments = resolveMacros("$ifw_api_arguments$");
+ String psHost = resolveMacros("$ifw_api_host$");
+ String psPort = resolveMacros("$ifw_api_port$");
+ String expectedSan = resolveMacros("$ifw_api_expected_san$");
+ String cert = resolveMacros("$ifw_api_cert$");
+ String key = resolveMacros("$ifw_api_key$");
+ String ca = resolveMacros("$ifw_api_ca$");
+ String crl = resolveMacros("$ifw_api_crl$");
+ String username = resolveMacros("$ifw_api_username$");
+ String password = resolveMacros("$ifw_api_password$");
+
+ Dictionary::Ptr params = new Dictionary();
+
+ if (arguments) {
+ ObjectLock oLock (arguments);
+ Array::Ptr emptyCmd = new Array();
+
+ for (auto& kv : arguments) {
+ Dictionary::Ptr argSpec;
+
+ if (kv.second.IsObjectType<Dictionary>()) {
+ argSpec = Dictionary::Ptr(kv.second)->ShallowClone();
+ } else {
+ argSpec = new Dictionary({{ "value", kv.second }});
+ }
+
+ // See default branch of below switch
+ argSpec->Set("repeat_key", false);
+
+ {
+ ObjectLock oLock (argSpec);
+
+ for (auto& kv : argSpec) {
+ if (kv.second.GetType() == ValueObject) {
+ auto now (Utility::GetTime());
+
+ ReportIfwCheckResult(
+ checkable, command->GetName(), cr,
+ "$ifw_api_arguments$ may not directly contain objects (especially functions).", now, now
+ );
+
+ return;
+ }
+ }
+ }
+
+ /* MacroProcessor::ResolveArguments() converts
+ *
+ * [ "check_example" ]
+ * and
+ * {
+ * "-f" = { set_if = "$example_flag$" }
+ * "-a" = "$example_arg$"
+ * }
+ *
+ * to
+ *
+ * [ "check_example", "-f", "-a", "X" ]
+ *
+ * but we need the args one-by-one like [ "-f" ] or [ "-a", "X" ].
+ */
+ Array::Ptr arg = MacroProcessor::ResolveArguments(
+ emptyCmd, new Dictionary({{kv.first, argSpec}}), resolvers, lcr, resolvedMacros, useResolvedMacros
+ );
+
+ switch (arg ? arg->GetLength() : 0) {
+ case 0:
+ break;
+ case 1: // [ "-f" ]
+ params->Set(arg->Get(0), true);
+ break;
+ case 2: // [ "-a", "X" ]
+ params->Set(arg->Get(0), arg->Get(1));
+ break;
+ default: { // [ "-a", "X", "Y" ]
+ auto k (arg->Get(0));
+
+ arg->Remove(0);
+ params->Set(k, arg);
+ }
+ }
+ }
+ }
+
+ auto checkTimeout (command->GetTimeout());
+ auto checkableTimeout (checkable->GetCheckTimeout());
+
+ if (!checkableTimeout.IsEmpty())
+ checkTimeout = checkableTimeout;
+
+ if (resolvedMacros && !useResolvedMacros)
+ return;
+
+ if (psHost.IsEmpty()) {
+ psHost = "localhost";
+ }
+
+ if (expectedSan.IsEmpty()) {
+ expectedSan = IcingaApplication::GetInstance()->GetNodeName();
+ }
+
+ if (cert.IsEmpty()) {
+ cert = ApiListener::GetDefaultCertPath();
+ }
+
+ if (key.IsEmpty()) {
+ key = ApiListener::GetDefaultKeyPath();
+ }
+
+ if (ca.IsEmpty()) {
+ ca = ApiListener::GetDefaultCaPath();
+ }
+
+ Url::Ptr uri = new Url();
+
+ uri->SetPath({ "v1", "checker" });
+ uri->SetQuery({{ "command", psCommand }});
+
+ static const auto userAgent ("Icinga/" + Application::GetAppVersion());
+ auto relative (uri->Format());
+ auto body (JsonEncode(params));
+ auto req (Shared<http::request<http::string_body>>::Make());
+
+ req->method(http::verb::post);
+ req->target(relative);
+ req->set(field::accept, "application/json");
+ req->set(field::content_type, "application/json");
+ req->set(field::host, expectedSan + ":" + psPort);
+ req->set(field::user_agent, userAgent);
+ req->body() = body;
+ req->content_length(req->body().size());
+
+ static const auto curlTlsMinVersion ((String("--") + DEFAULT_TLS_PROTOCOLMIN).ToLower());
+
+ Array::Ptr cmdLine = new Array({
+ "curl", "--verbose", curlTlsMinVersion, "--fail-with-body",
+ "--connect-to", expectedSan + ":" + psPort + ":" + psHost + ":" + psPort,
+ "--ciphers", DEFAULT_TLS_CIPHERS,
+ "--cert", cert,
+ "--key", key,
+ "--cacert", ca,
+ "--request", "POST",
+ "--url", "https://" + expectedSan + ":" + psPort + relative,
+ "--user-agent", userAgent,
+ "--header", "Accept: application/json",
+ "--header", "Content-Type: application/json",
+ "--data-raw", body
+ });
+
+ if (!crl.IsEmpty()) {
+ cmdLine->Add("--crlfile");
+ cmdLine->Add(crl);
+ }
+
+ if (!username.IsEmpty() && !password.IsEmpty()) {
+ auto authn (username + ":" + password);
+
+ req->set(field::authorization, "Basic " + Base64::Encode(authn));
+ cmdLine->Add("--user");
+ cmdLine->Add(authn);
+ }
+
+ auto& io (IoEngine::Get().GetIoContext());
+ auto strand (Shared<asio::io_context::strand>::Make(io));
+ Shared<asio::ssl::context>::Ptr ctx;
+ double start = Utility::GetTime();
+
+ try {
+ ctx = SetupSslContext(cert, key, ca, crl, DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
+ } catch (const std::exception& ex) {
+ ReportIfwCheckResult(checkable, cmdLine, cr, ex.what(), start, Utility::GetTime());
+ return;
+ }
+
+ auto conn (Shared<AsioTlsStream>::Make(io, *ctx, expectedSan));
+
+ IoEngine::SpawnCoroutine(
+ *strand,
+ [strand, checkable, cmdLine, cr, psCommand, psHost, expectedSan, psPort, conn, req, start, checkTimeout](asio::yield_context yc) {
+ Timeout::Ptr timeout = new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(checkTimeout * 1e6)),
+ [&conn, &checkable](boost::asio::yield_context yc) {
+ Log(LogNotice, "IfwApiCheckTask")
+ << "Timeout while checking " << checkable->GetReflectionType()->GetName()
+ << " '" << checkable->GetName() << "', cancelling attempt";
+
+ boost::system::error_code ec;
+ conn->lowest_layer().cancel(ec);
+ }
+ );
+
+ Defer cancelTimeout ([&timeout]() { timeout->Cancel(); });
+
+ DoIfwNetIo(yc, checkable, cmdLine, cr, psCommand, psHost, expectedSan, psPort, *conn, *req, start);
+ }
+ );
+}