From: Francis Dupont Date: Wed, 11 Apr 2018 14:42:23 +0000 (+0200) Subject: [master] Merging trac5374 (new classification) - conflicts resolved, regen needed X-Git-Tag: trac5458a_base~14 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0260a6284e5fb9ebf0187f7030bd663ba35f936f;p=thirdparty%2Fkea.git [master] Merging trac5374 (new classification) - conflicts resolved, regen needed --- 0260a6284e5fb9ebf0187f7030bd663ba35f936f diff --cc doc/Makefile.am index eb35605ad5,c39619a795..18ff094445 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@@ -20,7 -18,7 +20,8 @@@ nobase_dist_doc_DATA += examples/kea4/a nobase_dist_doc_DATA += examples/kea4/backends.json nobase_dist_doc_DATA += examples/kea4/cassandra.json nobase_dist_doc_DATA += examples/kea4/classify.json + nobase_dist_doc_DATA += examples/kea4/classify2.json +nobase_dist_doc_DATA += examples/kea4/comments.json nobase_dist_doc_DATA += examples/kea4/dhcpv4-over-dhcpv6.json nobase_dist_doc_DATA += examples/kea4/hooks.json nobase_dist_doc_DATA += examples/kea4/leases-expiration.json @@@ -36,7 -34,7 +37,8 @@@ nobase_dist_doc_DATA += examples/kea6/a nobase_dist_doc_DATA += examples/kea6/backends.json nobase_dist_doc_DATA += examples/kea6/cassandra.json nobase_dist_doc_DATA += examples/kea6/classify.json + nobase_dist_doc_DATA += examples/kea6/classify2.json +nobase_dist_doc_DATA += examples/kea6/comments.json nobase_dist_doc_DATA += examples/kea6/dhcpv4-over-dhcpv6.json nobase_dist_doc_DATA += examples/kea6/duid.json nobase_dist_doc_DATA += examples/kea6/hooks.json diff --cc doc/guide/classify.xml index c9ff02116c,49f58a7a05..23efe97e03 --- a/doc/guide/classify.xml +++ b/doc/guide/classify.xml @@@ -105,37 -166,46 +169,46 @@@ -
- Using Static Host Reservations In Classification - Classes can be statically assigned to the clients using techniques described - in and - . - -
- -
+
- Using Vendor Class Information In Classification + Builtin Client Classes - The server checks whether an incoming DHCPv4 packet includes - the vendor class identifier option (60) or an incoming DHCPv6 packet - includes the vendor class option (16). If it does, the content of that - option is prepended with "VENDOR_CLASS_" and the result is interpreted - as a class. For example, modern cable modems will send this option with - value "docsis3.0" and so the packet will belong to - class "VENDOR_CLASS_docsis3.0". + Some classes are builtin so do not need to be defined. The main + example uses Vendor Class information: The server checks whether + an incoming DHCPv4 packet includes the vendor class identifier + option (60) or an incoming DHCPv6 packet includes the vendor + class option (16). If it does, the content of that option is + prepended with "VENDOR_CLASS_" and the result is + interpreted as a class. For example, modern cable modems will + send this option with value "docsis3.0" and so the + packet will belong to class "VENDOR_CLASS_docsis3.0". + + + Other examples are: the ALL class which all incoming packets + belong to, and the KNOWN class assigned when host reservations exist + for the particular client. By convention, builtin classes' names + begin with all capital letters. + + + Currently recognized builtin class names are ALL and KNOWN + and prefixes VENDOR_CLASS_, AFTER_ and EXTERNAL_. The AFTER_ prefix + is a provision for a not yet written hook, the EXTERNAL_ prefix + can be freely used: builtin classes are implicitly defined so + never raise warnings if they do not appear in the configuration. +
-
+
Using Expressions In Classification - The expression portion of classification contains operators and values. - All values are currently strings and operators take a string or strings and - return another string. When all the operations have completed - the result should be a value of "true" or "false". - The packet belongs to - the class (and the class name is added to the list of classes) if the result - is "true". Expressions are written in standard format and can be nested. + The expression portion of classification contains operators and + values. All values are currently strings and operators take a + string or strings and return another string. When all the + operations have completed the result should be a value of + "true" or "false". The packet belongs to + the class (and the class name is added to the list of classes) + if the result is "true". Expressions are written in + standard format and can be nested. @@@ -161,13 -232,21 +235,21 @@@ remain the same. + + Dependencies between classes are checked too: for instance forward + dependencies are rejected when the configuration is parsed: + an expression can only depend on already defined classes (including + builtin classes) and which are evaluated in a previous or the + same evaluation phase. This does not apply to the KNOWN class. + + - +
List of Classification Values - - - - + + + + Name @@@ -644,15 -760,18 +763,17 @@@ concatenation of the stringshook to perform the necessary work. + complex or time consuming expressions you should write a hook to perform the necessary work. -
+
Configuring Classes - A class contains three items: a name, a test expression and option data. + A class contains five items: a name, a test expression, option data, + option definition and only-if-required flag. The name must exist and must be unique amongst all classes. The test - expression and option data are optional. + expression, option data and definition, and only-if-required flag are + optional. @@@ -667,9 -786,37 +788,37 @@@ - In the following example the class named "Client_foo" is defined. + The option definition is for DHCPv4 option 43 ( and DHCPv4 private options + (). + + + + Usually the test expression is evaluated before subnet selection + but in some cases it is useful to evaluate it later when the + subnet, shared-network or pools are known but output option + processing not yet done. The only-if-required flag, false by default, + allows to perform the evaluation of the test expression only + when it was required, i.e. in a require-client-classes list of the + selected subnet, shared-network or pool. + + + + The require-client-classes list which is valid for shared-network, + subnet and pool scope specifies the classes which are evaluated + in the second pass before output option processing. + The list is built in the reversed precedence order of option + data, i.e. an option data in a subnet takes precedence on one + in a shared-network but required class in a subnet is added + after one in a shared-network. + The mechanism is related to the only-if-required flag but it is + not mandatory that the flag was set to true. + + + + In the following example the class named "Client_foo" is defined. It is comprised of all clients whose client ids (option 61) start with the - string "foo". Members of this class will be given 192.0.2.1 and + string "foo". Members of this class will be given 192.0.2.1 and 192.0.2.2 as their domain name servers. @@@ -723,7 -870,15 +872,15 @@@
+
+ Using Static Host Reservations In Classification + Classes can be statically assigned to the clients using techniques described + in and + . + +
+ -
+
Configuring Subnets With Class Information In certain cases it beneficial to restrict access to certain subnets @@@ -845,49 -999,10 +1001,49 @@@ "pool": "192.0.2.10 - 192.0.2.20", "client-class": "Client_foo" } - ] + ] }, ... - ], - ... + ],, ++ +} + + + + The following example shows restricting access to an address pool. + This configuration will restrict use of the addresses 2001:db8:1::1 + to 2001:db8:1::FFFF to members of the "Client_enterprise" class. + +"Dhcp6": { + "client-classes": [ + { + "name": "Client_enterprise_", + "test": "substring(option[1].hex,0,6) == 0x0002AABBCCDD'", + "option-data": [ + { + "name": "dns-servers", + "code": 23, + "space": "dhcp6", + "csv-format": true, + "data": "2001:db8:0::1, 2001:db8:2::1" + } + ] + }, + ... + ], + "subnet6": [ + { + "subnet": "2001:db8:1::/64", + + "pools": [ + { + "pool": "2001:db8:1::-2001:db8:1::ffff", + "client-class": "Client_foo" + } + ] + }, + ... + ], ... } diff --cc doc/guide/dhcp4-srv.xml index ca6785d790,54b285af5c..9629f67b95 --- a/doc/guide/dhcp4-srv.xml +++ b/doc/guide/dhcp4-srv.xml @@@ -2225,9 -2081,12 +2227,15 @@@ It is merely echoed by the serve - Client classification can also be used to restrict access to specific - pools within a subnet. This is useful when to segregate clients belonging - to the same subnet into different address ranges. + When subnets belong to a shared network the classification applies + to subnet selection but not to pools, e.g., a pool in a subnet + limited to a particular class can still be used by clients which do not + belong to the class if the pool they are expected to use is exhausted. + So the limit access based on class information is also available - at the pool level, see . ++ at the pool level, see , ++ within a subnet. ++ This is useful when to segregate clients belonging to the same subnet ++ into different address ranges. @@@ -2380,9 -2252,80 +2401,80 @@@ }
+ +
+ Required Classification + + In some cases it is useful to limit the scope of a class to + a shared-network, subnet or pool. There are two parameters + which are used to limit the scope of the class by instructing + the server to perform evaluation of test expressions when + required. + + + + The first one is the per-class only-if-required + flag which is false by default. When it is set to + true the test expression of the class is not + evaluated at the reception of the incoming packet but later and + only if the class evaluation is required. + + + + The second is the require-client-classes which + takes a list of class names and is valid in shared-network, + subnet and pool scope. Classes in these lists are marked as + required and evaluated after selection of this specific + shared-network/subnet/pool and before output option processing. + + + + In this example, a class is assigned to the incoming packet + when the specified subnet is used. + + + "Dhcp4": { + "client-classes": [ + { + "name": "Client_foo", + "test": "member('ALL')", + "only-if-required": true + }, + ... + ], + "subnet4": [ + { + "subnet": "192.0.2.0/24", + "pools": [ { "pool": "192.0.2.10 - 192.0.2.20" } ], + "require-client-classes": [ "Client_foo" ], + ... + }, + ... + ], + ... + } + + + + Required evaluation can be used to express complex dependencies, + for example, subnet membership. It can also be used to reverse the + precedence: if you set an option-data in a subnet it takes + precedence over an option-data in a class. When you move the + option-data to a required class and require it in + the subnet, a class evaluated earlier may take precedence. + + + + Required evaluation is also available at shared-network and + pool levels. The order in which required classes are considered is: + shared-network, subnet and pool, i.e. the opposite order + option-data are processed. + + +
-
+
DDNS for DHCPv4 As mentioned earlier, kea-dhcp4 can be configured to generate requests to the @@@ -3520,21 -3464,29 +3612,32 @@@ Static class assignments, as shown above, can be used in conjunction - with classification using expressions. + with classification using expressions. The "KNOWN" builtin class is + added to the packet and any class depending on it directly or indirectly + and not only-if-required is evaluated. + + + + If you want to force the evaluation of a class expression after + the host reservation lookup, for instance because of a dependency on + "reserved-class1" from the previous example, you should add a + "member('KNOWN')" in the expression. + +
-
- Storing Host Reservations in MySQL or PostgreSQL +
+ Storing Host Reservations in MySQL, PostgreSQL or Cassandra - It is possible to store host reservations in MySQL or PostgreSQL. See for information on how to configure Kea to use - reservations stored in MySQL or PostgreSQL. Kea provides dedicated hook for + It is possible to store host reservations in MySQL, PostgreSQL or Cassandra. See + for information on how to configure Kea to use + reservations stored in MySQL, PostgreSQL or Cassandra. Kea provides dedicated hook for managing reservations in a database, section provide - detailed information. + detailed information. http://kea.isc.org/wiki/HostReservationsHowTo + provides some examples how to conduct common host reservation operations. In Kea maximum length of an option specified per host is diff --cc doc/guide/dhcp6-srv.xml index 39d449124a,16ef42e1e7..9199ad5009 --- a/doc/guide/dhcp6-srv.xml +++ b/doc/guide/dhcp6-srv.xml @@@ -2225,11 -1937,15 +2223,15 @@@ should include options from the isc opt users from playing with their cable modems. For details on how to set up class restrictions on subnets, see . -- -- - Client classification can also be used to restrict access to specific - pools within a subnet. This is useful when to segregate clients belonging - to the same subnet into different address or prefix ranges. + When subnets belong to a shared network the classification applies + to subnet selection but not to pools, e.g., a pool in a subnet + limited to a particular class can still be used by clients which do not + belong to the class if the pool they are expected to use is exhausted. + So the limit access based on class information is also available + at the address/prefix pool level, see . ++ linkend="classification-pools"/> within a subnet. ++ This is useful when to segregate clients belonging to the same subnet ++ into different address ranges. @@@ -2319,9 -2043,84 +2329,84 @@@
+ +
+ Required classification + + In some cases it is useful to limit the scope of a class to + a shared-network, subnet or pool. There are two parameters + which are used to limit the scope of the class by instructing + the server to perform evaluation of test expressions when + required. + + + + The first one is the per-class only-if-required + flag which is false by default. When it is set to + true the test expression of the class is not + evaluated at the reception of the incoming packet but later and + only if the class evaluation is required. + + + + The second is the require-client-classes which + takes a list of class names and is valid in shared-network, + subnet and pool scope. Classes in these lists are marked as + required and evaluated after selection of this specific + shared-network/subnet/pool and before output option processing. + + + + In this example, a class is assigned to the incoming packet + when the specified subnet is used. + + + "Dhcp6": { + "client-classes": [ + { + "name": "Client_foo", + "test": "member('ALL')", + "only-if-required": true + }, + ... + ], + "subnet6": [ + { + "subnet": "2001:db8:1::/64" + "pools": [ + { + "pool": "2001:db8:1::-2001:db8:1::ffff" + } + ], + "require-client-classes": [ "Client_foo" ], + ... + }, + ... + ], + ... + } + + + + Required evaluation can be used to express complex dependencies, + for example, subnet membership. It can also be used to reverse the + precedence: if you set an option-data in a subnet it takes + precedence over an option-data in a class. When you move the + option-data to a required class and require it in + the subnet, a class evaluated earlier may take precedence. + + + + Required evaluation is also available at shared-network and + pool/pd-pool levels. The order in which required classes are + considered is: shared-network, subnet and (pd-)pool, i.e. + the opposite order option-data are processed. + + +
-
+
DDNS for DHCPv6 As mentioned earlier, kea-dhcp6 can be configured to generate requests to @@@ -3184,22 -2984,29 +3269,33 @@@ Static class assignments, as shown above, can be used in conjunction - with classification using expressions. + with classification using expressions. The "KNOWN" builtin class is + added to the packet and any class depending on it directly or indirectly + and not only-if-required is evaluated. + + + + If you want to force the evaluation of a class expression after + the host reservation lookup, for instance because of a dependency on + "reserved-class1" from the previous example, you should add a + "member('KNOWN')" in the expression. + +
-
- Storing Host Reservations in MySQL or PostgreSQL +
+ Storing Host Reservations in MySQL, PostgreSQL or Cassandra - It is possible to store host reservations in MySQL or PostgreSQL. See for information on how to configure Kea to use - reservations stored in MySQL or PostgreSQL. Kea provides dedicated hook for + It is possible to store host reservations in MySQL, PostgreSQL or Cassandra. See + for information on how to configure Kea to use + reservations stored in MySQL, PostgreSQL or Cassandra. Kea provides dedicated hook for managing reservations in a database, section provide - detailed information. + detailed information. The Kea wiki http://kea.isc.org/wiki/HostReservationsHowTo + provides some examples how to conduct some common operations + on host reservations. In Kea maximum length of an option specified per host is diff --cc src/bin/dhcp4/dhcp4_parser.yy index e6b322e6c3,d503708e29..8606eb1e6a --- a/src/bin/dhcp4/dhcp4_parser.yy +++ b/src/bin/dhcp4/dhcp4_parser.yy @@@ -1121,9 -1061,8 +1134,10 @@@ shared_network_param: nam | relay | reservation_mode | client_class + | require_client_classes | valid_lifetime + | user_context + | comment | unknown_map_entry ; @@@ -1409,8 -1344,8 +1423,9 @@@ pool_params: pool_para pool_param: pool_entry | option_data_list | client_class + | require_client_classes | user_context + | comment | unknown_map_entry ; diff --cc src/bin/dhcp4/tests/config_parser_unittest.cc index a527419f1a,71bb4c1b3b..61fc5ffbfe --- a/src/bin/dhcp4/tests/config_parser_unittest.cc +++ b/src/bin/dhcp4/tests/config_parser_unittest.cc @@@ -5775,241 -5600,7 +5771,240 @@@ TEST_F(Dhcp4ParserTest, sharedNetworksD EXPECT_EQ(1, subs->size()); s = checkSubnet(*subs, "192.0.3.0/24", 1, 2, 4); - classes = s->getClientClasses(); - EXPECT_TRUE(classes.empty()); + EXPECT_TRUE(s->getClientClass().empty()); } +// This test checks multiple host data sources. +TEST_F(Dhcp4ParserTest, hostsDatabases) { + + string config = PARSER_CONFIGS[4]; + extractConfig(config); + configure(config, CONTROL_RESULT_SUCCESS, ""); + + // Check database config + ConstCfgDbAccessPtr cfgdb = + CfgMgr::instance().getStagingCfg()->getCfgDbAccess(); + ASSERT_TRUE(cfgdb); + const std::list& hal = cfgdb->getHostDbAccessStringList(); + ASSERT_EQ(2, hal.size()); + // Keywords are in alphabetical order + EXPECT_EQ("name=keatest1 password=keatest type=mysql user=keatest", hal.front()); + EXPECT_EQ("name=keatest2 password=keatest type=mysql user=keatest", hal.back()); +} + +// This test checks comments. Please keep it last. +TEST_F(Dhcp4ParserTest, comments) { + + string config = PARSER_CONFIGS[5]; + extractConfig(config); + configure(config, CONTROL_RESULT_SUCCESS, ""); + + // Check global user context. + ConstElementPtr ctx = CfgMgr::instance().getStagingCfg()->getContext(); + ASSERT_TRUE(ctx); + ASSERT_EQ(1, ctx->size()); + ASSERT_TRUE(ctx->get("comment")); + EXPECT_EQ("\"A DHCPv4 server\"", ctx->get("comment")->str()); + + // There is a network interface configuration. + ConstCfgIfacePtr iface = CfgMgr::instance().getStagingCfg()->getCfgIface(); + ASSERT_TRUE(iface); + + // Check network interface configuration user context. + ConstElementPtr ctx_iface = iface->getContext(); + ASSERT_TRUE(ctx_iface); + ASSERT_EQ(1, ctx_iface->size()); + ASSERT_TRUE(ctx_iface->get("comment")); + EXPECT_EQ("\"Use wildcard\"", ctx_iface->get("comment")->str()); + + // There is a global option definition. + const OptionDefinitionPtr& opt_def = + LibDHCP::getRuntimeOptionDef("isc", 100); + ASSERT_TRUE(opt_def); + EXPECT_EQ("foo", opt_def->getName()); + EXPECT_EQ(100, opt_def->getCode()); + EXPECT_FALSE(opt_def->getArrayType()); + EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, opt_def->getType()); + EXPECT_TRUE(opt_def->getEncapsulatedSpace().empty()); + + // Check option definition user context. + ConstElementPtr ctx_opt_def = opt_def->getContext(); + ASSERT_TRUE(ctx_opt_def); + ASSERT_EQ(1, ctx_opt_def->size()); + ASSERT_TRUE(ctx_opt_def->get("comment")); + EXPECT_EQ("\"An option definition\"", ctx_opt_def->get("comment")->str()); + + // There is an option descriptor aka option data. + const OptionDescriptor& opt_desc = + CfgMgr::instance().getStagingCfg()->getCfgOption()-> + get(DHCP4_OPTION_SPACE, DHO_DHCP_MESSAGE); + ASSERT_TRUE(opt_desc.option_); + EXPECT_EQ(DHO_DHCP_MESSAGE, opt_desc.option_->getType()); + + // Check option descriptor user context. + ConstElementPtr ctx_opt_desc = opt_desc.getContext(); + ASSERT_TRUE(ctx_opt_desc); + ASSERT_EQ(1, ctx_opt_desc->size()); + ASSERT_TRUE(ctx_opt_desc->get("comment")); + EXPECT_EQ("\"Set option value\"", ctx_opt_desc->get("comment")->str()); + + // And there are some client classes. + const ClientClassDictionaryPtr& dict = + CfgMgr::instance().getStagingCfg()->getClientClassDictionary(); + ASSERT_TRUE(dict); + EXPECT_EQ(3, dict->getClasses()->size()); + ClientClassDefPtr cclass = dict->findClass("all"); + ASSERT_TRUE(cclass); + EXPECT_EQ("all", cclass->getName()); + EXPECT_EQ("'' == ''", cclass->getTest()); + + // Check client class user context. + ConstElementPtr ctx_class = cclass->getContext(); + ASSERT_TRUE(ctx_class); + ASSERT_EQ(1, ctx_class->size()); + ASSERT_TRUE(ctx_class->get("comment")); + EXPECT_EQ("\"match all\"", ctx_class->get("comment")->str()); + + // The 'none' class has no user-context/comment. + cclass = dict->findClass("none"); + ASSERT_TRUE(cclass); + EXPECT_EQ("none", cclass->getName()); + EXPECT_EQ("", cclass->getTest()); + EXPECT_FALSE(cclass->getContext()); + + // The 'both' class has a user context and a comment. + cclass = dict->findClass("both"); + EXPECT_EQ("both", cclass->getName()); + EXPECT_EQ("", cclass->getTest()); + ctx_class = cclass->getContext(); + ASSERT_TRUE(ctx_class); + ASSERT_EQ(2, ctx_class->size()); + ASSERT_TRUE(ctx_class->get("comment")); + EXPECT_EQ("\"a comment\"", ctx_class->get("comment")->str()); + ASSERT_TRUE(ctx_class->get("version")); + EXPECT_EQ("1", ctx_class->get("version")->str()); + + // There is a control socket. + ConstElementPtr socket = + CfgMgr::instance().getStagingCfg()->getControlSocketInfo(); + ASSERT_TRUE(socket); + ASSERT_TRUE(socket->get("socket-type")); + EXPECT_EQ("\"unix\"", socket->get("socket-type")->str()); + ASSERT_TRUE(socket->get("socket-name")); + EXPECT_EQ("\"/tmp/kea4-ctrl-socket\"", socket->get("socket-name")->str()); + + // Check control socket comment and user context. + ConstElementPtr ctx_socket = socket->get("user-context"); + ASSERT_EQ(1, ctx_socket->size()); + ASSERT_TRUE(ctx_socket->get("comment")); + EXPECT_EQ("\"Indirect comment\"", ctx_socket->get("comment")->str()); + + // Now verify that the shared network was indeed configured. + const CfgSharedNetworks4Ptr& cfg_net = + CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks4(); + ASSERT_TRUE(cfg_net); + const SharedNetwork4Collection* nets = cfg_net->getAll(); + ASSERT_TRUE(nets); + ASSERT_EQ(1, nets->size()); + SharedNetwork4Ptr net = nets->at(0); + ASSERT_TRUE(net); + EXPECT_EQ("foo", net->getName()); + + // Check shared network user context. + ConstElementPtr ctx_net = net->getContext(); + ASSERT_TRUE(ctx_net); + ASSERT_EQ(1, ctx_net->size()); + ASSERT_TRUE(ctx_net->get("comment")); + EXPECT_EQ("\"A shared network\"", ctx_net->get("comment")->str()); + + // The shared network has a subnet. + const Subnet4Collection* subs = net->getAllSubnets(); + ASSERT_TRUE(subs); + ASSERT_EQ(1, subs->size()); + Subnet4Ptr sub = subs->at(0); + ASSERT_TRUE(sub); + EXPECT_EQ(100, sub->getID()); + EXPECT_EQ("192.0.1.0/24", sub->toText()); + + // Check subnet user context. + ConstElementPtr ctx_sub = sub->getContext(); + ASSERT_TRUE(ctx_sub); + ASSERT_EQ(1, ctx_sub->size()); + ASSERT_TRUE(ctx_sub->get("comment")); + EXPECT_EQ("\"A subnet\"", ctx_sub->get("comment")->str()); + + // The subnet has a pool. + const PoolCollection& pools = sub->getPools(Lease::TYPE_V4); + ASSERT_EQ(1, pools.size()); + PoolPtr pool = pools.at(0); + ASSERT_TRUE(pool); + + // Check pool user context. + ConstElementPtr ctx_pool = pool->getContext(); + ASSERT_TRUE(ctx_pool); + ASSERT_EQ(1, ctx_pool->size()); + ASSERT_TRUE(ctx_pool->get("comment")); + EXPECT_EQ("\"A pool\"", ctx_pool->get("comment")->str()); + + // The subnet has a host reservation. + uint8_t hw[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; + HWAddrPtr hwaddr(new HWAddr(hw, sizeof(hw), HTYPE_ETHER)); + ConstHostPtr host = + CfgMgr::instance().getStagingCfg()->getCfgHosts()->get4(100, hwaddr); + ASSERT_TRUE(host); + EXPECT_EQ(Host::IDENT_HWADDR, host->getIdentifierType()); + EXPECT_EQ("aa:bb:cc:dd:ee:ff", host->getHWAddress()->toText(false)); + EXPECT_FALSE(host->getDuid()); + EXPECT_EQ(100, host->getIPv4SubnetID()); + EXPECT_EQ(0, host->getIPv6SubnetID()); + EXPECT_EQ("foo.example.com", host->getHostname()); + + // Check host user context. + ConstElementPtr ctx_host = host->getContext(); + ASSERT_TRUE(ctx_host); + ASSERT_EQ(1, ctx_host->size()); + ASSERT_TRUE(ctx_host->get("comment")); + EXPECT_EQ("\"A host reservation\"", ctx_host->get("comment")->str()); + + // The host reservation has an option data. + ConstCfgOptionPtr opts = host->getCfgOption4(); + ASSERT_TRUE(opts); + EXPECT_FALSE(opts->empty()); + const OptionDescriptor& host_desc = + opts->get(DHCP4_OPTION_SPACE, DHO_DOMAIN_NAME); + ASSERT_TRUE(host_desc.option_); + EXPECT_EQ(DHO_DOMAIN_NAME, host_desc.option_->getType()); + + // Check embedded option data user context. + ConstElementPtr ctx_host_desc = host_desc.getContext(); + ASSERT_TRUE(ctx_host_desc); + ASSERT_EQ(1, ctx_host_desc->size()); + ASSERT_TRUE(ctx_host_desc->get("comment")); + EXPECT_EQ("\"An option in a reservation\"", + ctx_host_desc->get("comment")->str()); + + // Finally dynamic DNS update configuration. + const D2ClientConfigPtr& d2 = + CfgMgr::instance().getStagingCfg()->getD2ClientConfig(); + ASSERT_TRUE(d2); + EXPECT_FALSE(d2->getEnableUpdates()); + + // Check dynamic DNS update configuration user context. + ConstElementPtr ctx_d2 = d2->getContext(); + ASSERT_TRUE(ctx_d2); + ASSERT_EQ(1, ctx_d2->size()); + ASSERT_TRUE(ctx_d2->get("comment")); + EXPECT_EQ("\"No dynamic DNS\"", ctx_d2->get("comment")->str()); + +#if 0 + // Loggers section supports comments too. + + string logging = "{\n" + "\"loggers\": [ {\n" + " \"comment\": \"A logger\",\n" + " \"name\": \"kea-dhcp4\"\n" + "} ]\n"; +#endif +} + } diff --cc src/bin/dhcp4/tests/dhcp4_client.cc index 0340a01cea,8475989105..19565308a4 --- a/src/bin/dhcp4/tests/dhcp4_client.cc +++ b/src/bin/dhcp4/tests/dhcp4_client.cc @@@ -527,15 -531,14 +540,21 @@@ Dhcp4Client::sendMsg(const Pkt4Ptr& msg msg_copy->setRemoteAddr(msg->getLocalAddr()); msg_copy->setLocalAddr(dest_addr_); msg_copy->setIface(iface_name_); + // Copy classes + const ClientClasses& classes = msg->getClasses(); + for (ClientClasses::const_iterator cclass = classes.cbegin(); + cclass != classes.cend(); ++cclass) { + msg_copy->addClass(*cclass); + } srv_->fakeReceive(msg_copy); - srv_->run(); + + try { + // Invoke run_one instead of run, because we want to avoid triggering + // IO service. + srv_->run_one(); + } catch (...) { + // Suppress errors, as the DHCPv4 server does. + } } void diff --cc src/bin/dhcp4/tests/dhcp4_srv_unittest.cc index 99789e1cf5,f650d33867..02eda7cd73 --- a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc @@@ -2376,26 -2303,22 +2376,95 @@@ TEST_F(Dhcpv4SrvTest, clientClassify) // This discover does not belong to foo class, so it will not // be serviced - EXPECT_FALSE(srv_.selectSubnet(dis)); + bool drop = false; + EXPECT_FALSE(srv_.selectSubnet(dis, drop)); + EXPECT_FALSE(drop); + + // Let's add the packet to bar class and try again. + dis->addClass("bar"); + + // Still not supported, because it belongs to wrong class. + EXPECT_FALSE(srv_.selectSubnet(dis, drop)); + EXPECT_FALSE(drop); + + // Let's add it to matching class. + dis->addClass("foo"); + + // This time it should work + EXPECT_TRUE(srv_.selectSubnet(dis, drop)); + EXPECT_FALSE(drop); +} + +// Checks if the client-class field is indeed used for pool selection. ++TEST_F(Dhcpv4SrvTest, clientPoolClassify) { ++ IfaceMgrTestConfig test_config(true); ++ IfaceMgr::instance().openSockets4(); ++ ++ NakedDhcpv4Srv srv(0); ++ ++ // This test configures 2 pools. ++ // The second pool does not play any role here. The client's ++ // IP address belongs to the first pool, so only that first ++ // pool is being tested. ++ string config = "{ \"interfaces-config\": {" ++ " \"interfaces\": [ \"*\" ]" ++ "}," ++ "\"rebind-timer\": 2000, " ++ "\"renew-timer\": 1000, " ++ "\"subnet4\": [ " ++ "{ \"pools\": [ { " ++ " \"pool\": \"192.0.2.1 - 192.0.2.100\", " ++ " \"client-class\": \"foo\" }, " ++ " { \"pool\": \"192.0.3.1 - 192.0.3.100\", " ++ " \"client-class\": \"xyzzy\" } ], " ++ " \"subnet\": \"192.0.0.0/16\" } " ++ "]," ++ "\"valid-lifetime\": 4000 }"; ++ ++ ConstElementPtr json; ++ ASSERT_NO_THROW(json = parseDHCP4(config, true)); ++ ++ ConstElementPtr status; ++ EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); ++ ++ CfgMgr::instance().commit(); ++ ++ // check if returned status is OK ++ ASSERT_TRUE(status); ++ comment_ = config::parseAnswer(rcode_, status); ++ ASSERT_EQ(0, rcode_); ++ ++ // Create a simple packet that we'll use for classification ++ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234)); ++ dis->setRemoteAddr(IOAddress("192.0.2.1")); ++ dis->setCiaddr(IOAddress("192.0.2.1")); ++ dis->setIface("eth0"); ++ OptionPtr clientid = generateClientId(); ++ dis->addOption(clientid); ++ ++ // This discover does not belong to foo class, so it will not ++ // be serviced ++ Pkt4Ptr offer = srv.processDiscover(dis); ++ EXPECT_FALSE(offer); + + // Let's add the packet to bar class and try again. + dis->addClass("bar"); + + // Still not supported, because it belongs to wrong class. - EXPECT_FALSE(srv_.selectSubnet(dis)); ++ offer = srv.processDiscover(dis); ++ EXPECT_FALSE(offer); + + // Let's add it to matching class. + dis->addClass("foo"); + + // This time it should work - EXPECT_TRUE(srv_.selectSubnet(dis)); ++ offer = srv.processDiscover(dis); ++ ASSERT_TRUE(offer); ++ EXPECT_EQ(DHCPOFFER, offer->getType()); ++ EXPECT_FALSE(offer->getYiaddr().isV4Zero()); + } + + // Checks if the client-class field is indeed used for pool selection. TEST_F(Dhcpv4SrvTest, clientPoolClassify) { IfaceMgrTestConfig test_config(true); IfaceMgr::instance().openSockets4(); diff --cc src/bin/dhcp4/tests/parser_unittest.cc index b3c822e24a,eefc89cab1..b73cf22578 --- a/src/bin/dhcp4/tests/parser_unittest.cc +++ b/src/bin/dhcp4/tests/parser_unittest.cc @@@ -265,7 -244,7 +265,8 @@@ TEST(ParserTest, file) "backends.json", "cassandra.json", "classify.json", + "classify2.json", + "comments.json", "dhcpv4-over-dhcpv6.json", "hooks.json", "leases-expiration.json", diff --cc src/bin/dhcp6/dhcp6_parser.yy index ef73454555,ab3f2b3109..38337d8bde --- a/src/bin/dhcp6/dhcp6_parser.yy +++ b/src/bin/dhcp6/dhcp6_parser.yy @@@ -1373,8 -1307,8 +1387,9 @@@ pool_params: pool_para pool_param: pool_entry | option_data_list | client_class + | require_client_classes | user_context + | comment | unknown_map_entry ; @@@ -1713,9 -1600,8 +1729,10 @@@ not_empty_client_class_params: client_c client_class_param: client_class_name | client_class_test + | only_if_required | option_data_list + | user_context + | comment | unknown_map_entry ; diff --cc src/bin/dhcp6/dhcp6_srv.cc index 5b39edaad9,c25e9d49d0..987280acab --- a/src/bin/dhcp6/dhcp6_srv.cc +++ b/src/bin/dhcp6/dhcp6_srv.cc @@@ -2689,15 -2607,9 +2691,16 @@@ Dhcpv6Srv::processConfirm(const Pkt6Ptr // Let's create a simplified client context here. AllocEngine::ClientContext6 ctx; - initContext(confirm, ctx); + bool drop = false; + initContext(confirm, ctx, drop); + + // Stop here if initContext decided to drop the packet. + if (drop) { + return (Pkt6Ptr()); + } + setReservedClientClasses(confirm, ctx); + requiredClassify(confirm, ctx); // Get IA_NAs from the Confirm. If there are none, the message is // invalid and must be discarded. There is nothing more to do. @@@ -2789,15 -2701,9 +2792,16 @@@ Dhcpv6Srv::processRelease(const Pkt6Ptr // Let's create a simplified client context here. AllocEngine::ClientContext6 ctx; - initContext(release, ctx); + bool drop = false; + initContext(release, ctx, drop); + + // Stop here if initContext decided to drop the packet. + if (drop) { + return (Pkt6Ptr()); + } + setReservedClientClasses(release, ctx); + requiredClassify(release, ctx); Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, release->getTransid())); @@@ -2825,15 -2731,9 +2829,16 @@@ Dhcpv6Srv::processDecline(const Pkt6Ptr // Let's create a simplified client context here. AllocEngine::ClientContext6 ctx; - initContext(decline, ctx); + bool drop = false; + initContext(decline, ctx, drop); + + // Stop here if initContext decided to drop the packet. + if (drop) { + return (Pkt6Ptr()); + } + setReservedClientClasses(decline, ctx); + requiredClassify(decline, ctx); // Copy client options (client-id, also relay information if present) copyClientOptions(decline, reply); @@@ -3113,15 -3013,9 +3118,16 @@@ Dhcpv6Srv::processInfRequest(const Pkt6 // Let's create a simplified client context here. AllocEngine::ClientContext6 ctx; - initContext(inf_request, ctx); + bool drop = false; + initContext(inf_request, ctx, drop); + + // Stop here if initContext decided to drop the packet. + if (drop) { + return (Pkt6Ptr()); + } + setReservedClientClasses(inf_request, ctx); + requiredClassify(inf_request, ctx); // Create a Reply packet, with the same trans-id as the client's. Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, inf_request->getTransid())); diff --cc src/bin/dhcp6/tests/classify_unittests.cc index b63a297c54,c7d90acdbd..631ae9f777 --- a/src/bin/dhcp6/tests/classify_unittests.cc +++ b/src/bin/dhcp6/tests/classify_unittests.cc @@@ -617,121 -969,19 +946,99 @@@ TEST_F(ClassifyTest, clientClassifySubn sol->addClass("foo"); // This time it should work - EXPECT_TRUE(srv_.selectSubnet(sol)); + EXPECT_TRUE(srv_.selectSubnet(sol, drop)); + EXPECT_FALSE(drop); +} + +// Checks if the client-class field is indeed used for pool selection. +TEST_F(ClassifyTest, clientClassifyPool) { + IfaceMgrTestConfig test_config(true); + + NakedDhcpv6Srv srv(0); + + // This test configures 2 pools. + // The second pool does not play any role here. The client's + // IP address belongs to the first pool, so only that first + // pool is being tested. + std::string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"client-classes\": [ " + " { " + " \"name\": \"foo\" " + " }, " + " { " + " \"name\": \"bar\" " + " } " + "], " + "\"subnet6\": [ " + " { \"pools\": [ " + " { " + " \"pool\": \"2001:db8:1::/64\", " + " \"client-class\": \"foo\" " + " }, " + " { " + " \"pool\": \"2001:db8:2::/64\", " + " \"client-class\": \"xyzzy\" " + " } " + " ], " + " \"subnet\": \"2001:db8:2::/40\" " + " } " + "], " + "\"valid-lifetime\": 4000 }"; + + ASSERT_NO_THROW(configure(config)); + + Pkt6Ptr query1 = createSolicit("2001:db8:1::3"); + Pkt6Ptr query2 = createSolicit("2001:db8:1::3"); + Pkt6Ptr query3 = createSolicit("2001:db8:1::3"); + + // This discover does not belong to foo class, so it will not + // be serviced + srv.classifyPacket(query1); + Pkt6Ptr response1 = srv.processSolicit(query1); + ASSERT_TRUE(response1); + OptionPtr ia_na1 = response1->getOption(D6O_IA_NA); + ASSERT_TRUE(ia_na1); + EXPECT_TRUE(ia_na1->getOption(D6O_STATUS_CODE)); + EXPECT_FALSE(ia_na1->getOption(D6O_IAADDR)); + + // Let's add the packet to bar class and try again. + query2->addClass("bar"); + // Still not supported, because it belongs to wrong class. + srv.classifyPacket(query2); + Pkt6Ptr response2 = srv.processSolicit(query2); + ASSERT_TRUE(response2); + OptionPtr ia_na2 = response2->getOption(D6O_IA_NA); + ASSERT_TRUE(ia_na2); + EXPECT_TRUE(ia_na2->getOption(D6O_STATUS_CODE)); + EXPECT_FALSE(ia_na2->getOption(D6O_IAADDR)); + + // Let's add it to matching class. + query3->addClass("foo"); + // This time it should work + srv.classifyPacket(query3); + Pkt6Ptr response3 = srv.processSolicit(query3); + ASSERT_TRUE(response3); + OptionPtr ia_na3 = response3->getOption(D6O_IA_NA); + ASSERT_TRUE(ia_na3); + EXPECT_FALSE(ia_na3->getOption(D6O_STATUS_CODE)); + EXPECT_TRUE(ia_na3->getOption(D6O_IAADDR)); } - // Tests whether a packet with custom vendor-class (not erouter or docsis) - // is classified properly. - TEST_F(ClassifyTest, vendorClientClassification2) { - NakedDhcpv6Srv srv(0); - - // Let's create a SOLICIT. - Pkt6Ptr sol = createSolicit("2001:db8:1::3"); - - // Now let's add a vendor-class with id=1234 and content "foo" - OptionVendorClassPtr vendor_class(new OptionVendorClass(Option::V6, 1234)); - OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES); - tuple = "foo"; - vendor_class->addTuple(tuple); - sol->addOption(vendor_class); - - // Now the server classifies the packet. - srv.classifyPacket(sol); - - // The packet should now belong to VENDOR_CLASS_foo. - EXPECT_TRUE(sol->inClass(srv.VENDOR_CLASS_PREFIX + "foo")); - - // It should not belong to "foo" - EXPECT_FALSE(sol->inClass("foo")); - } + // Checks if the client-class field is indeed used for pool selection. + TEST_F(ClassifyTest, clientClassifyPool) { + IfaceMgrTestConfig test_config(true); - // Checks if relay IP address specified in the relay-info structure can be - // used together with client-classification. - TEST_F(ClassifyTest, relayOverrideAndClientClass) { + NakedDhcpv6Srv srv(0); - // This test configures 2 subnets. They both are on the same link, so they - // have the same relay-ip address. Furthermore, the first subnet is - // reserved for clients that belong to class "foo". + // This test configures 2 pools. + // The second pool does not play any role here. The client's + // IP address belongs to the first pool, so only that first + // pool is being tested. std::string config = "{ \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" "}," diff --cc src/bin/dhcp6/tests/parser_unittest.cc index 9a88454206,b221dd760e..a83e4a1b1a --- a/src/bin/dhcp6/tests/parser_unittest.cc +++ b/src/bin/dhcp6/tests/parser_unittest.cc @@@ -271,7 -248,7 +271,8 @@@ TEST(ParserTest, file) configs.push_back("backends.json"); configs.push_back("cassandra.json"); configs.push_back("classify.json"); + configs.push_back("classify2.json"); + configs.push_back("comments.json"); configs.push_back("dhcpv4-over-dhcpv6.json"); configs.push_back("duid.json"); configs.push_back("hooks.json"); diff --cc src/lib/dhcpsrv/alloc_engine.cc index f57669139e,effbe40efe..58a778aab9 --- a/src/lib/dhcpsrv/alloc_engine.cc +++ b/src/lib/dhcpsrv/alloc_engine.cc @@@ -916,8 -865,6 +917,7 @@@ AllocEngine::allocateUnreservedLeases6( continue; } uint64_t max_attempts = (attempts_ > 0 ? attempts_ : possible_attempts); - + Network::HRMode hr_mode = subnet->getHostReservationMode(); for (uint64_t i = 0; i < max_attempts; ++i) { diff --cc src/lib/dhcpsrv/client_class_def.cc index e60a42626e,6937df72a2..ca91116c7f --- a/src/lib/dhcpsrv/client_class_def.cc +++ b/src/lib/dhcpsrv/client_class_def.cc @@@ -187,16 -200,16 +204,18 @@@ voi ClientClassDictionary::addClass(const std::string& name, const ExpressionPtr& match_expr, const std::string& test, + bool required, const CfgOptionPtr& cfg_option, CfgOptionDefPtr cfg_option_def, + ConstElementPtr user_context, asiolink::IOAddress next_server, const std::string& sname, const std::string& filename) { ClientClassDefPtr cclass(new ClientClassDef(name, match_expr, cfg_option)); cclass->setTest(test); + cclass->setRequired(required); cclass->setCfgOptionDef(cfg_option_def); + cclass->setContext(user_context), cclass->setNextServer(next_server); cclass->setSname(sname); cclass->setFilename(filename); diff --cc src/lib/dhcpsrv/client_class_def.h index 81e4a179aa,98d9bc5ba1..623710eb83 --- a/src/lib/dhcpsrv/client_class_def.h +++ b/src/lib/dhcpsrv/client_class_def.h @@@ -235,9 -251,9 +252,10 @@@ public /// @param name Name to assign to this class /// @param match_expr Expression the class will use to determine membership /// @param test Original version of match_expr + /// @param required Original value of the only if required flag /// @param options Collection of options members should be given /// @param defs Option definitions (optional) + /// @param user_context User context (optional) /// @param next_server next-server value for this class (optional) /// @param sname server-name value for this class (optional) /// @param filename boot-file-name value for this class (optional) @@@ -246,9 -262,9 +264,10 @@@ /// dictionary. See @ref dhcp::ClientClassDef::ClientClassDef() for /// others. void addClass(const std::string& name, const ExpressionPtr& match_expr, - const std::string& test, const CfgOptionPtr& options, + const std::string& test, bool required, + const CfgOptionPtr& options, CfgOptionDefPtr defs = CfgOptionDefPtr(), + isc::data::ConstElementPtr user_context = isc::data::ConstElementPtr(), asiolink::IOAddress next_server = asiolink::IOAddress("0.0.0.0"), const std::string& sname = std::string(), const std::string& filename = std::string()); diff --cc src/lib/dhcpsrv/parsers/client_class_def_parser.cc index bd1ac03510,c6012a23e0..f44efbec2a --- a/src/lib/dhcpsrv/parsers/client_class_def_parser.cc +++ b/src/lib/dhcpsrv/parsers/client_class_def_parser.cc @@@ -127,9 -133,12 +133,15 @@@ ClientClassDefParser::parse(ClientClass opts_parser.parse(options, option_data); } + // Parse user context + ConstElementPtr user_context = class_def_cfg->get("user-context"); + + // Let's try to parse the only-if-required flag + bool required = false; + if (class_def_cfg->contains("only-if-required")) { + required = getBoolean(class_def_cfg, "only-if-required"); + } + // Let's try to parse the next-server field IOAddress next_server("0.0.0.0"); if (class_def_cfg->contains("next-server")) { diff --cc src/lib/dhcpsrv/parsers/dhcp_parsers.cc index eb80578712,d064b123dc..8b3f2efdcd --- a/src/lib/dhcpsrv/parsers/dhcp_parsers.cc +++ b/src/lib/dhcpsrv/parsers/dhcp_parsers.cc @@@ -891,6 -923,20 +922,19 @@@ PdPoolParser::parse(PoolStoragePtr pool } } + if (class_list) { + const std::vector& classes = class_list->listValue(); + for (auto cclass = classes.cbegin(); + cclass != classes.cend(); ++cclass) { + if (((*cclass)->getType() != Element::string) || + (*cclass)->stringValue().empty()) { + isc_throw(DhcpConfigError, "invalid class name (" + << (*cclass)->getPosition() << ")"); + } + pool_->requireClientClass((*cclass)->stringValue()); + } + } + - // Add the local pool to the external storage ptr. pools->push_back(pool_); } diff --cc src/lib/dhcpsrv/parsers/dhcp_parsers.h index 12dd20cb0b,f6a4cf7dc8..abf1e57a02 --- a/src/lib/dhcpsrv/parsers/dhcp_parsers.h +++ b/src/lib/dhcpsrv/parsers/dhcp_parsers.h @@@ -651,16 -651,9 +651,15 @@@ private /// A storage for pool specific option values. CfgOptionPtr options_; + /// @brief User context (optional, may be null) + /// + /// User context is arbitrary user data, to be used by hooks. isc::data::ConstElementPtr user_context_; + /// @brief Client class (a client has to belong to to use this pd-pool) + /// + /// If null, everyone is allowed. isc::data::ConstElementPtr client_class_; - }; /// @brief Parser for a list of prefix delegation pools. diff --cc src/lib/dhcpsrv/parsers/shared_network_parser.cc index ec6bdbbdfc,7270699002..2b65776e64 --- a/src/lib/dhcpsrv/parsers/shared_network_parser.cc +++ b/src/lib/dhcpsrv/parsers/shared_network_parser.cc @@@ -70,9 -70,18 +70,22 @@@ SharedNetwork4Parser::parse(const data: } } + ConstElementPtr user_context = shared_network_data->get("user-context"); + if (user_context) { + shared_network->setContext(user_context); ++ + if (shared_network_data->contains("require-client-classes")) { + const std::vector& class_list = + shared_network_data->get("require-client-classes")->listValue(); + for (auto cclass = class_list.cbegin(); + cclass != class_list.cend(); ++cclass) { + if (((*cclass)->getType() != Element::string) || + (*cclass)->stringValue().empty()) { + isc_throw(DhcpConfigError, "invalid class name (" + << (*cclass)->getPosition() << ")"); + } + shared_network->requireClientClass((*cclass)->stringValue()); + } } } catch (const DhcpConfigError&) { @@@ -116,9 -125,18 +129,22 @@@ SharedNetwork6Parser::parse(const data: } } + ConstElementPtr user_context = shared_network_data->get("user-context"); + if (user_context) { + shared_network->setContext(user_context); ++ + if (shared_network_data->contains("require-client-classes")) { + const std::vector& class_list = + shared_network_data->get("require-client-classes")->listValue(); + for (auto cclass = class_list.cbegin(); + cclass != class_list.cend(); ++cclass) { + if (((*cclass)->getType() != Element::string) || + (*cclass)->stringValue().empty()) { + isc_throw(DhcpConfigError, "invalid class name (" + << (*cclass)->getPosition() << ")"); + } + shared_network->requireClientClass((*cclass)->stringValue()); + } } if (shared_network_data->contains("subnet6")) { diff --cc src/lib/dhcpsrv/pool.h index bba123cfcf,30d1e2f7ed..53bd28d9bd --- a/src/lib/dhcpsrv/pool.h +++ b/src/lib/dhcpsrv/pool.h @@@ -97,57 -96,79 +97,119 @@@ public return (cfg_option_); } - /// @brief Returns const pointer to the user context. - data::ConstElementPtr getContext() const { - return (user_context_); + /// @brief Checks whether this pool supports client that belongs to + /// specified classes. + /// + /// @todo: currently doing the same as network which needs improving. + /// + /// @param client_classes list of all classes the client belongs to + /// @return true if client can be supported, false otherwise + bool clientSupported(const ClientClasses& client_classes) const; + + /// @brief Adds class class_name to the list of supported classes + /// + /// @param class_name client class to be supported by this pool + void allowClientClass(const ClientClass& class_name); + + /// @brief returns the client class white list + /// + /// @note Currently white list is empty or has one element + /// @note The returned reference is only valid as long as the object + /// returned is valid. + /// + /// @return client classes @ref white_list_ + const ClientClasses& getClientClasses() const { + return (white_list_); } - /// @brief Sets user context. - /// @param ctx user context to be stored. - void setContext(const data::ConstElementPtr& ctx) { - user_context_ = ctx; + /// @brief returns the last address that was tried from this pool + /// + /// @return address/prefix that was last tried from this pool + isc::asiolink::IOAddress getLastAllocated() const { + return last_allocated_; + } + + /// @brief checks if the last address is valid + /// @return true if the last address is valid + bool isLastAllocatedValid() const { + return last_allocated_valid_; + } + + /// @brief sets the last address that was tried from this pool + /// + /// @param addr address/prefix to that was tried last + void setLastAllocated(const isc::asiolink::IOAddress& addr) { + last_allocated_ = addr; + last_allocated_valid_ = true; + } + + /// @brief resets the last address to invalid + void resetLastAllocated() { + last_allocated_valid_ = false; } + /// @brief Checks whether this pool supports client that belongs to + /// specified classes. + /// + /// @param client_classes list of all classes the client belongs to + /// @return true if client can be supported, false otherwise + bool clientSupported(const ClientClasses& client_classes) const; + + /// @brief Sets the supported class to class class_name + /// + /// @param class_name client class to be supported by this pool + void allowClientClass(const ClientClass& class_name); + + /// @brief returns the client class + /// + /// @note The returned reference is only valid as long as the object + /// returned is valid. + /// + /// @return client class @ref client_class_ + const ClientClass& getClientClass() const { + return (client_class_); + } + + /// @brief Adds class class_name to classes required to be evaluated + /// + /// @param class_name client class required to be evaluated + void requireClientClass(const ClientClass& class_name) { + if (!required_classes_.contains(class_name)) { + required_classes_.insert(class_name); + } + } + + /// @brief Returns classes which are required to be evaluated + const ClientClasses& getRequiredClasses() const { + return (required_classes_); + } + + /// @brief returns the last address that was tried from this pool + /// + /// @return address/prefix that was last tried from this pool + isc::asiolink::IOAddress getLastAllocated() const { + return last_allocated_; + } + + /// @brief checks if the last address is valid + /// @return true if the last address is valid + bool isLastAllocatedValid() const { + return last_allocated_valid_; + } + + /// @brief sets the last address that was tried from this pool + /// + /// @param addr address/prefix to that was tried last + void setLastAllocated(const isc::asiolink::IOAddress& addr) { + last_allocated_ = addr; + last_allocated_valid_ = true; + } + + /// @brief resets the last address to invalid + void resetLastAllocated() { + last_allocated_valid_ = false; + } + /// @brief Unparse a pool object. /// /// @return A pointer to unparsed pool configuration. diff --cc src/lib/dhcpsrv/subnet.h index cb41bb2ebd,a22e364f21..176e3dbb43 --- a/src/lib/dhcpsrv/subnet.h +++ b/src/lib/dhcpsrv/subnet.h @@@ -83,18 -80,7 +83,18 @@@ public /// @return address/prefix that was last tried from this subnet isc::asiolink::IOAddress getLastAllocated(Lease::Type type) const; + /// @brief Returns the timestamp when the @c setLastAllocated function + /// was called. + /// + /// @param lease_type Lease type for which last allocation timestamp should + /// be returned. + /// + /// @return Time when a lease of a specified type has been allocated from + /// this subnet. The negative infinity time is returned if a lease type is + /// not recognized (which is unlikely). + boost::posix_time::ptime getLastAllocatedTime(const Lease::Type& lease_type) const; + - /// @brief sets the last address that was tried from this pool + /// @brief sets the last address that was tried from this subnet /// /// This method sets the last address that was attempted to be allocated /// from this subnet. This is used as helper information for the next diff --cc src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc index 52a5aac6b7,97733e2915..0a8afebc32 --- a/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc +++ b/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc @@@ -211,9 -212,9 +212,10 @@@ TEST_F(AllocEngine6Test, IterativeAlloc cc_.insert("bar"); for (int i = 0; i < 1000; ++i) { - IOAddress candidate = alloc->pickAddress(subnet_, cc_, duid_, IOAddress("::")); + IOAddress candidate = alloc->pickAddress(subnet_, cc_, + duid_, IOAddress("::")); EXPECT_TRUE(subnet_->inPool(Lease::TYPE_NA, candidate)); + EXPECT_TRUE(subnet_->inPool(Lease::TYPE_NA, candidate, cc_)); } } diff --cc src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc index fb2f22a554,cb6a96626c..6cd404aed5 --- a/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc +++ b/src/lib/dhcpsrv/tests/cfg_subnets4_unittest.cc @@@ -739,12 -739,9 +739,14 @@@ TEST(CfgSubnets4Test, unparseSubnet) subnet2->setIface("lo"); subnet2->setRelayInfo(IOAddress("10.0.0.1")); subnet3->setIface("eth1"); + subnet3->requireClientClass("foo"); + subnet3->requireClientClass("bar"); + data::ElementPtr ctx1 = data::Element::fromJSON("{ \"comment\": \"foo\" }"); + subnet1->setContext(ctx1); + data::ElementPtr ctx2 = data::Element::createMap(); + subnet2->setContext(ctx2); + cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); @@@ -820,12 -816,7 +823,13 @@@ TEST(CfgSubnets4Test, unparsePool) Pool4Ptr pool1(new Pool4(IOAddress("192.0.2.1"), IOAddress("192.0.2.10"))); Pool4Ptr pool2(new Pool4(IOAddress("192.0.2.64"), 26)); pool2->allowClientClass("bar"); + + std::string json1 = "{ \"comment\": \"foo\", \"version\": 1 }"; + data::ElementPtr ctx1 = data::Element::fromJSON(json1); + pool1->setContext(ctx1); + data::ElementPtr ctx2 = data::Element::fromJSON("{ \"foo\": \"bar\" }"); + pool2->setContext(ctx2); + pool2->requireClientClass("foo"); subnet->addPool(pool1); subnet->addPool(pool2); @@@ -851,15 -842,13 +855,16 @@@ " \"option-data\": [],\n" " \"pools\": [\n" " {\n" + " \"comment\": \"foo\",\n" " \"option-data\": [ ],\n" - " \"pool\": \"192.0.2.1-192.0.2.10\"\n" + " \"pool\": \"192.0.2.1-192.0.2.10\",\n" + " \"user-context\": { \"version\": 1 }\n" " },{\n" " \"option-data\": [ ],\n" - " \"pool\": \"192.0.2.64/26\"\n," + " \"pool\": \"192.0.2.64/26\",\n" ++ " \"user-context\": { \"foo\": \"bar\" },\n" " \"client-class\": \"bar\",\n" - " \"user-context\": { \"foo\": \"bar\" }\n" + " \"require-client-classes\": [ \"foo\" ]\n" " }\n" " ]\n" "} ]\n"; diff --cc src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc index 5565387d26,227c60e175..c9dd4b0fd0 --- a/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc +++ b/src/lib/dhcpsrv/tests/cfg_subnets6_unittest.cc @@@ -438,12 -438,9 +438,14 @@@ TEST(CfgSubnets6Test, unparseSubnet) subnet2->setIface("lo"); subnet2->setRelayInfo(IOAddress("2001:db8:ff::2")); subnet3->setIface("eth1"); + subnet3->requireClientClass("foo"); + subnet3->requireClientClass("bar"); + data::ElementPtr ctx1 = data::Element::fromJSON("{ \"comment\": \"foo\" }"); + subnet1->setContext(ctx1); + data::ElementPtr ctx2 = data::Element::createMap(); + subnet2->setContext(ctx2); + cfg.add(subnet1); cfg.add(subnet2); cfg.add(subnet3); @@@ -511,12 -507,7 +514,13 @@@ TEST(CfgSubnets6Test, unparsePool) IOAddress("2001:db8:1::199"))); Pool6Ptr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 64)); pool2->allowClientClass("bar"); + + std::string json1 = "{ \"comment\": \"foo\", \"version\": 1 }"; + data::ElementPtr ctx1 = data::Element::fromJSON(json1); + pool1->setContext(ctx1); + data::ElementPtr ctx2 = data::Element::fromJSON("{ \"foo\": \"bar\" }"); + pool2->setContext(ctx2); + pool2->requireClientClass("foo"); subnet->addPool(pool1); subnet->addPool(pool2); @@@ -542,9 -531,9 +546,10 @@@ " \"option-data\": [ ]\n" " },{\n" " \"pool\": \"2001:db8:1:1::/64\",\n" - " \"client-class\": \"bar\",\n" + " \"user-context\": { \"foo\": \"bar\" },\n" - " \"option-data\": [ ]\n" + " \"option-data\": [ ],\n" + " \"client-class\": \"bar\",\n" + " \"require-client-classes\": [ \"foo\" ]\n" " }\n" " ],\n" " \"pd-pools\": [ ],\n" @@@ -565,10 -554,8 +570,12 @@@ TEST(CfgSubnets6Test, unparsePdPool) IOAddress("2001:db8:2::"), 48, 64)); Pool6Ptr pdpool2(new Pool6(IOAddress("2001:db8:3::"), 48, 56, IOAddress("2001:db8:3::"), 64)); + pdpool2->allowClientClass("bar"); + + data::ElementPtr ctx1 = data::Element::fromJSON("{ \"foo\": [ \"bar\" ] }"); + pdpool1->setContext(ctx1); + pdpool1->requireClientClass("bar"); + pdpool2->allowClientClass("bar"); subnet->addPool(pdpool1); subnet->addPool(pdpool2); @@@ -592,8 -579,8 +599,9 @@@ " \"prefix\": \"2001:db8:2::\",\n" " \"prefix-len\": 48,\n" " \"delegated-len\": 64,\n" + " \"user-context\": { \"foo\": [ \"bar\" ] },\n" - " \"option-data\": [ ]\n" + " \"option-data\": [ ],\n" + " \"require-client-classes\": [ \"bar\" ]\n" " },{\n" " \"prefix\": \"2001:db8:3::\",\n" " \"prefix-len\": 48,\n" diff --cc src/lib/dhcpsrv/tests/client_class_def_unittest.cc index ed16d27d8b,9d8eabb27d..c29fc45d30 --- a/src/lib/dhcpsrv/tests/client_class_def_unittest.cc +++ b/src/lib/dhcpsrv/tests/client_class_def_unittest.cc @@@ -386,10 -397,7 +397,11 @@@ TEST(ClientClassDef, unparseDef) ASSERT_NO_THROW(cclass.reset(new ClientClassDef(name, expr))); std::string test = "option[12].text == 'foo'"; cclass->setTest(test); + std::string comment = "bar"; + std::string user_context = "{ \"comment\": \"" + comment + "\", "; + user_context += "\"bar\": 1 }"; + cclass->setContext(isc::data::Element::fromJSON(user_context)); + cclass->setRequired(true); std::string next_server = "1.2.3.4"; cclass->setNextServer(IOAddress(next_server)); std::string sname = "my-server.example.com"; @@@ -399,9 -407,9 +411,10 @@@ // Unparse it std::string expected = "{\n" + "\"comment\": \"" + comment + "\",\n" "\"name\": \"" + name + "\",\n" "\"test\": \"" + test + "\",\n" + "\"only-if-required\": true,\n" "\"next-server\": \"" + next_server + "\",\n" "\"server-hostname\": \"" + sname + "\",\n" "\"boot-file-name\": \"" + filename + "\",\n" diff --cc src/lib/dhcpsrv/tests/pool_unittest.cc index 27960e0254,494e40663b..acf5b690d2 --- a/src/lib/dhcpsrv/tests/pool_unittest.cc +++ b/src/lib/dhcpsrv/tests/pool_unittest.cc @@@ -230,44 -230,35 +230,73 @@@ TEST(Pool4Test, clientClass) EXPECT_TRUE(pool->clientSupported(three_classes)); } +// This test checks that handling for multiple client-classes is valid. +TEST(Pool4Test, clientClasses) { + // Create a pool. + Pool4Ptr pool(new Pool4(IOAddress("192.0.2.0"), + IOAddress("192.0.2.255"))); + + // This client does not belong to any class. + isc::dhcp::ClientClasses no_class; + + // This client belongs to foo only. + isc::dhcp::ClientClasses foo_class; + foo_class.insert("foo"); + + // This client belongs to bar only. I like that client. + isc::dhcp::ClientClasses bar_class; + bar_class.insert("bar"); + + // No class restrictions defined, any client should be supported + EXPECT_EQ(0, pool->getClientClasses().size()); + EXPECT_TRUE(pool->clientSupported(no_class)); + EXPECT_TRUE(pool->clientSupported(foo_class)); + EXPECT_TRUE(pool->clientSupported(bar_class)); + + // Let's allow clients belonging to "bar" or "foo" class. + pool->allowClientClass("bar"); + pool->allowClientClass("foo"); + EXPECT_EQ(2, pool->getClientClasses().size()); + + // Class-less clients are to be rejected. + EXPECT_FALSE(pool->clientSupported(no_class)); + + // Clients in foo class should be accepted. + EXPECT_TRUE(pool->clientSupported(foo_class)); + + // Clients in bar class should be accepted as well. + EXPECT_TRUE(pool->clientSupported(bar_class)); +} + + // This test checks that handling for require-client-classes is valid. + TEST(Pool4Test, requiredClasses) { + // Create a pool. + Pool4Ptr pool(new Pool4(IOAddress("192.0.2.0"), + IOAddress("192.0.2.255"))); + + // This client starts with no required classes. + EXPECT_TRUE(pool->getRequiredClasses().empty()); + + // Add the first class + pool->requireClientClass("router"); + EXPECT_EQ(1, pool->getRequiredClasses().size()); + + // Add a second class + pool->requireClientClass("modem"); + EXPECT_EQ(2, pool->getRequiredClasses().size()); + EXPECT_TRUE(pool->getRequiredClasses().contains("router")); + EXPECT_TRUE(pool->getRequiredClasses().contains("modem")); + EXPECT_FALSE(pool->getRequiredClasses().contains("foo")); + + // Check that it's ok to add the same class repeatedly + EXPECT_NO_THROW(pool->requireClientClass("foo")); + EXPECT_NO_THROW(pool->requireClientClass("foo")); + EXPECT_NO_THROW(pool->requireClientClass("foo")); + + // Check that 'foo' is marked for required evaluation + EXPECT_TRUE(pool->getRequiredClasses().contains("foo")); + } + // This test checks that handling for last allocated address/prefix is valid. TEST(Pool4Test, lastAllocated) { // Create a pool. @@@ -634,44 -625,35 +663,73 @@@ TEST(Pool6Test, clientClass) EXPECT_TRUE(pool.clientSupported(three_classes)); } +// This test checks that handling for multiple client-classes is valid. +TEST(Pool6Test, clientClasses) { + // Create a pool. + Pool6 pool(Lease::TYPE_NA, IOAddress("2001:db8::1"), + IOAddress("2001:db8::2")); + + // This client does not belong to any class. + isc::dhcp::ClientClasses no_class; + + // This client belongs to foo only. + isc::dhcp::ClientClasses foo_class; + foo_class.insert("foo"); + + // This client belongs to bar only. I like that client. + isc::dhcp::ClientClasses bar_class; + bar_class.insert("bar"); + + // No class restrictions defined, any client should be supported + EXPECT_EQ(0, pool.getClientClasses().size()); + EXPECT_TRUE(pool.clientSupported(no_class)); + EXPECT_TRUE(pool.clientSupported(foo_class)); + EXPECT_TRUE(pool.clientSupported(bar_class)); + + // Let's allow clients belonging to "bar" or "foo" class. + pool.allowClientClass("bar"); + pool.allowClientClass("foo"); + EXPECT_EQ(2, pool.getClientClasses().size()); + + // Class-less clients are to be rejected. + EXPECT_FALSE(pool.clientSupported(no_class)); + + // Clients in foo class should be accepted. + EXPECT_TRUE(pool.clientSupported(foo_class)); + + // Clients in bar class should be accepted as well. + EXPECT_TRUE(pool.clientSupported(bar_class)); +} + + // This test checks that handling for require-client-classes is valid. + TEST(Pool6Test, requiredClasses) { + // Create a pool. + Pool6 pool(Lease::TYPE_NA, IOAddress("2001:db8::1"), + IOAddress("2001:db8::2")); + + // This client starts with no required classes. + EXPECT_TRUE(pool.getRequiredClasses().empty()); + + // Add the first class + pool.requireClientClass("router"); + EXPECT_EQ(1, pool.getRequiredClasses().size()); + + // Add a second class + pool.requireClientClass("modem"); + EXPECT_EQ(2, pool.getRequiredClasses().size()); + EXPECT_TRUE(pool.getRequiredClasses().contains("router")); + EXPECT_TRUE(pool.getRequiredClasses().contains("modem")); + EXPECT_FALSE(pool.getRequiredClasses().contains("foo")); + + // Check that it's ok to add the same class repeatedly + EXPECT_NO_THROW(pool.requireClientClass("foo")); + EXPECT_NO_THROW(pool.requireClientClass("foo")); + EXPECT_NO_THROW(pool.requireClientClass("foo")); + + // Check that 'foo' is marked for required evaluation + EXPECT_TRUE(pool.getRequiredClasses().contains("foo")); + } + // This test checks that handling for last allocated address/prefix is valid. TEST(Pool6Test, lastAllocated) { // Create a pool. diff --cc src/lib/dhcpsrv/tests/shared_network_unittest.cc index c3933477a0,9b74d3a591..2649289b8f --- a/src/lib/dhcpsrv/tests/shared_network_unittest.cc +++ b/src/lib/dhcpsrv/tests/shared_network_unittest.cc @@@ -277,9 -195,7 +277,10 @@@ TEST(SharedNetwork4Test, unparse) network->setValid(200); network->setMatchClientId(false); + std::string uc = "{ \"comment\": \"bar\", \"foo\": 1}"; + data::ElementPtr ctx = data::Element::fromJSON(uc); + network->setContext(ctx); + network->requireClientClass("foo"); // Add several subnets. Subnet4Ptr subnet1(new Subnet4(IOAddress("10.0.0.0"), 8, 10, 20, 30, @@@ -300,6 -215,6 +301,7 @@@ " \"ip-address\": \"0.0.0.0\"\n" " },\n" " \"renew-timer\": 100,\n" ++ " \"require-client-classes\": [ \"foo\" ],\n" " \"reservation-mode\": \"all\"," " \"subnet4\": [\n" " {\n" @@@ -665,10 -482,8 +667,12 @@@ TEST(SharedNetwork6Test, unparse) network->setPreferred(200); network->setValid(300); network->setRapidCommit(true); + network->requireClientClass("foo"); + data::ElementPtr ctx = data::Element::fromJSON("{ \"foo\": \"bar\" }"); + network->setContext(ctx); ++ network->requireClientClass("foo"); + // Add several subnets. Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 30, 40, SubnetID(1))); @@@ -688,6 -503,6 +692,7 @@@ " \"ip-address\": \"::\"\n" " },\n" " \"renew-timer\": 100,\n" ++ " \"require-client-classes\": [ \"foo\" ],\n" " \"reservation-mode\": \"all\"," " \"subnet6\": [\n" " {\n"