kea-custom-hooks
FeM custom hooks libraries for Kea DHCP
main.cpp
Go to the documentation of this file.
1#include <cassert>
2#include <chrono>
3#include <cstdlib>
4#include <fstream>
5#include <functional>
6#include <iostream>
7#include <limits>
8#include <map>
9#include <optional>
10#include <sstream>
11#include <string>
12#include <string_view>
13
14#include <pqxx/except>
15
16#include <asiolink/io_address.h>
17#include <dhcp/hwaddr.h>
18
19#include <boost/program_options.hpp>
20
21#include <nlohmann/json.hpp>
22
23#include <curl/curl.h>
24
28#include "kch_config.h"
29
30namespace po = boost::program_options;
31
32namespace
33{
34enum class Icinga2CheckResult : unsigned int
35{
36 OK = 0,
37 WARNING = 1,
38 CRITICAL = 2,
39 UNKNOWN = 3
40};
41
42template <typename T = std::int64_t>
43struct Icinga2PerfValue
44{
45 T value;
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{};
51};
52
53template <typename T>
54std::ostream& operator<<(std::ostream& os, const Icinga2PerfValue<T>& perf)
55{
56 os << perf.value;
57 if (perf.unit) {
58 os << *perf.unit;
59 }
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) : "");
64 return os;
65}
66
67using curl_ptr = std::unique_ptr<CURL, std::function<void(CURL*)>>;
68using icinga2_perfdata = std::map<std::string, Icinga2PerfValue<>>;
69
70std::ostream& vout();
71po::variables_map parse_command_line(int argc, char** argv);
72void print_usage(std::string_view argv0, const po::options_description& opt_desc);
73int run_action(const po::variables_map& args, const ahri::Config& config);
74int run_full_sync(const po::variables_map& args, const ahri::Config& config);
75int run_prune(const po::variables_map& args, const ahri::Config& config);
76int run_show_reservation(const po::variables_map& vm, const ahri::Config& config);
77void submit_icinga2_check_result(const ahri::Config& config,
78 std::string_view service,
79 Icinga2CheckResult result,
80 std::string_view message,
81 curl_ptr curl,
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,
85 void* userp);
86
87bool verbose{false};
88std::string config_file_path{"/etc/admindb-host-reservation-importer.conf"};
89const std::map<
90 std::string,
91 std::pair<std::string, std::function<int(const po::variables_map&, const ahri::Config&)>>>
92 ACTION_MAP{
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}},
95 {"show-reservation",
96 {"Show host reservation for a given MAC or IP address", run_show_reservation}}};
97
98class NullOstream : public std::ostream
99{
100public:
101 template <typename... T>
102 std::ostream& operator<<([[maybe_unused]] T... /* unused */)
103 {
104 return *this;
105 }
106};
107NullOstream null_out;
108} // namespace
109
110int main(int argc, char** argv)
111{
112 po::variables_map vm;
113 try {
114 vm = parse_command_line(argc, argv);
115 }
116 catch (po::unknown_option&) {
117 return 1;
118 }
119
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';
123 return 1;
124 }
125
126 try {
127 ahri::Config config{config_file};
128
129 curl_global_init(CURL_GLOBAL_ALL);
130 int res = run_action(vm, config);
131 curl_global_cleanup();
132 return res;
133 }
134 catch (ahri::Config::ParseError& e) {
135 std::clog << "Problem in configuration file: " << e.what() << '\n';
136 }
137 catch (boost::program_options::invalid_config_file_syntax& e) {
138 std::clog << "Problem in configuration file: " << e.what() << '\n';
139 }
140
141 return 1;
142}
143
144namespace
145{
146
147std::ostream& vout()
148{
149 if (verbose) {
150 return std::clog;
151 }
152
153 return null_out;
154}
155
156po::variables_map parse_command_line(int argc, char** argv)
157{
158 po::options_description common_opts{"Common options"};
159 po::options_description positional_opts{"Positional options"};
160 // clang-format off
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")
167 ;
168 positional_opts.add_options()
169 ("action", po::value<std::string>(), "action to run")
170 ;
171 // clang-format on
172 po::options_description all_opts;
173 all_opts.add(common_opts).add(positional_opts);
174
175 po::positional_options_description po_desc;
176 po_desc.add("action", 1);
177
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);
183
184 try {
185 po::store(parser.run(), vm);
186 po::notify(vm);
187 }
188 catch (po::unknown_option& e) {
189 std::clog << "Unknown option " << e.get_option_name() << "\n\n";
190 print_usage(argv[0], visible_opts); // NOLINT(*-pointer-arithmetic)
191 throw e;
192 }
193
194 bool show_usage = false;
195
196 if (vm.count("help") > 0U) {
197 show_usage = true;
198 }
199
200 if (vm.count("help") == 0U && vm.count("action") == 0U) {
201 std::clog << "Unspecified action\n\n";
202 show_usage = true;
203 }
204
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";
208 show_usage = true;
209 }
210
211 if (show_usage) {
212 print_usage(argv[0], visible_opts);
213 std::exit(1);
214 }
215
216 return vm;
217}
218
219void print_usage(std::string_view argv0, const po::options_description& opt_desc)
220{
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);
227 }
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';
232 }
233 std::clog << '\n' << opt_desc;
234 std::clog.flags(fmtflags);
235}
236
237int run_action(const po::variables_map& args, const ahri::Config& config)
238{
239 for (const auto& [name, action] : ACTION_MAP) {
240 const auto& [desc, fun] = action;
241 if (name == args["action"].as<std::string>()) {
242 return fun(args, config);
243 }
244 }
245 // We should never get here in the first place
246 throw std::logic_error{"Invalid action " + args["action"].as<std::string>()};
247}
248
249int run_full_sync([[maybe_unused]] const po::variables_map& /* unused */,
250 const ahri::Config& config)
251{
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};
254 // TODO handle a broken handle
255 assert(hdl);
256
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;
261
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;
267
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());
274
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();
281
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"}}});
289 }
290 return 1;
291 }
292
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"];
296
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';
302
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"}}});
309 }
310 return 0;
311 }
312
313 std::clog << "Error syncing local state with AdminDB. Control agent returned " << return_code
314 << '\n';
315 if (response.contains("text")) {
316 std::clog << "The error message was: " << response["text"] << '\n';
317 }
318
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",
322 std::move(hdl),
323 {{"duration", Icinga2PerfValue<>{sync_duration_ms, "ms"}}});
324 }
325 return 1;
326}
327
328int run_prune([[maybe_unused]] const po::variables_map& /* unused */, const ahri::Config& config)
329{
330 vout() << "Connecting to AdminDB... ";
331 ahri::AdminDBClient db{config.admindb().servers.at(0).host, config.admindb().servers.at(0).port,
332 config.admindb().user, config.admindb().password,
333 config.admindb().database};
334 vout() << "done\n";
335 try {
336 std::cout << "Pruning old host updates... ";
337 db.prune_old_host_updates();
338 std::cout << "done\n";
339 return 0;
340 }
341 catch (pqxx::failure& e) {
342 std::cout << "failed\n";
343 std::clog << "Error whilst pruning old host updates:\n" << e.what() << '\n';
344 }
345 return 1;
346}
347
348int run_show_reservation(const po::variables_map& vm, const ahri::Config& config)
349{
350 if (vm.count("ip") + vm.count("mac") != 1) {
351 std::clog << "Error: must specify exactly one of --ip or --mac\n";
352 return 1;
353 }
354
355 ahri::LocalDBClient db{config.localdb().server.host, config.localdb().server.port,
356 config.localdb().user, config.localdb().password,
357 config.localdb().database};
358
359 std::vector<ahri::AdminDBClient::HostUpdate> hosts;
360
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);
368 }
369
370 for (auto& host : hosts) {
371 std::cout << host.mac.toText(false) << " -> " << host.ipv4.toText() << " (subnet "
372 << host.subnet_id << ")\n";
373 }
374
375 return 0;
376}
377
378void submit_icinga2_check_result(const ahri::Config& config,
379 std::string_view service,
380 const Icinga2CheckResult result,
381 std::string_view message,
382 curl_ptr curl,
383 const icinga2_perfdata& perfdata)
384{
385 assert(curl);
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;
389 // clang-format off
390 const nlohmann::json post_data{
391 {"type", "Service"},
392 {"filter",
393 "host.name == \"" + config.icinga2().host
394 + "\" && service.name == \"" + config.icinga2().services.at(std::string{service})
395 + "\""
396 },
397 {"exit_status", static_cast<unsigned int>(result)},
398 {"plugin_output", message},
399 {"performance_data", perfdata_to_json(perfdata)}
400 };
401 // clang-format on
402
403 const std::string post_data_str = post_data.dump();
404 vout() << "URL: " << url << '\n' << "Request: " << post_data_str << '\n';
405
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;
413
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());
421
422 vout() << "Performing request ...";
423 const CURLcode res = curl_easy_perform(curl.get());
424 vout() << "done\n";
425
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';
429 } else {
430 std::clog << "Failed to perform request: " << curl_easy_strerror(res) << '\n';
431 }
432}
433
434nlohmann::json perfdata_to_json(const icinga2_perfdata& perfdata)
435{
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());
441 }
442 return j;
443}
444
445extern "C" std::size_t curl_buf_write(char* buffer, [[maybe_unused]] std::size_t size,
446 std::size_t nmemb, void* userp)
447{
448 assert(buffer);
449 assert(userp);
450 // size is constant according to CURLOPT_WRITEFUNCTION(3)
451 assert(size == 1);
452
453 // NOLINTNEXTLINE(*-reinterpret-cast)
454 std::ostringstream& curl_write_buf = *reinterpret_cast<std::ostringstream*>(userp);
455
456 vout() << "Received " << nmemb << " bytes of data\n";
457 try {
458 curl_write_buf.write(buffer, size * nmemb);
459 return size * nmemb;
460 }
461 catch (std::ios_base::failure& e) {
462 std::clog << "Failure storing received data: " << e.what() << '\n';
463 return 0;
464 }
465}
466} // namespace
int main(int argc, char **argv)
Definition: main.cpp:81
Client for interaction with a single AdminDB server.
Exception for errors during configuration parsing.
Definition: Config.hpp:83
const char * what() const noexcept override
Definition: Config.hpp:93
Configuration parser for admindb-host-reservation-importer configuration.
Definition: Config.hpp:24
Interface to the local Kea database.
std::unique_ptr< ahri::Config > config
Definition: init.cpp:27