Parcourir la source

[master] DHCP servers now support client-classes element

    Merges in branch 'trac4096'
Thomas Markwalder il y a 9 ans
Parent
commit
d21fd69259

+ 65 - 1
src/bin/dhcp4/dhcp4.spec

@@ -278,7 +278,71 @@
             }
         ]
       },
-
+      { "item_name": "client-classes",
+        "item_type": "list",
+        "item_optional": true,
+        "item_default": [],
+        "list_item_spec":
+        {
+            "item_name": "client-class",
+            "item_type": "map",
+            "item_optional": false,
+            "item_default": {},
+            "map_item_spec": [
+                { "item_name": "name",
+                  "item_type": "string",
+                  "item_optional": false,
+                  "item_default": ""
+                },
+                { "item_name": "test",
+                  "item_type": "string",
+                  "item_optional": true,
+                  "item_default": ""
+                },
+                { "item_name": "option-data",
+                  "item_type": "list",
+                  "item_optional": true,
+                  "item_default": [],
+                  "list_item_spec":
+                  {
+                    "item_name": "single-option-data",
+                    "item_type": "map",
+                    "item_optional": false,
+                    "item_default": {},
+                    "map_item_spec": [
+                    {
+                      "item_name": "name",
+                      "item_type": "string",
+                      "item_optional": false,
+                      "item_default": ""
+                    },
+                    {
+                      "item_name": "code",
+                      "item_type": "integer",
+                      "item_optional": false,
+                      "item_default": 0
+                    },
+                    {
+                      "item_name": "data",
+                      "item_type": "string",
+                      "item_optional": false,
+                      "item_default": ""
+                    },
+                    { "item_name": "csv-format",
+                      "item_type": "boolean",
+                      "item_optional": false,
+                      "item_default": false
+                      },
+                    { "item_name": "space",
+                      "item_type": "string",
+                      "item_optional": false,
+                      "item_default": "dhcp4"
+                    } ]
+                  }
+                }
+            ]
+        }
+      },
       { "item_name": "subnet4",
         "item_type": "list",
         "item_optional": false,

+ 15 - 0
src/bin/dhcp4/json_config_parser.cc

@@ -20,6 +20,7 @@
 #include <dhcp/option_definition.h>
 #include <dhcpsrv/cfg_option.h>
 #include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/parsers/client_class_def_parser.h>
 #include <dhcp4/json_config_parser.h>
 #include <dhcpsrv/option_space_container.h>
 #include <dhcpsrv/parsers/dbaccess_parser.h>
@@ -451,6 +452,8 @@ DhcpConfigParser* createGlobalDhcp4ConfigParser(const std::string& config_id,
         parser = new ControlSocketParser(config_id);
     } else if (config_id.compare("expired-leases-processing") == 0) {
         parser = new ExpirationConfigParser();
+    } else if (config_id.compare("client-classes") == 0) {
+        parser = new ClientClassDefListParser(config_id, globalContext());
     } else {
         isc_throw(DhcpConfigError,
                 "unsupported global configuration parameter: "
@@ -524,6 +527,7 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
     ParserPtr option_parser;
     ParserPtr iface_parser;
     ParserPtr leases_parser;
+    ParserPtr client_classes_parser;
 
     // Some of the parsers alter the state of the system in a way that can't
     // easily be undone. (Or alter it in a way such that undoing the change has
@@ -574,6 +578,8 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
                 // but defer the commit until everything else has committed.
                 hooks_parser = parser;
                 parser->build(config_pair.second);
+            } else if (config_pair.first == "client-classes") {
+                client_classes_parser = parser;
             } else {
                 // Those parsers should be started before other
                 // parsers so we can call build straight away.
@@ -595,6 +601,15 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
             option_parser->commit();
         }
 
+        // The class definitions parser is the next one to be run.
+        std::map<std::string, ConstElementPtr>::const_iterator cc_config =
+            values_map.find("client-classes");
+        if (cc_config != values_map.end()) {
+            config_pair.first = "client-classes";
+            client_classes_parser->build(cc_config->second);
+            client_classes_parser->commit();
+        }
+
         // The subnet parser is the next one to be run.
         std::map<std::string, ConstElementPtr>::const_iterator subnet_config =
             values_map.find("subnet4");

+ 74 - 0
src/bin/dhcp4/tests/config_parser_unittest.cc

@@ -3897,4 +3897,78 @@ TEST_F(Dhcp4ParserTest, 4o6subnetInterfaceId) {
     EXPECT_EQ(0, memcmp(&data[0], exp, data.size()));
 }
 
+// Verifies that simple list of valid classes parses and
+// is staged for commit.
+TEST_F(Dhcp4ParserTest, validClientClassDictionary) {
+    string config = "{ " + genIfaceConfig() + "," +
+        "\"valid-lifetime\": 4000, \n"
+        "\"rebind-timer\": 2000, \n"
+        "\"renew-timer\": 1000, \n"
+        "\"client-classes\" : [ \n"
+        "   { \n"
+        "       \"name\": \"one\" \n"
+        "   }, \n"
+        "   { \n"
+        "       \"name\": \"two\" \n"
+        "   }, \n"
+        "   { \n"
+        "       \"name\": \"three\" \n"
+        "   } \n"
+        "], \n"
+        "\"subnet4\": [ {  \n"
+        "    \"pools\": [ { \"pool\":  \"192.0.2.1 - 192.0.2.100\" } ], \n"
+        "    \"subnet\": \"192.0.2.0/24\"  \n"
+        " } ] \n"
+        "} \n";
+
+    ConstElementPtr status;
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+    ASSERT_TRUE(status);
+    checkResult(status, 0);
+
+    // We check staging config because CfgMgr::commit hasn't been executed.
+    ClientClassDictionaryPtr dictionary;
+    dictionary = CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+    ASSERT_TRUE(dictionary);
+    EXPECT_EQ(3, dictionary->getClasses()->size());
+
+    // Execute the commit
+    ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+    // Verify that after commit, the current config has the correct dictionary
+    dictionary = CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
+    ASSERT_TRUE(dictionary);
+    EXPECT_EQ(3, dictionary->getClasses()->size());
+}
+
+// Verifies that a class list containing an invalid
+// class definition causes a configuration error.
+TEST_F(Dhcp4ParserTest, invalidClientClassDictionary) {
+    string config = "{ " + genIfaceConfig() + "," +
+        "\"valid-lifetime\": 4000, \n"
+        "\"rebind-timer\": 2000, \n"
+        "\"renew-timer\": 1000, \n"
+        "\"client-classes\" : [ \n"
+        "   { \n"
+        "       \"name\": \"one\", \n"
+        "       \"bogus\": \"bad\" \n"
+        "   } \n"
+        "], \n"
+        "\"subnet4\": [ {  \n"
+        "    \"pools\": [ { \"pool\":  \"192.0.2.1 - 192.0.2.100\" } ], \n"
+        "    \"subnet\": \"192.0.2.0/24\"  \n"
+        " } ] \n"
+        "} \n";
+
+    ConstElementPtr status;
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+    ASSERT_TRUE(status);
+    checkResult(status, 1);
+}
+
+
 }

+ 65 - 1
src/bin/dhcp6/dhcp6.spec

@@ -260,7 +260,71 @@
             }
         ]
       },
-
+      { "item_name": "client-classes",
+        "item_type": "list",
+        "item_optional": true,
+        "item_default": [],
+        "list_item_spec":
+        {
+            "item_name": "client-class",
+            "item_type": "map",
+            "item_optional": false,
+            "item_default": {},
+            "map_item_spec": [
+                { "item_name": "name",
+                  "item_type": "string",
+                  "item_optional": false,
+                  "item_default": ""
+                },
+                { "item_name": "test",
+                  "item_type": "string",
+                  "item_optional": true,
+                  "item_default": ""
+                },
+                { "item_name": "option-data",
+                  "item_type": "list",
+                  "item_optional": true,
+                  "item_default": [],
+                  "list_item_spec":
+                  {
+                    "item_name": "single-option-data",
+                    "item_type": "map",
+                    "item_optional": false,
+                    "item_default": {},
+                    "map_item_spec": [
+                    {
+                      "item_name": "name",
+                      "item_type": "string",
+                      "item_optional": false,
+                      "item_default": ""
+                    },
+                    {
+                      "item_name": "code",
+                      "item_type": "integer",
+                      "item_optional": false,
+                      "item_default": 0
+                    },
+                    {
+                      "item_name": "data",
+                      "item_type": "string",
+                      "item_optional": false,
+                      "item_default": ""
+                    },
+                    { "item_name": "csv-format",
+                      "item_type": "boolean",
+                      "item_optional": false,
+                      "item_default": false
+                      },
+                    { "item_name": "space",
+                      "item_type": "string",
+                      "item_optional": false,
+                      "item_default": "dhcp4"
+                    } ]
+                  }
+                }
+            ]
+        }
+      },
       { "item_name": "subnet6",
         "item_type": "list",
         "item_optional": false,

+ 15 - 0
src/bin/dhcp6/json_config_parser.cc

@@ -28,6 +28,7 @@
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/timer_mgr.h>
 #include <dhcpsrv/triplet.h>
+#include <dhcpsrv/parsers/client_class_def_parser.h>
 #include <dhcpsrv/parsers/dbaccess_parser.h>
 #include <dhcpsrv/parsers/dhcp_config_parser.h>
 #include <dhcpsrv/parsers/dhcp_parsers.h>
@@ -701,6 +702,8 @@ DhcpConfigParser* createGlobal6DhcpConfigParser(const std::string& config_id,
         parser = new ControlSocketParser(config_id);
     } else if (config_id.compare("expired-leases-processing") == 0) {
         parser = new ExpirationConfigParser();
+    } else if (config_id.compare("client-classes") == 0) {
+        parser = new ClientClassDefListParser(config_id, globalContext());
     } else {
         isc_throw(DhcpConfigError,
                 "unsupported global configuration parameter: "
@@ -763,6 +766,7 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
     ParserPtr option_parser;
     ParserPtr iface_parser;
     ParserPtr leases_parser;
+    ParserPtr client_classes_parser;
 
     // Some of the parsers alter state of the system that can't easily
     // be undone. (Or alter it in a way such that undoing the change
@@ -815,6 +819,8 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
                 // can be run here before other parsers.
                 parser->build(config_pair.second);
                 iface_parser = parser;
+            } else if (config_pair.first == "client-classes") {
+                client_classes_parser = parser;
             } else {
                 // Those parsers should be started before other
                 // parsers so we can call build straight away.
@@ -836,6 +842,15 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
             option_parser->commit();
         }
 
+        // The class definitions parser is the next one to be run.
+        std::map<std::string, ConstElementPtr>::const_iterator cc_config =
+            values_map.find("client-classes");
+        if (cc_config != values_map.end()) {
+            config_pair.first = "client-classes";
+            client_classes_parser->build(cc_config->second);
+            client_classes_parser->commit();
+        }
+
         // The subnet parser is the next one to be run.
         std::map<std::string, ConstElementPtr>::const_iterator subnet_config =
             values_map.find("subnet6");

+ 72 - 0
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -3995,5 +3995,77 @@ TEST_F(Dhcp6ParserTest, expiredLeasesProcessingError) {
     EXPECT_TRUE(errorContainsPosition(status, "<string>"));
 }
 
+// Verifies that simple list of valid classes parses and
+// is staged for commit.
+TEST_F(Dhcp6ParserTest, validClientClassDictionary) {
+
+    string config = "{ " + genIfaceConfig() + ","
+        "\"preferred-lifetime\": 3000, \n"
+        "\"rebind-timer\": 2000,  \n"
+        "\"renew-timer\": 1000,  \n"
+        "\"client-classes\" : [ \n"
+        "   { \n"
+        "       \"name\": \"one\" \n"
+        "   }, \n"
+        "   { \n"
+        "       \"name\": \"two\" \n"
+        "   }, \n"
+        "   { \n"
+        "       \"name\": \"three\" \n"
+        "   } \n"
+        "], \n"
+        "\"subnet6\": [ {  \n"
+        "    \"pools\": [ { \"pool\": \"2001:db8:1::1 - 2001:db8:1::ffff\" } ], \n"
+        "    \"subnet\": \"2001:db8:1::/64\" } ], \n"
+        "\"valid-lifetime\": 4000 } \n";
+
+    ConstElementPtr status;
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+    ASSERT_TRUE(status);
+    checkResult(status, 0);
+
+    // We check staging config because CfgMgr::commit hasn't been executed.
+    ClientClassDictionaryPtr dictionary;
+    dictionary = CfgMgr::instance().getStagingCfg()->getClientClassDictionary();
+    ASSERT_TRUE(dictionary);
+    EXPECT_EQ(3, dictionary->getClasses()->size());
+
+    // Execute the commit
+    ASSERT_NO_THROW(CfgMgr::instance().commit());
+
+    // Verify that after commit, the current config has the correct dictionary
+    dictionary = CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
+    ASSERT_TRUE(dictionary);
+    EXPECT_EQ(3, dictionary->getClasses()->size());
+}
+
+// Verifies that a class list containing an invalid
+// class definition causes a configuration error.
+TEST_F(Dhcp6ParserTest, invalidClientClassDictionary) {
+    string config = "{ " + genIfaceConfig() + "," +
+        "\"valid-lifetime\": 4000, \n"
+        "\"rebind-timer\": 2000, \n"
+        "\"renew-timer\": 1000, \n"
+        "\"client-classes\" : [ \n"
+        "   { \n"
+        "       \"name\": \"one\", \n"
+        "       \"bogus\": \"bad\" \n"
+        "   } \n"
+        "], \n"
+        "\"subnet4\": [ {  \n"
+        "    \"pools\": [ { \"pool\":  \"192.0.2.1 - 192.0.2.100\" } ], \n"
+        "    \"subnet\": \"192.0.2.0/24\"  \n"
+        " } ] \n"
+        "} \n";
+
+    ConstElementPtr status;
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+    ASSERT_TRUE(status);
+    checkResult(status, 1);
+}
 
 };

+ 1 - 1
src/lib/Makefile.am

@@ -1,3 +1,3 @@
 # The following build order must be maintained.
 SUBDIRS = exceptions util log hooks cryptolink dns cc asiolink dhcp config stats \
-          asiodns testutils dhcp_ddns dhcpsrv cfgrpt eval
+          asiodns testutils dhcp_ddns eval dhcpsrv cfgrpt

+ 11 - 1
src/lib/dhcpsrv/Makefile.am

@@ -26,13 +26,20 @@ AM_CXXFLAGS = $(KEA_CXXFLAGS)
 # Whenever new file is added to the parsers folder, it must be
 # added here.
 EXTRA_DIST =
+EXTRA_DIST += parsers/client_class_def_parser.cc
+EXTRA_DIST += parsers/client_class_def_parser.h
+EXTRA_DIST += parsers/dhcp_config_parser.h
 EXTRA_DIST += parsers/dbaccess_parser.cc
 EXTRA_DIST += parsers/dbaccess_parser.h
 EXTRA_DIST += parsers/dhcp_parsers.cc
 EXTRA_DIST += parsers/dhcp_parsers.h
+EXTRA_DIST += parsers/expiration_config_parser.cc
+EXTRA_DIST += parsers/expiration_config_parser.h
 EXTRA_DIST += parsers/host_reservation_parser.cc
 EXTRA_DIST += parsers/host_reservation_parser.h
 EXTRA_DIST += parsers/host_reservations_list_parser.h
+EXTRA_DIST += parsers/ifaces_config_parser.cc
+EXTRA_DIST += parsers/ifaces_config_parser.h
 
 # Define rule to build logging source files from message file
 alloc_engine_messages.h alloc_engine_messages.cc dhcpsrv_messages.h \
@@ -133,6 +140,8 @@ libkea_dhcpsrv_la_SOURCES += utils.h
 libkea_dhcpsrv_la_SOURCES += writable_host_data_source.h
 
 # Configuration parsers
+libkea_dhcpsrv_la_SOURCES += parsers/client_class_def_parser.cc
+libkea_dhcpsrv_la_SOURCES += parsers/client_class_def_parser.h
 libkea_dhcpsrv_la_SOURCES += parsers/dhcp_config_parser.h
 libkea_dhcpsrv_la_SOURCES += parsers/dbaccess_parser.cc
 libkea_dhcpsrv_la_SOURCES += parsers/dbaccess_parser.h
@@ -153,7 +162,8 @@ nodist_libkea_dhcpsrv_la_SOURCES += hosts_messages.h hosts_messages.cc
 
 libkea_dhcpsrv_la_CXXFLAGS = $(AM_CXXFLAGS)
 libkea_dhcpsrv_la_CPPFLAGS = $(AM_CPPFLAGS)
-libkea_dhcpsrv_la_LIBADD   = $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
+libkea_dhcpsrv_la_LIBADD  = $(top_builddir)/src/lib/eval/libkea-eval.la
+libkea_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
 libkea_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/stats/libkea-stats.la
 libkea_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/config/libkea-cfgclient.la
 libkea_dhcpsrv_la_LIBADD  += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la

+ 70 - 23
src/lib/dhcpsrv/client_class_def.cc

@@ -13,6 +13,7 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 #include "client_class_def.h"
+#include <boost/foreach.hpp>
 
 namespace isc {
 namespace dhcp {
@@ -21,18 +22,34 @@ namespace dhcp {
 
 ClientClassDef::ClientClassDef(const std::string& name,
                                const ExpressionPtr& match_expr,
-                               const OptionCollectionPtr& options)
-    : name_(name), match_expr_(match_expr), options_(options) {
+                               const CfgOptionPtr& cfg_option)
+    : name_(name), match_expr_(match_expr), cfg_option_(cfg_option) {
 
     // Name can't be blank
     if (name_.empty()) {
-        isc_throw(BadValue, "ClientClassDef name cannot be empty");
+        isc_throw(BadValue, "Client Class name cannot be blank");
     }
-    // @todo Does it make sense for a class to NOT have match expression?
+
+    // We permit an empty expression for now.  This will likely be useful
+    // for automatic classes such as vendor class.
 
     // For classes without options, make sure we have an empty collection
-    if (!options_) {
-        options_.reset(new OptionCollection());
+    if (!cfg_option_) {
+        cfg_option_.reset(new CfgOption());
+    }
+}
+
+ClientClassDef::ClientClassDef(const ClientClassDef& rhs)
+    : name_(rhs.name_), match_expr_(ExpressionPtr()),
+      cfg_option_(new CfgOption()) {
+
+    if (rhs.match_expr_) {
+        match_expr_.reset(new Expression());
+        *match_expr_ = *(rhs.match_expr_);
+    }
+
+    if (rhs.cfg_option_) {
+        rhs.cfg_option_->copyTo(*cfg_option_);
     }
 }
 
@@ -59,26 +76,25 @@ ClientClassDef::setMatchExpr(const ExpressionPtr& match_expr) {
     match_expr_ = match_expr;
 }
 
-const OptionCollectionPtr&
-ClientClassDef::getOptions() const {
-    return (options_);
+const CfgOptionPtr&
+ClientClassDef::getCfgOption() const {
+    return (cfg_option_);
 }
 
 void
-ClientClassDef::setOptions(const OptionCollectionPtr& options) {
-    options_ = options;
+ClientClassDef::setCfgOption(const CfgOptionPtr& cfg_option) {
+    cfg_option_ = cfg_option;
 }
 
-OptionPtr
-ClientClassDef::findOption(uint16_t option_code) const {
-    if (options_) {
-        isc::dhcp::OptionCollection::iterator it = options_->find(option_code);
-        if (it != options_->end()) {
-            return ((*it).second);
-        }
-    }
-
-    return (OptionPtr());
+bool
+ClientClassDef::equals(const ClientClassDef& other) const {
+    return ((name_ == other.name_) &&
+        ((!match_expr_ && !other.match_expr_) ||
+        (match_expr_ && other.match_expr_ &&
+         (*match_expr_ == *(other.match_expr_)))) &&
+        ((!cfg_option_ && !other.cfg_option_) ||
+        (cfg_option_ && other.cfg_option_ &&
+         (*cfg_option_ == *other.cfg_option_))));
 }
 
 std::ostream& operator<<(std::ostream& os, const ClientClassDef& x) {
@@ -92,14 +108,22 @@ ClientClassDictionary::ClientClassDictionary()
     : classes_(new ClientClassDefMap()) {
 }
 
+ClientClassDictionary::ClientClassDictionary(const ClientClassDictionary& rhs)
+    : classes_(new ClientClassDefMap()) {
+    BOOST_FOREACH(ClientClassMapPair cclass, *(rhs.classes_)) {
+        ClientClassDefPtr copy(new ClientClassDef(*(cclass.second)));
+        addClass(copy);
+    }
+}
+
 ClientClassDictionary::~ClientClassDictionary() {
 }
 
 void
 ClientClassDictionary::addClass(const std::string& name,
                                 const ExpressionPtr& match_expr,
-                                const OptionCollectionPtr& options) {
-    ClientClassDefPtr cclass(new ClientClassDef(name, match_expr, options));
+                                const CfgOptionPtr& cfg_option) {
+    ClientClassDefPtr cclass(new ClientClassDef(name, match_expr, cfg_option));
     addClass(cclass);
 }
 
@@ -138,5 +162,28 @@ ClientClassDictionary::getClasses() const {
     return (classes_);
 }
 
+bool
+ClientClassDictionary::equals(const ClientClassDictionary& other) const {
+    if (classes_->size() != other.classes_->size()) {
+        return (false);
+    }
+
+    ClientClassDefMap::iterator this_class = classes_->begin();
+    ClientClassDefMap::iterator other_class = other.classes_->begin();
+    while (this_class != classes_->end() &&
+           other_class != other.classes_->end()) {
+        if (!(*this_class).second || !(*other_class).second ||
+            (*(*this_class).second) != (*(*other_class).second)) {
+                return false;
+        }
+
+        ++this_class;
+        ++other_class;
+    }
+
+    return (true);
+}
+
+
 } // namespace isc::dhcp
 } // namespace isc

+ 71 - 20
src/lib/dhcpsrv/client_class_def.h

@@ -15,7 +15,7 @@
 #ifndef CLIENT_CLASS_DEF_H
 #define CLIENT_CLASS_DEF_H
 
-#include <dhcp/option.h>
+#include <dhcpsrv/cfg_option.h>
 #include <eval/token.h>
 #include <exceptions/exceptions.h>
 
@@ -46,14 +46,18 @@ public:
 
 /// @brief Embodies a single client class definition
 class ClientClassDef {
-  public:
+public:
     /// @brief Constructor
     ///
     /// @param name Name to assign to this class
     /// @param match_expr Expression the class will use to determine membership
     /// @param options Collection of options members should be given
     ClientClassDef(const std::string& name, const ExpressionPtr& match_expr,
-                const OptionCollectionPtr& options = OptionCollectionPtr());
+                   const CfgOptionPtr& options = CfgOptionPtr());
+
+
+    /// Copy constructor
+    ClientClassDef(const ClientClassDef& rhs);
 
     /// @brief Destructor
     virtual ~ClientClassDef();
@@ -75,52 +79,74 @@ class ClientClassDef {
     void setMatchExpr(const ExpressionPtr& match_expr);
 
     /// @brief Fetches the class's option collection
-    const OptionCollectionPtr& getOptions() const;
+    const CfgOptionPtr& getCfgOption() const;
 
     /// @brief Sets the class's option collection
     ///
     /// @param options the option collection to assign the class
-    void setOptions(const OptionCollectionPtr& options);
+    void setCfgOption(const CfgOptionPtr& cfg_option);
 
-    /// @brief Fetches an option from the class's collection by code
+    /// @brief Compares two @c ClientClassDef objects for equality.
     ///
-    /// @param option_code Option code value of the desired option
-    /// @return A pointer to the option if found, otherwise an
-    /// empty pointer
-    OptionPtr findOption(uint16_t option_code) const;
+    /// @param other Other client class definition to compare to.
+    ///
+    /// @return true if objects are equal, false otherwise.
+    bool equals(const ClientClassDef& other) const;
+
+    /// @brief Equality operator.
+    ///
+    /// @param other Other client class definition to compare to.
+    ///
+    /// @return true if the definitions equal, false otherwise.
+    bool operator==(const ClientClassDef& other) const {
+        return (equals(other));
+    }
+
+    /// @brief Inequality operator.
+    ///
+    /// @param other Other client class definition to compare to.
+    ///
+    /// @return true if the definitions are not equal, false otherwise.
+    bool operator!=(const ClientClassDef& other) const {
+        return (!(equals(other)));
+    }
 
     /// @brief Provides a convenient text representation of the class
     friend std::ostream& operator<<(std::ostream& os, const ClientClassDef& x);
 
-  private:
+private:
     /// @brief Unique text identifier by which this class is known.
     std::string name_;
 
-    /// @brief The logical expression which deteremines membership in
+    /// @brief The logical expression which determines membership in
     /// this class.
     ExpressionPtr match_expr_;
 
-    /// @brief The collection of options members should be given
-    /// Currently this is a multimap, not sure we need/want that complexity
-    OptionCollectionPtr options_;
+    /// @brief The option data configuration for this class
+    CfgOptionPtr cfg_option_;
 };
 
 /// @brief a pointer to an ClientClassDef
 typedef boost::shared_ptr<ClientClassDef> ClientClassDefPtr;
 
 /// @brief Defines a map of ClientClassDef's, keyed by the class name.
-typedef std::map<std::string,ClientClassDefPtr> ClientClassDefMap;
+typedef std::map<std::string, ClientClassDefPtr> ClientClassDefMap;
 
 /// @brief Defines a pointer to a ClientClassDefMap
 typedef boost::shared_ptr<ClientClassDefMap> ClientClassDefMapPtr;
 
+/// @brief Defines a pair for working with ClientClassMap
+typedef std::pair<std::string, ClientClassDefPtr> ClientClassMapPair;
+
 /// @brief Maintains a list of ClientClassDef's
 class ClientClassDictionary {
 
-  public:
+public:
     /// @brief Constructor
     ClientClassDictionary();
 
+    ClientClassDictionary(const ClientClassDictionary& rhs);
+
     /// @brief Destructor
     ~ClientClassDictionary();
 
@@ -134,7 +160,7 @@ class ClientClassDictionary {
     /// dictionary.  See @ref dhcp::ClientClassDef::ClientClassDef() for
     /// others.
     void addClass(const std::string& name, const ExpressionPtr& match_expr,
-                  const OptionCollectionPtr& options);
+                  const CfgOptionPtr& options);
 
     /// @brief Adds a new class to the list
     ///
@@ -154,7 +180,7 @@ class ClientClassDictionary {
 
     /// @brief Removes a given class definition from the dictionary
     ///
-    /// Removes the class defintion from the map if it exists, otherwise
+    /// Removes the class definition from the map if it exists, otherwise
     /// no harm, no foul.
     ///
     /// @param name the name of the class to remove
@@ -165,7 +191,32 @@ class ClientClassDictionary {
     /// @return ClientClassDefMapPtr to the map of classes
     const ClientClassDefMapPtr& getClasses() const;
 
-  private:
+    /// @brief Compares two @c ClientClassDictionary objects for equality.
+    ///
+    /// @param other Other client class definition to compare to.
+    ///
+    /// @return true if descriptors equal, false otherwise.
+    bool equals(const ClientClassDictionary& other) const;
+
+    /// @brief Equality operator.
+    ///
+    /// @param other Other client class dictionary to compare to.
+    ///
+    /// @return true if the dictionaries are equal, false otherwise.
+    bool operator==(const ClientClassDictionary& other) const {
+        return (equals(other));
+    }
+
+    /// @brief Inequality operator.
+    ///
+    /// @param other Other client class dictionary to compare to.
+    ///
+    /// @return true if the dictionaries are not equal, false otherwise.
+    bool operator!=(const ClientClassDictionary& other) const {
+        return (!equals(other));
+    }
+
+private:
 
     /// @brief Map of the class definitions
     ClientClassDefMapPtr classes_;

+ 162 - 0
src/lib/dhcpsrv/parsers/client_class_def_parser.cc

@@ -0,0 +1,162 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <cc/data.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/client_class_def.h>
+#include <dhcpsrv/parsers/client_class_def_parser.h>
+#include <eval/eval_context.h>
+
+#include <boost/foreach.hpp>
+
+using namespace isc::data;
+
+/// @file client_class_def.cc
+///
+/// @brief Method implementations for client class definition parsing
+
+namespace isc {
+namespace dhcp {
+
+// ********************** ExpressionParser ****************************
+
+ExpressionParser::ExpressionParser(const std::string&,
+    ExpressionPtr& expression, ParserContextPtr global_context)
+    : local_expression_(ExpressionPtr()), expression_(expression),
+      global_context_(global_context) {
+}
+
+void
+ExpressionParser::build(ConstElementPtr expression_cfg) {
+    if (expression_cfg->getType() != Element::string) {
+        isc_throw(DhcpConfigError, "expression ["
+            << expression_cfg->str() << "] must be a string, at ("
+            << expression_cfg->getPosition() << ")");
+    }
+
+    // Get the expression's text via getValue() as the text returned
+    // by str() enclosed in quotes.
+    std::string value;
+    expression_cfg->getValue(value);
+    try {
+        EvalContext eval_ctx;
+        eval_ctx.parseString(value);
+        local_expression_.reset(new Expression());
+        *local_expression_ = eval_ctx.expression;
+    } catch (const std::exception& ex) {
+        // Append position if there is a failure.
+        isc_throw(DhcpConfigError,
+                  "expression: [" << value
+                  <<  "] error: " << ex.what() << " at ("
+                  <<  expression_cfg->getPosition() << ")");
+    }
+}
+
+void
+ExpressionParser::commit() {
+    expression_ = local_expression_;
+}
+
+// ********************** ClientClassDefParser ****************************
+
+ClientClassDefParser::ClientClassDefParser(const std::string&,
+    ClientClassDictionaryPtr& class_dictionary, ParserContextPtr global_context)
+    : string_values_(new StringStorage()),
+      match_expr_(ExpressionPtr()),
+      options_(new CfgOption()),
+      class_dictionary_(class_dictionary),
+      global_context_(global_context) {
+}
+
+void
+ClientClassDefParser::build(ConstElementPtr class_def_cfg) {
+    // Parse the elements that make up the option definition.
+    BOOST_FOREACH(ConfigPair param, class_def_cfg->mapValue()) {
+        std::string entry(param.first);
+        ParserPtr parser;
+        if (entry == "name") {
+            StringParserPtr str_parser(new StringParser(entry, string_values_));
+            parser = str_parser;
+        } else if (entry == "test") {
+            ExpressionParserPtr exp_parser(new ExpressionParser(entry,
+                                                                match_expr_,
+                                                                global_context_));
+            parser = exp_parser;
+        } else if (entry == "option-data") {
+            OptionDataListParserPtr opts_parser;
+            uint16_t family = (global_context_->universe_ == Option::V4 ?
+                                                             AF_INET : AF_INET6);
+
+            opts_parser.reset(new OptionDataListParser(entry, options_, family));
+            parser = opts_parser;
+        } else {
+            isc_throw(DhcpConfigError, "invalid parameter '" << entry
+                      << "' (" << param.second->getPosition() << ")");
+        }
+
+        parser->build(param.second);
+        parser->commit();
+    }
+
+    std::string name;
+    try {
+        name = string_values_->getParam("name");
+    } catch (const std::exception& ex) {
+        isc_throw(DhcpConfigError, ex.what() << " ("
+                  << class_def_cfg->getPosition() << ")");
+    }
+
+    try {
+        // an OptionCollectionPtr
+        class_dictionary_->addClass(name, match_expr_, options_);
+    } catch (const std::exception& ex) {
+        isc_throw(DhcpConfigError, ex.what()
+                  << " (" << class_def_cfg->getPosition() << ")");
+    }
+}
+
+// ****************** ClientClassDefListParser ************************
+
+ClientClassDefListParser::ClientClassDefListParser(const std::string&,
+                                                   ParserContextPtr
+                                                   global_context)
+    : local_dictionary_(new ClientClassDictionary()),
+      global_context_(global_context) {
+}
+
+void
+ClientClassDefListParser::build(ConstElementPtr client_class_def_list) {
+    if (!client_class_def_list) {
+        isc_throw(DhcpConfigError, "parser error: a pointer to a list of"
+                  << " client class definitions is NULL ("
+                  << client_class_def_list->getPosition() << ")");
+    }
+
+    BOOST_FOREACH(ConstElementPtr client_class_def,
+                  client_class_def_list->listValue()) {
+        boost::shared_ptr<ClientClassDefParser>
+            parser(new ClientClassDefParser("client-class-def",
+                                            local_dictionary_,
+                                            global_context_));
+        parser->build(client_class_def);
+    }
+}
+
+void
+ClientClassDefListParser::commit() {
+    CfgMgr::instance().getStagingCfg()->setClientClassDictionary(local_dictionary_);
+}
+
+} // end of namespace isc::dhcp
+} // end of namespace isc

+ 193 - 0
src/lib/dhcpsrv/parsers/client_class_def_parser.h

@@ -0,0 +1,193 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef CLIENT_CLASS_DEF_PARSER_H
+#define CLIENT_CLASS_DEF_PARSER_H
+
+#include <dhcpsrv/client_class_def.h>
+#include <dhcpsrv/parsers/dhcp_parsers.h>
+
+/// @file client_class_def.h
+///
+/// @brief Parsers for client class definitions
+///
+/// These parsers are used to parse lists of client class definitions
+/// into a ClientClassDictionary of ClientClassDef instances.  Each
+/// ClientClassDef consists of (at least) a name, an expression, and
+/// option-data.  The latter two are currently optional.
+///
+/// There parsers defined are:
+///
+/// ClientClassDefListParser  - creates a ClientClassDictionary from a list
+/// of element maps, where each map contains the entries that specifiy a
+/// single class.  The names of the classes in the are expected to be
+/// unique.  Attempting to define a duplicate class will result in an
+/// DhcpConfigError throw.  Invoking @c commit() method causes the dictionary
+/// to be stored by the CfgMgr.
+///
+/// ClientClassDefParser - creates a ClientClassDefinition from an element
+/// map. The elements are as follows:
+///
+/// -# "name" - a string containing the name of the class
+///
+/// -# "test" - a string containing the logical expression used to determine
+/// membership in the class. This is passed into the eval parser.
+///
+/// -# "option-data" - a list which defines the options that should be
+/// assigned to memebers of the class.  This element is optional and parsed
+/// using the @ref dhcp::OptionDataListParser.
+///
+/// ExpressionParser  - creates an eval::Expression from a string element,
+/// using the Eval Parser.
+///
+namespace isc {
+namespace dhcp {
+
+/// @brief Parser for a logical expression
+///
+/// This parser creates an instance of an Expression from a string.  The
+/// string is passed to the Eval Parser and the resultant Expression is
+/// stored into the ExpressionPtr reference passed into the constructor.
+class ExpressionParser : public DhcpConfigParser {
+public:
+    /// @brief Constructor.
+    ///
+    /// @param dummy first argument is ignored, all Parser constructors
+    /// accept string as first argument.
+    /// @param expression variable in which to store the new expression
+    /// @param global_context is a pointer to the global context which
+    /// stores global scope parameters, options, option defintions.
+    ExpressionParser(const std::string& dummy, ExpressionPtr& expression,
+                     ParserContextPtr global_context);
+
+    /// @brief Parses an expression configuration element into an Expression
+    ///
+    /// @param expression_cfg the configuration entry to be parsed.
+    ///
+    /// @throw DhcpConfigError if parsing was unsuccessful.
+    void build(isc::data::ConstElementPtr expression_cfg);
+
+    /// @brief Stores the parsed expression to the supplied storage.
+    void commit();
+
+private:
+    /// @brief Local storage for the parsed expression
+    ExpressionPtr local_expression_;
+
+    /// @brief Storage into which the parsed expression should be committed
+    ExpressionPtr& expression_;
+
+    /// @brief Parsing context which contains global values, options and option
+    /// definitions.
+    ParserContextPtr global_context_;
+};
+
+typedef boost::shared_ptr<ExpressionParser> ExpressionParserPtr;
+
+/// @brief Parser for a single client class definition.
+///
+/// This parser creates an instance of a client class definition.
+class ClientClassDefParser : public DhcpConfigParser {
+public:
+    /// @brief Constructor.
+    ///
+    /// @param dummy first argument is ignored, all Parser constructors
+    /// accept string as first argument.
+    /// @param class_dictionary dictionary into which the class should be added
+    /// @param global_context is a pointer to the global context which
+    /// stores global scope parameters, options, option defintions.
+    ClientClassDefParser(const std::string& dummy,
+                         ClientClassDictionaryPtr& class_dictionary,
+                         ParserContextPtr global_context);
+
+    /// @brief Parses an entry that describes single client class definition.
+    ///
+    /// Attempts to add the new class direclty into the given dictionary.
+    /// This done here to detect duplicate classes prior to commit().
+    /// @param client_class_def a configuration entry to be parsed.
+    ///
+    /// @throw DhcpConfigError if parsing was unsuccessful.
+    void build(isc::data::ConstElementPtr client_class_def);
+
+    /// @brief Does nothing.
+    void commit() {};
+
+private:
+
+    /// @brief Storage for class string values.
+    StringStoragePtr string_values_;
+
+    /// @brief Storage for the class match expression
+    ExpressionPtr match_expr_;
+
+    /// @brief Storage for the class options
+    CfgOptionPtr options_;
+
+    /// @brief Dictionary to which the new class should be added
+    ClientClassDictionaryPtr class_dictionary_;
+
+    /// @brief Parsing context which contains global values, options and option
+    /// definitions.
+    ParserContextPtr global_context_;
+};
+
+/// @brief Defines a pointer to a ClientClassDefParser
+typedef boost::shared_ptr<ClientClassDefParser> ClientClassDefParserPtr;
+
+/// @brief Parser for a list of client class definitions.
+///
+/// This parser iterates over all configuration entries that define
+/// client classes and creates ClientClassDef instances for each.
+/// If the parsing done in build() is successful, the collection of
+/// created definitions is given to the CfgMgr.
+class ClientClassDefListParser : public DhcpConfigParser {
+public:
+    /// @brief Constructor.
+    ///
+    /// @param dummy first argument is ignored, all Parser constructors
+    /// accept string as first argument.
+    /// @param global_context is a pointer to the global context which
+    /// stores global scope parameters, options, option defintions.
+    ClientClassDefListParser(const std::string& dummy,
+                        ParserContextPtr global_context);
+
+    /// @brief Parse configuration entries.
+    ///
+    /// This function parses configuration entries, creates instances
+    /// of client class definitions and tries to adds them to the a
+    /// local dictionary.
+    ///
+    /// @param class_def_list pointer to an element that holds entries
+    /// for client class definitions.
+    /// @throw DhcpConfigError if configuration parsing fails.
+    void build(isc::data::ConstElementPtr option_def_list);
+
+    /// @brief Commits class definitions to CfgMgr's global storage.
+    void commit();
+
+    /// @brief Local class dictionary to store classes as they are being parsed
+    ClientClassDictionaryPtr local_dictionary_;
+
+    /// Parsing context which contains global values, options and option
+    /// definitions.
+    ParserContextPtr global_context_;
+};
+
+/// @brief Defines a pointer to a ClientClassDefListParser
+typedef boost::shared_ptr<ClientClassDefListParser> ClientClassDefListParserPtr;
+
+} // end of namespace isc::dhcp
+} // end of namespace isc
+
+#endif // CLIENT_CLASS_DEF_PARSER_H

+ 2 - 0
src/lib/dhcpsrv/parsers/dhcp_parsers.h

@@ -734,6 +734,8 @@ private:
 
 };
 
+typedef boost::shared_ptr<OptionDataListParser> OptionDataListParserPtr;
+
 
 /// @brief Parser for a single option definition.
 ///

+ 7 - 1
src/lib/dhcpsrv/srv_config.cc

@@ -32,6 +32,7 @@ SrvConfig::SrvConfig()
       cfg_subnets4_(new CfgSubnets4()), cfg_subnets6_(new CfgSubnets6()),
       cfg_hosts_(new CfgHosts()), cfg_rsoo_(new CfgRSOO()),
       cfg_expiration_(new CfgExpiration()),
+      class_dictionary_(new ClientClassDictionary()),
       decline_timer_(0) {
 }
 
@@ -41,6 +42,7 @@ SrvConfig::SrvConfig(const uint32_t sequence)
       cfg_subnets4_(new CfgSubnets4()), cfg_subnets6_(new CfgSubnets6()),
       cfg_hosts_(new CfgHosts()), cfg_rsoo_(new CfgRSOO()),
       cfg_expiration_(new CfgExpiration()),
+      class_dictionary_(new ClientClassDictionary()),
       decline_timer_(0) {
 }
 
@@ -103,6 +105,9 @@ SrvConfig::copy(SrvConfig& new_config) const {
     // Replace option definitions.
     cfg_option_def_->copyTo(*new_config.cfg_option_def_);
     cfg_option_->copyTo(*new_config.cfg_option_);
+    // Replace the client class dictionary
+    new_config.class_dictionary_.reset(new ClientClassDictionary(*class_dictionary_));
+
 }
 
 void
@@ -146,7 +151,8 @@ SrvConfig::equals(const SrvConfig& other) const {
     // Logging information is equal between objects, so check other values.
     return ((*cfg_iface_ == *other.cfg_iface_) &&
             (*cfg_option_def_ == *other.cfg_option_def_) &&
-            (*cfg_option_ == *other.cfg_option_));
+            (*cfg_option_ == *other.cfg_option_) &&
+            (*class_dictionary_ == *other.class_dictionary_));
 }
 
 void

+ 22 - 0
src/lib/dhcpsrv/srv_config.h

@@ -24,6 +24,7 @@
 #include <dhcpsrv/cfg_subnets4.h>
 #include <dhcpsrv/cfg_subnets6.h>
 #include <dhcpsrv/cfg_mac_source.h>
+#include <dhcpsrv/client_class_def.h>
 #include <dhcpsrv/logging_info.h>
 #include <cc/data.h>
 #include <boost/shared_ptr.hpp>
@@ -302,6 +303,24 @@ public:
         control_socket_ = control_socket;
     }
 
+    /// @brief Returns pointer to the dictionary of global client
+    /// class definitions
+    ClientClassDictionaryPtr getClientClassDictionary() {
+        return (class_dictionary_);
+    }
+
+    /// @brief Returns pointer to const dictionary of global client
+    /// class definitions
+    const ClientClassDictionaryPtr getClientClassDictionary() const {
+        return (class_dictionary_);
+    }
+
+    /// @brief Sets the client class dictionary
+    /// @param dictionary pointer to the new class dictionary
+    void setClientClassDictionary(const ClientClassDictionaryPtr& dictionary) {
+        class_dictionary_ = dictionary;
+    }
+
     /// @brief Copies the currnet configuration to a new configuration.
     ///
     /// This method copies the parameters stored in the configuration to
@@ -461,6 +480,9 @@ private:
     /// @brief Pointer to the control-socket information
     isc::data::ConstElementPtr control_socket_;
 
+    /// @brief Pointer to the dictionary of global client class definitions
+    ClientClassDictionaryPtr class_dictionary_;
+
     /// @brief Decline Period time
     ///
     /// This timer specifies decline probation period, the time after a declined

+ 2 - 0
src/lib/dhcpsrv/tests/Makefile.am

@@ -76,6 +76,7 @@ libdhcpsrv_unittests_SOURCES += cfg_subnets4_unittest.cc
 libdhcpsrv_unittests_SOURCES += cfg_subnets6_unittest.cc
 libdhcpsrv_unittests_SOURCES += cfgmgr_unittest.cc
 libdhcpsrv_unittests_SOURCES += client_class_def_unittest.cc
+libdhcpsrv_unittests_SOURCES += client_class_def_parser_unittest.cc
 libdhcpsrv_unittests_SOURCES += csv_lease_file4_unittest.cc
 libdhcpsrv_unittests_SOURCES += csv_lease_file6_unittest.cc
 libdhcpsrv_unittests_SOURCES += d2_client_unittest.cc
@@ -144,6 +145,7 @@ endif
 
 libdhcpsrv_unittests_LDADD  = $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la

+ 540 - 0
src/lib/dhcpsrv/tests/client_class_def_parser_unittest.cc

@@ -0,0 +1,540 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcp/option_string.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/parsers/client_class_def_parser.h>
+#include <eval/evaluate.h>
+#include <gtest/gtest.h>
+#include <sstream>
+#include <stdint.h>
+#include <string>
+
+/// @file client_class_def_parser_unittest.cc Unit tests for client class
+/// definition parsing.
+
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace {
+
+/// @brief Test fixture class for @c ClientClassDefParser.
+class ClientClassDefParserTest : public ::testing::Test {
+protected:
+
+    /// @brief Convenience method for parsing a configuration
+    ///
+    /// Attempt to parse a given client class defintion.
+    ///
+    /// @param config - JSON string containing the client class configuration
+    /// to parse.
+    /// @param universe - the universe in which the parsing context should
+    /// occur.
+    /// @return Returns a pointer to class instance created, or NULL if
+    /// for some unforeseen reason it wasn't created in the local dictionary
+    /// @throw indirectly, exceptions convertring the JSON text to elements,
+    /// or by the parsing itself are not caught
+    ClientClassDefPtr parseClientClassDef(const std::string& config,
+                                          Option::Universe universe) {
+        // Create local dicitonary to which the parser add the class.
+        ClientClassDictionaryPtr dictionary(new ClientClassDictionary());
+        // Create the "global" context for the parser.
+        ParserContextPtr context(new ParserContext(universe));
+
+        // Turn config into elements.  This may emit exceptions.
+        ElementPtr config_element = Element::fromJSON(config);
+
+        // Parse the configuration. This may emit exceptions.
+        ClientClassDefParser parser("", dictionary, context);
+        parser.build(config_element);
+
+        // If we didn't throw, then return the first and only class
+        ClientClassDefMapPtr classes = dictionary->getClasses();
+        ClientClassDefMap::iterator it = classes->begin();
+        if (it != classes->end()) {
+            return  (*it).second;
+        }
+
+        // Return NULL if for some reason the class doesn't exist.
+        return (ClientClassDefPtr());
+    }
+};
+
+/// @brief Test fixture class for @c ClientClassDefListParser.
+class ClientClassDefListParserTest : public ::testing::Test {
+protected:
+
+    /// @brief Convenience method for parsing a list of client class
+    /// definitions.
+    ///
+    /// Attempt to parse a given list of client class defintions into a
+    /// ClientClassDictionary.
+    ///
+    /// @param config - JSON string containing the list of definitions to parse.
+    /// @param universe - the universe in which the parsing context should
+    /// occur.
+    /// @return Returns a pointer to class dictionary created
+    /// @throw indirectly, execptions convertring the JSON text to elements,
+    /// or by the parsing itself are not caught
+    ClientClassDictionaryPtr parseClientClassDefList(const std::string& config,
+                                                     Option::Universe universe)
+    {
+        // Create the "global" context for the parser.
+        ParserContextPtr context(new ParserContext(universe));
+
+        // Turn config into elements.  This may emit exceptions.
+        ElementPtr config_element = Element::fromJSON(config);
+
+        // Parse the configuration. This may emit exceptions.
+        ClientClassDefListParser parser("", context);
+        parser.build(config_element);
+
+        // Commit should push it to CfgMgr staging
+        parser.commit();
+
+        // Return the parser's local dicationary
+        return (parser.local_dictionary_);
+    }
+};
+
+// Verifies that given a valid expression, the ExpressionParser
+// produces an Expression which can be evaluated against a v4 packet.
+TEST(ExpressionParserTest, validExpression4) {
+    ParserContextPtr context(new ParserContext(Option::V4));
+    ExpressionParserPtr parser;
+    ExpressionPtr parsed_expr;
+
+    // Turn config into elements.  This may emit exceptions.
+    std::string cfg_txt = "\"option[100] == 'hundred4'\"";
+    ElementPtr config_element = Element::fromJSON(cfg_txt);
+
+    // Create the parser.
+    ASSERT_NO_THROW(parser.reset(new ExpressionParser("", parsed_expr,
+                                                      context)));
+    // Expression should parse and commit.
+    ASSERT_NO_THROW(parser->build(config_element));
+    ASSERT_NO_THROW(parser->commit());
+
+    // Parsed expression should exist.
+    ASSERT_TRUE(parsed_expr);
+
+    // Build a packet that will fail evaluation.
+    Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 123));
+    EXPECT_FALSE(evaluate(*parsed_expr, *pkt4));
+
+    // Now add the option so it will pass.
+    OptionPtr opt(new OptionString(Option::V4, 100, "hundred4"));
+    pkt4->addOption(opt);
+    EXPECT_TRUE(evaluate(*parsed_expr, *pkt4));
+}
+
+// Verifies that given a valid expression, the ExpressionParser
+// produces an Expression which can be evaluated against a v6 packet.
+TEST(ExpressionParserTest, validExpression6) {
+    ParserContextPtr context(new ParserContext(Option::V6));
+    ExpressionParserPtr parser;
+    ExpressionPtr parsed_expr;
+
+    // Turn config into elements.  This may emit exceptions.
+    std::string cfg_txt = "\"option[100] == 'hundred6'\"";
+    ElementPtr config_element = Element::fromJSON(cfg_txt);
+
+    // Create the parser.
+    ASSERT_NO_THROW(parser.reset(new ExpressionParser("", parsed_expr,
+                                                      context)));
+    // Expression should parse and commit.
+    ASSERT_NO_THROW(parser->build(config_element));
+    ASSERT_NO_THROW(parser->commit());
+
+    // Parsed expression should exist.
+    ASSERT_TRUE(parsed_expr);
+
+    // Build a packet that will fail evaluation.
+    Pkt6Ptr pkt6(new Pkt6(DHCPDISCOVER, 123));
+    EXPECT_FALSE(evaluate(*parsed_expr, *pkt6));
+
+    // Now add the option so it will pass.
+    OptionPtr opt(new OptionString(Option::V6, 100, "hundred6"));
+    pkt6->addOption(opt);
+    EXPECT_TRUE(evaluate(*parsed_expr, *pkt6));
+}
+
+
+// Verifies that an the ExpressionParser only accepts StringElements.
+TEST(ExpressionParserTest, invalidExpressionElement) {
+    ParserContextPtr context(new ParserContext(Option::V4));
+    ExpressionParserPtr parser;
+    ExpressionPtr parsed_expr;
+
+    // This will create an integer element should fail.
+    std::string cfg_txt = "777";
+    ElementPtr config_element = Element::fromJSON(cfg_txt);
+
+    // Create the parser.
+    ASSERT_NO_THROW(parser.reset(new ExpressionParser("", parsed_expr,
+                                                      context)));
+    // Expressionn build() should fail.
+    ASSERT_THROW(parser->build(config_element), DhcpConfigError);
+}
+
+// Verifies that given an invalid expression with a syntax error,
+// the Expression parser will throw a DhdpConfigError.  Note this
+// is not intended to be an exhaustive test or expression syntax.
+// It is simply to ensure that if the parser fails, it does so
+// Properly.
+TEST(ExpressionParserTest, expressionSyntaxError) {
+    ParserContextPtr context(new ParserContext(Option::V4));
+    ExpressionParserPtr parser;
+    ExpressionPtr parsed_expr;
+
+    // Turn config into elements.
+    std::string cfg_txt = "\"option 'bogus'\"";
+    ElementPtr config_element = Element::fromJSON(cfg_txt);
+
+    // Create the parser.
+    ASSERT_NO_THROW(parser.reset(new ExpressionParser("", parsed_expr,
+                                                      context)));
+    // Expressionn build() should fail.
+    ASSERT_THROW(parser->build(config_element), DhcpConfigError);
+}
+
+// Verifies you can create a class with only a name
+// Whether that's useful or not, remains to be seen.
+// For now the class allows it.
+TEST_F(ClientClassDefParserTest, nameOnlyValid) {
+    std::string cfg_text =
+        "{ \n"
+        "    \"name\": \"MICROSOFT\" \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, Option::V4));
+
+    // We should find our class.
+    ASSERT_TRUE(cclass);
+    EXPECT_EQ("MICROSOFT", cclass->getName());
+
+    // CfgOption should be a non-null pointer but there
+    // should be no options.  Currently there's no good
+    // way to test that there no options.
+    CfgOptionPtr cfg_option;
+    cfg_option = cclass->getCfgOption();
+    ASSERT_TRUE(cfg_option);
+    OptionContainerPtr oc;
+    ASSERT_TRUE(oc = cclass->getCfgOption()->getAll("dhcp4"));
+    EXPECT_EQ(0, oc->size());
+
+    // Verify we have no expression.
+    ASSERT_FALSE(cclass->getMatchExpr());
+}
+
+// Verifies you can create a class with a name, expression,
+// but no options.
+TEST_F(ClientClassDefParserTest, nameAndExpressionClass) {
+
+    std::string cfg_text =
+        "{ \n"
+        "    \"name\": \"class_one\", \n"
+        "    \"test\": \"option[100] == 'works right'\" \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, Option::V4));
+
+    // We should find our class.
+    ASSERT_TRUE(cclass);
+    EXPECT_EQ("class_one", cclass->getName());
+
+    // CfgOption should be a non-null pointer but there
+    // should be no options.  Currently there's no good
+    // way to test that there no options.
+    CfgOptionPtr cfg_option;
+    cfg_option = cclass->getCfgOption();
+    ASSERT_TRUE(cfg_option);
+    OptionContainerPtr oc;
+    ASSERT_TRUE(oc = cclass->getCfgOption()->getAll("dhcp4"));
+    EXPECT_EQ(0, oc->size());
+
+    // Verify we can retrieve the expression
+    ExpressionPtr match_expr = cclass->getMatchExpr();
+    ASSERT_TRUE(match_expr);
+
+    // Build a packet that will fail evaluation.
+    Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 123));
+    EXPECT_FALSE(evaluate(*match_expr, *pkt4));
+
+    // Now add the option so it will pass.
+    OptionPtr opt(new OptionString(Option::V4, 100, "works right"));
+    pkt4->addOption(opt);
+    EXPECT_TRUE(evaluate(*match_expr, *pkt4));
+}
+
+// Verifies you can create a class with a name and options,
+// but no expression.
+TEST_F(ClientClassDefParserTest, nameAndOptionsClass) {
+
+    std::string cfg_text =
+        "{ \n"
+        "    \"name\": \"MICROSOFT\", \n"
+        "    \"option-data\": [ \n"
+        "        { \n"
+        "           \"name\": \"domain-name-servers\", \n"
+        "           \"code\": 6, \n"
+        "           \"space\": \"dhcp4\", \n"
+        "           \"csv-format\": true, \n"
+        "           \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+        "        } \n"
+        "      ] \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, Option::V4));
+
+    // We should find our class.
+    ASSERT_TRUE(cclass);
+    EXPECT_EQ("MICROSOFT", cclass->getName());
+
+    // Our one option should exist.
+    OptionDescriptor od = cclass->getCfgOption()->get("dhcp4", 6);
+    ASSERT_TRUE(od.option_);
+    EXPECT_EQ(6, od.option_->getType());
+
+    // Verify we have no expression
+    ASSERT_FALSE(cclass->getMatchExpr());
+}
+
+
+// Verifies you can create a class with a name, expression,
+// and options.
+TEST_F(ClientClassDefParserTest, basicValidClass) {
+
+    std::string cfg_text =
+        "{ \n"
+        "    \"name\": \"MICROSOFT\", \n"
+        "    \"test\": \"option[100] == 'booya'\", \n"
+        "    \"option-data\": [ \n"
+        "        { \n"
+        "           \"name\": \"domain-name-servers\", \n"
+        "           \"code\": 6, \n"
+        "           \"space\": \"dhcp4\", \n"
+        "           \"csv-format\": true, \n"
+        "           \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+        "        } \n"
+        "      ] \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_NO_THROW(cclass = parseClientClassDef(cfg_text, Option::V4));
+
+    // We should find our class.
+    ASSERT_TRUE(cclass);
+    EXPECT_EQ("MICROSOFT", cclass->getName());
+
+    // Our one option should exist.
+    OptionDescriptor od = cclass->getCfgOption()->get("dhcp4", 6);
+    ASSERT_TRUE(od.option_);
+    EXPECT_EQ(6, od.option_->getType());
+
+    // Verify we can retrieve the expression
+    ExpressionPtr match_expr = cclass->getMatchExpr();
+    ASSERT_TRUE(match_expr);
+
+    // Build a packet that will fail evaluation.
+    Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 123));
+    EXPECT_FALSE(evaluate(*match_expr, *pkt4));
+
+    // Now add the option so it will pass.
+    OptionPtr opt(new OptionString(Option::V4, 100, "booya"));
+    pkt4->addOption(opt);
+    EXPECT_TRUE(evaluate(*match_expr, *pkt4));
+}
+
+// Verifies that a class with no name, fails to parse.
+TEST_F(ClientClassDefParserTest, noClassName) {
+
+    std::string cfg_text =
+        "{ \n"
+        "    \"test\": \"option[123] == 'abc'\", \n"
+        "    \"option-data\": [ \n"
+        "        { \n"
+        "           \"name\": \"domain-name-servers\", \n"
+        "           \"code\": 6, \n"
+        "           \"space\": \"dhcp4\", \n"
+        "           \"csv-format\": true, \n"
+        "           \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+        "        } \n"
+        "      ] \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_THROW(cclass = parseClientClassDef(cfg_text, Option::V4),
+                 DhcpConfigError);
+}
+
+// Verifies that a class with a blank name, fails to parse.
+TEST_F(ClientClassDefParserTest, blankClassName) {
+
+    std::string cfg_text =
+        "{ \n"
+        "    \"name\": \"\", \n"
+        "    \"test\": \"option[123] == 'abc'\", \n"
+        "    \"option-data\": [ \n"
+        "        { \n"
+        "           \"name\": \"domain-name-servers\", \n"
+        "           \"code\": 6, \n"
+        "           \"space\": \"dhcp4\", \n"
+        "           \"csv-format\": true, \n"
+        "           \"data\": \"192.0.2.1, 192.0.2.2\" \n"
+        "        } \n"
+        "      ] \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_THROW(cclass = parseClientClassDef(cfg_text, Option::V4),
+                 DhcpConfigError);
+}
+
+
+// Verifies that a class with an unknown element, fails to parse.
+TEST_F(ClientClassDefParserTest, unknownElement) {
+    std::string cfg_text =
+        "{ \n"
+        "    \"name\": \"one\", \n"
+        "    \"bogus\": \"bad\" \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_THROW(cclass = parseClientClassDef(cfg_text, Option::V4),
+                 DhcpConfigError);
+}
+
+// Verifies that a class with an invalid expression, fails to parse.
+TEST_F(ClientClassDefParserTest, invalidExpression) {
+    std::string cfg_text =
+        "{ \n"
+        "    \"name\": \"one\", \n"
+        "    \"test\": 777 \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_THROW(cclass = parseClientClassDef(cfg_text, Option::V4),
+                 DhcpConfigError);
+}
+
+// Verifies that a class with invalid option-data, fails to parse.
+TEST_F(ClientClassDefParserTest, invalidOptionData) {
+    std::string cfg_text =
+        "{ \n"
+        "    \"name\": \"one\", \n"
+        "    \"option-data\": [ \n"
+        "      { \"bogus\": \"bad\" } \n"
+        "      ] \n"
+        "} \n";
+
+    ClientClassDefPtr cclass;
+    ASSERT_THROW(cclass = parseClientClassDef(cfg_text, Option::V4),
+                 DhcpConfigError);
+}
+
+
+// Verifies that a valid list of client classes will parse.
+TEST_F(ClientClassDefListParserTest, simpleValidList) {
+    std::string cfg_text =
+        "[ \n"
+        "   { \n"
+        "       \"name\": \"one\" \n"
+        "   }, \n"
+        "   { \n"
+        "       \"name\": \"two\" \n"
+        "   }, \n"
+        "   { \n"
+        "       \"name\": \"three\" \n"
+        "   } \n"
+        "] \n";
+
+    // Parsing the list should succeed.
+    ClientClassDictionaryPtr dictionary;
+    ASSERT_NO_THROW(dictionary = parseClientClassDefList(cfg_text, Option::V4));
+    ASSERT_TRUE(dictionary);
+
+    // We should have three classes in the dictionary.
+    EXPECT_EQ(3, dictionary->getClasses()->size());
+
+    // Make sure we can find all three.
+    ClientClassDefPtr cclass;
+    ASSERT_NO_THROW(cclass = dictionary->findClass("one"));
+    ASSERT_TRUE(cclass);
+    EXPECT_EQ("one", cclass->getName());
+
+    ASSERT_NO_THROW(cclass = dictionary->findClass("two"));
+    ASSERT_TRUE(cclass);
+    EXPECT_EQ("two", cclass->getName());
+
+    ASSERT_NO_THROW(cclass = dictionary->findClass("three"));
+    ASSERT_TRUE(cclass);
+    EXPECT_EQ("three", cclass->getName());
+
+    // For good measure, make sure we can't find a non-existant class.
+    ASSERT_NO_THROW(cclass = dictionary->findClass("bogus"));
+    EXPECT_FALSE(cclass);
+
+    // Verify that the dictionary was pushed to the CfgMgr's staging config.
+    SrvConfigPtr staging = CfgMgr::instance().getStagingCfg();
+    ASSERT_TRUE(staging);
+    ClientClassDictionaryPtr staged_dictionary = staging->getClientClassDictionary();
+    ASSERT_TRUE(staged_dictionary);
+    EXPECT_TRUE(*staged_dictionary == *dictionary);
+}
+
+// Verifies that class list containing a duplicate class entries, fails
+// to parse.
+TEST_F(ClientClassDefListParserTest, duplicateClass) {
+    std::string cfg_text =
+        "[ \n"
+        "   { \n"
+        "       \"name\": \"one\" \n"
+        "   }, \n"
+        "   { \n"
+        "       \"name\": \"two\" \n"
+        "   }, \n"
+        "   { \n"
+        "       \"name\": \"two\" \n"
+        "   } \n"
+        "] \n";
+
+    ClientClassDictionaryPtr dictionary;
+    ASSERT_THROW(dictionary = parseClientClassDefList(cfg_text, Option::V4),
+                 DhcpConfigError);
+}
+
+// Verifies that a class list containing an invalid class entry, fails to
+// parse.
+TEST_F(ClientClassDefListParserTest, invalidClass) {
+    std::string cfg_text =
+        "[ \n"
+        "   { \n"
+        "       \"name\": \"one\", \n"
+        "       \"bogus\": \"bad\" \n"
+        "   } \n"
+        "] \n";
+
+    ClientClassDictionaryPtr dictionary;
+    ASSERT_THROW(dictionary = parseClientClassDefList(cfg_text, Option::V4),
+                 DhcpConfigError);
+}
+
+} // end of anonymous namespace

+ 189 - 52
src/lib/dhcpsrv/tests/client_class_def_unittest.cc

@@ -19,6 +19,9 @@
 
 #include <gtest/gtest.h>
 
+/// @file client_class_def_unittest.cc Unit tests for client class storage
+/// classes.
+
 using namespace std;
 using namespace isc::dhcp;
 using namespace isc::util;
@@ -26,82 +29,175 @@ using namespace isc;
 
 namespace {
 
+// Tests basic construction of ClientClassDef
 TEST(ClientClassDef, construction) {
     boost::scoped_ptr<ClientClassDef> cclass;
 
     std::string name = "class1";
     ExpressionPtr expr;
-    OptionCollectionPtr options;
+    CfgOptionPtr cfg_option;
 
     // Classes cannot have blank names
-    ASSERT_THROW(cclass.reset(new ClientClassDef("", expr, options)), BadValue);
+    ASSERT_THROW(cclass.reset(new ClientClassDef("", expr, cfg_option)),
+                 BadValue);
 
-    // Verify we can create a class with a name, expression, and no options
+    // Verify we can create a class with a name, expression, and no cfg_option
     ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr)));
     EXPECT_EQ(name, cclass->getName());
     ASSERT_FALSE(cclass->getMatchExpr());
 
-    // Verify we get an empty collection of options
-    options = cclass->getOptions();
-    ASSERT_TRUE(options);
-    EXPECT_EQ(0, options->size());
+    // Verify we get an empty collection of cfg_option
+    cfg_option = cclass->getCfgOption();
+    ASSERT_TRUE(cfg_option);
+    //EXPECT_EQ(0, cfg_option->size());
 }
 
-TEST(ClientClassDef, optionsBasics) {
+// Tests options operations.  Note we just do the basics
+// as CfgOption is heavily tested elsewhere.
+TEST(ClientClassDef, cfgOptionBasics) {
     boost::scoped_ptr<ClientClassDef> cclass;
 
     std::string name = "class1";
     ExpressionPtr expr;
-    OptionCollectionPtr options;
+    CfgOptionPtr test_options;
+    CfgOptionPtr class_options;
     OptionPtr opt;
 
     // First construct the class with empty option pointer
-    ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr, options)));
+    ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr, test_options)));
 
     // We should get back a collection with no entries,
     // not an empty collection pointer
-    options = cclass->getOptions();
-    ASSERT_TRUE(options);
-    EXPECT_EQ(0, options->size());
-
-    // We should not be able find an option
-    opt = cclass->findOption(17);
-    ASSERT_FALSE(opt);
-
-    // Create an option container with two options
-    options.reset(new OptionCollection());
-    EXPECT_NO_THROW(opt.reset(new Option(Option::V4, 17)));
-    options->insert(make_pair(17, opt));
-    EXPECT_NO_THROW(opt.reset(new Option(Option::V4, 18)));
-    options->insert(make_pair(18, opt));
-
-    // Now remake the client class with options
-    ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr, options)));
-
-    options = cclass->getOptions();
-    ASSERT_TRUE(options);
-    EXPECT_EQ(2, options->size());
-
-    // We should be able to find option 17
-    opt = cclass->findOption(17);
-    ASSERT_TRUE(opt);
-    EXPECT_EQ(17, opt->getType());
-
-    // We should be able to find option 18
-    opt = cclass->findOption(18);
-    ASSERT_TRUE(opt);
-    EXPECT_EQ(18, opt->getType());
-
-    // We should not be able to find option 90
-    opt = cclass->findOption(90);
-    ASSERT_FALSE(opt);
+    class_options = cclass->getCfgOption();
+    ASSERT_TRUE(class_options);
+
+    // Create an option container and add some options
+    OptionPtr option;
+    test_options.reset(new CfgOption());
+    option.reset(new Option(Option::V4, 17, OptionBuffer(10, 0xFF)));
+    ASSERT_NO_THROW(test_options->add(option, false, "dhcp4"));
+
+    option.reset(new Option(Option::V6, 101, OptionBuffer(10, 0xFF)));
+    ASSERT_NO_THROW(test_options->add(option, false, "isc"));
+
+    option.reset(new Option(Option::V6, 100, OptionBuffer(10, 0xFF)));
+    ASSERT_NO_THROW(test_options->add(option, false, "dhcp6"));
+
+    // Now remake the client class with cfg_option
+    ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr, test_options)));
+    class_options = cclass->getCfgOption();
+    ASSERT_TRUE(class_options);
+
+    // Now make sure we can find all the options
+    OptionDescriptor opt_desc = class_options->get("dhcp4",17);
+    ASSERT_TRUE(opt_desc.option_);
+    EXPECT_EQ(17, opt_desc.option_->getType());
+
+    opt_desc = class_options->get("isc",101);
+    ASSERT_TRUE(opt_desc.option_);
+    EXPECT_EQ(101, opt_desc.option_->getType());
+
+    opt_desc = class_options->get("dhcp6",100);
+    ASSERT_TRUE(opt_desc.option_);
+    EXPECT_EQ(100, opt_desc.option_->getType());
 }
 
+// Verifies copy constructor and equality tools (methods/operators)
+TEST(ClientClassDef, copyAndEquality) {
+
+    boost::scoped_ptr<ClientClassDef> cclass;
+    ExpressionPtr expr;
+    CfgOptionPtr test_options;
+    OptionPtr opt;
+
+    // Make an expression
+    expr.reset(new Expression());
+    TokenPtr token(new TokenString("boo"));
+    expr->push_back(token);
+
+    // Create an option container with an option
+    OptionPtr option;
+    test_options.reset(new CfgOption());
+    option.reset(new Option(Option::V4, 17, OptionBuffer(10, 0xFF)));
+    ASSERT_NO_THROW(test_options->add(option, false, "dhcp4"));
+
+    // Now remake the client class with cfg_option
+    ASSERT_NO_THROW(cclass.reset(new ClientClassDef("class_one", expr,
+                                                    test_options)));
+
+    // Now lets make a copy of it.
+    boost::scoped_ptr<ClientClassDef> cclass2;
+    ASSERT_NO_THROW(cclass2.reset(new ClientClassDef(*cclass)));
+
+    // The allocated Expression pointers should not match
+    EXPECT_TRUE(cclass->getMatchExpr().get() !=
+                 cclass2->getMatchExpr().get());
+
+    // The allocated CfgOption pointers should not match
+    EXPECT_TRUE(cclass->getCfgOption().get() !=
+                 cclass2->getCfgOption().get());
+
+    // Verify the equality tools reflect that the classes are equal.
+    EXPECT_TRUE(cclass->equals(*cclass2));
+    EXPECT_TRUE(*cclass == *cclass2);
+    EXPECT_FALSE(*cclass != *cclass2);
+
+    // Make a class that differs from the first class only by name and
+    // verify that the equality tools reflect that the classes are not equal.
+    ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_two", expr,
+                                                     test_options)));
+    EXPECT_FALSE(cclass->equals(*cclass2));
+    EXPECT_FALSE(*cclass == *cclass2);
+    EXPECT_TRUE(*cclass != *cclass2);
+
+    // Make a class with the same name and options, but no expression
+    // verify that the equality tools reflect that the classes are not equal.
+    expr.reset();
+    ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr,
+                                                     test_options)));
+    EXPECT_FALSE(cclass->equals(*cclass2));
+    EXPECT_FALSE(*cclass == *cclass2);
+    EXPECT_TRUE(*cclass != *cclass2);
+
+    // Make a class with the same name and options, but different expression,
+    // verify that the equality tools reflect that the classes are not equal.
+    expr.reset(new Expression());
+    token.reset(new TokenString("yah"));
+    expr->push_back(token);
+    ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr,
+                                                      test_options)));
+    EXPECT_FALSE(cclass->equals(*cclass2));
+    EXPECT_FALSE(*cclass == *cclass2);
+    EXPECT_TRUE(*cclass != *cclass2);
+
+    // Make a class with same name and expression, but no options
+    // verify that the equality tools reflect that the classes are not equal.
+    test_options.reset(new CfgOption());
+    ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr,
+                                                     test_options)));
+    EXPECT_FALSE(cclass->equals(*cclass2));
+    EXPECT_FALSE(*cclass == *cclass2);
+    EXPECT_TRUE(*cclass != *cclass2);
+
+    // Make a class that with same name and expression, but different options
+    // verify that the equality tools reflect that the classes are not equal.
+    option.reset(new Option(Option::V4, 20, OptionBuffer(10, 0xFF)));
+    ASSERT_NO_THROW(test_options->add(option, false, "dhcp4"));
+    ASSERT_NO_THROW(cclass2.reset(new ClientClassDef("class_one", expr,
+                                                     test_options)));
+    EXPECT_FALSE(cclass->equals(*cclass2));
+    EXPECT_FALSE(*cclass == *cclass2);
+    EXPECT_TRUE(*cclass != *cclass2);
+}
+
+
+// Tests the basic operation of ClientClassDictionary
+// This includes adding, finding, and removing classes
 TEST(ClientClassDictionary, basics) {
     ClientClassDictionaryPtr dictionary;
     ClientClassDefPtr cclass;
     ExpressionPtr expr;
-    OptionCollectionPtr options;
+    CfgOptionPtr cfg_option;
 
     // Verify constructor doesn't throw
     ASSERT_NO_THROW(dictionary.reset(new ClientClassDictionary()));
@@ -113,19 +209,19 @@ TEST(ClientClassDictionary, basics) {
     EXPECT_EQ(0, classes->size());
 
     // Verify that we can add classes with both addClass variants
-    // First addClass(name, expression, options)
-    ASSERT_NO_THROW(dictionary->addClass("cc1", expr, options));
-    ASSERT_NO_THROW(dictionary->addClass("cc2", expr, options));
+    // First addClass(name, expression, cfg_option)
+    ASSERT_NO_THROW(dictionary->addClass("cc1", expr, cfg_option));
+    ASSERT_NO_THROW(dictionary->addClass("cc2", expr, cfg_option));
 
     // Verify duplicate add attempt throws
-    ASSERT_THROW(dictionary->addClass("cc2", expr, options),
+    ASSERT_THROW(dictionary->addClass("cc2", expr, cfg_option),
                  DuplicateClientClassDef);
 
     // Verify that you cannot add a class with no name.
-    ASSERT_THROW(dictionary->addClass("", expr, options), BadValue);
+    ASSERT_THROW(dictionary->addClass("", expr, cfg_option), BadValue);
 
     // Now with addClass(class pointer)
-    ASSERT_NO_THROW(cclass.reset(new ClientClassDef("cc3", expr, options)));
+    ASSERT_NO_THROW(cclass.reset(new ClientClassDef("cc3", expr, cfg_option)));
     ASSERT_NO_THROW(dictionary->addClass(cclass));
 
     // Verify duplicate add attempt throws
@@ -169,4 +265,45 @@ TEST(ClientClassDictionary, basics) {
     EXPECT_EQ(2, classes->size());
 }
 
+// Verifies copy constructor and equality tools (methods/operators)
+TEST(ClientClassDictionary, copyAndEquality) {
+    ClientClassDictionaryPtr dictionary;
+    ClientClassDictionaryPtr dictionary2;
+    ClientClassDefPtr cclass;
+    ExpressionPtr expr;
+    CfgOptionPtr options;
+
+    dictionary.reset(new ClientClassDictionary());
+    ASSERT_NO_THROW(dictionary->addClass("one", expr, options));
+    ASSERT_NO_THROW(dictionary->addClass("two", expr, options));
+    ASSERT_NO_THROW(dictionary->addClass("three", expr, options));
+
+    // Copy constructor should succeed.
+    ASSERT_NO_THROW(dictionary2.reset(new ClientClassDictionary(*dictionary)));
+
+    // Allocated class map pointers should not be equal
+    EXPECT_NE(dictionary->getClasses().get(), dictionary2->getClasses().get());
+
+    // Equality tools should reflect that the dictionaries are equal.
+    EXPECT_TRUE(dictionary->equals(*dictionary2));
+    EXPECT_TRUE(*dictionary == *dictionary2);
+    EXPECT_FALSE(*dictionary != *dictionary2);
+
+    // Remove a class from dictionary2.
+    ASSERT_NO_THROW(dictionary2->removeClass("two"));
+
+    // Equality tools should reflect that the dictionaries are not equal.
+    EXPECT_FALSE(dictionary->equals(*dictionary2));
+    EXPECT_FALSE(*dictionary == *dictionary2);
+    EXPECT_TRUE(*dictionary != *dictionary2);
+
+    // Create an empty dictionary.
+    dictionary2.reset(new ClientClassDictionary());
+
+    // Equality tools should reflect that the dictionaries are not equal.
+    EXPECT_FALSE(dictionary->equals(*dictionary2));
+    EXPECT_FALSE(*dictionary == *dictionary2);
+    EXPECT_TRUE(*dictionary != *dictionary2);
+}
+
 } // end of anonymous namespace

+ 43 - 1
src/lib/dhcpsrv/tests/srv_config_unittest.cc

@@ -40,7 +40,8 @@ public:
     /// Creates IPv4 and IPv6 subnets for unit test. The number of subnets
     /// is @c TEST_SUBNETS_NUM for IPv4 and IPv6 each.
     SrvConfigTest()
-        : iface_mgr_test_config_(true) {
+        : iface_mgr_test_config_(true),
+          ref_dictionary_(new ClientClassDictionary()) {
 
         // Disable DDNS.
         enableDDNS(false);
@@ -69,8 +70,14 @@ public:
             Subnet6Ptr subnet(new Subnet6(prefix, 64, 1000, 2000, 3000, 4000));
             test_subnets6_.push_back(subnet);
         }
+
+        // Build our reference dictionary of client classes
+        ref_dictionary_->addClass("cc1", ExpressionPtr(), CfgOptionPtr());
+        ref_dictionary_->addClass("cc2", ExpressionPtr(), CfgOptionPtr());
+        ref_dictionary_->addClass("cc3", ExpressionPtr(), CfgOptionPtr());
     }
 
+
     /// @brief Destructor.
     virtual ~SrvConfigTest() {
     }
@@ -118,6 +125,8 @@ public:
     /// @brief Fakes interface configuration.
     isc::dhcp::test::IfaceMgrTestConfig iface_mgr_test_config_;
 
+    /// @brief Client class dictionary with fixed content
+    ClientClassDictionaryPtr ref_dictionary_;
 };
 
 void
@@ -239,6 +248,26 @@ TEST_F(SrvConfigTest, summarySubnets) {
 
 }
 
+// Verifies that we can get and set the client class dictionary
+TEST_F(SrvConfigTest, classDictionaryBasics) {
+    ClientClassDictionaryPtr d1;
+    SrvConfig conf(32);
+
+    // Upon construction the dictionary should be empty.
+    ASSERT_TRUE(d1 = conf.getClientClassDictionary());
+    EXPECT_EQ(0, d1->getClasses()->size());
+
+    // Verify we can replace it with a new dictionary.
+    ASSERT_NO_THROW(conf.setClientClassDictionary(ref_dictionary_));
+    ASSERT_TRUE(d1 = conf.getClientClassDictionary());
+    EXPECT_EQ(ref_dictionary_->getClasses()->size(), d1->getClasses()->size());
+
+    // Verify const fetcher works too.
+    const ClientClassDictionaryPtr cd = conf.getClientClassDictionary();
+    ASSERT_TRUE(cd);
+    EXPECT_EQ(ref_dictionary_->getClasses()->size(), cd->getClasses()->size());
+}
+
 // This test checks if entire configuration can be copied and that the sequence
 // number is not affected.
 TEST_F(SrvConfigTest, copy) {
@@ -265,6 +294,9 @@ TEST_F(SrvConfigTest, copy) {
     OptionPtr option(new Option(Option::V6, 1000, OptionBuffer(10, 0xFF)));
     conf1.getCfgOption()->add(option, true, "dhcp6");
 
+    // Add a class dictionary
+    conf1.setClientClassDictionary(ref_dictionary_);
+
     // Make sure both configurations are different.
     ASSERT_TRUE(conf1 != conf2);
 
@@ -342,6 +374,16 @@ TEST_F(SrvConfigTest, equality) {
 
     EXPECT_TRUE(conf1 == conf2);
     EXPECT_FALSE(conf1 != conf2);
+
+    // Add a class dictionary to conf1
+    conf1.setClientClassDictionary(ref_dictionary_);
+    EXPECT_FALSE(conf1 == conf2);
+    EXPECT_TRUE(conf1 != conf2);
+
+    // Add same class dictionary to conf2
+    conf2.setClientClassDictionary(ref_dictionary_);
+    EXPECT_TRUE(conf1 == conf2);
+    EXPECT_FALSE(conf1 != conf2);
 }
 
 } // end of anonymous namespace