]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3278] New classes PerfMonMgr, PerfMonConfig
authorThomas Markwalder <tmark@isc.org>
Tue, 19 Mar 2024 19:58:13 +0000 (15:58 -0400)
committerThomas Markwalder <tmark@isc.org>
Tue, 26 Mar 2024 19:33:28 +0000 (19:33 +0000)
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

14 files changed:
src/bin/dhcp4/dhcp4_srv.h
src/hooks/dhcp/perfmon/Makefile.am
src/hooks/dhcp/perfmon/monitored_duration.cc
src/hooks/dhcp/perfmon/monitored_duration.h
src/hooks/dhcp/perfmon/perfmon_callouts.cc
src/hooks/dhcp/perfmon/perfmon_config.cc [new file with mode: 0644]
src/hooks/dhcp/perfmon/perfmon_config.h [new file with mode: 0644]
src/hooks/dhcp/perfmon/perfmon_mgr.cc [new file with mode: 0644]
src/hooks/dhcp/perfmon/perfmon_mgr.h [new file with mode: 0644]
src/hooks/dhcp/perfmon/tests/Makefile.am
src/hooks/dhcp/perfmon/tests/alarm_parser_unittests.cc [new file with mode: 0644]
src/hooks/dhcp/perfmon/tests/duration_key_parser_unittests.cc [new file with mode: 0644]
src/hooks/dhcp/perfmon/tests/perfmon_config_unittests.cc [new file with mode: 0644]
src/hooks/dhcp/perfmon/tests/perfmon_mgr_unittests.cc [new file with mode: 0644]

index 85f9bce975ae58f2c6e4c108deeb9ef4f87f303d..a8e1d3c013456ba6eba6af77da06491f647f176a 100644 (file)
@@ -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).
index 36302b8cc25b700f7d063d3c8380ea1dab5d5139..9b698236f064a9c88439d5dafe6f93300cedeca2 100644 (file)
@@ -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)
index ec6bb9d5df24acdced79598a7f29c10b9adf4253..230531030973b8209865ba5fc274dee2bc510d4e 100644 (file)
@@ -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
 
index 6efa261498a60ff8036e1374ac8210cffa5dc09b..59cc304683f4268abca94bdfa40d7bbd8af1fd8a 100644 (file)
@@ -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<DurationKey> DurationKeyPtr;
 
index 853ba1c6cfb916fbb54b1011c7918236f1551cb6..959fd3de2a55be128e0deed0446ec47b6f6d1ca7 100644 (file)
@@ -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 (file)
index 0000000..93778ab
--- /dev/null
@@ -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 <config.h>
+
+#include <perfmon_config.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+
+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<std::string, uint16_t> 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<std::string, uint16_t> 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<SubnetID>(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<long long>(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 (file)
index 0000000..1f65c57
--- /dev/null
@@ -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 <cc/data.h>
+#include <cc/simple_parser.h>
+#include <alarm_store.h>
+#include <monitored_duration.h>
+
+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<PerfMonConfig> 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 (file)
index 0000000..a039776
--- /dev/null
@@ -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 <config.h>
+
+#include <perfmon_mgr.h>
+
+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 (file)
index 0000000..3164fb8
--- /dev/null
@@ -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 <perfmon_config.h>
+#include <monitored_duration_store.h>
+#include <asiolink/io_service.h>
+#include <asiolink/interval_timer.h>
+
+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<std::mutex> mutex_;
+};
+
+/// @brief Defines a shared pointer to a PerfMonMgr.
+typedef boost::shared_ptr<PerfMonMgr> PerfMonMgrPtr;
+
+} // end of namespace perfmon
+} // end of namespace isc
+
+#endif
index e74c1d8c3395f175f29b44483b0161d26ae16e9b..e38c046c832ac3f8b232cbf3bf366b99ee3ef0ad 100644 (file)
@@ -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 (file)
index 0000000..a775672
--- /dev/null
@@ -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 <config.h>
+#include <dhcp/dhcp6.h>
+#include <perfmon_config.h>
+#include <testutils/gtest_utils.h>
+
+#include <gtest/gtest.h>
+#include <list>
+
+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<ValidScenario> 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<InvalidScenario> 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<InvalidScenario>& 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<InvalidScenario> 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<InvalidScenario> 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 (file)
index 0000000..fb83d53
--- /dev/null
@@ -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 <config.h>
+#include <dhcp/dhcp6.h>
+#include <perfmon_config.h>
+#include <testutils/gtest_utils.h>
+
+#include <gtest/gtest.h>
+#include <list>
+
+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<ValidScenario>& 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<InvalidScenario>& 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<ValidScenario> 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<InvalidScenario> 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<ValidScenario> 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<InvalidScenario> 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 (file)
index 0000000..2c5b8cf
--- /dev/null
@@ -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 <config.h>
+#include <perfmon_config.h>
+#include <dhcp/dhcp6.h>
+#include <testutils/gtest_utils.h>
+
+#include <gtest/gtest.h>
+#include <list>
+
+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<Scenario> 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<Scenario> 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<DurationKeyPtr> 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<DurationKeyPtr> 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<DurationKeyPtr> 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 (file)
index 0000000..95167e0
--- /dev/null
@@ -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 <config.h>
+#include <perfmon_mgr.h>
+#include <dhcp/dhcp6.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/multi_threading_utils.h>
+
+#include <gtest/gtest.h>
+#include <sstream>
+
+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