From: Thomas Markwalder Date: Tue, 19 Mar 2024 19:58:13 +0000 (-0400) Subject: [#3278] New classes PerfMonMgr, PerfMonConfig X-Git-Tag: Kea-2.5.8~114 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=dae3cb04ecc47a7ddf4c1a50bb58b873136f3082;p=thirdparty%2Fkea.git [#3278] New classes PerfMonMgr, PerfMonConfig New files: src/hooks/dhcp/perfmon/perfmon_config.cc src/hooks/dhcp/perfmon/perfmon_config.h src/hooks/dhcp/perfmon/perfmon_mgr.cc src/hooks/dhcp/perfmon/perfmon_mgr.h src/hooks/dhcp/perfmon/tests/alarm_parser_unittests.cc src/hooks/dhcp/perfmon/tests/duration_key_parser_unittests.cc src/hooks/dhcp/perfmon/tests/perfmon_config_unittests.cc src/hooks/dhcp/perfmon/tests/perfmon_mgr_unittests.cc src/bin/dhcp4/dhcp4_srv.h Fixed unrelated doxygen error src/hooks/dhcp/perfmon/Makefile.am Added new files src/hooks/dhcp/perfmon/monitored_duration.* Added DurationKey << operator src/hooks/dhcp/perfmon/perfmon_callouts.cc Fixed comments src/hooks/dhcp/perfmon/tests/Makefile.am Added new files --- diff --git a/src/bin/dhcp4/dhcp4_srv.h b/src/bin/dhcp4/dhcp4_srv.h index 85f9bce975..a8e1d3c013 100644 --- a/src/bin/dhcp4/dhcp4_srv.h +++ b/src/bin/dhcp4/dhcp4_srv.h @@ -1130,7 +1130,7 @@ protected: /// /// @note This is done in two phases: first the content of the /// vendor-class-identifier option is used as a class, by - /// calling @ref classifyByVendor(). Second classification match + /// calling (private) classifyByVendor(). Second classification match /// expressions are evaluated. The resulting classes will be stored /// in the packet (see @ref isc::dhcp::Pkt4::classes_ and /// @ref isc::dhcp::Pkt4::inClass). diff --git a/src/hooks/dhcp/perfmon/Makefile.am b/src/hooks/dhcp/perfmon/Makefile.am index 36302b8cc2..9b698236f0 100644 --- a/src/hooks/dhcp/perfmon/Makefile.am +++ b/src/hooks/dhcp/perfmon/Makefile.am @@ -21,6 +21,8 @@ libperfmon_la_SOURCES += monitored_duration.cc monitored_duration.h libperfmon_la_SOURCES += alarm.cc alarm.h libperfmon_la_SOURCES += monitored_duration_store.cc monitored_duration_store.h libperfmon_la_SOURCES += alarm_store.cc alarm_store.h +libperfmon_la_SOURCES += perfmon_config.cc perfmon_config.h +libperfmon_la_SOURCES += perfmon_mgr.cc perfmon_mgr.h libperfmon_la_SOURCES += version.cc libperfmon_la_CXXFLAGS = $(AM_CXXFLAGS) diff --git a/src/hooks/dhcp/perfmon/monitored_duration.cc b/src/hooks/dhcp/perfmon/monitored_duration.cc index ec6bb9d5df..2305310309 100644 --- a/src/hooks/dhcp/perfmon/monitored_duration.cc +++ b/src/hooks/dhcp/perfmon/monitored_duration.cc @@ -198,6 +198,11 @@ DurationKey::operator<(const DurationKey& other) const { (subnet_id_ < other.subnet_id_)); } +std::ostream& +operator<<(std::ostream& os, const DurationKey& key) { + os << key.getLabel(); + return (os); +} // MonitoredDuration methods diff --git a/src/hooks/dhcp/perfmon/monitored_duration.h b/src/hooks/dhcp/perfmon/monitored_duration.h index 6efa261498..59cc304683 100644 --- a/src/hooks/dhcp/perfmon/monitored_duration.h +++ b/src/hooks/dhcp/perfmon/monitored_duration.h @@ -190,7 +190,6 @@ public: /// @brief Get a composite label of the member values with text message types. /// - /// @param family Protocol family of the key (AF_INET or AF_INET6) /// The format of the string: /// /// @code @@ -260,6 +259,9 @@ protected: isc::dhcp::SubnetID subnet_id_; }; +std::ostream& +operator<<(std::ostream& os, const DurationKey& key); + /// @brief Defines a pointer to a DurationKey instance. typedef boost::shared_ptr DurationKeyPtr; diff --git a/src/hooks/dhcp/perfmon/perfmon_callouts.cc b/src/hooks/dhcp/perfmon/perfmon_callouts.cc index 853ba1c6cf..959fd3de2a 100644 --- a/src/hooks/dhcp/perfmon/perfmon_callouts.cc +++ b/src/hooks/dhcp/perfmon/perfmon_callouts.cc @@ -26,7 +26,7 @@ extern "C" { int dhcp4_srv_configured(CalloutHandle& /* handle */) { // We do this here rather than in load() to ensure we check after the - // filter has been determined. + // packet filter has been determined. LOG_DEBUG(perfmon_logger, DBGLVL_TRACE_BASIC, PERFMON_DHCP4_SOCKET_RECEIVED_TIME_SUPPORT) .arg(IfaceMgr::instance().isSocketReceivedTimeSupported() ? "Yes" : "No"); @@ -35,7 +35,7 @@ int dhcp4_srv_configured(CalloutHandle& /* handle */) { int dhcp6_srv_configured(CalloutHandle& /* handle */) { // We do this here rather than in load() to ensure we check after the - // filter has been determined. + // packet filter has been determined. LOG_DEBUG(perfmon_logger, DBGLVL_TRACE_BASIC, PERFMON_DHCP6_SOCKET_RECEIVED_TIME_SUPPORT) .arg(IfaceMgr::instance().isSocketReceivedTimeSupported() ? "Yes" : "No"); diff --git a/src/hooks/dhcp/perfmon/perfmon_config.cc b/src/hooks/dhcp/perfmon/perfmon_config.cc new file mode 100644 index 0000000000..93778abc59 --- /dev/null +++ b/src/hooks/dhcp/perfmon/perfmon_config.cc @@ -0,0 +1,341 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include +#include +#include + +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace boost::posix_time; + +namespace isc { +namespace perfmon { + +const data::SimpleKeywords +DurationKeyParser::CONFIG_KEYWORDS = +{ + { "query-type", Element::string }, + { "response-type", Element::string }, + { "start-event", Element::string }, + { "stop-event", Element::string }, + { "subnet-id", Element::integer } +}; + +uint16_t +DurationKeyParser::getMessageNameType4(const std::string& name) { + static std::map name_type_map = { + {"", DHCP_NOTYPE}, + {"DHCPDISCOVER", DHCPDISCOVER}, + {"DHCPOFFER", DHCPOFFER}, + {"DHCPREQUEST", DHCPREQUEST}, + {"DHCPDECLINE", DHCPDECLINE}, + {"DHCPACK", DHCPACK}, + {"DHCPNAK", DHCPNAK}, + {"DHCPRELEASE", DHCPRELEASE}, + {"DHCPINFORM", DHCPINFORM}, + {"DHCPLEASEQUERY", DHCPLEASEQUERY}, + {"DHCPLEASEUNASSIGNED", DHCPLEASEUNASSIGNED}, + {"DHCPLEASEUNKNOWN", DHCPLEASEUNKNOWN}, + {"DHCPLEASEACTIVE", DHCPLEASEACTIVE}, + {"DHCPBULKLEASEQUERY", DHCPBULKLEASEQUERY}, + {"DHCPLEASEQUERYDONE", DHCPLEASEQUERYDONE}, + {"DHCPLEASEQUERYSTATUS", DHCPLEASEQUERYSTATUS}, + {"DHCPTLS", DHCPTLS} + }; + + try { + const auto& found = name_type_map.at(name); + return (found); + } catch (const std::out_of_range& ex) { + isc_throw(BadValue, "'" << name << "' is not a valid DHCP message type"); + } +} + +uint16_t +DurationKeyParser::getMessageNameType6(const std::string& name) { + static std::map name_type_map = { + {"", DHCPV6_NOTYPE}, + {"SOLICIT", DHCPV6_SOLICIT}, + {"ADVERTISE", DHCPV6_ADVERTISE}, + {"REQUEST", DHCPV6_REQUEST}, + {"CONFIRM", DHCPV6_CONFIRM}, + {"RENEW", DHCPV6_RENEW}, + {"REBIND", DHCPV6_REBIND}, + {"REPLY", DHCPV6_REPLY}, + {"RELEASE", DHCPV6_RELEASE}, + {"DECLINE", DHCPV6_DECLINE}, + {"RECONFIGURE", DHCPV6_RECONFIGURE}, + {"INFORMATION_REQUEST", DHCPV6_INFORMATION_REQUEST}, + {"RELAY_FORW", DHCPV6_RELAY_FORW}, + {"RELAY_REPL", DHCPV6_RELAY_REPL}, + {"LEASEQUERY", DHCPV6_LEASEQUERY}, + {"LEASEQUERY_REPLY", DHCPV6_LEASEQUERY_REPLY}, + {"LEASEQUERY_DONE", DHCPV6_LEASEQUERY_DONE}, + {"LEASEQUERY_DATA", DHCPV6_LEASEQUERY_DATA}, + {"RECONFIGURE_REQUEST", DHCPV6_RECONFIGURE_REQUEST}, + {"RECONFIGURE_REPLY", DHCPV6_RECONFIGURE_REPLY}, + {"DHCPV4_QUERY", DHCPV6_DHCPV4_QUERY}, + {"DHCPV4_RESPONSE", DHCPV6_DHCPV4_RESPONSE}, + {"ACTIVELEASEQUERY", DHCPV6_ACTIVELEASEQUERY}, + {"STARTTLS", DHCPV6_STARTTLS}, + {"BNDUPD", DHCPV6_BNDUPD}, + {"BNDREPLY", DHCPV6_BNDREPLY}, + {"POOLREQ", DHCPV6_POOLREQ}, + {"POOLRESP", DHCPV6_POOLRESP}, + {"UPDREQ", DHCPV6_UPDREQ}, + {"UPDREQALL", DHCPV6_UPDREQALL}, + {"UPDDONE", DHCPV6_UPDDONE}, + {"CONNECT", DHCPV6_CONNECT}, + {"CONNECTREPLY", DHCPV6_CONNECTREPLY}, + {"DISCONNECT", DHCPV6_DISCONNECT}, + {"STATE", DHCPV6_STATE}, + {"CONTACT", DHCPV6_CONTACT} + }; + + try { + const auto& found = name_type_map.at(name); + return(found); + } catch (const std::out_of_range& ex) { + isc_throw(BadValue, "'" << name << "' is not a valid DHCPV6 message type"); + } +} + +uint16_t +DurationKeyParser::getMessageType(data::ConstElementPtr config, uint16_t family, + const std::string param_name, bool required /*= true */) { + // Parse members. + uint16_t msg_type = 0; + ConstElementPtr elem = config->get(param_name); + if (elem) { + try { + msg_type = (family == AF_INET ? getMessageNameType4(elem->stringValue()) + : getMessageNameType6(elem->stringValue())); + } catch (const std::exception& ex) { + isc_throw(DhcpConfigError, "'" << param_name << "' parameter is invalid, " << ex.what()); + } + } else { + if (required) { + isc_throw(DhcpConfigError, "'" << param_name << "' parameter is required"); + } + } + + return (msg_type); +} + +DurationKeyPtr +DurationKeyParser::parse(data::ConstElementPtr config, uint16_t family) { + // Note checkKeywords() will throw DhcpConfigError if there is a problem. + SimpleParser::checkKeywords(CONFIG_KEYWORDS, config); + + // Parse members. + auto query_type = getMessageType(config, family, "query-type"); + + auto response_type = getMessageType(config, family, "response-type"); + + std::string start_event; + ConstElementPtr elem = config->get("start-event"); + if (elem) { + start_event = elem->stringValue(); + } else { + isc_throw(DhcpConfigError, "'start-event' parameter is required"); + } + + std::string stop_event; + elem = config->get("stop-event"); + if (elem) { + stop_event = elem->stringValue(); + } else { + isc_throw(DhcpConfigError, "'stop-event' parameter is required"); + } + + SubnetID subnet_id = SUBNET_ID_GLOBAL; + elem = config->get("subnet-id"); + if (elem) { + subnet_id = static_cast(elem->intValue()); + } + + return (DurationKeyPtr(new DurationKey(family, query_type, response_type, + start_event, stop_event, subnet_id))); +} + +data::ElementPtr +DurationKeyParser::toElement(DurationKeyPtr key) { + if (!key) { + isc_throw(BadValue, "DurationKeyParser::toElement() - key is empty"); + } + + ElementPtr map = Element::createMap(); + if (key->getFamily() == AF_INET) { + map->set("query-type", Element::create(Pkt4::getName(key->getQueryType()))); + map->set("response-type", Element::create(Pkt4::getName(key->getResponseType()))); + } else { + map->set("query-type", Element::create(Pkt6::getName(key->getQueryType()))); + map->set("response-type", Element::create(Pkt6::getName(key->getResponseType()))); + } + + map->set("start-event", Element::create(key->getStartEventLabel())); + map->set("stop-event", Element::create(key->getStopEventLabel())); + map->set("subnet-id", Element::create(static_cast(key->getSubnetId()))); + return (map); +} + +const data::SimpleKeywords +AlarmParser::CONFIG_KEYWORDS = +{ + {"duration-key", Element::map}, + {"enable-alarm", Element::boolean}, + {"high-water-ms", Element::integer}, + {"low-water-ms", Element::integer} +}; + +AlarmPtr +AlarmParser::parse(data::ConstElementPtr config, uint16_t family) { + // Note checkKeywords() will throw DhcpConfigError if there is a problem. + SimpleParser::checkKeywords(CONFIG_KEYWORDS, config); + + // First parse the duration-key. + ConstElementPtr elem = config->get("duration-key"); + if (!elem) { + isc_throw(DhcpConfigError, "'duration-key'" <<" parameter is required"); + } + + DurationKeyPtr key = DurationKeyParser::parse(elem, family); + + // Parse scalar members. + elem = config->get("enable-alarm"); + bool enable_alarm = (elem ? elem->boolValue() : true); + + elem = config->get("high-water-ms"); + uint64_t high_water_ms = 0; + if (elem) { + int64_t value = elem->intValue(); + if (value <= 0) { + isc_throw(DhcpConfigError, "high-water-ms: '" + << value << "', must be greater than 0"); + } + + high_water_ms = value; + } else { + isc_throw(DhcpConfigError, "'high-water-ms'" <<" parameter is required"); + } + + elem = config->get("low-water-ms"); + uint64_t low_water_ms = 0; + if (elem) { + int64_t value = elem->intValue(); + if (value <= 0) { + isc_throw(DhcpConfigError, "low-water-ms: '" + << value << "', must be greater than 0"); + } + + low_water_ms = value; + } else { + isc_throw(DhcpConfigError, "'low-water-ms'" <<" parameter is required"); + } + + if (low_water_ms >= high_water_ms) { + isc_throw(DhcpConfigError, "'low-water-ms': " << low_water_ms + << ", must be less than 'high-water-ms': " << high_water_ms); + } + + return (AlarmPtr(new Alarm(*key, milliseconds(low_water_ms), + milliseconds(high_water_ms), enable_alarm))); +} + +const data::SimpleKeywords +PerfMonConfig::CONFIG_KEYWORDS = +{ + { "enable-monitoring", Element::boolean }, + { "interval-width-secs", Element::integer }, + { "stats-mgr-reporting", Element::boolean }, + { "alarm-report-secs", Element::integer}, + { "alarms", Element::list} +}; + +PerfMonConfig::PerfMonConfig(uint16_t family) + : family_(family), + enable_monitoring_(true), + interval_width_secs_(60), + stats_mgr_reporting_(true), + alarm_report_secs_(300) { + if (family_ != AF_INET && family_ != AF_INET6) { + isc_throw (BadValue, "PerfmonConfig: family must be AF_INET or AF_INET6"); + } + + alarm_store_.reset(new AlarmStore(family_)); +} + +void +PerfMonConfig::parse(data::ConstElementPtr config) { + // Use a local instance to collect values. This way we + // avoid corrupting current values if there are any errors. + PerfMonConfig local(family_); + + // Note checkKeywords() will throw DhcpConfigError if there is a problem. + SimpleParser::checkKeywords(CONFIG_KEYWORDS, config); + + // Parse members. + ConstElementPtr elem = config->get("enable-monitoring"); + if (elem) { + local.setEnableMonitoring(elem->boolValue()); + } + + elem = config->get("interval-width-secs"); + if (elem) { + int64_t value = elem->intValue(); + if (value <= 0) { + isc_throw(DhcpConfigError, "invalid interval-width-secs: '" + << value << "', must be greater than 0"); + } + + local.setIntervalWidthSecs(value); + } + + elem = config->get("stats-mgr-reporting"); + if (elem) { + local.setStatsMgrReporting(elem->boolValue()); + } + + elem = config->get("alarm-report-secs"); + if (elem) { + int64_t value = elem->intValue(); + if (value < 0) { + isc_throw(DhcpConfigError, "invalid alarm-report-secs: '" + << value << "', cannot be less than 0"); + } + + local.setAlarmReportSecs(value); + } + + elem = config->get("alarms"); + if (elem) { + local.parseAlarms(elem); + } + + // All values good, shallow copy from local instance. + *this = local; +} + +void +PerfMonConfig::parseAlarms(data::ConstElementPtr config) { + alarm_store_.reset(new AlarmStore(family_)); + for (auto const& alarm_elem : config->listValue()) { + try { + AlarmPtr alarm = AlarmParser::parse(alarm_elem, family_); + alarm_store_->addAlarm(alarm); + } catch (const std::exception& ex) { + isc_throw(DhcpConfigError, "cannot add Alarm to store: " << ex.what()); + } + } +} + +} // end of namespace perfmon +} // end of namespace isc diff --git a/src/hooks/dhcp/perfmon/perfmon_config.h b/src/hooks/dhcp/perfmon/perfmon_config.h new file mode 100644 index 0000000000..1f65c57681 --- /dev/null +++ b/src/hooks/dhcp/perfmon/perfmon_config.h @@ -0,0 +1,264 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef PERFMON_CONFIG_H +#define PERFMON_CONFIG_H + +#include +#include +#include +#include + +namespace isc { +namespace perfmon { + +/// @file perfmon_config.h The classes herein parse PerfMon hook library's 'parameters' element +/// depicted below: +/// +/// @code +/// { +/// "library": "lib/kea/hooks/libdhcp_perf_mon.so", +/// "parameters": { +/// "enable-monitoring" : true, +/// "interval-width-secs" : 5, +/// "stats-mgr-reporting" : true, +/// "alarm-report-secs" : 600, +/// "alarms": [ +/// { +/// "duration-key": { +/// "query-type" : "DHCPDISCOVER", +/// "response-type" : "DHCPOFFER", +/// "start-event" : "process-started", +/// "stop-event" : "process-completed", +/// "subnet-id" : 0 +/// }, +/// "enable-alarm" : true, +/// "high-water-ms" : 500, +/// "low-water-ms" : 25, +/// }, +/// .. +/// }] +/// } +/// } +/// @endcode + +/// @brief Parses configuration parameters for a single DurationKey +/// +/// DurationKey is used to identify both MonitoredDurations and +/// Alarms, thus they could be use to define either in configuration +/// as well as identifiy either in API calls. Given this, it seems +/// prudent to define a "duration-key" element with its own +/// parser. +class DurationKeyParser { +public: + /// @brief List of valid parameters and expected types. + static const data::SimpleKeywords CONFIG_KEYWORDS; + + /// @brief Constructor + explicit DurationKeyParser() = default; + + /// @brief Destructor + ~DurationKeyParser() = default; + + /// @brief Convert a configuration parameter to family-specific message type + /// + /// @param config element map containing the duration key parameters. + /// @param family protocol family AF_INET or AF_INET6 + /// @param param_name configuration parameter name + /// @param required if true then function will throw if the parameter does + /// not exist in the configuration. Defaults to true. + /// + /// @return numeric message type, returns DHCP_NOTYPE if name is empty. + /// @throw DhcpConfigError if parameter type or value is not valid, or when + /// requrred is true and the parameter is not in the map. + static uint16_t getMessageType(data::ConstElementPtr config, + uint16_t family, const std::string param_name, + bool required = true); + + /// @brief Convert string message name to DHCP message type + /// + /// @param name upper-case message name (e.g "DHCPDISCOVER", "DHCPOFFER") + /// + /// @return numeric message type, returns DHCP_NOTYPE if name is empty. + /// @throw BadValue if the message name is unknown. + static uint16_t getMessageNameType4(const std::string& name); + + /// @brief Convert string message name to DHCP6 message type + /// + /// @param name upper-case message name (e.g "DHCPV6_SOLICIT", "DHCV6_REPLY") + /// + /// @return numeric message type, returns DHCPV6_NOTYPE if name is empty. + /// @throw BadValue if the message name is unknown. + static uint16_t getMessageNameType6(const std::string& name); + + /// @brief Convert a map of Elements into a DurationKey. + /// + /// @param config element map containing the duration key parameters. + /// @param family protocol family AF_INET or AF_INET6 + static DurationKeyPtr parse(data::ConstElementPtr config, uint16_t family); + + /// @brief Convert a DurationKey into a map of Elements. + /// + /// @param key DurationKey to convert. + /// + /// @return Pointer to a map of elements. + static data::ElementPtr toElement(DurationKeyPtr key); +}; + +/// @brief Parses configuration parameters for a single Alarm +class AlarmParser { +public: + /// @brief List of valid parameters and expected types. + static const data::SimpleKeywords CONFIG_KEYWORDS; + + /// @brief Constructor + explicit AlarmParser(); + + /// @brief Destructor + ~AlarmParser() = default; + + /// @brief + /// + /// @param config element map containing the alarm parameters. + /// @param family protocol family AF_INET or AF_INET6 + static AlarmPtr parse(data::ConstElementPtr config, uint16_t family); +}; + +/// @brief Houses the PerfMon configuration parameters for a single scope +/// (e.g. global, subnet...); +class PerfMonConfig { +public: + /// @brief List of valid parameters and expected types. + static const data::SimpleKeywords CONFIG_KEYWORDS; + + /// @brief List of valid parameter defaults. + static const data::SimpleDefaults SIMPLE_DEFAULTS; + + /// @brief Constructor + explicit PerfMonConfig(uint16_t family); + + /// @brief Destructor + virtual ~PerfMonConfig() = default; + + /// @brief Extracts member values from an Element::map + /// + /// @param config map of configuration parameters + /// + /// @throw DhcpConfigError if invalid values are detected. + void parse(data::ConstElementPtr config); + + /// @brief Re-creates the AlarmStore and populates it by parsing a + /// list of alarm elements. + /// + /// @param config list of alarm configuration elements + /// + /// @throw DhcpConfigError if a parsing error occurs or + /// there are duplicate alarm keys. + void parseAlarms(data::ConstElementPtr config); + + /// @brief Fetches the value of enable-monitoring + /// + /// @return boolean value of enable-monitoring + bool getEnableMonitoring() const { + return (enable_monitoring_); + }; + + /// @brief Sets the value of enable-monitoring + /// + /// @param value new value for enable-monitoring + void setEnableMonitoring(bool value) { + enable_monitoring_ = value; + } + + /// @brief Fetches the value of interval-width-secs + /// + /// @return integer value of interval-width-secs + uint32_t getIntervalWidthSecs() const { + return (interval_width_secs_); + } + + /// @brief Sets the value of interval-width-secs + /// + /// @param value new value for interval-width-secs + void setIntervalWidthSecs(uint32_t value) { + interval_width_secs_ = value; + } + + /// @brief Fetches the value of stats-mgr-reporting + /// + /// @return boolean value of stats-mgr-reporting + bool getStatsMgrReporting() const { + return (stats_mgr_reporting_); + }; + + /// @brief Sets the value of stats-mgr-reporting + /// + /// @param value new value for stats-mgr-reporting + void setStatsMgrReporting(bool value) { + stats_mgr_reporting_ = value; + } + + /// @brief Fetches the value of alarm-report-secs + /// + /// @return integer value of alarm-report-secs + uint32_t getAlarmReportSecs() const { + return (alarm_report_secs_); + } + + /// @brief Sets the value of alarm-report-secs + /// + /// @param value new value for alarm-report-secs + void setAlarmReportSecs(uint32_t value) { + alarm_report_secs_ = value; + } + + /// @brief Get protocol family + /// + /// @return uint16_t containing the family (AF_INET or AF_INET6) + uint16_t getFamily() { + return (family_); + } + + /// @brief Get the alarm store + /// + /// @return pointer to the alarm store + AlarmStorePtr getAlarmStore() { + return alarm_store_; + } + +protected: + /// @brief Protocol family AF_INET or AF_INET6. + uint16_t family_; + + /// @brief If true, performance data is processed/reported. Defaults to + /// true. If false the library loads and configures but does nothing. + /// Gives users a way to keep the library loaded without it being active. + /// Should be accessible via explicit API command. + bool enable_monitoring_; + + /// @brief Number of seconds a duration accumulates samples until reporting. + /// Defaults to 60. + uint32_t interval_width_secs_; + + /// @brief If true durations report to StatsMgr at the end of each interval. + /// Defaults to true. + bool stats_mgr_reporting_; + + /// @brief Nubmer of seconds between reports of a raised alarm. + /// Defaults to 300. A value of zero disables alarms. + uint32_t alarm_report_secs_; + + /// @brief Stores the configured alarms. + AlarmStorePtr alarm_store_; +}; + +/// @brief Defines a shared pointer to a PerfMonConfig. +typedef boost::shared_ptr PerfMonConfigPtr; + +} // end of namespace perfmon +} // end of namespace isc + +#endif diff --git a/src/hooks/dhcp/perfmon/perfmon_mgr.cc b/src/hooks/dhcp/perfmon/perfmon_mgr.cc new file mode 100644 index 0000000000..a039776963 --- /dev/null +++ b/src/hooks/dhcp/perfmon/perfmon_mgr.cc @@ -0,0 +1,100 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Functions accessed by the hooks framework use C linkage to avoid the name +// mangling that accompanies use of the C++ compiler as well as to avoid +// issues related to namespaces. +#include + +#include + +namespace isc { +namespace perfmon { + +using namespace isc::data; +using namespace boost::posix_time; + +PerfMonMgr::PerfMonMgr(uint16_t family_) + : PerfMonConfig(family_) { + // Set defaults. + interval_duration_ = seconds(interval_width_secs_); + alarm_report_interval_ = seconds(alarm_report_secs_); + duration_store_.reset(new MonitoredDurationStore(family_, interval_duration_)); +} + +void PerfMonMgr::configure(const ConstElementPtr & params) { + if (!params) { + isc_throw(dhcp::DhcpConfigError, "params must not be null"); + return; + } + + if (params->getType() != Element::map) { + isc_throw(dhcp::DhcpConfigError, "params must be an Element::map"); + return; + } + + // Parse 'parameters' map. + try { + parse(params); + } catch (std::exception& ex) { + isc_throw(dhcp::DhcpConfigError, + "PerfMonMgr::configure failed - " << ex.what()); + } + + // Set convenience values. + interval_duration_ = seconds(interval_width_secs_); + alarm_report_interval_ = seconds(alarm_report_secs_); + + // Re-create the duration store. + duration_store_.reset(new MonitoredDurationStore(family_, interval_duration_)); +} + +void PerfMonMgr::processPktEventStack(isc::dhcp::PktPtr /* query */, + isc::dhcp::PktPtr /* response */, + const isc::dhcp::SubnetID& /* subnet_id */) { + isc_throw (NotImplemented, __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__); +} + +void +PerfMonMgr::addDurationSample(DurationKeyPtr key, const Duration& sample) { + // Update duration - duration is only returned if its time to report. + MonitoredDurationPtr duration = duration_store_->addDurationSample(key, sample); + if (duration) { + // Report to stat mgr, returns average duration. + Duration average = reportToStatsMgr(duration); + + // Check the average against an alarm, if one exists. + AlarmPtr alarm = alarm_store_->checkDurationSample(duration, average, alarm_report_interval_); + + // If an alarm had a reportable outcome, report it. + if (alarm) { + reportAlarm(alarm, average); + } + } +} + +Duration +PerfMonMgr::reportToStatsMgr(MonitoredDurationPtr /* duration */) { + isc_throw (NotImplemented, __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__); +} + +void +PerfMonMgr::reportAlarm(AlarmPtr /* alarm */, const Duration& /* average */) { + isc_throw (NotImplemented, __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__); +} + +void +PerfMonMgr::reportTimerExpired() { + isc_throw (NotImplemented, __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__); +} + +void +PerfMonMgr::setNextReportExpiration() { + isc_throw (NotImplemented, __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__); +} + +} // end of namespace perfmon +} // end of namespace isc diff --git a/src/hooks/dhcp/perfmon/perfmon_mgr.h b/src/hooks/dhcp/perfmon/perfmon_mgr.h new file mode 100644 index 0000000000..3164fb8f1d --- /dev/null +++ b/src/hooks/dhcp/perfmon/perfmon_mgr.h @@ -0,0 +1,157 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Functions accessed by the hooks framework use C linkage to avoid the name +// mangling that accompanies use of the C++ compiler as well as to avoid +// issues related to namespaces. + +#ifndef PERFMON_MGR_H +#define PERFMON_MGR_H + +#include +#include +#include +#include + +namespace isc { +namespace perfmon { + +/// @brief Singleton which provides overall configuration, control, and state of +/// the PerfMon hook library. It owns the MonitoredDurationStore and AlarmStore +/// instances and supplies callout and command API handlers. It derives from +/// PerfMonConfig. +class PerfMonMgr : public PerfMonConfig { +public: + /// @brief Constructor + /// + /// @param family Protocol family AF_INET or AF_INET6. + explicit PerfMonMgr(uint16_t family); + + /// @brief Destructor + virtual ~PerfMonMgr() = default; + + /// @brief Parses the hook library 'parameters' element. + /// + /// @param params map of configuration parameters to parse. + void configure(const isc::data::ConstElementPtr& params); + + /// @brief Processes the event stack of a query packet + /// + /// @todo DETAILS TO FOLLOW + /// + /// @param query query packet whose stack is to be processed + /// @param response response packet generated for the query + /// @param subnet_id id of the selected subnet + void processPktEventStack(isc::dhcp::PktPtr query, + isc::dhcp::PktPtr response, + const isc::dhcp::SubnetID& subnet_id); + + /// @brief Adds a duration sample to a MonitoredDuration + /// + /// The MonitoredDuration identified by the given key is fetched from + /// the store and updated with the sample. If the update returns the + /// duration this means it is time to report the duration via StatsMgr. + /// The reported average is then checked against an alarm, if one exists. + /// If the check returns the alarm, then the alarm has undergone a + /// reportable event and is passed to reporting. + /// + /// @param key identifies the duration to update + /// @param sample amount of time that elapsed between the two events + /// identified in the key + void addDurationSample(DurationKeyPtr key, const Duration& sample); + + /// @brief Emits an entry to StatsMgr for a given duration + /// + /// Calculates the average duration for the reportable interval and + /// reports the value to StatsMgr if stat-mgr-reporting is true. + /// + /// @param duration duration to report + /// + /// @return Always returns the average duration for reportable interval. + Duration reportToStatsMgr(MonitoredDurationPtr duration); + + /// @brief Emits a report for a given alarm + /// + /// Emits a WARN log if the alarm state is TRIGGERED or an + /// INFO log if it is CLEARED. This may expand in the future to + /// accomodate additional reporting mechanisms. + /// + /// @param alarm Alarm to report + /// @param average Duration average which caused the state transition. + void reportAlarm(AlarmPtr alarm, const Duration& average); + + /// @brief Handler invoked when the report timer expires. + /// + /// Fetches a list of the durations which are overdue to report and submits + /// them for reporting. + void reportTimerExpired(); + + /// @brief Updates the report timer. + /// + /// MonitoredDurationPtr next = durations->getReportsNext() + /// if next + /// reschedule report timer for (next->getIntervalStart() + interval_duration_); + /// else + /// cancel report timer + void setNextReportExpiration(); + + /// @brief Get the interval duration. + /// + /// @return interval-width-secs as a Duration. + Duration getIntervalDuration() { + return (interval_duration_); + } + + /// @brief Get the alarm report interval. + /// + /// @return alarm-report-secs as a Duration. + Duration getAlarmReportInterval() { + return (alarm_report_interval_); + } + + /// @brief Get the duration store + /// + /// @return pointer to the duration store + MonitoredDurationStorePtr getDurationStore() { + return (duration_store_); + } + +private: + /// @brief Length of time a MonitoredDuration accumulates samples until reporting. + Duration interval_duration_; + + /// @brief Length of time between raised Alarm reports. + /// It's a conversion of alarm-report-secs to a Duration set during configuration + /// parsing. + Duration alarm_report_interval_; + + /// @brief In-memory store of MonitoredDurations. + MonitoredDurationStorePtr duration_store_; + + /// @todo Not sure if we really care. When not in service, traffic will + /// effectively stop. Any active durations will eventually report once via + /// timer but nothing more until traffic resumes. + /// + /// @brief Tracks whether or not the server is processing DHCP packets. + ///dhcp::NetworkStatePtr network_state_; + + /// @brief IOService instance used to the timer. + asiolink::IOServicePtr io_service_; + + /// @brief Timer which tracks the next duration due to report. + asiolink::IntervalTimerPtr report_timer_; + + /// @brief The mutex used to protect internal state. + const boost::scoped_ptr mutex_; +}; + +/// @brief Defines a shared pointer to a PerfMonMgr. +typedef boost::shared_ptr PerfMonMgrPtr; + +} // end of namespace perfmon +} // end of namespace isc + +#endif diff --git a/src/hooks/dhcp/perfmon/tests/Makefile.am b/src/hooks/dhcp/perfmon/tests/Makefile.am index e74c1d8c33..e38c046c83 100644 --- a/src/hooks/dhcp/perfmon/tests/Makefile.am +++ b/src/hooks/dhcp/perfmon/tests/Makefile.am @@ -31,6 +31,10 @@ perfmon_unittests_SOURCES += monitored_duration_unittests.cc perfmon_unittests_SOURCES += alarm_unittests.cc perfmon_unittests_SOURCES += monitored_duration_store_unittests.cc perfmon_unittests_SOURCES += alarm_store_unittests.cc +perfmon_unittests_SOURCES += perfmon_config_unittests.cc +perfmon_unittests_SOURCES += perfmon_mgr_unittests.cc +perfmon_unittests_SOURCES += duration_key_parser_unittests.cc +perfmon_unittests_SOURCES += alarm_parser_unittests.cc perfmon_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES) diff --git a/src/hooks/dhcp/perfmon/tests/alarm_parser_unittests.cc b/src/hooks/dhcp/perfmon/tests/alarm_parser_unittests.cc new file mode 100644 index 0000000000..a775672d6a --- /dev/null +++ b/src/hooks/dhcp/perfmon/tests/alarm_parser_unittests.cc @@ -0,0 +1,430 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// @file This file contains tests which exercise the PerfmonConfig class. + +#include +#include +#include +#include + +#include +#include + +using namespace std; +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::perfmon; +using namespace boost::posix_time; + +namespace { + +// These tests excerise AlarmParser which, with the help of DurationKeyParser +// (tested rigourously elsewhere), parses a map of paramters as shown below: +// +// { +// "duration-key": { +// "query-type" : "DHCPDISCOVER", +// "response-type" : "DHCPOFFER", +// "start-event" : "process-started", +// "stop-event" : "process-completed", +// "subnet-id" : 70 +// }, +// "enable-alarm" : true, +// "high-water-ms" : 500, +// "low-water-ms" : 25, +// } + +/// @brief Describes a valid test scenario. +struct ValidScenario { + int line_; // Scenario line number + std::string json_; // JSON configuration to parse + Alarm::State exp_state_; // Expected value for Alarm::state + uint64_t exp_high_water_ms_; // Expected value for high-water-ms + uint64_t exp_low_water_ms_; // Expected value for low-water-ms +}; + +/// @brief Describes an invalid test scenario. +struct InvalidScenario { + int line_; // Scenario line number + std::string json_; // JSON configuration to parse + std::string exp_message_; // Expected error text +}; + +/// @brief Base class test fixture for testing AlarmParser. +class AlarmParserTest: public ::testing::Test { +public: + /// @brief Constructor. + explicit AlarmParserTest(uint16_t family) : family_(family) { + } + + /// @brief Destructor. + virtual ~AlarmParserTest() = default; + + /// @brief Prepends json for a family-valid 'duration-key' to json text. + /// + /// @param scenario_json text to prepend with the duration-key. + /// + /// @return string containing the prepended text. + virtual std::string makeValidKeyConfig(const std::string& scenario_json) = 0; + + /// @brief Runs a list of valid configurations through AlarmParser::parse(). + void testValidScenarios() { + // List of test scenarios to run. + const std::list scenarios = { + { + // All parameters + __LINE__, + R"( + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25 + )", + Alarm::CLEAR, 500, 25 + }, + { + // No enable-alarm, should default to CLEAR state. + __LINE__, + R"( + "high-water-ms" : 500, + "low-water-ms" : 25 + )", + Alarm::CLEAR, 500, 25 + }, + { + // State should be DISALBED when enable-alarm is false + __LINE__, + R"( + "enable-alarm" : false, + "high-water-ms" : 500, + "low-water-ms" : 25 + )", + Alarm::DISABLED, 500, 25 + } + }; + + // Iterate over the scenarios. + for (auto const& scenario : scenarios) { + stringstream oss; + oss << "scenario at line: " << scenario.line_; + SCOPED_TRACE(oss.str()); + + // Construct valid key + scenario JSON + auto json = makeValidKeyConfig(scenario.json_); + + // Convert JSON text to Element map. + ConstElementPtr json_elements; + ASSERT_NO_THROW(json_elements = Element::fromJSON(json)) + << " json: " << json; + + // Parsing should succeed. + AlarmPtr alarm; + ASSERT_NO_THROW_LOG(alarm = AlarmParser::parse(json_elements, family_)); + + // Verify expected values. + ASSERT_TRUE(alarm); + ASSERT_EQ(*alarm, *expected_key_); + + EXPECT_EQ(alarm->getState(), scenario.exp_state_); + EXPECT_EQ(alarm->getHighWater(), milliseconds(scenario.exp_high_water_ms_)); + EXPECT_EQ(alarm->getLowWater(), milliseconds(scenario.exp_low_water_ms_)); + } + } + + /// @brief Test scenarios that have valid duration-key elements but flawed + /// Alarm scalar parameters. + void testInvalidAlarmScenarios() { + // List of test scenarios to run. These will be prepended with a valid, + // family-specific duration-key element prior to parsing. + list scenarios = { + { + // Spurious parameter + __LINE__, + R"( + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25, + "bogus": true + )", + "spurious 'bogus' parameter" + }, + { + // Invalid type enable-alarm + __LINE__, + R"( + "enable-alarm" : "bogus", + "high-water-ms" : 500, + "low-water-ms" : 25, + "bogus": true + )", + "'enable-alarm' parameter is not a boolean" + }, + { + // Missing high-water-ms + __LINE__, + R"( + "enable-alarm" : true, + "low-water-ms" : 25 + )", + "'high-water-ms' parameter is required" + }, + { + // Invalid type for high-water-ms + __LINE__, + R"( + "enable-alarm" : true, + "high-water-ms" : "bogus", + "low-water-ms" : 25 + )", + "'high-water-ms' parameter is not an integer" + }, + { + // Missing low-water-ms + __LINE__, + R"( + "enable-alarm" : true, + "high-water-ms" : 500 + )", + "'low-water-ms' parameter is required" + }, + { + // Invalid type for low-water-ms + __LINE__, + R"( + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : "bogus" + )", + "'low-water-ms' parameter is not an integer" + }, + { + // Invalid threshold combination + __LINE__, + R"( + "enable-alarm" : true, + "high-water-ms" : 25, + "low-water-ms" : 500 + )", + "'low-water-ms': 500, must be less than 'high-water-ms': 25" + }, + }; + + testInvalidScenarios(scenarios, true); + } + + /// @brief Runs a list of invalid configurations through AlarmParser::parse(). + /// + /// @param list of valid scenarios to run + /// @param add_key When true, scenario json will be prepended with valid, family-specific + /// duration-key element prior to parsing. + void testInvalidScenarios(std::list& scenarios, + bool add_key = true) { + // Iterate over the scenarios. + for (auto const& scenario : scenarios) { + stringstream oss; + oss << "scenario at line: " << scenario.line_; + SCOPED_TRACE(oss.str()); + + // If add_key is true prepend the scenario with valid key json + auto json = (add_key ? makeValidKeyConfig(scenario.json_) : scenario.json_); + + // Convert JSON texts to Element map. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(json)); + + // Parsing elements should succeed. + ASSERT_THROW_MSG(AlarmParser::parse(json_elements, family_), DhcpConfigError, + scenario.exp_message_); + } + } + + /// @brief Protocol family AF_INET or AF_INET6 + uint16_t family_; + + /// @brief Expected DurationKey in Alarm after valid parsing. + DurationKeyPtr expected_key_; +}; + +/// @brief Test fixture for testing AlarmParser for DHCP(v4). +class AlarmParserTest4: public AlarmParserTest { +public: + /// @brief Constructor. + explicit AlarmParserTest4() : AlarmParserTest(AF_INET) { + expected_key_.reset(new DurationKey(family_, DHCPDISCOVER, DHCPOFFER, + "start_here", "stop_there", 33)); + } + + /// @brief Destructor. + virtual ~AlarmParserTest4() = default; + + /// @brief Prepends json for a valid, DHCP 'duration-key' to json text. + /// + /// @param scenario_json text to prepend with the duration-key. + /// + /// @return string containing the prepended text. + virtual std::string makeValidKeyConfig(const std::string& scenario_json) { + std::stringstream oss; + oss << "{" + << + R"( "duration-key": { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 33 + })" + << "," + << scenario_json << "}"; + + return (oss.str()); + } +}; + +/// @brief Test fixture for testing AlarmParser for DHCPV6. +class AlarmParserTest6: public AlarmParserTest { +public: + /// @brief Constructor. + explicit AlarmParserTest6() : AlarmParserTest(AF_INET6) { + expected_key_.reset(new DurationKey(family_, DHCPV6_REQUEST, DHCPV6_REPLY, + "start_here", "stop_there", 33)); + } + + /// @brief Destructor. + virtual ~AlarmParserTest6() = default; + + /// @brief Prepends json for a valid, DHCPV6 'duration-key' to json text. + /// + /// @param scenario_json text to prepend with the duration-key. + /// + /// @return string containing the prepended text. + virtual std::string makeValidKeyConfig(const std::string& scenario_json) { + std::stringstream oss; + oss << "{" + << + R"( "duration-key": { + "query-type": "REQUEST", + "response-type": "REPLY", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 33 + })" + << "," + << scenario_json << "}"; + + return (oss.str()); + } +}; + +TEST_F(AlarmParserTest4, validScenarios4) { + testValidScenarios(); +} + +TEST_F(AlarmParserTest4, invalidAlarmScenarios) { + testInvalidAlarmScenarios(); +} + +TEST_F(AlarmParserTest4, invalidDurationKey) { + // We test just enough key errors to ensure they're caught. + // List of test scenarios to run. + list scenarios = { + { + // Missing duration-key element + __LINE__, + R"({ + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25 + })", + "'duration-key' parameter is required" + }, + { + // Invalid type for duration-key. + __LINE__, + R"({ + "duration-key": "not-a-map", + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25 + })", + "'duration-key' parameter is not a map" + }, + { + // Wrong messages type for v4. + __LINE__, + R"({ + "duration-key": { + "query-type": "REQUEST", + "response-type": "REPLY", + "start-event" : "start-here", + "stop-event" : "stop-here" + }, + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25 + })", + "'query-type' parameter is invalid, 'REQUEST' is not a valid DHCP message type" + }, + }; + + testInvalidScenarios(scenarios, false); +} + +TEST_F(AlarmParserTest6, validScenarios) { + testValidScenarios(); +} + +TEST_F(AlarmParserTest6, invalidScenarios) { + testInvalidAlarmScenarios(); +} + +TEST_F(AlarmParserTest6, invalidDurationKey) { + // We test just enough key errors to ensure they're caught. + // List of test scenarios to run. + list scenarios = { + { + // Missing duration-key element + __LINE__, + R"({ + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25 + })", + "'duration-key' parameter is required" + }, + { + // Invalid type for duration-key. + __LINE__, + R"({ + "duration-key": "not-a-map", + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25 + })", + "'duration-key' parameter is not a map" + }, + { + // Wrong messages type for v6. + __LINE__, + R"({ + "duration-key": { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event" : "start-here", + "stop-event" : "stop-here" + }, + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25 + })", + "'query-type' parameter is invalid, 'DHCPDISCOVER' is not a valid DHCPV6 message type" + }, + }; + + testInvalidScenarios(scenarios, false); +} + +} // end of anonymous namespace diff --git a/src/hooks/dhcp/perfmon/tests/duration_key_parser_unittests.cc b/src/hooks/dhcp/perfmon/tests/duration_key_parser_unittests.cc new file mode 100644 index 0000000000..fb83d53a1c --- /dev/null +++ b/src/hooks/dhcp/perfmon/tests/duration_key_parser_unittests.cc @@ -0,0 +1,555 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// @file This file contains tests which exercise the PerfmonConfig class. + +#include +#include +#include +#include + +#include +#include + +using namespace std; +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::perfmon; + +namespace { + +// These tests excerise DurationKeyParser which parses a map of +// paramters as shown below: +// "duration-key": { +// "query-type" : "DHCPDISCOVER", +// "response-type" : "DHCPOFFER", +// "start-event" : "process-started", +// "stop-event" : "process-completed", +// "subnet-id" : 70 +// } + +/// @brief Describes a valid test scenario. +struct ValidScenario { + int line_; // Scenario line number + std::string json_; // JSON configuration to parse + uint16_t exp_query_type_; // Expected value for query-type + uint16_t exp_response_type_; // Expected value for response-type + std::string exp_start_event_; // Expected value for start-event + std::string exp_stop_event_; // Expected value for stop-event + SubnetID exp_subnet_id_; // Expected value for subnet-id +}; + +/// @brief Describes an invalid test scenario. +struct InvalidScenario { + int line_; // Scenario line number + std::string json_; // JSON configuration to parse + std::string exp_message_; // Expected error text +}; + +/// @brief Test fixture for testing DurationKeyParser. +class DurationKeyParserTest: public ::testing::Test { +public: + /// @brief Constructor. + DurationKeyParserTest() = default; + + /// @brief Destructor. + virtual ~DurationKeyParserTest() = default; + + /// @brief Runs a list of valid configurations through parsing. + /// + /// @param list of valid scenarios to run + /// @param family protocol family to use when parsing + void testValidScenarios(std::list& scenarios, uint16_t family) { + // Iterate over the scenarios. + for (auto const& scenario : scenarios) { + stringstream oss; + oss << "scenario at line: " << scenario.line_; + SCOPED_TRACE(oss.str()); + + // Convert JSON texts to Element map. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_)); + + // Parsing elements should succeed. + DurationKeyPtr key; + ASSERT_NO_THROW_LOG(key = DurationKeyParser::parse(json_elements, family)); + + // Verify expected values. + ASSERT_TRUE(key); + EXPECT_EQ(key->getQueryType(), scenario.exp_query_type_); + EXPECT_EQ(key->getResponseType(), scenario.exp_response_type_); + EXPECT_EQ(key->getStartEventLabel(), scenario.exp_start_event_); + EXPECT_EQ(key->getStopEventLabel(), scenario.exp_stop_event_); + EXPECT_EQ(key->getSubnetId(), scenario.exp_subnet_id_); + } + } + + /// @brief Runs a list of invalid configurations through parsing. + /// + /// @param list of valid scenarios to run + /// @param family protocol family to use when parsing + void testInvalidScenarios(std::list& scenarios, uint16_t family) { + // Iterate over the scenarios. + for (auto const& scenario : scenarios) { + stringstream oss; + oss << "scenario at line: " << scenario.line_; + SCOPED_TRACE(oss.str()); + + // Convert JSON texts to Element map. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_)); + + // Parsing elements should succeed. + ASSERT_THROW_MSG(DurationKeyParser::parse(json_elements, family), DhcpConfigError, + scenario.exp_message_); + } + } +}; + +TEST_F(DurationKeyParserTest, validScenarios4) { + // List of test scenarios to run. + std::list scenarios = { + { + // All parameters, + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + DHCPDISCOVER, DHCPOFFER, "start_here", "stop_there", 700 + }, + { + // Empty message types - we allow empty types for API lookups + __LINE__, + R"( + { + "query-type": "", + "response-type": "", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + DHCP_NOTYPE, DHCP_NOTYPE, "start_here", "stop_there", 700 + }, + { + // Empty event labels - we allow empty events for API lookups + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": "", + "stop-event": "", + "subnet-id": 700 + })", + DHCPDISCOVER, DHCPOFFER, "", "", 700 + }, + { + // Subnet id zero, + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 0 + })", + DHCPDISCOVER, DHCPOFFER, "start_here", "stop_there", SUBNET_ID_GLOBAL + }, + }; + + testValidScenarios(scenarios, AF_INET); +} + +TEST_F(DurationKeyParserTest, invalidScenarios4) { + // List of test scenarios to run. + list scenarios = { + { + // Spurious parameter + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700, + "bogus": true + })", + "spurious 'bogus' parameter" + }, + { + // Missing query-type + __LINE__, + R"( + { + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'query-type' parameter is required" + }, + { + // Non-string value for query-type + __LINE__, + R"( + { + "query-type": 1234, + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'query-type' parameter is not a string" + }, + { + // Non-existent query-type + __LINE__, + R"( + { + "query-type": "BOGUS", + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'query-type' parameter is invalid, 'BOGUS' is not a valid DHCP message type" + }, + { + // Missing response-type + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'response-type' parameter is required" + }, + { + // Non-string value for response-type + __LINE__, + R"( + { + "query-type": "DHCDISCOVER", + "response-type": 5768, + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'response-type' parameter is not a string" + }, + { + // Non-existent response-type + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "BOGUS", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'response-type' parameter is invalid, 'BOGUS' is not a valid DHCP message type" + }, + { + // Missing start-event + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'start-event' parameter is required" + }, + { + // Non-string start-event + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": 5678, + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'start-event' parameter is not a string" + }, + { + // Missing stop-event + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": "start_here", + "subnet-id": 700 + })", + "'stop-event' parameter is required" + }, + { + // Non-string start-event + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": 1234, + "subnet-id": 700 + })", + "'stop-event' parameter is not a string" + }, + { + // Non-integer subnet-id + __LINE__, + R"( + { + "query-type": "DHCPDISCOVER", + "response-type": "DHCPOFFER", + "start-event": "start_here", + "stop-event": "stop_here", + "subnet-id": false + })", + "'subnet-id' parameter is not an integer" + }, + }; + + testInvalidScenarios(scenarios, AF_INET); +} + +TEST_F(DurationKeyParserTest, parseValidScenarios6) { + // List of test scenarios to run. + std::list scenarios = { + { + // All parameters, + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + DHCPV6_SOLICIT, DHCPV6_ADVERTISE, "start_here", "stop_there", 700 + }, + { + // Empty message types - we allow empty types for API lookups + __LINE__, + R"( + { + "query-type": "", + "response-type": "", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + DHCP_NOTYPE, DHCP_NOTYPE, "start_here", "stop_there", 700 + }, + { + // Empty event labels - we allow empty events for API lookups + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "start-event": "", + "stop-event": "", + "subnet-id": 700 + })", + DHCPV6_SOLICIT, DHCPV6_ADVERTISE, "", "", 700 + }, + { + // Subnet id zero, + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 0 + })", + DHCPV6_SOLICIT, DHCPV6_ADVERTISE, "start_here", "stop_there", SUBNET_ID_GLOBAL + }, + }; + + testValidScenarios(scenarios, AF_INET6); +} + +TEST_F(DurationKeyParserTest, invalidScenarios6) { + // List of test scenarios to run. + list scenarios = { + { + // Spurious parameter + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700, + "bogus": true + })", + "spurious 'bogus' parameter" + }, + { + // Missing query-type + __LINE__, + R"( + { + "response-type": "ADVERTISE", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'query-type' parameter is required" + }, + { + // Non-string value for query-type + __LINE__, + R"( + { + "query-type": 1234, + "response-type": "ADVERTISE", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'query-type' parameter is not a string" + }, + { + // Non-existent query-type + __LINE__, + R"( + { + "query-type": "BOGUS", + "response-type": "ADVERTISE", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'query-type' parameter is invalid, 'BOGUS' is not a valid DHCPV6 message type" + }, + { + // Missing response-type + __LINE__, + R"( + { + "query-type": "SOLICIT", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'response-type' parameter is required" + }, + { + // Non-string value for response-type + __LINE__, + R"( + { + "query-type": "DHCDISCOVER", + "response-type": 5768, + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'response-type' parameter is not a string" + }, + { + // Non-existent response-type + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "BOGUS", + "start-event": "start_here", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'response-type' parameter is invalid, 'BOGUS' is not a valid DHCPV6 message type" + }, + { + // Missing start-event + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'start-event' parameter is required" + }, + { + // Non-string start-event + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "start-event": 5678, + "stop-event": "stop_there", + "subnet-id": 700 + })", + "'start-event' parameter is not a string" + }, + { + // Missing stop-event + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "start-event": "start_here", + "subnet-id": 700 + })", + "'stop-event' parameter is required" + }, + { + // Non-string start-event + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "start-event": "start_here", + "stop-event": 1234, + "subnet-id": 700 + })", + "'stop-event' parameter is not a string" + }, + { + // Non-integer subnet-id + __LINE__, + R"( + { + "query-type": "SOLICIT", + "response-type": "ADVERTISE", + "start-event": "start_here", + "stop-event": "stop_here", + "subnet-id": false + })", + "'subnet-id' parameter is not an integer" + }, + }; + + testInvalidScenarios(scenarios, AF_INET6); +} + +} // end of anonymous namespace diff --git a/src/hooks/dhcp/perfmon/tests/perfmon_config_unittests.cc b/src/hooks/dhcp/perfmon/tests/perfmon_config_unittests.cc new file mode 100644 index 0000000000..2c5b8cf7cc --- /dev/null +++ b/src/hooks/dhcp/perfmon/tests/perfmon_config_unittests.cc @@ -0,0 +1,479 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// @file This file contains tests which exercise the PerfMonConfig class. + +#include +#include +#include +#include + +#include +#include + +using namespace std; +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::perfmon; + +namespace { + +/// @brief Test fixture for testing PerfMonConfig parsing of the +/// hook library's 'parameters' element. +class PerfMonConfigTest : public ::testing::Test { +public: + /// @brief Constructor. + explicit PerfMonConfigTest(uint16_t family) : family_(family) { + } + + /// @brief Destructor. + virtual ~PerfMonConfigTest() = default; + + /// @brief Verifies PerfMonConfig constructors and accessors. + void testBasics() { + PerfMonConfigPtr config; + + // Verify that an invalid family is caught. + ASSERT_THROW_MSG(config.reset(new PerfMonConfig(777)), BadValue, + "PerfmonConfig: family must be AF_INET or AF_INET6"); + + // Verify initial values. + ASSERT_NO_THROW_LOG(config.reset(new PerfMonConfig(family_))); + ASSERT_TRUE(config); + EXPECT_TRUE(config->getEnableMonitoring()); + EXPECT_EQ(config->getIntervalWidthSecs(), 60); + EXPECT_TRUE(config->getStatsMgrReporting()); + EXPECT_EQ(config->getAlarmReportSecs(), 300); + EXPECT_TRUE(config->getAlarmStore()); + + // Verify accessors. + EXPECT_NO_THROW_LOG(config->setEnableMonitoring(false)); + EXPECT_FALSE(config->getEnableMonitoring()); + + EXPECT_NO_THROW_LOG(config->setIntervalWidthSecs(4)); + EXPECT_EQ(config->getIntervalWidthSecs(), 4); + + EXPECT_NO_THROW_LOG(config->setStatsMgrReporting(false)); + EXPECT_FALSE(config->getStatsMgrReporting()); + + EXPECT_NO_THROW_LOG(config->setAlarmReportSecs(120)); + EXPECT_EQ(config->getAlarmReportSecs(), 120); + + // Verify shallow copy construction. + PerfMonConfigPtr config2(new PerfMonConfig(*config)); + EXPECT_FALSE(config2->getEnableMonitoring()); + EXPECT_EQ(config2->getIntervalWidthSecs(), 4); + EXPECT_FALSE(config2->getStatsMgrReporting()); + EXPECT_EQ(config2->getAlarmReportSecs(), 120); + EXPECT_EQ(config2->getAlarmStore(), config->getAlarmStore()); + } + + /// @brief Exercises PerfMonConfig parameter parsing with valid configuration + /// permutations. + /// @todo add alarms + void testValidScenarios() { + // Describes a test scenario. + struct Scenario { + int line_; // Scenario line number + std::string json_; // JSON configuration to parse + bool exp_enable_monitoring_; // Expected value for enable-monitoring + uint32_t exp_interval_width_secs_; // Expected value for interval-width-secs + bool exp_stats_mgr_reporting_; // Expected value for stats-mgr-reporting + uint32_t exp_alarm_report_secs_; // Expected value for alarm-report-secs + }; + + // List of test scenarios to run. + list scenarios = { + { + // Empty map + __LINE__, + R"({ })", + true, 60, true, 300 + }, + { + // Only enable-monitoring", + __LINE__, + R"({ "enable-monitoring" : false })", + false, 60, true, 300 + }, + { + // Only interval-width-secs", + __LINE__, + R"({ "interval-width-secs" : 3 })", + true, 3, true, 300 + }, + { + // Only stats-mgr-reporting", + __LINE__, + R"({ "stats-mgr-reporting" : false })", + true, 60, false, 300 + }, + { + // Only alarm-report-secs", + __LINE__, + R"({ "alarm-report-secs" : 77 })", + true, 60, true, 77 + }, + { + // All parameters", + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 2, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 120 + })", + false, 2, false, 120 + }, + }; + + // Iterate over the scenarios. + for (auto const& scenario : scenarios) { + stringstream oss; + oss << "scenario at line: " << scenario.line_; + SCOPED_TRACE(oss.str()); + + // Convert JSON texts to Element map. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_)); + + // Parsing elements should succeed. + PerfMonConfig config(family_); + ASSERT_NO_THROW_LOG(config.parse(json_elements)); + + // Verify expected values. + EXPECT_EQ(config.getEnableMonitoring(), scenario.exp_enable_monitoring_); + EXPECT_EQ(config.getIntervalWidthSecs(), scenario.exp_interval_width_secs_); + EXPECT_EQ(config.getStatsMgrReporting(), scenario.exp_stats_mgr_reporting_); + EXPECT_EQ(config.getAlarmReportSecs(), scenario.exp_alarm_report_secs_); + } + } + + /// @brief Exercises PerfMonConfig parameter parsing with invalid configuration + /// permutations. Duplicate alarms are tested elsewhere. + void testInvalidScenarios() { + // Describes a test scenario. + struct Scenario { + int line_; // Scenario line number + string json_; // JSON configuration to parse + string exp_message_; // Expected exception message + }; + + // List of test scenarios to run. Most scenario supply + // all valid parameters except one in error. This allows + // us to verify that no values are changed if any are in error. + list scenarios = { + { + // Unknown parameter + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 3, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 90, + "bogus" : false + })", + "spurious 'bogus' parameter" + }, + { + // Invalid type for enable-monitoring + __LINE__, + R"( + { + "enable-monitoring" : "not bool", + "interval-width-secs" : 3, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 90 + })", + "'enable-monitoring' parameter is not a boolean" + }, + { + // Value of interval-width-secs is zero + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 0, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 90 + })", + "invalid interval-width-secs: '0', must be greater than 0" + }, + { + // Value of interval-width-secs less than zero + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : -2, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 90 + })", + "invalid interval-width-secs: '-2', must be greater than 0" + }, + { + // Non-boolean type for stats-mgr-reporting + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 1, + "stats-mgr-reporting" : "not bool", + "alarm-report-secs" : 90 + })", + "'stats-mgr-reporting' parameter is not a boolean" + }, + { + // Value of alarm-report-secs is zero + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 1, + "stats-mgr-reporting" : false, + "alarm-report-secs" : -3 + })", + "invalid alarm-report-secs: '-3', cannot be less than 0" + }, + { + // Value of alarm-report-secs less than zero + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 1, + "stats-mgr-reporting" : false, + "alarm-report-secs" : -3 + })", + "invalid alarm-report-secs: '-3', cannot be less than 0" + }, + { + // Value for alarms is not a list. + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 60, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 90, + "alarms": {} + })", + "'alarms' parameter is not a list" + }, + { + // Alarms list contains an invalid entry + __LINE__, + R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 60, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 90, + "alarms": [{ "bogus": "alarm" }] + })", + "cannot add Alarm to store: spurious 'bogus' parameter" + } + }; + + // Iterate over the scenarios. + PerfMonConfig default_config(family_); + for (auto const& scenario : scenarios) { + stringstream oss; + oss << "scenario at line: " << scenario.line_; + SCOPED_TRACE(oss.str()); + + // Convert JSON text to a map of parameters. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(scenario.json_)); + + // Parsing parameters should throw. + PerfMonConfig config(family_); + ASSERT_THROW_MSG(config.parse(json_elements), DhcpConfigError, + scenario.exp_message_); + + // Original values should be intact. + EXPECT_EQ(default_config.getEnableMonitoring(), config.getEnableMonitoring()); + EXPECT_EQ(default_config.getIntervalWidthSecs(), config.getIntervalWidthSecs()); + EXPECT_EQ(default_config.getStatsMgrReporting(), config.getStatsMgrReporting()); + EXPECT_EQ(default_config.getAlarmReportSecs(), config.getAlarmReportSecs()); + } + } + + /// @brief Creates a valid configuration with a list of alarms. + /// + /// @parameter keys list of DurationKeyPtrs for alarms that should appear + /// in the list. + /// + /// @return JSON text for the configuration. + std::string makeConfigWithAlarms(std::vector keys) { + // Create valid configuration test which includes an arbitrary number of + // family-specific alarms from a set of DurationKeys. + stringstream joss; + joss << R"( + { + "enable-monitoring" : false, + "interval-width-secs" : 60, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 90, + "alarms": [ + )"; + + std::string comma=""; + for (auto const& key : keys) { + joss << comma << "\t{"; + joss << R"("duration-key": )"; + auto key_elems = DurationKeyParser::toElement(key); + key_elems->toJSON(joss); + joss << R"(, + "high-water-ms": 500, + "low-water-ms": 25 + } + )"; + + comma = ","; + } + + joss << "]}"; + return (joss.str()); + } + + /// @brief Verifies a valid configuration that includes a list of Alarms. + void testValidAlarmsList() { + // Create valid configuration test which includes an arbitrary number of + // family-specific alarms from a pre-defined set of unique DurationKeys. + std::string json_text = makeConfigWithAlarms(keys_); + + // Convert JSON text to a map of parameters. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(json_text)); + + // Parsing parameters should throw. + PerfMonConfig config(family_); + ASSERT_NO_THROW_LOG(config.parse(json_elements)); + + // Get all should retrieve the alarms in ascending order. + AlarmCollectionPtr alarms = config.getAlarmStore()->getAll(); + ASSERT_EQ(alarms->size(), keys_.size()); + + int idx = 0; + for (auto const& d : *alarms) { + EXPECT_EQ(*d, *keys_[idx]) << "failed on pass :" << idx; + ++idx; + } + } + + /// @brief Verifies a valid configuration with a list duplicate Alarms. + void testDuplicateAlarms() { + std::vector duplicate_keys; + duplicate_keys.push_back(keys_[0]); + duplicate_keys.push_back(keys_[0]); + + // Create valid configuration test which includes an arbitrary number of + // family-specific alarms from a pre-defined set of unique DurationKeys. + std::string json_text = makeConfigWithAlarms(duplicate_keys); + + // Convert JSON text to a map of parameters. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(json_text)); + + // Parsing parameters should throw. + PerfMonConfig config(family_); + if (family_ == AF_INET) { + ASSERT_THROW_MSG(config.parse(json_elements), DhcpConfigError, + "cannot add Alarm to store: AlarmStore::addAlarm:" + " alarm already exists for:" + " DHCPDISCOVER-DHCPOFFER.socket_received-buffer_read.0"); + } else { + ASSERT_THROW_MSG(config.parse(json_elements), DhcpConfigError, + "cannot add Alarm to store: AlarmStore::addAlarm:" + " alarm already exists for:" + " SOLICIT-REPLY.socket_received-buffer_read.0"); + } + } + + /// @brief Protocol family AF_INET or AF_INET6 + uint16_t family_; + + /// @brief Collection of valid family-specific keys. + std::vector keys_; +}; + +/// @brief Test fixture for testing PerfMonConfig for DHCP(v4). +class PerfMonConfigTest4: public PerfMonConfigTest { +public: + /// @brief Constructor. + explicit PerfMonConfigTest4() : PerfMonConfigTest(AF_INET) { + for (int subnet = 0; subnet < 3; ++subnet) { + DurationKeyPtr key(new DurationKey(AF_INET, DHCPDISCOVER, DHCPOFFER, + "socket_received", "buffer_read", subnet)); + keys_.push_back(key); + } + } + + /// @brief Destructor. + virtual ~PerfMonConfigTest4() = default; +}; + +/// @brief Test fixture for testing PerfMonConfig for DHCPV6. +class PerfMonConfigTest6: public PerfMonConfigTest { +public: + /// @brief Constructor. + explicit PerfMonConfigTest6() : PerfMonConfigTest(AF_INET6) { + for (int subnet = 0; subnet < 3; ++subnet) { + DurationKeyPtr key(new DurationKey(AF_INET6, DHCPV6_SOLICIT, DHCPV6_REPLY, + "socket_received", "buffer_read", subnet)); + keys_.push_back(key); + } + } + + /// @brief Destructor. + virtual ~PerfMonConfigTest6() = default; +}; + +TEST_F(PerfMonConfigTest4, basics) { + testBasics(); +} + +TEST_F(PerfMonConfigTest6, basics) { + testBasics(); +} + +TEST_F(PerfMonConfigTest4, validScenarios) { + testValidScenarios(); +} + +TEST_F(PerfMonConfigTest6, validScenarios) { + testValidScenarios(); +} + +TEST_F(PerfMonConfigTest4, invalidScenarios) { + testInvalidScenarios(); +} + +TEST_F(PerfMonConfigTest6, invalidScenarios) { + testInvalidScenarios(); +} + +TEST_F(PerfMonConfigTest4, validAlarmsList) { + testValidAlarmsList(); +} + +TEST_F(PerfMonConfigTest6, validAlarmsList) { + testValidAlarmsList(); +} + +TEST_F(PerfMonConfigTest4, duplicateAlarms) { + testDuplicateAlarms(); +} + +TEST_F(PerfMonConfigTest6, duplicateAlarms) { + testDuplicateAlarms(); +} + +} // end of anonymous namespace diff --git a/src/hooks/dhcp/perfmon/tests/perfmon_mgr_unittests.cc b/src/hooks/dhcp/perfmon/tests/perfmon_mgr_unittests.cc new file mode 100644 index 0000000000..95167e0172 --- /dev/null +++ b/src/hooks/dhcp/perfmon/tests/perfmon_mgr_unittests.cc @@ -0,0 +1,215 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// @file This file contains tests which exercise the PerfmonMgr class. +#include +#include +#include +#include +#include + +#include +#include + +using namespace std; +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::perfmon; +using namespace isc::test; +using namespace boost::posix_time; + +namespace { + +// Verifies MonitoredDurationStore valid construction. +TEST(PerfMonMgr, constructor) { + PerfMonMgrPtr mgr; + + EXPECT_NO_THROW_LOG(mgr.reset(new PerfMonMgr(AF_INET))); + ASSERT_TRUE(mgr); + EXPECT_EQ(mgr->getFamily(), AF_INET); + + EXPECT_NO_THROW_LOG(mgr.reset(new PerfMonMgr(AF_INET6))); + ASSERT_TRUE(mgr); + EXPECT_EQ(mgr->getFamily(), AF_INET6); +} + +/// @brief Test fixture for testing PerfMonMgr +class PerfMonMgrTest : public ::testing::Test { +public: + /// @brief Constructor. + explicit PerfMonMgrTest(uint16_t family) : family_(family) { + } + + /// @brief Destructor. + virtual ~PerfMonMgrTest() = default; + + /// @brief Verifies PerfMonConfig constructors and accessors. + void testBasics() { + PerfMonMgrPtr mgr; + + // Verify that an invalid family is caught. + ASSERT_THROW_MSG(mgr.reset(new PerfMonMgr(777)), BadValue, + "PerfmonConfig: family must be AF_INET or AF_INET6"); + + // Verify initial values. + ASSERT_NO_THROW_LOG(mgr.reset(new PerfMonMgr(family_))); + ASSERT_TRUE(mgr); + EXPECT_TRUE(mgr->getEnableMonitoring()); + EXPECT_EQ(mgr->getIntervalDuration(), seconds(60)); + EXPECT_TRUE(mgr->getStatsMgrReporting()); + EXPECT_EQ(mgr->getAlarmReportInterval(), seconds(300)); + + // Alarm store should exist but be empty. + EXPECT_TRUE(mgr->getAlarmStore()); + EXPECT_EQ(mgr->getAlarmStore()->getFamily(), family_); + AlarmCollectionPtr alarms = mgr->getAlarmStore()->getAll(); + ASSERT_EQ(alarms->size(), 0); + + // Duration store should exist but be empty. + EXPECT_TRUE(mgr->getDurationStore()); + EXPECT_EQ(mgr->getDurationStore()->getFamily(), family_); + MonitoredDurationCollectionPtr durations = mgr->getDurationStore()->getAll(); + ASSERT_EQ(durations->size(), 0); + } + + /// @brief Exercises PerfMonConfig parameter parsing with valid configuration + /// permutations. + /// @todo add alarms + void testValidConfig() { + std::string valid_config = + R"({ + "enable-monitoring" : false, + "interval-width-secs" : 5, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 600, + "alarms": [{ + "duration-key": { + "query-type" : "", + "response-type" : "", + "start-event" : "process-started", + "stop-event" : "process-completed", + "subnet-id" : 70 + }, + "enable-alarm" : true, + "high-water-ms" : 500, + "low-water-ms" : 25 + }] + })"; + + // Convert JSON texts to Element map. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(valid_config)); + + PerfMonMgrPtr mgr(new PerfMonMgr(family_)); + ASSERT_NO_THROW_LOG(mgr->configure(json_elements)); + + EXPECT_FALSE(mgr->getEnableMonitoring()); + EXPECT_EQ(mgr->getIntervalDuration(), seconds(5)); + EXPECT_FALSE(mgr->getStatsMgrReporting()); + EXPECT_EQ(mgr->getAlarmReportInterval(), seconds(600)); + + // AlarmStore should have one alarm. + EXPECT_TRUE(mgr->getAlarmStore()); + EXPECT_EQ(mgr->getAlarmStore()->getFamily(), family_); + AlarmCollectionPtr alarms = mgr->getAlarmStore()->getAll(); + ASSERT_EQ(alarms->size(), 1); + DurationKeyPtr key(new DurationKey(family_, 0, 0, "process-started", "process-completed", 70)); + AlarmPtr alarm = (*alarms)[0]; + ASSERT_TRUE(alarm); + EXPECT_EQ(*alarm, *key) << "alarm:" << alarm->getLabel(); + EXPECT_EQ(alarm->getState(), Alarm::CLEAR); + EXPECT_EQ(alarm->getHighWater(), milliseconds(500)); + EXPECT_EQ(alarm->getLowWater(), milliseconds(25)); + + // Duration store should exist but be empty. + EXPECT_TRUE(mgr->getDurationStore()); + EXPECT_EQ(mgr->getDurationStore()->getFamily(), family_); + MonitoredDurationCollectionPtr durations = mgr->getDurationStore()->getAll(); + ASSERT_EQ(durations->size(), 0); + } + + /// @brief Exercises PerfMonConfig parameter parsing with valid configuration + /// permutations. + /// @todo add alarms + void testInvalidConfig() { + std::string valid_config = + R"({ + "enable-monitoring" : false, + "interval-width-secs" : 5, + "stats-mgr-reporting" : false, + "alarm-report-secs" : 600, + "alarms": "bogus" + })"; + + // Convert JSON texts to Element map. + ConstElementPtr json_elements; + ASSERT_NO_THROW_LOG(json_elements = Element::fromJSON(valid_config)); + + PerfMonMgrPtr mgr(new PerfMonMgr(family_)); + ASSERT_NO_THROW_LOG(mgr->configure(json_elements)); + + EXPECT_FALSE(mgr->getEnableMonitoring()); + EXPECT_EQ(mgr->getIntervalDuration(), seconds(5)); + EXPECT_FALSE(mgr->getStatsMgrReporting()); + EXPECT_EQ(mgr->getAlarmReportInterval(), seconds(600)); + + // Alarm store should exist but be empty. + EXPECT_TRUE(mgr->getAlarmStore()); + EXPECT_EQ(mgr->getAlarmStore()->getFamily(), family_); + AlarmCollectionPtr alarms = mgr->getAlarmStore()->getAll(); + ASSERT_EQ(alarms->size(), 0); + + // Duration store should exist but be empty. + EXPECT_TRUE(mgr->getDurationStore()); + EXPECT_EQ(mgr->getDurationStore()->getFamily(), family_); + MonitoredDurationCollectionPtr durations = mgr->getDurationStore()->getAll(); + ASSERT_EQ(durations->size(), 0); + } + + /// @brief Protocol family AF_INET or AF_INET6 + uint16_t family_; +}; + +/// @brief Test fixture for testing PerfMonConfig for DHCP(v4). +class PerfMonMgrTest4: public PerfMonMgrTest { +public: + /// @brief Constructor. + explicit PerfMonMgrTest4() : PerfMonMgrTest(AF_INET) { + } + + /// @brief Destructor. + virtual ~PerfMonMgrTest4() = default; +}; + +/// @brief Test fixture for testing PerfMonConfig for DHCPV6. +class PerfMonMgrTest6: public PerfMonMgrTest { +public: + /// @brief Constructor. + explicit PerfMonMgrTest6() : PerfMonMgrTest(AF_INET6) { + } + + /// @brief Destructor. + virtual ~PerfMonMgrTest6() = default; +}; + +TEST_F(PerfMonMgrTest4, basics) { + testBasics(); +} + +TEST_F(PerfMonMgrTest6, basics) { + testBasics(); +} + +TEST_F(PerfMonMgrTest4, validConfig) { + testValidConfig(); +} + +TEST_F(PerfMonMgrTest6, validConfig) { + testValidConfig(); +} + +} // end of anonymous namespace