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 000000000..940902567 Binary files /dev/null and b/doc/images/distributed-monitoring/icinga2_distributed_windows_nscp_api_drivesize_icingaweb2.png differ 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; +}