From b9244c2ebc75170857a38aee89230eaae9729a69 Mon Sep 17 00:00:00 2001 From: Jean Christophe Roques Date: Fri, 21 Jun 2024 09:40:18 +0200 Subject: [PATCH] use json configuration instead --- agent/CMakeLists.txt | 3 +- agent/doc/agent-doc.md | 7 +- agent/inc/com/centreon/agent/config.hh | 67 +++++++++++ agent/src/config.cc | 147 +++++++++++++++++++++++++ agent/src/main.cc | 136 ++++++----------------- agent/test/CMakeLists.txt | 1 + agent/test/config_test.cc | 67 +++++++++++ 7 files changed, 318 insertions(+), 110 deletions(-) create mode 100644 agent/inc/com/centreon/agent/config.hh create mode 100644 agent/src/config.cc create mode 100644 agent/test/config_test.cc diff --git a/agent/CMakeLists.txt b/agent/CMakeLists.txt index b289ab3dd41..217a6bed0dc 100644 --- a/agent/CMakeLists.txt +++ b/agent/CMakeLists.txt @@ -17,7 +17,7 @@ # # Global options. -project("Centreon agent" C CXX) +project("Centreon agent" CXX) # Set directories. set(INCLUDE_DIR "${PROJECT_SOURCE_DIR}/inc/com/centreon/agent") @@ -104,6 +104,7 @@ add_library(centagent_lib STATIC ${SRC_DIR}/opentelemetry/proto/metrics/v1/metrics.pb.cc ${SRC_DIR}/opentelemetry/proto/common/v1/common.pb.cc ${SRC_DIR}/opentelemetry/proto/resource/v1/resource.pb.cc + ${SRC_DIR}/config.cc ) include_directories( diff --git a/agent/doc/agent-doc.md b/agent/doc/agent-doc.md index f8c167ab93b..04ef3f679e7 100644 --- a/agent/doc/agent-doc.md +++ b/agent/doc/agent-doc.md @@ -2,9 +2,8 @@ ## Introduction -The goal of this program is to execute checks in both windows and linux OS -It's full asynchronous, excepted grpc layers, it's single threaded and you won't find mutex in non grpc code. -This is why when we receive request, we post it to asio in order to process it in the main thread. +The purpose of this program is to run checks in Windows and Linux operating systems. It is entirely asynchronous, with the exception of the gRPC layers. It is also single-threaded and therefore needs no mutexes, except in the gRPC part. +This is why, when a request is received, it is posted to ASIO for processing in the main thread. ## Configuration configuration is given by Engine by a AgentConfiguration sent over grpc @@ -12,7 +11,7 @@ The configuration object is embedded in MessageToAgent::config ## Scheduler We trie to spread checks over check_period. -Example: We have 10 checks to execute during one second. check1 will start at now + 0.1s, second at now + 0.2s.. +Example: We have 10 checks to execute during one second. check1 will start at now, second at now + 0.1s.. When Agent receives configuration, all checks are recreated. For example, we have 100 checks to execute in 10 minute, at it is 12:00:00. diff --git a/agent/inc/com/centreon/agent/config.hh b/agent/inc/com/centreon/agent/config.hh new file mode 100644 index 00000000000..1c834d9b92f --- /dev/null +++ b/agent/inc/com/centreon/agent/config.hh @@ -0,0 +1,67 @@ + +/** + * Copyright 2024 Centreon + * Licensed under the Apache License, Version 2.0(the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ +#ifndef CENTREON_AGENT_CONFIG_HH +#define CENTREON_AGENT_CONFIG_HH + +#include "com/centreon/common/grpc/grpc_config.hh" + +namespace com::centreon::agent { + +class config { + public: + enum log_type { stdout, file }; + + private: + std::string _endpoint; + spdlog::level::level_enum _log_level; + log_type _log_type; + std::string _log_file; + unsigned _log_max_file_size; + unsigned _log_max_files; + + bool _encryption; + std::string _certificate_file; + std::string _private_key_file; + std::string _ca_certificate_file; + std::string _ca_name; + std::string _host; + bool _reverse_connection; + + public: + config(const std::string& path); + + const std::string& get_endpoint() const { return _endpoint; } + spdlog::level::level_enum get_log_level() const { return _log_level; }; + log_type get_log_type() const { return _log_type; } + const std::string& get_log_file() const { return _log_file; } + unsigned get_log_max_file_size() const { return _log_max_file_size; } + unsigned get_log_max_files() const { return _log_max_files; } + + bool use_encryption() const { return _encryption; } + const std::string& get_certificate_file() const { return _certificate_file; } + const std::string& get_private_key_file() const { return _private_key_file; } + const std::string& get_ca_certificate_file() const { + return _ca_certificate_file; + } + const std::string& get_ca_name() const { return _ca_name; } + const std::string& get_host() const { return _host; } + bool use_reverse_connection() const { return _reverse_connection; } +}; +}; // namespace com::centreon::agent + +#endif diff --git a/agent/src/config.cc b/agent/src/config.cc new file mode 100644 index 00000000000..5551ecab4ab --- /dev/null +++ b/agent/src/config.cc @@ -0,0 +1,147 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#include +#include + +#include "com/centreon/common/rapidjson_helper.hh" +#include "com/centreon/exceptions/msg_fmt.hh" +#include "config.hh" + +using namespace com::centreon::agent; +using com::centreon::common::rapidjson_helper; + +static constexpr std::string_view _config_schema(R"( +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "agent config", + "properties": { + "host": { + "description": "IP or dns name, if not given, hostname will be used", + "type": "string", + "minLength": 5 + }, + "endpoint": { + "description": "endpoint of poller where agent have to connect or listen endpoint in case of reverse_connection", + "type": "string", + "pattern": "[\\w\\.:]+:\\w+" + }, + "encryption": { + "description": "true if https, default: false", + "type": "boolean" + }, + "public_cert": { + "description": "path of certificate file .crt", + "type": "string" + }, + "private_key": { + "description": "path of certificate file .key", + "type": "string" + }, + "ca_certificate": { + "description": "path of authority certificate file .crt", + "type": "string" + }, + "ca_name": { + "description": "name of authority certificate", + "type": "string" + }, + "reverse_connection": { + "description": "if true, centagent is a server and centengine will connect to, default:false", + "type": "boolean" + }, + "log_level": { + "description": "log level, may be critical, error, info, debug, trace", + "type": "string", + "pattern": "critical|error|info|debug|trace" + }, + "log_type": { + "description": "stdout or file if log to a file (path must be given by log_file), default: stdout", + "type": "string", + "pattern": "stdout|file" + }, + "log_file": { + "description": "path of the log file", + "type": "string", + "minLength": 5 + }, + "log_max_file_size": { + "description:": "max size in Mo of the log file before rotate, to be valid log_max_files must be also be specified", + "type": "integer", + "min": 1 + }, + "log_max_files": { + "description:": "max of log files before remove, to be valid log_max_file_size must be also be specified", + "type": "integer", + "min": 1 + } + }, + "required": [ + "endpoint" + ], + "type": "object" +} + +)"); + +config::config(const std::string& path) { + static common::json_validator validator(_config_schema); + rapidjson::Document file_content_d; + try { + file_content_d = rapidjson_helper::read_from_file(path); + } catch (const std::exception& e) { + SPDLOG_ERROR("incorrect json file{}: {} ", path, e.what()); + throw; + } + + common::rapidjson_helper json_config(file_content_d); + + try { + json_config.validate(validator); + } catch (const std::exception& e) { + SPDLOG_ERROR("forbidden values in agent config: {}", e.what()); + throw; + } + + _endpoint = json_config.get_string("endpoint"); + + // pattern schema doesn't work so we do it ourselves + if (!RE2::FullMatch(_endpoint, "[\\w\\.:]+:\\w+")) { + throw exceptions::msg_fmt( + "bad format for endpoint {}, it must match to the regex: " + "[\\w\\.:]+:\\w+", + _endpoint); + } + _log_level = + spdlog::level::from_str(json_config.get_string("log_level", "info")); + _log_type = + json_config.get_string("log_type", "stdout") == "file" ? file : stdout; + _log_file = json_config.get_string("log_file", ""); + _log_max_file_size = json_config.get_unsigned("log_max_file_size", 0); + _log_max_files = json_config.get_unsigned("log_max_files", 0); + _encryption = json_config.get_bool("encryption", false); + _certificate_file = json_config.get_string("certificate_file", ""); + _private_key_file = json_config.get_string("private_key_file", ""); + _ca_certificate_file = json_config.get_string("ca_certificate_file", ""); + _ca_name = json_config.get_string("ca_name", ""); + _host = json_config.get_string("host", ""); + if (_host.empty()) { + _host = boost::asio::ip::host_name(); + } + _reverse_connection = json_config.get_bool("reverse_connection", false); +} \ No newline at end of file diff --git a/agent/src/main.cc b/agent/src/main.cc index aa20e99483f..161f9c4f4c5 100644 --- a/agent/src/main.cc +++ b/agent/src/main.cc @@ -20,9 +20,9 @@ #include #include -#include "com/centreon/common/grpc/grpc_config.hh" +#include "config.hh" -namespace po = boost::program_options; +using namespace com::centreon::agent; std::shared_ptr g_io_context = std::make_shared(); @@ -58,14 +58,12 @@ static void signal_handler(const boost::system::error_code& error, } } -static std::string read_crypto_file(const char* field, - const po::variables_map& vm) { - if (!vm.count(field)) { +static std::string read_crypto_file(const std::string& file_path) { + if (file_path.empty()) { return {}; } - std::string path = vm[field].as(); try { - std::ifstream file(path); + std::ifstream file(file_path); if (file.is_open()) { std::stringstream ss; ss << file.rdbuf(); @@ -73,63 +71,24 @@ static std::string read_crypto_file(const char* field, return ss.str(); } } catch (const std::exception& e) { - SPDLOG_LOGGER_ERROR(g_logger, "{} fail to read {}: {}", field, path, + SPDLOG_LOGGER_ERROR(g_logger, "{} fail to read {}: {}", file_path, e.what()); } return ""; } int main(int argc, char* argv[]) { - po::options_description desc("Allowed options"); - desc.add_options()("help,h", "produce help message")( - "log-level", po::value()->default_value("info"), - "log level, may be critical, error, info, debug, trace")( - "endpoint", po::value(), - "connect or listen endpoint ex: 192.168.1.1:4443")( - "config-file", po::value(), - "command line options in a file with format key=value")( - "encryption", po::value()->default_value(false), - "true if encryption")("certificate", po::value(), - "path of the certificate file")( - "private_key", po::value(), - "path of the certificate key file")( - "ca_certificate", po::value(), - "path of the certificate authority file")( - "ca_name", po::value(), "hostname of the certificate")( - "host", po::value(), - "host supervised by this agent (if none given we use name of this " - "host)")("grpc-streaming", po::value()->default_value(true), - "this agent connect to engine in streaming mode")( - "reversed-grpc-streaming", po::value()->default_value(false), - "this agent accept connection from engine in streaming mode")( - "log-type", po::value()->default_value("stdout"), - "type of logger: stdout, file")( - "log-file", po::value(), - "log file used in case of log_type = file")( - "log-max-file-size", po::value(), - "max size of log file in Mo before rotate")( - "log-max-files", po::value(), "max log files"); - - po::variables_map vm; + if (argc < 2) { + SPDLOG_ERROR("usage: {} conf; try { - po::store(po::parse_command_line(argc, argv, desc), vm); - po::notify(vm); - - if (vm.count("help")) { - std::cout << desc << "\n"; - return 1; - } - - if (vm.count("config-file")) { - po::store(po::parse_config_file( - vm["config-file"].as().c_str(), desc), - vm); - } - + conf = std::make_unique(argv[1]); } catch (const std::exception& e) { - SPDLOG_ERROR("fail to parse arguments {}", e.what()); - return 2; + SPDLOG_ERROR("fail to parse config file {}: {}", argv[1], e.what()); + return 1; } SPDLOG_INFO( @@ -139,22 +98,20 @@ int main(int argc, char* argv[]) { const std::string logger_name = "centreon-agent"; - std::string log_type = vm["log-type"].as(); - - if (log_type == "file") { + if (conf->get_log_type() == config::file) { try { - if (vm.count("log-file")) { - if (vm.count("log-max-file-size") && vm.count("log-max-files")) { + if (!conf->get_log_file().empty()) { + if (conf->get_log_max_file_size() > 0 && + conf->get_log_max_files() > 0) { g_logger = spdlog::rotating_logger_mt( - logger_name, vm["log-file"].as(), - vm["log-max-file-size"].as(), - vm["log-max-files"].as()); + logger_name, conf->get_log_file(), + conf->get_log_max_file_size() * 0x100000, + conf->get_log_max_files()); } else { SPDLOG_INFO( "no log-max-file-size option or no log-max-files option provided " "=> logs will not be rotated by centagent"); - g_logger = spdlog::basic_logger_mt(logger_name, - vm["log-file"].as()); + g_logger = spdlog::basic_logger_mt(logger_name, conf->get_log_file()); } } else { SPDLOG_ERROR( @@ -162,64 +119,33 @@ int main(int argc, char* argv[]) { g_logger = spdlog::stdout_color_mt(logger_name); } } catch (const std::exception& e) { - SPDLOG_CRITICAL("Can't log to {}: {}", vm["log-file"].as(), - e.what()); + SPDLOG_CRITICAL("Can't log to {}: {}", conf->get_log_file(), e.what()); return 2; } } else { g_logger = spdlog::stdout_color_mt(logger_name); } - spdlog::set_level(spdlog::level::info); - if (vm.count("log-level")) { - std::string log_level = vm["log-level"].as(); - if (log_level == "critical") { - g_logger->set_level(spdlog::level::critical); - } else if (log_level == "error") { - g_logger->set_level(spdlog::level::err); - } else if (log_level == "debug") { - g_logger->set_level(spdlog::level::debug); - } else if (log_level == "trace") { - g_logger->set_level(spdlog::level::trace); - } - } + g_logger->set_level(conf->get_log_level()); SPDLOG_LOGGER_INFO(g_logger, "centreon-agent start, you can decrease log " "verbosity by kill -USR1 {} or increase by kill -USR2 {}", getpid(), getpid()); - std::shared_ptr conf; - std::string supervised_host; + std::shared_ptr grpc_conf; try { // ignored but mandatory because of forks _signals.add(SIGPIPE); _signals.async_wait(signal_handler); - if (!vm.count("endpoint")) { - SPDLOG_CRITICAL( - "endpoint param is mandatory (represents where to connect or where " - "to listen example: 127.0.0.1:4317)"); - return -1; - } - std::string host_port = vm["endpoint"].as(); - std::string ca_name; - if (vm.count("ca_name")) { - ca_name = vm["ca_name"].as(); - } - - if (vm.count("host")) { - supervised_host = vm["host"].as(); - } - if (supervised_host.empty()) { - supervised_host = boost::asio::ip::host_name(); - } - conf = std::make_shared( - host_port, vm["encryption"].as(), - read_crypto_file("certificate", vm), - read_crypto_file("private_key", vm), - read_crypto_file("ca_certificate", vm), ca_name, true, 30); + grpc_conf = std::make_shared( + conf->get_endpoint(), conf->use_encryption(), + read_crypto_file(conf->get_certificate_file()), + read_crypto_file(conf->get_private_key_file()), + read_crypto_file(conf->get_ca_certificate_file()), conf->get_ca_name(), + true, 30); } catch (const std::exception& e) { SPDLOG_CRITICAL("fail to parse input params: {}", e.what()); diff --git a/agent/test/CMakeLists.txt b/agent/test/CMakeLists.txt index 1b73659a6a5..a7acf6d8297 100644 --- a/agent/test/CMakeLists.txt +++ b/agent/test/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(ut_agent + config_test.cc test_main.cc ${TESTS_SOURCES}) diff --git a/agent/test/config_test.cc b/agent/test/config_test.cc new file mode 100644 index 00000000000..6ebba2835e2 --- /dev/null +++ b/agent/test/config_test.cc @@ -0,0 +1,67 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#include +#include + +#include "config.hh" + +using namespace com::centreon::agent; + +static const std::string _json_config_path = + std::filesystem::temp_directory_path() / "config_test.json"; + +TEST(config, bad_format) { + ::remove(_json_config_path.c_str()); + std::ofstream f(_json_config_path); + f << "g,lezjrgerg"; + f.close(); + ASSERT_THROW(config conf(_json_config_path), std::exception); +} + +TEST(config, no_endpoint) { + ::remove(_json_config_path.c_str()); + std::ofstream f(_json_config_path); + f << R"({"encryption":false})"; + f.close(); + ASSERT_THROW(config conf(_json_config_path), std::exception); +} + +TEST(config, bad_endpoint) { + ::remove(_json_config_path.c_str()); + std::ofstream f(_json_config_path); + f << R"({"endpoint":"taratata"})"; + f.close(); + ASSERT_THROW(config conf(_json_config_path), std::exception); +} + +TEST(config, good_endpoint) { + ::remove(_json_config_path.c_str()); + std::ofstream f(_json_config_path); + f << R"({"endpoint":"host1.domain2:4317"})"; + f.close(); + ASSERT_NO_THROW(config conf(_json_config_path)); +} + +TEST(config, bad_log_level) { + ::remove(_json_config_path.c_str()); + std::ofstream f(_json_config_path); + f << R"({"endpoint":"host1.domain2:4317","log_level":"erergeg"})"; + f.close(); + ASSERT_THROW(config conf(_json_config_path), std::exception); +}