]> granicus.if.org Git - icinga2/commitdiff
Add check_nscp_api plugin for NSClient++ API checks 5239/head
authorJean Flach <jean-marcel.flach@netways.de>
Mon, 10 Oct 2016 09:42:18 +0000 (11:42 +0200)
committerMichael Friedrich <michael.friedrich@icinga.com>
Tue, 13 Jun 2017 19:17:16 +0000 (21:17 +0200)
refs #4721

12 files changed:
.travis.yml
doc/10-icinga-template-library.md
doc/5-service-monitoring.md
doc/6-distributed-monitoring.md
doc/images/distributed-monitoring/icinga2_distributed_windows_nscp_api_drivesize_icingaweb2.png [new file with mode: 0644]
icinga2.spec
itl/command-plugins-windows.conf
itl/command-plugins.conf
lib/remote/httpresponse.cpp
lib/remote/url.cpp
plugins/CMakeLists.txt
plugins/check_nscp_api.cpp [new file with mode: 0644]

index 2b6b03eb30c31ecedb3efb5e9b0af6d23817c0f1..d17501f0f7a08cd5ec28f6cae4ec2efeff1ba357 100644 (file)
@@ -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
index 2ea03200a360197cbb5f5599015e5da4e39a8f15..4abe1ba99545c8940c18ae058a6f2c11d973511f 100644 (file)
@@ -1637,7 +1637,61 @@ users\_win\_crit | **Optional**. The critical threshold.
 
 ## <a id="nscp-plugin-check-commands"></a> 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.
+
+### <a id="nscp-check-api"></a> 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 <plugins>
+
+`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
+```
+
+
+### <a id="nscp-check-local"></a> 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.
 
-### <a id="nscp-check-local"></a> 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):
 
index 89f742c567e1b6333eb5bb45151f05b55ada4331..1ca6f89b21aae3d995da118f06282d7f25188558 100644 (file)
@@ -193,7 +193,7 @@ Instead, choose a plugin and configure its parameters and thresholds. The follow
 ### <a id="service-monitoring-windows"></a> 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
 
index 7553cce33a524f45a551ea57dda6b53c03aeff5e..2b890015409513bb8cbdd601c81132c27fd69989 100644 (file)
@@ -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.
 
 ### <a id="distributed-monitoring-windows-nscp"></a> 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.
+
+#### <a id="distributed-monitoring-windows-nscp-check-api"></a> 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.
+
+#### <a id="distributed-monitoring-windows-nscp-check-local"></a> 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)
 
 ## <a id="distributed-monitoring-advanced-hints"></a> 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 (file)
index 0000000..9409025
Binary files /dev/null and b/doc/images/distributed-monitoring/icinga2_distributed_windows_nscp_api_drivesize_icingaweb2.png differ
index f5f4458544b954149608a466fec4834c95dbcd50..dd36da89e18e1749b5d5c0292a10c070fc239947 100644 (file)
@@ -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
index 2909c698f62cbd168f8b7f0222ba6d531fee4ac4..a23aa1f02cbe6777b2ecd10590ff0d2acbd20120 100644 (file)
@@ -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$"
index 93b7bf84f3108df09cffc505a59bfd15397f4816..12b59a0cbaff0425865541067ed382782d599aaa 100644 (file)
@@ -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$"
+}
index d66758690318af0d82a54a123abc5937950874f9..d8c4e099729fdc619188824f06b679bb003db310 100644 (file)
@@ -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) {
index 6168019a9b078e3e94776777868b0d89d501b277..b6cdcb8baf519c16978ec55e399079ab1608ed3c 100644 (file)
@@ -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)))
index 77d4a959dee3aae5e482090c50e501a34e4b3f8a..196f73a26592038b899c871db64d8027d5d12df0 100644 (file)
 # 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 (file)
index 0000000..6ed5d06
--- /dev/null
@@ -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 <boost/program_options.hpp>
+#include <boost/algorithm/string/split.hpp>
+
+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<HttpRequest> 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<String>(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<String>()->required(), "REQUIRED: NSCP API Host")
+               ("port,P", po::value<String>()->default_value("8443"), "NSCP API Port (Default: 8443)")
+               ("password", po::value<String>()->required(), "REQUIRED: NSCP API Password")
+               ("query,q", po::value<String>()->required(), "REQUIRED: NSCP API Query endpoint")
+               ("arguments,a", po::value<std::vector<String>>()->multitoken(), "NSCP API Query arguments for the endpoint");
+
+       po::basic_command_line_parser<char> 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<String>();
+       if (!vm.count("arguments"))
+               endpoint += '/';
+       else {
+               endpoint += '?';
+               for (String argument : vm["arguments"].as<std::vector<String>>()) {
+                       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<String>(), vm["port"].as<String>(),
+           vm["password"].as<String>(), endpoint);
+
+       // Application::Exit() is the clean way to exit after calling InitializeBase()
+       Application::Exit(FormatOutput(result));
+       return 255;
+}