Merge pull request #5239 from Icinga/feature/check_nscp-4721

Add NSCP API check plugin for NSClient++ HTTP API
This commit is contained in:
Michael Friedrich 2017-06-13 21:56:58 +02:00 committed by GitHub
commit c6b375dcbd
12 changed files with 549 additions and 52 deletions

View 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

View 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):

View 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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View 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

View 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$"
}

View 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,6 +150,8 @@ bool HttpResponse::Parse(StreamReadContext& src, bool may_wait)
BOOST_THROW_EXCEPTION(std::invalid_argument("Unsupported HTTP version"));
StatusCode = Convert::ToLong(tokens[1]);
if (tokens.size() >= 3)
StatusMessage = tokens[2]; // TODO: Join tokens[2..end]
m_State = HttpResponseHeaders;

View 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)))

View File

@ -15,6 +15,22 @@
# 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 )
@ -27,8 +43,9 @@ if ( WIN32 )
)
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 )
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}" )

310
plugins/check_nscp_api.cpp Normal file
View File

@ -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;
}