Commit 4f81789e authored by Tomek Mrugalski's avatar Tomek Mrugalski 🛰

[master] Merge branch 'trac5023' (contexts in v6 pools)

# Conflicts:
#	src/lib/dhcpsrv/pool.h
parents 0a9d5bc9 0dc31ffa
......@@ -3713,6 +3713,51 @@ src/lib/dhcpsrv/cfg_host_operations.cc -->
</itemizedlist>
</section>
<section>
<title>User context in IPv4 pools</title>
<para>
Kea allows loading hook libraries that sometimes could benefit from
additional parameters. If such a parameter is specific to the whole
library, it is typically defined as a parameter for the hook library.
However, sometimes there is a need to specify parameters that are
different for each pool.
</para>
<para>
Let's consider an imaginary case of devices that have color LED
lights. Depending on their location, they should glow red, blue or
green. It would be easy to write a hook library that would send
specific values as maybe vendor option. However, the server has to
have some way to specify that value for each pool. This need is
addressed by user contexts. In essence, any user data can specified
in the user context as long as it is a valid JSON map. For example,
the forementioned case of LED devices could be configured in the
following way:
<screen>
"Dhcp4": {
"subnet4": [
{
"subnet": "192.0.2.0/24",
"pools": [ { "pool": "192.0.2.10 - 192.0.2.20" } ],
<userinput>"user-context": { "colour": "red" }</userinput>
},
...
],
...
}</screen>
</para>
<para>
It should be noted that Kea will not use that information, but will
simply store and make it available to hook libraries. It is up to the
hook library to extract that information and make use of it.</para>
<para>
Currently only pools allow definition of user contexts, but this
concept is expected to be enhanced to other structures in the future.
For more background information, see <xref linkend="user-context"/>
</para>
</section>
<section id="dhcp4-limit">
<title>DHCPv4 Server Limitations</title>
<para>These are the current limitations of the DHCPv4 server
......
......@@ -4077,6 +4077,66 @@ If not specified, the default value is:
<xref linkend="command-shutdown" />, respectively.</para>
</section>
<section>
<title>User context in IPv6 pools</title>
<para>
Kea allows loading hook libraries that sometimes could benefit from
additional parameters. If such a parameter is specific to the whole
library, it is typically defined as a parameter for the hook library.
However, sometimes there is a need to specify parameters that are
different for each pool.
</para>
<para>
Let's consider a lightweight 4over6 deployment as an example. It is an
IPv6 transition technology that allows mapping IPv6 prefix into full
or parts of IPv4 addresses. In DHCP context, these are certain
parameters that are supposed to be delivered to clients in form of
additional options. Values of those options are correlated to
delegated prefixes, so it is reasonable to keep those parameters
together with the PD pool. On the other hand, lightweight 4over6 is
not a commonly used feature, so it is not a part of the base Kea
code. The solution to this problem is to use user context. For each PD
pool that is expected to be used for lightweight 4over6, user context
with extra parameters is defined. Those extra parameters will be used
by hook library that would be loaded only when dynamic calculation of
the lightweight 4over6 option is actually needed. An example
configuration looks as follows:
<screen>
"Dhcp4": {
"subnet6": [ {
"pd-pools": [
{
"prefix": "2001:db8::",
"prefix-len": 56,
"delegated-len": 64,
<userinput>"user-context": {
"lw4over6-sharing-ratio": 64,
"lw4over6-v4-pool": "192.0.2.0/24",
"lw4over6-sysports-exclude": true,
"lw4over6-bind-prefix-len": 56
}</userinput>
} ],
"subnet": "2001:db8::/32"
} ],
...
}</screen>
</para>
<para>
It should be noted that Kea will not use any information in the user
context, but will simply store and make it available to the hook
libraries. It is up to the hook library to extract that information
and make use of it.
</para>
<para>
Currently only address and prefix pools allow definition of user
contexts, but this concept is expected to be enhanced to other
structures in the future. For more background information, see <xref
linkend="user-context"/>
</para>
</section>
<section id="dhcp6-std">
<title>Supported DHCPv6 Standards</title>
<para>The following standards are currently
......
......@@ -545,4 +545,23 @@ link address: 3001::1, hop count: 1, identified by remote-id:
</section>
</section>
</section>
<section id="user-context">
<title>User contexts</title>
<para>Hook libraries can have their own configuration parameters. That is
convenient if the parameter applies to the whole library. However,
sometimes it is very useful if certain configuration entities are extended
with additional configuration data. This is where the concept of user
contexts comes in. A sysadmin can define an arbitrary set of data and
attach it to Kea structures, as long as the data is specified as JSON map.
In particular, it is possible to define fields that are integers, strings,
boolean, lists and maps. It is possible to define nested structures of
arbitrary complexity. Kea does not use that data on its own, simply stores
and makes it available for the hook libraries.
</para>
<para>
As of Kea 1.2, the only structures that allow user contexts are address
and prefix pools, but it is expected to extend other structures with the
user context capability.
</para>
</section>
</chapter> <!-- hooks-libraries -->
......@@ -50,6 +50,64 @@ using namespace isc::hooks;
using namespace std;
namespace {
const char* PARSER_CONFIGS[] = {
// CONFIGURATION 0: one subnet with one pool, no user contexts
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet4\": [ {"
" \"pools\": [ "
" { \"pool\": \"192.0.2.0/28\" }"
" ],"
" \"subnet\": \"192.0.2.0/24\""
" } ]"
"}",
// Configuration 1: one pool with empty user context
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet4\": [ {"
" \"pools\": [ "
" { \"pool\": \"192.0.2.0/28\","
" \"user-context\": {"
" }"
" }"
" ],"
" \"subnet\": \"192.0.2.0/24\""
" } ]"
"}",
// Configuration 2: one pool with user context containing lw4over6 parameters
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet4\": [ {"
" \"pools\": [ "
" { \"pool\": \"192.0.2.0/28\","
" \"user-context\": {"
" \"integer-param\": 42,"
" \"string-param\": \"Sagittarius\","
" \"bool-param\": true"
" }"
" }"
" ],"
" \"subnet\": \"192.0.2.0/24\""
" } ]"
"}"
};
/// @brief Prepends the given name with the DHCP4 source directory
///
......@@ -501,6 +559,37 @@ public:
return (ReturnType());
}
/// @brief This utility method attempts to configure using specified
/// config and then returns requested pool from requested subnet
///
/// @param config configuration to be applied
/// @param subnet_index index of the subnet to be returned (0 - the first subnet)
/// @param pool_index index of the pool within a subnet (0 - the first pool)
/// @param pool [out] Pool pointer will be stored here (if found)
void getPool(const std::string& config, size_t subnet_index,
size_t pool_index, PoolPtr& pool) {
ConstElementPtr status;
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
ASSERT_TRUE(status);
checkResult(status, 0);
ConstCfgSubnets4Ptr subnets4 = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
ASSERT_TRUE(subnets4);
const Subnet4Collection* subnets = subnets4->getAll();
ASSERT_TRUE(subnets);
ASSERT_GE(subnets->size(), subnet_index + 1);
const PoolCollection pools = subnets->at(subnet_index)->getPools(Lease::TYPE_V4);
ASSERT_GE(pools.size(), pool_index + 1);
pool = pools.at(pool_index);
EXPECT_TRUE(pool);
}
boost::scoped_ptr<Dhcpv4Srv> srv_; ///< DHCP4 server under test
int rcode_; ///< Return code from element parsing
ConstElementPtr comment_; ///< Reason for parse fail
......@@ -4277,5 +4366,61 @@ TEST_F(Dhcp4ParserTest, invalidClientClassDictionary) {
checkResult(status, 1);
}
// Test verifies that regular configuration does not provide any user context
// in the address pool.
TEST_F(Dhcp4ParserTest, poolUserContextMissing) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[0]), 0, 0, pool);
ASSERT_TRUE(pool);
EXPECT_FALSE(pool->getContext());
}
// Test verifies that it's possible to specify empty user context in the
// address pool.
TEST_F(Dhcp4ParserTest, poolUserContextEmpty) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[1]), 0, 0, pool);
ASSERT_TRUE(pool);
ConstElementPtr ctx = pool->getContext();
ASSERT_TRUE(ctx);
// The context should be of type map and not contain any parameters.
EXPECT_EQ(Element::map, ctx->getType());
EXPECT_EQ(0, ctx->size());
}
// Test verifies that it's possible to specify parameters in the user context
// in the address pool.
TEST_F(Dhcp4ParserTest, poolUserContextData) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[2]), 0, 0, pool);
ASSERT_TRUE(pool);
ConstElementPtr ctx = pool->getContext();
ASSERT_TRUE(ctx);
// The context should be of type map and contain 4 parameters.
EXPECT_EQ(Element::map, ctx->getType());
EXPECT_EQ(3, ctx->size());
ConstElementPtr int_param = ctx->get("integer-param");
ConstElementPtr str_param = ctx->get("string-param");
ConstElementPtr bool_param = ctx->get("bool-param");
ASSERT_TRUE(int_param);
ASSERT_EQ(Element::integer, int_param->getType());
int64_t int_value;
EXPECT_NO_THROW(int_param->getValue(int_value));
EXPECT_EQ(42L, int_value);
ASSERT_TRUE(str_param);
ASSERT_EQ(Element::string, str_param->getType());
EXPECT_EQ("Sagittarius", str_param->stringValue());
ASSERT_TRUE(bool_param);
bool bool_value;
ASSERT_EQ(Element::boolean, bool_param->getType());
EXPECT_NO_THROW(bool_param->getValue(bool_value));
EXPECT_EQ(true, bool_value);
}
}
......@@ -188,7 +188,9 @@ public:
options_,
AF_INET6));
parser = option_parser;
} else if (entry == "user-context") {
user_context_ = param.second;
continue; // no parser to remember, simply store the value
} else {
isc_throw(DhcpConfigError, "unsupported parameter: " << entry
<< " (" << param.second->getPosition() << ")");
......@@ -222,6 +224,10 @@ public:
isc_throw(isc::dhcp::DhcpConfigError, ex.what()
<< " (" << pd_pool_->getPosition() << ")");
}
if (user_context_) {
pool_->setUserContext(user_context_);
}
}
// @brief Commits the constructed local pool to the pool storage.
......@@ -248,6 +254,8 @@ protected:
/// A storage for pool specific option values.
CfgOptionPtr options_;
isc::data::ConstElementPtr user_context_;
};
/// @brief Parser for a list of prefix delegation pools.
......
......@@ -55,6 +55,135 @@ using namespace std;
namespace {
const char* PARSER_CONFIGS[] = {
// CONFIGURATION 0: one subnet with one pool, no user contexts
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"preferred-lifetime\": 3000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet6\": [ {"
" \"pools\": [ "
" { \"pool\": \"2001:db8::/64\" }"
" ],"
" \"subnet\": \"2001:db8::/32\""
" } ]"
"}",
// Configuration 1: one pool with empty user context
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"preferred-lifetime\": 3000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet6\": [ {"
" \"pools\": [ "
" { \"pool\": \"2001:db8::/64\","
" \"user-context\": {"
" }"
" }"
" ],"
" \"subnet\": \"2001:db8::/32\""
" } ]"
"}",
// Configuration 2: one pool with user context containing lw4over6 parameters
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"preferred-lifetime\": 3000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet6\": [ {"
" \"pools\": [ "
" { \"pool\": \"2001:db8::/64\","
" \"user-context\": {"
" \"lw4over6-sharing-ratio\": 64,"
" \"lw4over6-v4-pool\": \"192.0.2.0/24\","
" \"lw4over6-sysports-exclude\": true,"
" \"lw4over6-bind-prefix-len\": 56"
" }"
" }"
" ],"
" \"subnet\": \"2001:db8::/32\""
" } ]"
"}",
// Configuration 3: pd-pool without any user-context
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"preferred-lifetime\": 3000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet6\": [ {"
" \"pd-pools\": [ "
" { \"prefix\": \"2001:db8::\","
" \"prefix-len\": 56,"
" \"delegated-len\": 64 }"
" ],"
" \"subnet\": \"2001:db8::/32\""
" } ]"
"}",
// Configuration 4: pd-pool with empty user-context
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"preferred-lifetime\": 3000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet6\": [ {"
" \"pd-pools\": [ "
" { \"prefix\": \"2001:db8::\","
" \"prefix-len\": 56,"
" \"delegated-len\": 64,"
" \"user-context\": { }"
" }"
" ],"
" \"subnet\": \"2001:db8::/32\""
" } ]"
"}",
// Configuration 5: pd-pool with user-context with lw4over6 parameters
"{"
" \"interfaces-config\": {"
" \"interfaces\": [\"*\" ]"
" },"
" \"valid-lifetime\": 4000,"
" \"preferred-lifetime\": 3000,"
" \"rebind-timer\": 2000,"
" \"renew-timer\": 1000,"
" \"subnet6\": [ {"
" \"pd-pools\": [ "
" { \"prefix\": \"2001:db8::\","
" \"prefix-len\": 56,"
" \"delegated-len\": 64,"
" \"user-context\": {"
" \"lw4over6-sharing-ratio\": 64,"
" \"lw4over6-v4-pool\": \"192.0.2.0/24\","
" \"lw4over6-sysports-exclude\": true,"
" \"lw4over6-bind-prefix-len\": 56"
" }"
" }"
" ],"
" \"subnet\": \"2001:db8::/32\""
" } ]"
"}"
};
std::string specfile(const std::string& name) {
return (std::string(DHCP6_SRC_DIR) + "/" + name);
}
......@@ -562,6 +691,37 @@ public:
CfgMgr::instance().clear();
}
/// @brief This utility method attempts to configure using specified
/// config and then returns requested pool from requested subnet
///
/// @param config configuration to be applied
/// @param subnet_index index of the subnet to be returned (0 - the first subnet)
/// @param pool_index index of the pool within a subnet (0 - the first pool)
/// @param type Pool type (TYPE_NA or TYPE_PD)
/// @param pool [out] Pool pointer will be stored here (if found)
void getPool(const std::string& config, size_t subnet_index,
size_t pool_index, Lease::Type type, PoolPtr& pool) {
ConstElementPtr status;
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
ASSERT_TRUE(status);
checkResult(status, 0);
ConstCfgSubnets6Ptr subnets6 = CfgMgr::instance().getStagingCfg()->getCfgSubnets6();
ASSERT_TRUE(subnets6);
const Subnet6Collection* subnets = subnets6->getAll();
ASSERT_TRUE(subnets);
ASSERT_GE(subnets->size(), subnet_index + 1);
const PoolCollection pools = subnets->at(subnet_index)->getPools(type);
ASSERT_GE(pools.size(), pool_index + 1);
pool = pools.at(pool_index);
EXPECT_TRUE(pool);
}
int rcode_; ///< Return code (see @ref isc::config::parseAnswer)
Dhcpv6Srv srv_; ///< Instance of the Dhcp6Srv used during tests
ConstElementPtr comment_; ///< Comment (see @ref isc::config::parseAnswer)
......@@ -4581,4 +4741,129 @@ TEST_F(Dhcp6ParserTest, invalidClientClassDictionary) {
checkResult(status, 1);
}
// Test verifies that regular configuration does not provide any user context
// in the address pool.
TEST_F(Dhcp6ParserTest, poolUserContextMissing) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[0]), 0, 0, Lease::TYPE_NA, pool);
ASSERT_TRUE(pool);
EXPECT_FALSE(pool->getContext());
}
// Test verifies that it's possible to specify empty user context in the
// address pool.
TEST_F(Dhcp6ParserTest, poolUserContextEmpty) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[1]), 0, 0, Lease::TYPE_NA, pool);
ASSERT_TRUE(pool);
ConstElementPtr ctx = pool->getContext();
ASSERT_TRUE(ctx);
// The context should be of type map and not contain any parameters.
EXPECT_EQ(Element::map, ctx->getType());
EXPECT_EQ(0, ctx->size());
}
// Test verifies that it's possible to specify parameters in the user context
// in the address pool.
TEST_F(Dhcp6ParserTest, poolUserContextlw4over6) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[2]), 0, 0, Lease::TYPE_NA, pool);
ASSERT_TRUE(pool);
ConstElementPtr ctx = pool->getContext();
ASSERT_TRUE(ctx);
// The context should be of type map and contain 4 parameters.
EXPECT_EQ(Element::map, ctx->getType());
EXPECT_EQ(4, ctx->size());
ConstElementPtr ratio = ctx->get("lw4over6-sharing-ratio");
ConstElementPtr v4pool = ctx->get("lw4over6-v4-pool");
ConstElementPtr exclude = ctx->get("lw4over6-sysports-exclude");
ConstElementPtr v6len = ctx->get("lw4over6-bind-prefix-len");
ASSERT_TRUE(ratio);
ASSERT_EQ(Element::integer, ratio->getType());
int64_t int_value;
EXPECT_NO_THROW(ratio->getValue(int_value));
EXPECT_EQ(64L, int_value);
ASSERT_TRUE(v4pool);
ASSERT_EQ(Element::string, v4pool->getType());
EXPECT_EQ("192.0.2.0/24", v4pool->stringValue());
ASSERT_TRUE(exclude);
bool bool_value;
ASSERT_EQ(Element::boolean, exclude->getType());
EXPECT_NO_THROW(exclude->getValue(bool_value));
EXPECT_EQ(true, bool_value);
ASSERT_TRUE(v6len);
ASSERT_EQ(Element::integer, v6len->getType());
EXPECT_NO_THROW(v6len->getValue(int_value));
EXPECT_EQ(56L, int_value);
}
// Test verifies that regular configuration does not provide any user context
// in the address pool.
TEST_F(Dhcp6ParserTest, pdPoolUserContextMissing) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[3]), 0, 0, Lease::TYPE_PD, pool);
ASSERT_TRUE(pool);
EXPECT_FALSE(pool->getContext());
}
// Test verifies that it's possible to specify empty user context in the
// address pool.
TEST_F(Dhcp6ParserTest, pdPoolUserContextEmpty) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[4]), 0, 0, Lease::TYPE_PD, pool);
ASSERT_TRUE(pool);
ConstElementPtr ctx = pool->getContext();
ASSERT_TRUE(ctx);
// The context should be of type map and not contain any parameters.
EXPECT_EQ(Element::map, ctx->getType());
EXPECT_EQ(0, ctx->size());
}
// Test verifies that it's possible to specify parameters in the user context
// in the address pool.
TEST_F(Dhcp6ParserTest, pdPoolUserContextlw4over6) {
PoolPtr pool;
getPool(string(PARSER_CONFIGS[5]), 0, 0, Lease::TYPE_PD, pool);
ASSERT_TRUE(pool);
ConstElementPtr ctx = pool->getContext();
ASSERT_TRUE(ctx);
// The context should be of type map and contain 4 parameters.
EXPECT_EQ(Element::map, ctx->getType());
EXPECT_EQ(4, ctx->size());
ConstElementPtr ratio = ctx->get("lw4over6-sharing-ratio");
ConstElementPtr v4pool = ctx->get("lw4over6-v4-pool");
ConstElementPtr exclude = ctx->get("lw4over6-sysports-exclude");
ConstElementPtr v6len = ctx->get("lw4over6-bind-prefix-len");
ASSERT_TRUE(ratio);
ASSERT_EQ(Element::integer, ratio->getType());
int64_t int_value;
EXPECT_NO_THROW(ratio->getValue(int_value));
EXPECT_EQ(64L, int_value);
ASSERT_TRUE(v4pool);
ASSERT_EQ(Element::string, v4pool->getType());
EXPECT_EQ("192.0.2.0/24", v4pool->stringValue());
ASSERT_TRUE(exclude);
bool bool_value;
ASSERT_EQ(Element::boolean, exclude->getType());
EXPECT_NO_THROW(exclude->getValue(