Commit d21fd692 authored by Thomas Markwalder's avatar Thomas Markwalder
Browse files

[master] DHCP servers now support client-classes element

    Merges in branch 'trac4096'
parents ab2bcf4e b1a10eb2
......@@ -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,
......
......@@ -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");
......
......@@ -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);
}
}
......@@ -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,
......
......@@ -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");
......
......@@ -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);
}
};
# 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
......@@ -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
......
......@@ -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
......@@ -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