From 24de4fa534f533dea9c5a88ef33f3642221c1f82 Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Thu, 23 Jan 2025 15:11:37 -0500 Subject: [PATCH] [#3463] Added BindingVariable classes to lease cmds /src/hooks/dhcp/lease_cmds/Makefile.am added new files /src/hooks/dhcp/lease_cmds/binding_variables.cc /src/hooks/dhcp/lease_cmds/binding_variables.h new files /src/hooks/dhcp/lease_cmds/tests/Makefile.am added new file /src/hooks/dhcp/lease_cmds/tests/binding_variables_unittest.cc new file --- src/hooks/dhcp/lease_cmds/Makefile.am | 1 + .../dhcp/lease_cmds/binding_variables.cc | 154 ++++++++++++ src/hooks/dhcp/lease_cmds/binding_variables.h | 231 +++++++++++++++++ src/hooks/dhcp/lease_cmds/tests/Makefile.am | 4 +- .../tests/binding_variables_unittest.cc | 234 ++++++++++++++++++ 5 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 src/hooks/dhcp/lease_cmds/binding_variables.cc create mode 100644 src/hooks/dhcp/lease_cmds/binding_variables.h create mode 100644 src/hooks/dhcp/lease_cmds/tests/binding_variables_unittest.cc diff --git a/src/hooks/dhcp/lease_cmds/Makefile.am b/src/hooks/dhcp/lease_cmds/Makefile.am index c57fe33966..65b8f0817d 100644 --- a/src/hooks/dhcp/lease_cmds/Makefile.am +++ b/src/hooks/dhcp/lease_cmds/Makefile.am @@ -20,6 +20,7 @@ liblease_cmds_la_SOURCES += lease_cmds_exceptions.h liblease_cmds_la_SOURCES += lease_parser.h lease_parser.cc liblease_cmds_la_SOURCES += lease_cmds_log.cc lease_cmds_log.h liblease_cmds_la_SOURCES += lease_cmds_messages.cc lease_cmds_messages.h +liblease_cmds_la_SOURCES += binding_variables.h binding_variables.cc liblease_cmds_la_SOURCES += version.cc liblease_cmds_la_CXXFLAGS = $(AM_CXXFLAGS) diff --git a/src/hooks/dhcp/lease_cmds/binding_variables.cc b/src/hooks/dhcp/lease_cmds/binding_variables.cc new file mode 100644 index 0000000000..ecbd05bb0c --- /dev/null +++ b/src/hooks/dhcp/lease_cmds/binding_variables.cc @@ -0,0 +1,154 @@ +// Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Kea Hooks Basic +// Commercial End User License Agreement v2.0. See COPYING file in the premium/ +// directory. + +#include + +#include +#include + +#include +#include +#include + +using namespace isc::dhcp; +using namespace isc::data; + +namespace isc { +namespace lease_cmds { + +BindingVariable::BindingVariable(const std::string& name, + const std::string& expression_str, + const Source& source, + uint32_t family) + : name_(name), expression_str_(expression_str), source_(source), + family_(family) { + if (name_.empty()) { + isc_throw(BadValue, "BindingVariable - name cannot be empty"); + } + + /// @todo If we add socpes we may wish to allow higher order + /// scopes to override lower scopes with empty expressions. + if (expression_str_.empty()) { + isc_throw(BadValue, "BindingVariable - '" << name_ + << "' expression_str cannot be empty"); + } + + if (family_ != AF_INET && family_ != AF_INET6) { + isc_throw(BadValue, "BindingVariable - '" << name_ + << "', invalid family: " << family_); + } + + try { + EvalContext eval_ctx(family_ == AF_INET ? Option::V4 : Option::V6); + eval_ctx.parseString(expression_str_, EvalContext::PARSER_STRING); + expression_.reset(new Expression(eval_ctx.expression_)); + } catch (const std::exception& ex) { + isc_throw(BadValue, "BindingVariable - '" << name_ << "', error parsing expression: '" + << expression_str_ << "' : " << ex.what()); + } +} + +std::string +BindingVariable::evaluate(PktPtr packet) const { + try { + return (evaluateString(*expression_, *packet)); + } catch (const std::exception& ex) { + isc_throw(BadValue, "BindingVariable - " << name_ << ", error evaluating expression: [" + << expression_str_ << "] : " << ex.what()); + } +} + +/// @todo Not sure we need CfgElement derivation +ElementPtr +BindingVariable::toElement() const { + ElementPtr map = Element::createMap(); + map->set("name", Element::create(name_)); + map->set("expression_str", Element::create(expression_str_)); + map->set("source", Element::create((source_ == QUERY ? "query" : "response"))); + // family_ is contextual + return (map); +} + +BindingVariableCache::BindingVariableCache() + : variables_(), mutex_(new std::mutex) { +} + +void +BindingVariableCache::cacheVariable(BindingVariablePtr variable) { + util::MultiThreadingLock lock(*mutex_); + variables_.push_back(variable); +} + +void +BindingVariableCache::clear() { + util::MultiThreadingLock lock(*mutex_); + // Discard contents. + // We use modification time to remember the last time we flushed. + variables_.clear(); + updateModificationTime(); +} + +size_t +BindingVariableCache::size() { + util::MultiThreadingLock lock(*mutex_); + return (variables_.size()); +} + +boost::posix_time::ptime +BindingVariableCache::getLastFlushTime() { + util::MultiThreadingLock lock(*mutex_); + return (BaseStampedElement::getModificationTime()); +} + +/// @brief Tag for the name index. +//struct VariableNameTag { }; + +/// @brief Tag for the source index. +//struct VariableSourceTag { }; + + +BindingVariableListPtr +BindingVariableCache::getAll() { + util::MultiThreadingLock lock(*mutex_); + + BindingVariableListPtr var_list(new BindingVariableList()); + const auto& index = variables_.get(); + for (auto const& variable : index) { + /// For now we'll return the pointer, w/o making a copy + /// of the varaiable itself. We never updates variables + /// so we should be OK. + var_list->push_back(variable); + } + + return (var_list); +} + +BindingVariablePtr +BindingVariableCache::getByName(const std::string& name) { + util::MultiThreadingLock lock(*mutex_); + + const auto& index = variables_.get(); + auto var_iter = index.find(name); + return (var_iter == index.end() ? BindingVariablePtr() : *var_iter); +} + +BindingVariableListPtr +BindingVariableCache::getBySource(const BindingVariable::Source& source) { + util::MultiThreadingLock lock(*mutex_); + + BindingVariableListPtr var_list(new BindingVariableList()); + const auto& index = variables_.get(); + auto lower_limit = index.lower_bound(source); + auto upper_limit = index.upper_bound(source); + for (auto var_iter = lower_limit; var_iter != upper_limit; ++var_iter) { + var_list->push_back(*var_iter); + } + + return (var_list); +} + +} // end of namespace lease_cmds +} // end of namespace isc diff --git a/src/hooks/dhcp/lease_cmds/binding_variables.h b/src/hooks/dhcp/lease_cmds/binding_variables.h new file mode 100644 index 0000000000..6c7c88c698 --- /dev/null +++ b/src/hooks/dhcp/lease_cmds/binding_variables.h @@ -0,0 +1,231 @@ +// Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Kea Hooks Basic +// Commercial End User License Agreement v2.0. See COPYING file in the premium/ +// directory. + +#ifndef BINDING_VARIABLES_H +#define BINDING_VARIABLES_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace isc { +namespace lease_cmds { + +/// @brief Embodies a named expression, whose output when +/// evaluated can be stored in a lease's user-context. +class BindingVariable : public isc::data::CfgToElement { +public: + /// @brief Specifies the packet that the expression should be + /// evaluated against. + enum Source { + QUERY, + RESPONSE + }; + + /// @brief Constructor + /// + /// @param name name of the variable, must be unique. Used + /// both as the key and as the label for the value in the output. + /// @param expression_str Evaluation expression text. + /// @param source Source packet the expression should be + /// evaluated against, either QUERY or RESPONSE. + /// @param family Protocol family of the expression, either + /// AF_INET or AF_INET6. + /// + /// During construction the expression string is parsed for the + /// protocol family. + /// @throw BadValue if name if empty, or expression string fails + /// to parse. + explicit BindingVariable(const std::string& name, + const std::string& expression_str, + const Source& source, + uint32_t family); + + /// @brief Destructor + virtual ~BindingVariable() = default; + + /// @brief Evaluate the variable against the given packet. + /// + /// @param packet Pointer to the target packet. + /// @return string result of the evaluation. + /// @throw BadValue if an evaluation error occurs. + std::string evaluate(dhcp::PktPtr packet) const; + + /// @brief Fetches the variable's name. + /// + /// @return string containing the name. + std::string getName() const { + return (name_); + } + + /// @brief Fetches the variable's pre-parsed expression string. + /// + /// @return string containing the expression. + std::string getExpressionStr() const { + return (expression_str_); + } + + /// @brief Fetches the variable's parsed expression. + /// + /// @return pointer to the expression. + dhcp::ExpressionPtr getExpression() const { + return (expression_); + } + + /// @brief Fetches the variable's packet source. + /// + /// @return Source of the packet. + Source getSource() const { + return (source_); + } + + /// @brief Fetches the variable's protocol family. + /// + /// @return Family of the packet i.e AF_INET or AF_INET6. + uint32_t getFamily() const { + return (family_); + } + + /// @todo Not sure we need CfgElement derivation + virtual data::ElementPtr toElement() const; + +private: + /// @param source Source packet the expression should be + /// evaluated against, either QUERY or RESPONSE. + /// @param family Protocol family of the expression, either + + /// @brief name of the variable. + std::string name_; + + /// @brief Evaluation expression text. + std::string expression_str_; + + /// @brief Source packet the expression should be evaluated against. + Source source_; + + /// @brief Protocol family AF_INET or AF_INET6. + uint32_t family_; + + /// @brief Parsed evaluation expression. + dhcp::ExpressionPtr expression_; +}; + +/// @brief Defines a shared pointer to a BindingVariable. +typedef boost::shared_ptr BindingVariablePtr; + +/// @brief Defines a list of BindingVariablePtr instances. +typedef std::list BindingVariableList; + +/// @brief Defines a pointer to a list of BindingVariablePtrs. +typedef boost::shared_ptr BindingVariableListPtr; + +/// @brief Tag for the sequence index. +struct VariableSequenceTag { }; + +/// @brief Tag for the name index. +struct VariableNameTag { }; + +/// @brief Tag for the source index. +struct VariableSourceTag { }; + +/// @brief the client class multi-index. +typedef boost::multi_index_container< + BindingVariablePtr, + boost::multi_index::indexed_by< + // First index is by sequence. -- Do we need this one? + boost::multi_index::sequenced< + boost::multi_index::tag + >, + // Second index is by name. + boost::multi_index::hashed_unique< + boost::multi_index::tag, + boost::multi_index::const_mem_fun + >, + + // Third index is by source. + boost::multi_index::ordered_non_unique< + boost::multi_index::tag, + boost::multi_index::const_mem_fun + > + > +> BindingVariableContainer; + +/// @brief BindingVariableCache stores binding variables. +/// +/// Wrapper around the variable container that provides +/// thread-safe access and time-stamped management. The +/// later is available if/when supported scopes beyond +/// global are added. +class BindingVariableCache : public data::BaseStampedElement { +public: + /// @brief Constructor + BindingVariableCache(); + + /// @brief Destructor + virtual ~BindingVariableCache() = default; + + /// @brief Adds (or replaces) the variable in the cache. + /// + /// @param variable pointer to the variable to store. + void cacheVariable(BindingVariablePtr variable); + + /// @brief Delete all the entries in the cache. + void clear(); + + /// @brief Returns number of entries in the cache. + size_t size(); + + /// @brief Returns the last time the cache was flushed (or + /// the time it was created if it has never been flushed). + boost::posix_time::ptime getLastFlushTime(); + + /// @brief Fetches all of the binding variables in the order + /// they were added to the cache. + /// + /// @return Pointer to a list of the BindingVariables. + BindingVariableListPtr getAll(); + + /// @brief Fetches a binding variable by name + /// + /// @return A pointer to the variable or an empty pointer + /// if no match is found. + BindingVariablePtr getByName(const std::string& name); + + /// @brief Fetches all of the binding variables in the order + /// they were added to the cache that use a specific source. + /// + /// @return Pointer to a list of the BindingVariables. + BindingVariableListPtr getBySource(const BindingVariable::Source& source); + +private: + /// @brief Variable storage container. + BindingVariableContainer variables_; + + /// @brief The mutex used to protect internal state. + const boost::scoped_ptr mutex_; + +}; + +/// @brief Defines a shared pointer to a BindingVariableCache. +typedef boost::shared_ptr BindingVariableCachePtr; + +} // end of namespace lease_cmds +} // end of namespace isc +#endif diff --git a/src/hooks/dhcp/lease_cmds/tests/Makefile.am b/src/hooks/dhcp/lease_cmds/tests/Makefile.am index cb73aa25df..5c947eefc2 100644 --- a/src/hooks/dhcp/lease_cmds/tests/Makefile.am +++ b/src/hooks/dhcp/lease_cmds/tests/Makefile.am @@ -30,6 +30,7 @@ lease_cmds_unittests_SOURCES = run_unittests.cc lease_cmds_unittests_SOURCES += lease_cmds_unittest.h lease_cmds_unittest.cc lease_cmds_unittests_SOURCES += lease_cmds4_unittest.cc lease_cmds_unittests_SOURCES += lease_cmds6_unittest.cc +lease_cmds_unittests_SOURCES += binding_variables_unittest.cc lease_cmds_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES) @@ -37,7 +38,8 @@ lease_cmds_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) lease_cmds_unittests_CXXFLAGS = $(AM_CXXFLAGS) -lease_cmds_unittests_LDADD = $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la +lease_cmds_unittests_LDADD = $(top_builddir)/src/hooks/dhcp/lease_cmds/liblease_cmds.la +lease_cmds_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la lease_cmds_unittests_LDADD += $(top_builddir)/src/lib/process/libkea-process.la lease_cmds_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la lease_cmds_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la diff --git a/src/hooks/dhcp/lease_cmds/tests/binding_variables_unittest.cc b/src/hooks/dhcp/lease_cmds/tests/binding_variables_unittest.cc new file mode 100644 index 0000000000..8bd8beef56 --- /dev/null +++ b/src/hooks/dhcp/lease_cmds/tests/binding_variables_unittest.cc @@ -0,0 +1,234 @@ +// Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + + +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; +using namespace isc; +using namespace isc::data; +using namespace isc::test; + +using namespace isc::lease_cmds; + +namespace { + +/// @brief Test BindingVariable valid construction scenarios. +TEST(BindingVariableTest, validConstructor) { + BindingVariablePtr bv; + + struct Scenario { + uint32_t line_; + std::string name_; + std::string expression_str_; + BindingVariable::Source source_; + uint32_t family_; + }; + + std::string valid_v4_exp = "pkt4.mac"; + std::string valid_v6_exp = "pkt6.transid"; + + std::list scenarios = { + { + __LINE__, "my-var", valid_v4_exp, BindingVariable::QUERY ,AF_INET + }, + { + __LINE__, "my-var", valid_v4_exp, BindingVariable::RESPONSE ,AF_INET + }, + { + __LINE__, "my-var", valid_v6_exp, BindingVariable::QUERY, AF_INET6 + }, + { + __LINE__, "my-var", valid_v6_exp, BindingVariable::RESPONSE, AF_INET6 + } + }; + + for (auto const& scenario : scenarios) { + ASSERT_NO_THROW_LOG(bv.reset(new BindingVariable(scenario.name_, + scenario.expression_str_, + scenario.source_, + scenario.family_))); + ASSERT_TRUE(bv); + EXPECT_EQ(bv->getName(), scenario.name_); + EXPECT_EQ(bv->getExpressionStr(), scenario.expression_str_); + ASSERT_TRUE(bv->getExpression()); + EXPECT_EQ(bv->getSource(), scenario.source_); + EXPECT_EQ(bv->getFamily(), scenario.family_); + } +} + +/// @brief Test BindingVariable invalid construction scenarios. +TEST(BindingVariableTest, invalidConstructor) { + BindingVariablePtr bv; + + struct Scenario { + uint32_t line_; + std::string name_; + std::string expression_str_; + uint32_t family_; + std::string expected_error_; + }; + + std::string valid_v4_exp = "pkt4.mac"; + std::string valid_v6_exp = "pkt6.transid"; + + std::list scenarios = { + { + __LINE__, "", valid_v4_exp, AF_INET, + "BindingVariable - name cannot be empty" + }, + { + __LINE__, "my-var", "", AF_INET, + "BindingVariable - 'my-var' expression_str cannot be empty" + }, + { + __LINE__, "my-var", "bogus + stuff", AF_INET, + "BindingVariable - 'my-var', error parsing expression: " + "'bogus + stuff' : :1.1: Invalid character: b" + }, + { + __LINE__, "my-var", valid_v4_exp, 99, + "BindingVariable - 'my-var', invalid family: 99" + }, + { + __LINE__, "my-var", valid_v4_exp, AF_INET6, + "BindingVariable - 'my-var', error parsing expression: " + "'pkt4.mac' : :1.1-4: pkt4 can only be used in DHCPv4." + }, + { + __LINE__, "my-var", valid_v6_exp, AF_INET, + "BindingVariable - 'my-var', error parsing expression: " + "'pkt6.transid' : :1.1-4: pkt6 can only be used in DHCPv6." + } + }; + + for (auto const& scenario : scenarios) { + ASSERT_THROW_MSG(bv.reset(new BindingVariable(scenario.name_, + scenario.expression_str_, + BindingVariable::QUERY, + scenario.family_)), + BadValue, scenario.expected_error_); + } +} + +TEST(BindingVariableCacheTest, basics) { + + auto ref_time = boost::posix_time::second_clock::local_time(); + + // Create a new cache. + BindingVariableCachePtr cache(new BindingVariableCache()); + + // Verify last flush time has been set to approximately now. + EXPECT_GE(cache->getLastFlushTime(), ref_time); + ref_time = cache->getLastFlushTime(); + + // Ensure getters return empty lists or pointers without harm. + BindingVariableListPtr var_list; + ASSERT_NO_THROW_LOG(var_list = cache->getAll()); + ASSERT_TRUE(var_list); + EXPECT_EQ(var_list->size(), 0); + + BindingVariablePtr var; + ASSERT_NO_THROW_LOG(var = cache->getByName("foo")); + ASSERT_FALSE(var); + + ASSERT_NO_THROW_LOG(var_list = cache->getBySource(BindingVariable::QUERY)); + ASSERT_TRUE(var_list); + EXPECT_EQ(var_list->size(), 0); + + ASSERT_NO_THROW_LOG(var_list = cache->getBySource(BindingVariable::RESPONSE)); + ASSERT_TRUE(var_list); + EXPECT_EQ(var_list->size(), 0); + + // Add four variables. + std::string valid_v6_exp = "pkt6.transid"; + BindingVariableList ref_list; + ref_list.push_back(BindingVariablePtr(new BindingVariable("one", valid_v6_exp, + BindingVariable::QUERY, + AF_INET6))); + + ref_list.push_back(BindingVariablePtr(new BindingVariable("two", valid_v6_exp, + BindingVariable::RESPONSE, + AF_INET6))); + + ref_list.push_back(BindingVariablePtr(new BindingVariable("three", valid_v6_exp, + BindingVariable::RESPONSE, + AF_INET6))); + + ref_list.push_back(BindingVariablePtr(new BindingVariable("four", valid_v6_exp, + BindingVariable::QUERY, + AF_INET6))); + + for (auto const& ref_iter : ref_list) { + ASSERT_NO_THROW_LOG(cache->cacheVariable(ref_iter)); + } + + // Make sure getAll() returns all four in order added. + ASSERT_NO_THROW_LOG(var_list = cache->getAll()); + ASSERT_TRUE(var_list); + EXPECT_EQ(var_list->size(), 4); + + auto var_iter = var_list->begin(); + for (auto const& ref_iter : ref_list) { + EXPECT_EQ((*var_iter)->getName(), ref_iter->getName()); + EXPECT_EQ((*var_iter)->getSource(), ref_iter->getSource()); + ++var_iter; + } + + // Make sure getByName() can return each one. + for (auto const& ref_iter : ref_list) { + ASSERT_NO_THROW_LOG(var = cache->getByName(ref_iter->getName())); + ASSERT_TRUE(var); + EXPECT_EQ(var->getName(), ref_iter->getName()); + } + + // Make sure getBySource() works for QUERY. + ASSERT_NO_THROW_LOG(var_list = cache->getBySource(BindingVariable::QUERY)); + ASSERT_TRUE(var_list); + ASSERT_EQ(var_list->size(), 2); + + var_iter = var_list->begin(); + for (auto const& ref_iter : ref_list) { + if (ref_iter->getSource() == BindingVariable::QUERY) { + EXPECT_EQ((*var_iter)->getName(), ref_iter->getName()); + ++var_iter; + } + } + + // Make sure getBySource() works for RESPONSE. + ASSERT_NO_THROW_LOG(var_list = cache->getBySource(BindingVariable::RESPONSE)); + ASSERT_TRUE(var_list); + ASSERT_EQ(var_list->size(), 2); + + var_iter = var_list->begin(); + for (auto const& ref_iter : ref_list) { + if (ref_iter->getSource() == BindingVariable::RESPONSE) { + EXPECT_EQ((*var_iter)->getName(), ref_iter->getName()); + ++var_iter; + } + } + + // Make sure last flush time hasn't been touched. + EXPECT_EQ(cache->getLastFlushTime(), ref_time); + + // Sleep 1s so we can check flush time gets updated. + usleep(1000000); + ASSERT_NO_THROW_LOG(cache->clear()); + EXPECT_EQ(cache->size(), 0); + + EXPECT_GT(cache->getLastFlushTime(), ref_time); +} + +} // end of anonymous namespace