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)
--- /dev/null
+// 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 <config.h>
+
+#include <binding_variables.h>
+#include <iostream>
+
+#include <cc/data.h>
+#include <eval/eval_context.h>
+#include <util/multi_threading_mgr.h>
+
+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<VariableSequenceTag>();
+ 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<VariableNameTag>();
+ 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<VariableSourceTag>();
+ 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
--- /dev/null
+// 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 <cc/base_stamped_element.h>
+#include <cc/cfg_to_element.h>
+#include <cc/data.h>
+#include <eval/evaluate.h>
+#include <eval/token.h>
+#include <dhcp/pkt.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <boost/multi_index_container.hpp>
+#include <boost/multi_index/mem_fun.hpp>
+#include <boost/multi_index/hashed_index.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index/sequenced_index.hpp>
+
+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<BindingVariable> BindingVariablePtr;
+
+/// @brief Defines a list of BindingVariablePtr instances.
+typedef std::list<BindingVariablePtr> BindingVariableList;
+
+/// @brief Defines a pointer to a list of BindingVariablePtrs.
+typedef boost::shared_ptr<BindingVariableList> 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<VariableSequenceTag>
+ >,
+ // Second index is by name.
+ boost::multi_index::hashed_unique<
+ boost::multi_index::tag<VariableNameTag>,
+ boost::multi_index::const_mem_fun<BindingVariable,
+ std::string,
+ &BindingVariable::getName>
+ >,
+
+ // Third index is by source.
+ boost::multi_index::ordered_non_unique<
+ boost::multi_index::tag<VariableSourceTag>,
+ boost::multi_index::const_mem_fun<BindingVariable,
+ BindingVariable::Source,
+ &BindingVariable::getSource>
+ >
+ >
+> 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<std::mutex> mutex_;
+
+};
+
+/// @brief Defines a shared pointer to a BindingVariableCache.
+typedef boost::shared_ptr<BindingVariableCache> BindingVariableCachePtr;
+
+} // end of namespace lease_cmds
+} // end of namespace isc
+#endif
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)
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
--- /dev/null
+// 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 <config.h>
+
+
+#include <binding_variables.h>
+#include <exceptions/exceptions.h>
+#include <cc/data.h>
+#include <testutils/gtest_utils.h>
+#include <testutils/user_context_utils.h>
+#include <testutils/multi_threading_utils.h>
+
+#include <gtest/gtest.h>
+
+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<Scenario> 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<Scenario> 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' : <string>: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' : <string>: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' : <string>: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