From 39c24e9ec9102863c2a69eab30110c13d8d82da6 Mon Sep 17 00:00:00 2001 From: Jean Flach Date: Mon, 10 Oct 2016 11:42:18 +0200 Subject: [PATCH] Add check_nscp_api plugin for NSClient++ API checks refs #4721 --- .travis.yml | 2 +- doc/10-icinga-template-library.md | 60 +++- doc/5-service-monitoring.md | 2 +- doc/6-distributed-monitoring.md | 102 +++++- ..._windows_nscp_api_drivesize_icingaweb2.png | Bin 0 -> 38947 bytes icinga2.spec | 7 +- itl/command-plugins-windows.conf | 34 +- itl/command-plugins.conf | 33 ++ lib/remote/httpresponse.cpp | 8 +- lib/remote/url.cpp | 4 +- plugins/CMakeLists.txt | 39 ++- plugins/check_nscp_api.cpp | 310 ++++++++++++++++++ 12 files changed, 549 insertions(+), 52 deletions(-) create mode 100644 doc/images/distributed-monitoring/icinga2_distributed_windows_nscp_api_drivesize_icingaweb2.png create mode 100644 plugins/check_nscp_api.cpp diff --git a/.travis.yml b/.travis.yml index 2b6b03eb3..d17501f0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ addons: before_script: - mkdir build - cd build - - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/tmp/icinga2 + - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/tmp/icinga2 -DICINGA2_PLUGINDIR=/tmp/icinga2/sbin script: - make diff --git a/doc/10-icinga-template-library.md b/doc/10-icinga-template-library.md index 2ea03200a..4abe1ba99 100644 --- a/doc/10-icinga-template-library.md +++ b/doc/10-icinga-template-library.md @@ -1637,7 +1637,61 @@ users\_win\_crit | **Optional**. The critical threshold. ## Plugin Check Commands for NSClient++ -Icinga 2 can use the `nscp client` command to run arbitrary NSClient++ checks. +There are two methods available for querying NSClient++: + +* Query the [HTTP API](10-icinga-template-library.md#nscp-check-api) locally or remotely (requires a running NSClient++ service) +* Run a [local CLI check](10-icinga-template-library.md#nscp-check-local) (does not require NSClient++ as a service) + +Both methods have their advantages and disadvantages. One thing to +note: If you rely on performance counter delta calculations such as +CPU utilization, please use the HTTP API instead of the CLI sample call. + +### nscp_api + +`check_nscp_api` is part of the Icinga 2 plugins. This plugin is available for +both, Windows and Linux/Unix. + +Verify that the ITL CheckCommand is included: + + vim /etc/icinga2/icinga2.conf + + include + +`check_nscp_api` runs queries against the NSClient++ API. Therefore NSClient++ needs to have +the `webserver` module enabled, configured and loaded. + +You can install the webserver using the following CLI commands: + + ./nscp.exe web install + ./nscp.exe web password — –set icinga + +Now you can define specific [queries](https://docs.nsclient.org/reference/check/CheckHelpers.html#queries) +and integrate them into Icinga 2. + +The check plugin `check_nscp_api` can be integrated with the `nscp_api` CheckCommand object: + +Custom attributes: + +Name | Description +:----------------------|:---------------------- +nscp\_api\_host | **Required**. NSCP API host address. Defaults to "$address$" if the host's `address` attribute is set, "$address6$" otherwise. +nscp\_api\_port | **Optional**. NSCP API port. Defaults to `8443`. +nscp\_api\_passwd | **Required**. NSCP API password. Please check the NSCP documentation for setup details. +nscp\_api\_query | **Required**. NSCP API query endpoint. Refer to the NSCP documentation for possible values. +nscp\_api\_arguments | **Optional**. NSCP API arguments dictionary either as single strings or key-value pairs using `=`. Refer to the NSCP documentation. + +`nscp_api_arguments` can be used to pass required thresholds to the executed check. The example below +checks the CPU utilization and specifies warning and critical thresholds. + +``` +check_nscp_api --host 10.0.10.148 --password icinga --query check_cpu --arguments show-all warning='load>40' critical='load>30' +check_cpu CRITICAL: critical(5m: 48%, 1m: 36%), 5s: 0% | 'total 5m'=48%;40;30 'total 1m'=36%;40;30 'total 5s'=0%;40;30 +``` + + +### nscp-local + +Icinga 2 can use the `nscp client` command to run arbitrary NSClient++ checks locally on the client. You can enable these check commands by adding the following the include directive in your [icinga2.conf](4-configuring-icinga-2.md#icinga2-conf) configuration file: @@ -1655,9 +1709,7 @@ not be necessary to manually set this constant. Note that it is not necessary to run NSClient++ as a Windows service for these commands to work. -### nscp-local - -Check command object for NSClient++ +The check command object for NSClient++ is available as `nscp-local`. Custom attributes passed as [command parameters](3-monitoring-basics.md#command-passing-parameters): diff --git a/doc/5-service-monitoring.md b/doc/5-service-monitoring.md index 89f742c56..1ca6f89b2 100644 --- a/doc/5-service-monitoring.md +++ b/doc/5-service-monitoring.md @@ -193,7 +193,7 @@ Instead, choose a plugin and configure its parameters and thresholds. The follow ### Windows Monitoring * [check_wmi_plus](http://www.edcint.co.nz/checkwmiplus/) -* [NSClient++](https://www.nsclient.org) (in combination with the Icinga 2 client as [nscp-local](10-icinga-template-library.md#nscp-plugin-check-commands) check commands) +* [NSClient++](https://www.nsclient.org) (in combination with the Icinga 2 client and either [check_nscp_api](10-icinga-template-library.md#nscp-check-api) or [nscp-local](10-icinga-template-library.md#nscp-plugin-check-commands) check commands) * [Icinga 2 Windows Plugins](10-icinga-template-library.md#windows-plugins) (disk, load, memory, network, performance counters, ping, procs, service, swap, updates, uptime, users * vbs and Powershell scripts diff --git a/doc/6-distributed-monitoring.md b/doc/6-distributed-monitoring.md index 7553cce33..2b8900154 100644 --- a/doc/6-distributed-monitoring.md +++ b/doc/6-distributed-monitoring.md @@ -198,9 +198,9 @@ Here is an example of a master setup for the `icinga2-master1.localdomain` node [root@icinga2-master1.localdomain /]# icinga2 node wizard Welcome to the Icinga 2 Setup Wizard! - + We'll guide you through all required configuration details. - + Please specify if this is a satellite setup ('n' installs a master setup) [Y/n]: n Starting the Master setup routine... Please specify the common name (CN) [icinga2-master1.localdomain]: icinga2-master1.localdomain @@ -230,7 +230,7 @@ Here is an example of a master setup for the `icinga2-master1.localdomain` node information/cli: Updating constants file '/etc/icinga2/constants.conf'. information/cli: Updating constants file '/etc/icinga2/constants.conf'. Done. - + Now restart your Icinga 2 daemon to finish the installation! [root@icinga2-master1.localdomain /]# systemctl restart icinga2 @@ -350,9 +350,9 @@ is configured to accept configuration and commands from the master: [root@icinga2-client1.localdomain /]# icinga2 node wizard Welcome to the Icinga 2 Setup Wizard! - + We'll guide you through all required configuration details. - + Please specify if this is a satellite setup ('n' installs a master setup) [Y/n]: Starting the Node setup routine... Please specify the common name (CN) [icinga2-client1.localdomain]: icinga2-client1.localdomain @@ -369,22 +369,22 @@ is configured to accept configuration and commands from the master: information/base: Writing private key to '/etc/icinga2/pki/icinga2-client1.localdomain.key'. information/base: Writing X509 certificate to '/etc/icinga2/pki/icinga2-client1.localdomain.crt'. information/cli: Fetching public certificate from master (192.168.56.101, 5665): - + Certificate information: - + Subject: CN = icinga2-master1.localdomain Issuer: CN = Icinga CA Valid From: Feb 23 14:45:32 2016 GMT Valid Until: Feb 19 14:45:32 2031 GMT Fingerprint: AC 99 8B 2B 3D B0 01 00 E5 21 FA 05 2E EC D5 A9 EF 9E AA E3 - + Is this information correct? [y/N]: y information/cli: Received trusted master certificate. - + Please specify the request ticket generated on your Icinga 2 master. (Hint: # icinga2 pki ticket --cn 'icinga2-client1.localdomain'): 4f75d2ecd253575fe9180938ebff7cbca262f96e information/cli: Requesting certificate with ticket '4f75d2ecd253575fe9180938ebff7cbca262f96e'. - + information/cli: Created backup file '/etc/icinga2/pki/icinga2-client1.localdomain.crt.orig'. information/cli: Writing signed certificate to file '/etc/icinga2/pki/icinga2-client1.localdomain.crt'. information/cli: Writing CA certificate to file '/etc/icinga2/pki/ca.crt'. @@ -2133,6 +2133,85 @@ for the requirements. ### Windows Client and NSClient++ +There are two methods available for querying NSClient++: + +* Query the [HTTP API](6-distributed-monitoring.md#distributed-monitoring-windows-nscp-check-api) locally or remotely (requires a running NSClient++ service) +* Run a [local CLI check](6-distributed-monitoring.md#distributed-monitoring-windows-nscp-check-local) (does not require NSClient++ as a service) + +Both methods have their advantages and disadvantages. One thing to +note: If you rely on performance counter delta calculations such as +CPU utilization, please use the HTTP API instead of the CLI sample call. + +#### NSCLient++ with check_nscp_api + +The [Windows setup](6-distributed-monitoring.md#distributed-monitoring-setup-client-windows) already allows +you to install the NSClient++ package. In addition to the Windows plugins you can +use the [nscp_api command](10-icinga-template-library.md#nscp-check-api) provided by the Icinga Template Library (ITL). + +The initial setup for the NSClient++ API and the required arguments +is the described in the ITL chapter for the [nscp_api](10-icinga-template-library.md#nscp-check-api) CheckCommand. + +Based on the [master with clients](6-distributed-monitoring.md#distributed-monitoring-master-clients) +scenario we'll now add a local nscp check which queries the NSClient++ API to check the free disk space. + +Define a host object called `icinga2-client2.localdomain` on the master. Add the `nscp_api_password` +custom attribute and specify the drives to check. + + [root@icinga2-master1.localdomain /]# cd /etc/icinga2/zones.d/master + [root@icinga2-master1.localdomain /etc/icinga2/zones.d/master]# vim hosts.conf + + object Host "icinga2-client1.localdomain" { + check_command = "hostalive" + address = "192.168.56.111" + vars.client_endpoint = name //follows the convention that host name == endpoint name + vars.os_type = "Windows" + vars.nscp_api_password = "icinga" + vars.drives = [ "C:", "D:" ] + } + +The service checks are generated using an [apply for](3-monitoring-basics.md#using-apply-for) +rule based on `host.vars.drives`: + + [root@icinga2-master1.localdomain /etc/icinga2/zones.d/master]# vim services.conf + + apply Service for "nscp-api-" (drive in host.vars.drives) { + import "generic-service" + + check_command = "nscp_api" + command_endpoint = host.vars.client_endpoint + + //display_name = "nscp-drive-" + drive + + vars.nscp_api_host = "localhost" + vars.nscp_api_query = "check_drivesize" + vars.nscp_api_password = host.vars.nscp_api_password + vars.nscp_api_arguments = [ "drive=" + drive ] + + ignore where host.vars.os_type != "Windows" + } + +Validate the configuration and restart Icinga 2. + + [root@icinga2-master1.localdomain /]# icinga2 daemon -C + [root@icinga2-master1.localdomain /]# systemctl restart icinga2 + +Two new services ("nscp-drive-D:" and "nscp-drive-C:") will be visible in Icinga Web 2. + +![Icinga 2 Distributed Monitoring Windows Client with NSClient++ nscp-api](images/distributed-monitoring/icinga2_distributed_windows_nscp_api_drivesize_icingaweb2.png) + +Note: You can also omit the `command_endpoint` configuration to execute +the command on the master. This also requires a different value for `nscp_api_host` +which defaults to `host.address`. + + //command_endpoint = host.vars.client_endpoint + + //vars.nscp_api_host = "localhost" + +You can verify the check execution by looking at the `Check Source` attribute +in Icinga Web 2 or the REST API. + +#### NSCLient++ with nscp-local + The [Windows setup](6-distributed-monitoring.md#distributed-monitoring-setup-client-windows) already allows you to install the NSClient++ package. In addition to the Windows plugins you can use the [nscp-local commands](10-icinga-template-library.md#nscp-plugin-check-commands) @@ -2190,8 +2269,7 @@ Validate the configuration and restart Icinga 2. Open Icinga Web 2 and check your newly added Windows NSClient++ check :) -![Icinga 2 Distributed Monitoring Windows Client with NSClient++](images/distributed-monitoring/icinga2_distributed_windows_nscp_counter_icingaweb2.png) - +![Icinga 2 Distributed Monitoring Windows Client with NSClient++ nscp-local](images/distributed-monitoring/icinga2_distributed_windows_nscp_counter_icingaweb2.png) ## Advanced Hints diff --git a/doc/images/distributed-monitoring/icinga2_distributed_windows_nscp_api_drivesize_icingaweb2.png b/doc/images/distributed-monitoring/icinga2_distributed_windows_nscp_api_drivesize_icingaweb2.png new file mode 100644 index 0000000000000000000000000000000000000000..940902567660e298a2921a5aa43aef4bb83d9d7c GIT binary patch literal 38947 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 VUt1!UwtYdsp{r@AQKE*z{RfL24YL3M literal 0 HcmV?d00001 diff --git a/icinga2.spec b/icinga2.spec index f5f445854..dd36da89e 100644 --- a/icinga2.spec +++ b/icinga2.spec @@ -25,6 +25,7 @@ %endif %define _libexecdir %{_prefix}/lib/ +%define plugindir %{_libdir}/nagios/plugins %if "%{_vendor}" == "redhat" %define apachename httpd @@ -43,6 +44,7 @@ %endif %if "%{_vendor}" == "suse" +%define plugindir %{_prefix}/lib/nagios/plugins %define apachename apache2 %define apacheconfdir %{_sysconfdir}/apache2/conf.d %define apacheuser wwwrun @@ -330,7 +332,7 @@ CMAKE_OPTS="$CMAKE_OPTS \ %endif %if "%{_vendor}" != "suse" -CMAKE_OPTS="$CMAKE_OPTS -DICINGA2_PLUGINDIR=%{_libdir}/nagios/plugins" +CMAKE_OPTS="$CMAKE_OPTS -DICINGA2_PLUGINDIR=%{plugindir}" %else %if 0%{?suse_version} < 1310 CMAKE_OPTS="$CMAKE_OPTS -DBOOST_LIBRARYDIR=%{_libdir}/boost153 \ @@ -340,7 +342,7 @@ CMAKE_OPTS="$CMAKE_OPTS -DBOOST_LIBRARYDIR=%{_libdir}/boost153 \ -DBUILD_TESTING=FALSE \ -DBoost_NO_BOOST_CMAKE=TRUE" %endif -CMAKE_OPTS="$CMAKE_OPTS -DICINGA2_PLUGINDIR=%{_prefix}/lib/nagios/plugins" +CMAKE_OPTS="$CMAKE_OPTS -DICINGA2_PLUGINDIR=%{plugindir}" %endif %if 0%{?use_systemd} @@ -674,6 +676,7 @@ fi %{_sbindir}/%{name} %dir %{_libdir}/%{name}/sbin %{_libdir}/%{name}/sbin/%{name} +%{plugindir}/check_nscp_api %{_datadir}/%{name} %exclude %{_datadir}/%{name}/include %{_mandir}/man8/%{name}.8.gz diff --git a/itl/command-plugins-windows.conf b/itl/command-plugins-windows.conf index 2909c698f..a23aa1f02 100644 --- a/itl/command-plugins-windows.conf +++ b/itl/command-plugins-windows.conf @@ -19,7 +19,7 @@ object CheckCommand "disk-windows" { command = [ PluginDir + "/check_disk.exe" ] - + arguments = { "-w" = { value = "$disk_win_warn$" @@ -43,14 +43,14 @@ object CheckCommand "disk-windows" { description = "Exclude these drives from check" } } - + vars.disk_win_unit = "mb" //The default } - + object CheckCommand "load-windows" { command = [ PluginDir + "/check_load.exe" ] - + arguments = { "-w" = { value = "$load_win_warn$" @@ -65,7 +65,7 @@ object CheckCommand "load-windows" { object CheckCommand "memory-windows" { command = [ PluginDir + "/check_memory.exe" ] - + arguments = { "-w" = { value = "$memory_win_warn$" @@ -86,7 +86,7 @@ object CheckCommand "memory-windows" { object CheckCommand "network-windows" { command = [ PluginDir + "/check_network.exe" ] - + arguments = { "-w" = { value = "$network_win_warn$" @@ -106,7 +106,7 @@ object CheckCommand "network-windows" { object CheckCommand "perfmon-windows" { command = [ PluginDir + "/check_perfmon.exe" ] - + arguments = { "-w" = { value = "$perfmon_win_warn$" @@ -135,7 +135,7 @@ object CheckCommand "perfmon-windows" { } } - + vars.performance_win_wait = 1000 vars.perfmon_win_type = "double" //The default values @@ -168,7 +168,7 @@ template CheckCommand "ping-common-windows" { description = "Timeout in ms" } } - + vars.ping_win_packets = "5" vars.ping_win_timeout = "1000" //The default values @@ -199,7 +199,7 @@ object CheckCommand "ping6-windows" { object CheckCommand "procs-windows" { command = [ PluginDir + "/check_procs.exe" ] - + arguments = { "-w" = { value = "$procs_win_warn$" @@ -218,7 +218,7 @@ object CheckCommand "procs-windows" { object CheckCommand "service-windows" { command = [ PluginDir + "/check_service.exe" ] - + arguments = { "-w" = { set_if = "$service_win_warn$" @@ -234,7 +234,7 @@ object CheckCommand "service-windows" { object CheckCommand "swap-windows" { command = [ PluginDir + "/check_swap.exe" ] - + arguments = { "-w" = { value = "$swap_win_warn$" @@ -249,14 +249,14 @@ object CheckCommand "swap-windows" { description = "Unit to display swap in" } } - + vars.swap_win_unit = "mb" //The default } object CheckCommand "update-windows" { command = [ PluginDir + "/check_update.exe" ] - + arguments = { "-w" = { set_if = "$update_win_warn$" @@ -277,7 +277,7 @@ object CheckCommand "update-windows" { object CheckCommand "uptime-windows" { command = [ PluginDir + "/check_uptime.exe" ] - + arguments = { "-w" = { value = "$uptime_win_warn$" @@ -292,14 +292,14 @@ object CheckCommand "uptime-windows" { description = "Time unit to use" } } - + vars.uptime_win_unit = "s" //The default } object CheckCommand "users-windows" { command = [ PluginDir + "/check_users.exe" ] - + arguments = { "-w" = { value = "$users_win_warn$" diff --git a/itl/command-plugins.conf b/itl/command-plugins.conf index 93b7bf84f..12b59a0cb 100644 --- a/itl/command-plugins.conf +++ b/itl/command-plugins.conf @@ -3025,3 +3025,36 @@ object CheckCommand "radius" { vars.radius_address = "$check_address$" } + +object CheckCommand "nscp_api" { + import "ipv4-or-ipv6" + + command = [ PluginDir + "/check_nscp_api" ] + + arguments = { + "-H" = { + value = "$nscp_api_host$" + description = "NSCP API host address" + required = true + } + "-P" = { + value = "$nscp_api_port$" + description = "NSCP API host port. Defaults to 8443." + } + "--password" = { + value = "$nscp_api_password$" + description = "NSCP API password" + } + "-q" = { + value = "$nscp_api_query$" + description = "NSCPI API Query endpoint to use" + } + "-a" = { + value = "$nscp_api_arguments$" + description = "NSCP API Query arguments" + repeat_key = true + } + } + + vars.nscp_api_host = "$check_address$" +} diff --git a/lib/remote/httpresponse.cpp b/lib/remote/httpresponse.cpp index d66758690..d8c4e0997 100644 --- a/lib/remote/httpresponse.cpp +++ b/lib/remote/httpresponse.cpp @@ -139,8 +139,8 @@ bool HttpResponse::Parse(StreamReadContext& src, bool may_wait) boost::algorithm::split(tokens, line, boost::is_any_of(" ")); Log(LogDebug, "HttpRequest") << "line: " << line << ", tokens: " << tokens.size(); - if (tokens.size() < 3) - BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid HTTP request")); + if (tokens.size() < 2) + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid HTTP response (Status line)")); if (tokens[0] == "HTTP/1.0") ProtocolVersion = HttpVersion10; @@ -150,7 +150,9 @@ bool HttpResponse::Parse(StreamReadContext& src, bool may_wait) BOOST_THROW_EXCEPTION(std::invalid_argument("Unsupported HTTP version")); StatusCode = Convert::ToLong(tokens[1]); - StatusMessage = tokens[2]; // TODO: Join tokens[2..end] + + if (tokens.size() >= 3) + StatusMessage = tokens[2]; // TODO: Join tokens[2..end] m_State = HttpResponseHeaders; } else if (m_State == HttpResponseHeaders) { diff --git a/lib/remote/url.cpp b/lib/remote/url.cpp index 6168019a9..b6cdcb8ba 100644 --- a/lib/remote/url.cpp +++ b/lib/remote/url.cpp @@ -36,7 +36,9 @@ Url::Url(const String& base_url) if (url.GetLength() == 0) BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Empty URL.")); - size_t pHelper = url.Find(":"); + size_t pHelper = String::NPos; + if (url[0] != '/') + pHelper = url.Find(":"); if (pHelper != String::NPos) { if (!ParseScheme(url.SubStr(0, pHelper))) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 77d4a959d..196f73a26 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -15,28 +15,45 @@ # along with this program; if not, write to the Free Software Foundation # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +add_executable ( check_nscp_api check_nscp_api.cpp ) +target_link_libraries ( check_nscp_api ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_SYSTEM_LIBRARY} base remote ) +set_target_properties ( + check_nscp_api PROPERTIES + INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 + DEFINE_SYMBOL I2_PLUGINS_BUILD + FOLDER Plugins ) + +# Prefer the PluginDir constant which is set to /sbin on Windows + +if ( WIN32 ) + install ( TARGETS check_nscp_api RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} ) +else() + install ( TARGETS check_nscp_api RUNTIME DESTINATION ${ICINGA2_PLUGINDIR} ) +endif() + if ( WIN32 ) add_definitions ( -DUNICODE -D_UNICODE ) - + add_library ( thresholds thresholds ) set_target_properties ( thresholds PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 FOLDER Plugins ) - - list ( APPEND check_SOURCES - check_disk.cpp check_load.cpp check_memory.cpp check_network.cpp check_perfmon.cpp check_ping.cpp - check_procs.cpp check_service.cpp check_swap.cpp check_update.cpp check_uptime.cpp check_users.cpp ) - - foreach ( source ${check_SOURCES} ) + + list ( APPEND check_SOURCES + check_disk.cpp check_load.cpp check_memory.cpp check_network.cpp check_perfmon.cpp + check_ping.cpp check_procs.cpp check_service.cpp check_swap.cpp check_update.cpp check_uptime.cpp + check_users.cpp ) + + foreach ( source ${check_SOURCES} ) string ( REGEX REPLACE ".cpp\$" "" check_OUT "${source}" ) string ( REGEX REPLACE ".cpp\$" ".h" check_HEADER "${source}" ) - + add_executable ( ${check_OUT} ${source} ${check_HEADER} ) target_link_libraries ( ${check_OUT} thresholds Shlwapi.lib ${Boost_PROGRAM_OPTIONS_LIBRARY} ) - + set_target_properties ( ${check_OUT} PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 @@ -53,8 +70,8 @@ if ( WIN32 ) target_link_libraries ( check_users wtsapi32.lib ) install ( - TARGETS check_disk check_load check_memory check_network check_perfmon check_procs + TARGETS check_disk check_load check_memory check_network check_perfmon check_procs check_ping check_service check_swap check_update check_uptime check_users - RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} ) + RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} ) endif ( ) diff --git a/plugins/check_nscp_api.cpp b/plugins/check_nscp_api.cpp new file mode 100644 index 000000000..6ed5d0676 --- /dev/null +++ b/plugins/check_nscp_api.cpp @@ -0,0 +1,310 @@ +/****************************************************************************** +* Icinga 2 * +* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) * +* * +* This program is free software; you can redistribute it and/or * +* modify it under the terms of the GNU General Public License * +* as published by the Free Software Foundation; either version 2 * +* of the License, or (at your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, * +* but WITHOUT ANY WARRANTY; without even the implied warranty of * +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +* GNU General Public License for more details. * +* * +* You should have received a copy of the GNU General Public License * +* along with this program; if not, write to the Free Software Foundation * +* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * +******************************************************************************/ + +#define VERSION "1.0.0" + +#include "remote/httpclientconnection.hpp" +#include "remote/httprequest.hpp" +#include "remote/url-characters.hpp" +#include "base/application.hpp" +#include "base/json.hpp" +#include "base/string.hpp" +#include "base/exception.hpp" +#include +#include + +using namespace icinga; +namespace po = boost::program_options; + +bool l_Debug = false; + +/* + * This function is called by an 'HttpRequest' once the server answers. After doing a short check on the 'response' it + * decodes it to a Dictionary and then tells 'QueryEndpoint()' that it's done + */ +static void ResultHttpCompletionCallback(const HttpRequest& request, HttpResponse& response, bool& ready, + boost::condition_variable& cv, boost::mutex& mtx, Dictionary::Ptr& result) +{ + String body; + char buffer[1024]; + size_t count; + + while ((count = response.ReadBody(buffer, sizeof(buffer))) > 0) + body += String(buffer, buffer + count); + + if (l_Debug) { + std::cout << "Received answer\n" + << "\tHTTP code: " << response.StatusCode << "\n" + << "\tHTTP message: '" << response.StatusMessage << "'\n" + << "\tHTTP body: '" << body << "'.\n"; + } + + // Only try to decode the body if the 'HttpRequest' was successful + if (response.StatusCode != 200) + result = Dictionary::Ptr(); + else + result = JsonDecode(body); + + // Unlock our mutex, set ready and notify 'QueryEndpoint()' + boost::mutex::scoped_lock lock(mtx); + ready = true; + cv.notify_all(); +} + +/* + * This function takes all the information required to query an nscp instance on + * 'host':'port' with 'password'. The String 'endpoint' contains the specific + * query name and all the arguments formatted as an URL. + */ +static Dictionary::Ptr QueryEndpoint(const String& host, const String& port, const String& password, + const String& endpoint) +{ + HttpClientConnection::Ptr m_Connection = new HttpClientConnection(host, port, true); + + try { + bool ready = false; + boost::condition_variable cv; + boost::mutex mtx; + Dictionary::Ptr result; + boost::shared_ptr req = m_Connection->NewRequest(); + req->RequestMethod = "GET"; + + // Url() will call Utillity::UnescapeString() which will thrown an exception if it finds a lonely % + req->RequestUrl = new Url(endpoint); + req->AddHeader("password", password); + if (l_Debug) + std::cout << "Sending request to 'https://" << host << ":" << port << req->RequestUrl->Format() << "'\n"; + + // Submits the request. The 'ResultHttpCompletionCallback' is called once the HttpRequest receives an answer, + // which then sets 'ready' to true + m_Connection->SubmitRequest(req, boost::bind(ResultHttpCompletionCallback, _1, _2, + boost::ref(ready), boost::ref(cv), boost::ref(mtx), boost::ref(result))); + + // We need to spinlock here because our 'HttpRequest' works asynchronous + boost::mutex::scoped_lock lock(mtx); + while (!ready) { + cv.wait(lock); + } + + return result; + } + catch (const std::exception& ex) { + // Exceptions should only happen in extreme edge cases we can't recover from + std::cout << "Caught exception: " << DiagnosticInformation(ex, false) << '\n'; + return Dictionary::Ptr(); + } +} + +/* + * Takes a Dictionary 'result' and constructs an icinga compliant output string. + * If 'result' is not in the expected format it returns 3 ("UNKNOWN") and prints an informative, icinga compliant, + * output string. + */ +static int FormatOutput(const Dictionary::Ptr& result) +{ + if (!result) { + std::cout << "UNKNOWN: No data received.\n"; + return 3; + } + + if (l_Debug) + std::cout << "\tJSON Body:\n" << result->ToString() << '\n'; + + Array::Ptr payloads = result->Get("payload"); + if (!payloads) { + std::cout << "UNKNOWN: Answer format error: Answer is missing 'payload'.\n"; + return 3; + } + + if (payloads->GetLength() == 0) { + std::cout << "UNKNOWN: Answer format error: 'payload' was empty.\n"; + return 3; + } + + if (payloads->GetLength() > 1) { + std::cout << "UNKNOWN: Answer format error: Multiple payloads are not supported."; + return 3; + } + + Dictionary::Ptr payload; + try { + payload = payloads->Get(0); + } catch (const std::exception& ex) { + std::cout << "UNKNOWN: Answer format error: 'payload' was not a Dictionary.\n"; + return 3; + } + + Array::Ptr lines; + try { + lines = payload->Get("lines"); + } catch (const std::exception&) { + std::cout << "UNKNOWN: Answer format error: 'payload' is missing 'lines'.\n"; + return 3; + } + + if (!lines) { + std::cout << "UNKNOWN: Answer format error: 'lines' is Null.\n"; + return 3; + } + + std::stringstream ssout; + ObjectLock olock(lines); + + for (const Value& vline : lines) { + Dictionary::Ptr line; + try { + line = vline; + } catch (const std::exception& ex) { + std::cout << "UNKNOWN: Answer format error: 'lines' entry was not a Dictionary.\n"; + return 3; + } + if (!line) { + std::cout << "UNKNOWN: Answer format error: 'lines' entry was Null.\n"; + return 3; + } + + ssout << payload->Get("command") << ' ' << line->Get("message") << " | "; + + if (!line->Contains("perf")) { + ssout << '\n'; + break; + } + + Array::Ptr perfs = line->Get("perf"); + ObjectLock olock(perfs); + + for (const Dictionary::Ptr& perf : perfs) { + ssout << "'" << perf->Get("alias") << "'="; + Dictionary::Ptr values = perf->Contains("int_value") ? perf->Get("int_value") : perf->Get("float_value"); + ssout << values->Get("value") << values->Get("unit") << ';' << values->Get("warning") << ';' << values->Get("critical"); + + if (values->Contains("minimum") || values->Contains("maximum")) { + ssout << ';'; + + if (values->Contains("minimum")) + ssout << values->Get("minimum"); + + if (values->Contains("maximum")) + ssout << ';' << values->Get("maximum"); + } + + ssout << ' '; + } + + ssout << '\n'; + } + + //TODO: Fix + String state = static_cast(payload->Get("result")).ToUpper(); + int creturn = state == "OK" ? 0 : + state == "WARNING" ? 1 : + state == "CRITICAL" ? 2 : + state == "UNKNOWN" ? 3 : 4; + + if (creturn == 4) { + std::cout << "check_nscp UNKNOWN Answer format error: 'result' was not a known state.\n"; + return 3; + } + + std::cout << ssout.rdbuf(); + return creturn; +} + +/* + * Process arguments, initialize environment and shut down gracefully. + */ +int main(int argc, char **argv) +{ + po::variables_map vm; + po::options_description desc("Options"); + + desc.add_options() + ("help,h", "Print usage message and exit") + ("version,V", "Print version and exit") + ("debug,d", "Verbose/Debug output") + ("host,H", po::value()->required(), "REQUIRED: NSCP API Host") + ("port,P", po::value()->default_value("8443"), "NSCP API Port (Default: 8443)") + ("password", po::value()->required(), "REQUIRED: NSCP API Password") + ("query,q", po::value()->required(), "REQUIRED: NSCP API Query endpoint") + ("arguments,a", po::value>()->multitoken(), "NSCP API Query arguments for the endpoint"); + + po::basic_command_line_parser parser(argc, argv); + + try { + po::store( + parser + .options(desc) + .style( + po::command_line_style::unix_style | + po::command_line_style::allow_long_disguise) + .run(), + vm); + + if (vm.count("version")) { + std::cout << "Version: " << VERSION << '\n'; + Application::Exit(0); + } + + if (vm.count("help")) { + std::cout << argv[0] << " Help\n\tVersion: " << VERSION << '\n'; + std::cout << "check_nscp_api is a program used to query the NSClient++ API.\n"; + std::cout << desc; + std::cout << "For detailed information on possible queries and their arguments refer to the NSClient++ documentation.\n"; + Application::Exit(0); + } + + vm.notify(); + } catch (std::exception& e) { + std::cout << e.what() << '\n' << desc << '\n'; + Application::Exit(3); + } + + if (vm.count("debug")) { + l_Debug = true; + } + + // Create the URL string and escape certain characters since Url() follows RFC 3986 + String endpoint = "/query/" + vm["query"].as(); + if (!vm.count("arguments")) + endpoint += '/'; + else { + endpoint += '?'; + for (String argument : vm["arguments"].as>()) { + String::SizeType pos = argument.FindFirstOf("="); + if (pos == String::NPos) + endpoint += Utility::EscapeString(argument, ACQUERY_ENCODE, false); + else { + String key = argument.SubStr(0, pos); + String val = argument.SubStr(pos + 1); + endpoint += Utility::EscapeString(key, ACQUERY_ENCODE, false) + "=" + Utility::EscapeString(val, ACQUERY_ENCODE, false); + } + endpoint += '&'; + } + } + + // This needs to happen for HttpRequest to work + Application::InitializeBase(); + + Dictionary::Ptr result = QueryEndpoint(vm["host"].as(), vm["port"].as(), + vm["password"].as(), endpoint); + + // Application::Exit() is the clean way to exit after calling InitializeBase() + Application::Exit(FormatOutput(result)); + return 255; +} -- 2.50.1