16#include <asiolink/io_address.h>
17#include <dhcp/hwaddr.h>
19#include <boost/program_options.hpp>
21#include <nlohmann/json.hpp>
28#include "kch_config.h"
30namespace po = boost::program_options;
34enum class Icinga2CheckResult :
unsigned int
42template <
typename T = std::
int64_t>
43struct Icinga2PerfValue
46 std::optional<std::string> unit{};
47 std::optional<T> min{};
48 std::optional<T> max{};
49 std::optional<T> warning_threshold{};
50 std::optional<T> critical_threshold{};
54std::ostream& operator<<(std::ostream& os,
const Icinga2PerfValue<T>& perf)
60 os << (perf.min ? std::to_string(*perf.min) :
"") <<
";";
61 os << (perf.max ? std::to_string(*perf.max) :
"") <<
";";
62 os << (perf.warning_threshold ? std::to_string(*perf.warning_threshold) :
"") <<
";";
63 os << (perf.critical_threshold ? std::to_string(*perf.critical_threshold) :
"");
67using curl_ptr = std::unique_ptr<CURL, std::function<void(CURL*)>>;
68using icinga2_perfdata = std::map<std::string, Icinga2PerfValue<>>;
71po::variables_map parse_command_line(
int argc,
char** argv);
72void print_usage(std::string_view argv0,
const po::options_description& opt_desc);
78 std::string_view service,
79 Icinga2CheckResult result,
80 std::string_view message,
82 const icinga2_perfdata& perfdata = {});
83nlohmann::json perfdata_to_json(
const icinga2_perfdata& perfdata);
84extern "C" std::size_t curl_buf_write(
char* buffer, std::size_t size, std::size_t nmemb,
88std::string config_file_path{
"/etc/admindb-host-reservation-importer.conf"};
91 std::pair<std::string, std::function<int(
const po::variables_map&,
const ahri::Config&)>>>
93 {
"full-sync", {
"Synchronize the local hosts database with the AdminDB", run_full_sync}},
94 {
"prune", {
"Prune old host reservation updates in the AdminDB", run_prune}},
96 {
"Show host reservation for a given MAC or IP address", run_show_reservation}}};
98class NullOstream :
public std::ostream
101 template <
typename... T>
102 std::ostream& operator<<([[maybe_unused]] T... )
112 po::variables_map vm;
114 vm = parse_command_line(argc, argv);
116 catch (po::unknown_option&) {
120 std::ifstream config_file{config_file_path};
121 if (!config_file.good()) {
122 std::clog <<
"Failed to read configuration at " << config_file_path <<
'\n';
129 curl_global_init(CURL_GLOBAL_ALL);
130 int res = run_action(vm,
config);
131 curl_global_cleanup();
135 std::clog <<
"Problem in configuration file: " << e.
what() <<
'\n';
137 catch (boost::program_options::invalid_config_file_syntax& e) {
138 std::clog <<
"Problem in configuration file: " << e.
what() <<
'\n';
156po::variables_map parse_command_line(
int argc,
char** argv)
158 po::options_description common_opts{
"Common options"};
159 po::options_description positional_opts{
"Positional options"};
161 common_opts.add_options()
162 (
"help,h",
"show help and exit")
163 (
"config,c", po::value(&config_file_path),
"configuration file")
164 (
"verbose,v", po::bool_switch(&verbose),
"enable verbose output")
165 (
"ip", po::value<std::string>(),
"IP Address")
166 (
"mac", po::value<std::string>(),
"MAC address")
168 positional_opts.add_options()
169 (
"action", po::value<std::string>(),
"action to run")
172 po::options_description all_opts;
173 all_opts.add(common_opts).add(positional_opts);
175 po::positional_options_description po_desc;
176 po_desc.add(
"action", 1);
178 po::variables_map vm;
179 po::command_line_parser parser{argc, argv};
180 parser.options(all_opts).positional(po_desc);
181 po::options_description visible_opts;
182 visible_opts.add(common_opts);
185 po::store(parser.run(), vm);
188 catch (po::unknown_option& e) {
189 std::clog <<
"Unknown option " << e.get_option_name() <<
"\n\n";
190 print_usage(argv[0], visible_opts);
194 bool show_usage =
false;
196 if (vm.count(
"help") > 0U) {
200 if (vm.count(
"help") == 0U && vm.count(
"action") == 0U) {
201 std::clog <<
"Unspecified action\n\n";
205 if (vm.count(
"help") == 0U && vm.count(
"action") > 0U
206 && ACTION_MAP.find(vm[
"action"].as<std::string>()) == ACTION_MAP.end()) {
207 std::clog <<
"Invalid action " << vm[
"action"].as<std::string>() <<
"\n\n";
212 print_usage(argv[0], visible_opts);
219void print_usage(std::string_view argv0,
const po::options_description& opt_desc)
221 std::clog <<
"kea-custom-hooks by FeM e.V., Version " << KCH_VERSION <<
"\n\n"
222 <<
"Usage: " << argv0 <<
" [options...] [action]\n\n"
223 <<
"Available actions:\n\n";
224 int max_name_len = std::numeric_limits<int>::min();
225 for (
const auto& [name, action] : ACTION_MAP) {
226 max_name_len = std::max(
static_cast<int>(name.length()), max_name_len);
228 const auto fmtflags = std::clog.flags();
229 for (
const auto& [name, action] : ACTION_MAP) {
230 const auto& [desc, fun] = action;
231 std::clog << std::setw(max_name_len) << std::left << name <<
" " << desc <<
'\n';
233 std::clog <<
'\n' << opt_desc;
234 std::clog.flags(fmtflags);
239 for (
const auto& [name, action] : ACTION_MAP) {
240 const auto& [desc, fun] = action;
241 if (name == args[
"action"].as<std::string>()) {
246 throw std::logic_error{
"Invalid action " + args[
"action"].as<std::string>()};
249int run_full_sync([[maybe_unused]]
const po::variables_map& ,
252 vout() <<
"Starting full sync for node " <<
config.ahri_node().api_url <<
'\n';
253 std::unique_ptr<CURL, std::function<void(CURL*)>> hdl{curl_easy_init(), curl_easy_cleanup};
257 nlohmann::json req{{
"command",
"ahri-full-sync"},
258 {
"service", {
config.ahri_node().kea_service}}};
259 std::string post_data{req.dump()};
260 std::ostringstream curl_write_buf;
262 struct curl_slist* headers_ptr =
nullptr;
263 headers_ptr = curl_slist_append(headers_ptr,
"Content-Type: application/json");
264 std::unique_ptr<
struct curl_slist, std::function<void(struct curl_slist*)>> headers{
265 headers_ptr, curl_slist_free_all};
266 headers_ptr =
nullptr;
268 curl_easy_setopt(hdl.get(), CURLOPT_URL,
config.ahri_node().api_url.c_str());
269 curl_easy_setopt(hdl.get(), CURLOPT_WRITEFUNCTION, curl_buf_write);
270 curl_easy_setopt(hdl.get(), CURLOPT_WRITEDATA, &curl_write_buf);
271 curl_easy_setopt(hdl.get(), CURLOPT_POSTFIELDS, post_data.c_str());
272 curl_easy_setopt(hdl.get(), CURLOPT_POSTFIELDSIZE, post_data.size());
273 curl_easy_setopt(hdl.get(), CURLOPT_HTTPHEADER, headers.get());
275 const auto start_time = std::chrono::steady_clock::now();
276 CURLcode res = curl_easy_perform(hdl.get());
277 const auto end_time = std::chrono::steady_clock::now();
278 const auto sync_duration = end_time - start_time;
279 const auto sync_duration_ms =
280 std::chrono::duration_cast<std::chrono::milliseconds>(sync_duration).count();
282 if (res != CURLE_OK) {
283 std::clog <<
"Error syncing local state with AdminDB: " << curl_easy_strerror(res) <<
'\n';
284 if (
config.ahri_node().icinga2_reporting_enabled) {
285 submit_icinga2_check_result(
286 config,
"full-sync", Icinga2CheckResult::CRITICAL,
287 "full-sync CRITICAL - Error syncing local state with AdminDB", std::move(hdl),
288 {{
"duration", Icinga2PerfValue<>{sync_duration_ms,
"ms"}}});
293 nlohmann::json response = nlohmann::json::parse(curl_write_buf.str());
294 assert(response[0][
"result"].is_number());
295 const std::int64_t return_code = response[0][
"result"];
297 if (return_code == 0) {
298 assert(response[0][
"arguments"].is_number());
299 const std::int64_t sync_diff = response[0][
"arguments"];
300 std::clog <<
"Full sync complete\n";
301 std::cout <<
"Difference: " << sync_diff <<
'\n';
303 if (
config.ahri_node().icinga2_reporting_enabled) {
304 submit_icinga2_check_result(
305 config,
"full-sync", Icinga2CheckResult::OK,
306 "full-sync OK - " + std::to_string(sync_diff) +
" differences", std::move(hdl),
307 {{
"differences", Icinga2PerfValue<>{sync_diff}},
308 {
"duration", Icinga2PerfValue<>{sync_duration_ms,
"ms"}}});
313 std::clog <<
"Error syncing local state with AdminDB. Control agent returned " << return_code
315 if (response.contains(
"text")) {
316 std::clog <<
"The error message was: " << response[
"text"] <<
'\n';
319 if (
config.ahri_node().icinga2_reporting_enabled) {
320 submit_icinga2_check_result(
config,
"full-sync", Icinga2CheckResult::CRITICAL,
321 "full-sync CRITICAL - Error syncing local state with AdminDB",
323 {{
"duration", Icinga2PerfValue<>{sync_duration_ms,
"ms"}}});
330 vout() <<
"Connecting to AdminDB... ";
333 config.admindb().database};
336 std::cout <<
"Pruning old host updates... ";
337 db.prune_old_host_updates();
338 std::cout <<
"done\n";
341 catch (pqxx::failure& e) {
342 std::cout <<
"failed\n";
343 std::clog <<
"Error whilst pruning old host updates:\n" << e.what() <<
'\n';
350 if (vm.count(
"ip") + vm.count(
"mac") != 1) {
351 std::clog <<
"Error: must specify exactly one of --ip or --mac\n";
357 config.localdb().database};
359 std::vector<ahri::AdminDBClient::HostUpdate> hosts;
361 if (vm.count(
"ip") > 0) {
362 const isc::asiolink::IOAddress ip_addr{vm.at(
"ip").as<std::string>()};
363 hosts = db.query_host_reservation(ip_addr);
364 }
else if (vm.count(
"mac") > 0) {
365 const isc::dhcp::HWAddr mac_addr =
366 isc::dhcp::HWAddr::fromText(vm.at(
"mac").as<std::string>());
367 hosts = db.query_host_reservation(mac_addr);
370 for (
auto& host : hosts) {
371 std::cout << host.mac.toText(
false) <<
" -> " << host.ipv4.toText() <<
" (subnet "
372 << host.subnet_id <<
")\n";
379 std::string_view service,
380 const Icinga2CheckResult result,
381 std::string_view message,
383 const icinga2_perfdata& perfdata)
386 std::clog <<
"Submitting check result to Icinga2\n";
387 const std::string url =
config.icinga2().api_url +
"/v1/actions/process-check-result";
388 const std::string userpwd =
config.icinga2().api_user +
':' +
config.icinga2().api_password;
390 const nlohmann::json post_data{
393 "host.name == \"" +
config.icinga2().host
394 +
"\" && service.name == \"" +
config.icinga2().services.at(std::string{service})
397 {
"exit_status",
static_cast<unsigned int>(result)},
398 {
"plugin_output", message},
399 {
"performance_data", perfdata_to_json(perfdata)}
403 const std::string post_data_str = post_data.dump();
404 vout() <<
"URL: " << url <<
'\n' <<
"Request: " << post_data_str <<
'\n';
406 struct curl_slist* headers_ptr =
nullptr;
407 headers_ptr = curl_slist_append(headers_ptr,
"Content-Type: application/json");
408 headers_ptr = curl_slist_append(headers_ptr,
"Accept: application/json");
409 std::unique_ptr<
struct curl_slist, std::function<void(struct curl_slist*)>> headers{
410 headers_ptr, curl_slist_free_all};
411 headers_ptr =
nullptr;
412 std::ostringstream curl_write_buf;
414 curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str());
415 curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, curl_buf_write);
416 curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &curl_write_buf);
417 curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, post_data_str.c_str());
418 curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE, post_data_str.size());
419 curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers.get());
420 curl_easy_setopt(curl.get(), CURLOPT_USERPWD, userpwd.c_str());
422 vout() <<
"Performing request ...";
423 const CURLcode res = curl_easy_perform(curl.get());
426 if (res == CURLE_OK) {
427 std::clog <<
"Submitting check results to Icinga2 done\n";
428 vout() <<
"Response:\n" << nlohmann::json::parse(curl_write_buf.str()).dump(2) <<
'\n';
430 std::clog <<
"Failed to perform request: " << curl_easy_strerror(res) <<
'\n';
434nlohmann::json perfdata_to_json(
const icinga2_perfdata& perfdata)
436 nlohmann::json j =
"[]"_json;
437 for (
const auto& [name, val] : perfdata) {
438 std::ostringstream pd;
439 pd << name <<
"=" << val <<
" ";
440 j.push_back(pd.str());
445extern "C" std::size_t curl_buf_write(
char* buffer, [[maybe_unused]] std::size_t size,
446 std::size_t nmemb,
void* userp)
454 std::ostringstream& curl_write_buf = *
reinterpret_cast<std::ostringstream*
>(userp);
456 vout() <<
"Received " << nmemb <<
" bytes of data\n";
458 curl_write_buf.write(buffer, size * nmemb);
461 catch (std::ios_base::failure& e) {
462 std::clog <<
"Failure storing received data: " << e.what() <<
'\n';
int main(int argc, char **argv)
Client for interaction with a single AdminDB server.
Exception for errors during configuration parsing.
const char * what() const noexcept override
Configuration parser for admindb-host-reservation-importer configuration.
Interface to the local Kea database.
std::unique_ptr< ahri::Config > config